@asterql/store
v0.3.0
Published
Normalized entity store, scope cursors, and view-instance cache for AsterQL state sync.
Maintainers
Readme
@asterql/store
npm install @asterql/storeNormalized entity store, scope cursors, and view-instance cache for AsterQL
state sync. This is the client-side Protocol B substrate: it ingests
ServerEventEnvelopes from @asterql/view-protocol and keeps one confirmed
copy of every entity, while a view-instance cache implements the Protocol A
staleness policy over invalidation classes.
The mental model (see the state-sync architecture doc): the server owns truth and order; the client owns latency and intent; views own nothing.
API
State substrate:
ScopeCursors: monotonic per-scope sequence cursors —beginScope(scope, lastSeq),scopeLastSeq(scope), andingestSequenced(envelope, apply)returningapplied,duplicate, orgap. The cursor advances even whenapplythrows (the envelope was consumed at that sequence), and resetting a cursor is always explicit, so a stale snapshot can never rewind a scope past sequences it already applied.CoalescedEmitter<TTopic>: microtask-coalesced change notification with global, per-topic, and per-key listeners. Any number of marks within one task flush as a single notification per listener.EntityStore: a normalizedEntityRecordmap keyedkind:id, fed byingest(envelope)forpatch,snapshot, andinvalidateenvelope kinds. Patches apply throughapplyEntityPatches; record versions come from the envelope orhashRecordVersion.subscribeEntity,subscribeKind, andsubscribenotify precisely.snapshotHash()produces a canonicalh1_hash for cross-surface divergence checks.ViewInstanceCache:fresh | stale | loading | errorentries keyed byviewId.paramsHash.authScopeHash. An entry's recorded class versions double as its dependency list;invalidate(classes, classVersions?)marks only dependent entries stale, and only when a class actually advanced.
Example
import { EntityStore, ViewInstanceCache } from "@asterql/store";
const store = new EntityStore();
store.cursors.beginScope("chat:abc", 0);
const { result, changedKeys, invalidated } = store.ingest({
eventId: "ev1_…",
kind: "patch",
scope: "chat:abc",
seq: 1,
entities: [
{
key: "message:m1",
patches: [{ op: "appendText", path: "/text", delta: "Hello" }],
},
],
});
if (result.kind === "gap") {
// fetch /scopes/chat:abc/events?afterSeq=result.lastSeq, then re-ingest;
// escalate to a snapshot replace when the retention window is exceeded.
}
const views = new ViewInstanceCache();
views.invalidate(invalidated, { "entityKind:message": Date.now() });Fetching, transports, and optimistic mutations are deliberately out of scope: this package is the storage and staleness policy that those layers share.
