/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * A generic zustand store over one localStore collection. Holds the decrypted * payloads as a `Map` and exposes the CRUD verbs that (1) persist * through the encrypted {@link LocalStore} and (2) patch the in-memory Map. UI * reads through selectors and applies the app's own comparators/filters; this * layer stays domain-agnostic so every entity shares it. * * The persisted write verbs (`insert` / `update` / `upsert`) stamp the * last-write-wins fields themselves through {@link stampLww}, so an app can * never degrade conflict resolution by forgetting to. * * Reactivity note: local writes patch the Map optimistically after the localStore * write resolves. Pulled remote changes are applied per-doc by the sync layer * through `patch` / `drop` (no whole-collection re-hydrate), keeping multi-device * edits and tombstones live without re-hydrate storms. Those per-doc remote * patches are coalesced: a pull burst is buffered and flushed on a microtask as a * single Map clone + single `set`, so an initial device sync of N docs is one * re-render rather than N (and O(N) Map copies rather than O(N^2)). Interactive * local writes stay immediate so a single edit is snappy. */ import { type UseBoundStore, type StoreApi } from 'zustand'; /** * What the persisted write verbs accept: the payload WITHOUT its last-write-wins * fields, which the verb stamps itself. The fields stay optional rather than * forbidden so a caller that still supplies them keeps compiling -- their values * are overwritten. */ type WritablePayload = Omit & { updatedAt?: string; writerId?: string; }; export interface EntityStore { /** * Decrypted payloads keyed by logical uuid. */ byId: Map; /** * Decrypt every live row of the collection into the Map. */ hydrate: () => Promise; /** * Encrypt+insert a new doc, then add it to the Map. Fresh `updatedAt` / * `writerId` LWW fields are stamped by this verb, overwriting any the caller * supplied; the Map holds exactly what was persisted. */ insert: (doc: WritablePayload) => Promise; /** * Re-encrypt a doc in place (sequence+1), then replace it in the Map. Fresh * LWW fields are stamped by this verb, as for {@link EntityStore.insert}. */ update: (doc: WritablePayload) => Promise; /** * Encrypt+insert the doc when its uuid is new, otherwise re-encrypt it in * place (insert-or-update routed by the hydration index), then set it in the * Map. The verb for callers that do not track an insert-vs-update flag of * their own (e.g. a singleton document). Fresh LWW fields are stamped by this * verb, as for {@link EntityStore.insert}. */ upsert: (doc: WritablePayload) => Promise; /** * Tombstone a doc, then drop it from the Map. */ remove: (uuid: string) => Promise; /** * Replace the whole Map WITHOUT persisting. This is the app-facing bulk * reset verb: the registry pattern wires `StoreRegistryEntry.clear` to * `replaceAll([])` on logout (see the README). Discards any buffered pull * burst so it cannot resurrect docs past the reset. */ replaceAll: (docs: T[]) => void; /** * Upsert one already-decrypted doc into the Map WITHOUT persisting (the sync * stream owns the persisted row already). Used for per-doc reactive patching * of pulled/conflict-resolved remote changes; coalesced into one store update * per pull burst (see the reactivity note above). When both the incoming and * the held doc carry the LWW fields (`updatedAt` + `writerId`), a stale * incoming doc is discarded rather than clobbering the newer held one. */ patch: (doc: T) => void; /** * Drop one doc from the Map WITHOUT persisting (remote tombstone patch); * coalesced with `patch` into one store update per pull burst. */ drop: (uuid: string) => void; /** * Runs one server-side equality query against this collection and returns * the matching payloads WITHOUT touching the Map (a read verb, not a sync * path; the replica already holds this device's rows). Multiple `equals` * attributes AND together; values are string equality only, and every * attribute must appear in the collection config's declared `indexes`. * Requires a wallet-connected session. * * Works on either visibility. A public (plaintext) collection is queried * with the server-side `filter[attr]=value` form; a private (encrypted) one * with the blinded-index profile, which needs the collection to have been * provisioned with a blinded-index key. On a private collection the standing * limitation is that documents written through the local-first sync path do * not yet carry blinded index entries, so a query finds only documents * written through an index-aware cipher; they become findable once * rewritten that way. * * Pass the returned `cursor` back in to fetch the next page while * `hasMore` is true. */ query: (query: { equals: Record; limit?: number; cursor?: string; }) => Promise<{ docs: T[]; hasMore: boolean; cursor?: string; }>; } /** * Builds a zustand hook for the collection whose localStore key is `collectionKey`. * * @param collectionKey {string} * @returns {UseBoundStore>>} */ export declare function createEntityStore(collectionKey: string): UseBoundStore>>; export {}; //# sourceMappingURL=entityStore.d.ts.map