Skip to content

@repo/datadata/session

Sessions over documents: the live session, and staging sessions that collect changes as a changeset before they reach the documents.

Classes

LiveSession

export declare class LiveSession<Registry extends ValidatorRegistry = ValidatorRegistry, Commands extends CommandRegistry = CommandRegistry> implements Session<Registry>

A live Session over the folder client — the non-staging counterpart to a StagingSession. Reads return live head and writes land on the live document immediately, so edits (and their cursors) sync out to other viewers as they happen rather than waiting on a commit. There is no changeset, so Session.kind is "live" and there is nothing to commit, discard, or strand.

It owns the editing-scope lifecycle the folder client refuses to hold: - prepareDocument takes subscription leases (target + schema) and returns a PreparedDocument handle whose release() drops them; unreleased leases fall to LiveSession.dispose, mirroring StagingSession. - getYjsAwareness builds the same createYjsAwareness bridge the collaborative editor uses — over the TARGET document's own sys:awareness channel — so the cursor renders for every other live viewer exactly like a human peer.

[Symbol.dispose](): void;

Symbol.dispose alias for LiveSession.dispose.

constructor(client: ClientSessionBridge, options?: {
        presenceId?: string;
        presencePublishDebounceMs?: number;
        scopes?: readonly AccessScope[];
    });

Constructs a new instance of the LiveSession class

can(params: {
        kind: AccessKind;
        docId: string;
    }): boolean | null;

Predict whether an operation of kind on the EXISTING document docId would be permitted — the capability query for a document you hold only by id (a delete/rename/restore target). It resolves the document's docType the same way those ops do (the subscribed doc, else the sys:index / sys:trash listing) and runs the client's docType-keyed prediction, so a UI can gray out a delete/rename button without knowing the type. boolean when the type resolves and the authz facts are synced; null when the verdict is unknown — the type could not be resolved (subscribe the doc or its listing) or the facts have not synced. A false predicts the same local rejection the write would raise; the server stays authoritative. For create, whose type is not carried by an existing document, ask the client's can({ kind: "create", docType }).

command(params: CommandInvocation<Commands>): void;

Run a domain command (defineCommands) on a loaded document: its predicted effect shows at once as an optimistic update, and the server runs the command authoritatively over the document it holds. With the client's commands a defineCommands map, type names the docType, name one of the commands the map defines for it and args is typed by that command's shape (see CommandInvocation); type must be the document's own. Throws when the target is not loaded or not of type, the command is not defined for its docType or the args fail its shape, the command is outside the session's scopes or predicted unauthorized (command:<name>), or the session is disposed.

commandAndWait(params: CommandInvocation<Commands> & {
        eventId?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<CommandResult>;

LiveSession.command, resolving with the server's answer: committed: true, or committed: false when an "exact" command's guard missed (the document moved since the caller read it). A mutator's refusal rejects with a CommandRefusedError carrying its code; every other failure rejects too.

createDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        data: TypedDocumentData<Registry, Type>;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }): void;

Create a document and send it at once as an optimistic create; the session takes a lease on it, so it reads back immediately. yjsCallback seeds its embedded Y.Docs and name sets its index name. Throws when the create is refused locally (invalid id or data, out of scope, predicted unauthorized, schema not yet resolved), with nothing sent.

createDocument(params: {
        docId: string;
        type: string;
        data: unknown;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }): void;

Untyped create: data is validated at runtime against the schema of type.

createDocumentAndWait<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        data: TypedDocumentData<Registry, Type>;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<CreateResult>;

LiveSession.createDocument, resolving with the server's answer: created: false when the document already exists (it stays leased under this session), and a rejection for any other failure. A local refusal rejects rather than throws. Waits for the type's schema to resolve before sending. signal and timeoutMs bound the wait, not the write: a create already sent may still commit.

createDocumentAndWait(params: {
        docId: string;
        type: string;
        data: unknown;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<CreateResult>;

Untyped LiveSession.createDocumentAndWait.

deleteDocument(params: {
        docId: string;
        guard?: DeleteGuardMode;
    }): void;

Soft-delete a document, sent at once. With guard: "sequence" the delete applies only if the document is still at the sequence this client holds. The session keeps any lease it holds, so a later restore is delivered on it. Resolves the target type like LiveSession.renameDocument, and throws on the same refusals.

deleteDocumentAndWait(params: {
        docId: string;
        guard?: DeleteGuardMode;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<DeleteResult>;

LiveSession.deleteDocument, resolving with the server's answer: a failed guard resolves deleted: false, and any other failure, a local refusal included, rejects.

dispose(): void;

Release this session's leases, awareness bridges (clearing its published cursors) and read-only views. Idempotent; afterwards every write and resource-allocating call throws, while reads still forward to the client.

getCapabilities(params: {
        docId: string;
    }): AccessCapabilities;

The per-kind Session.can verdicts for docId, in one call (all null when the type is unresolvable).

getDocument(params: {
        docId: typeof CLIENT_DOCUMENTS_STATUS_DOCUMENT_ID;
        type?: typeof CLIENT_DOCUMENTS_STATUS_DOCUMENT_TYPE;
        skipOptimistic?: boolean;
    }): DatadataDocument<ClientDocumentsStatusDocument>;

Read a document's live value, the optimistic view including this client's pending writes (skipOptimistic reads the synced copy). This overload reads the client's documents-status document. See Session.getDocument.

getDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        skipOptimistic?: boolean;
    }): DatadataDocument<TypedDocumentData<Registry, Type>> | null;

Typed read: type names the docType and docId is a TypedDocumentId of it. Null when the document is absent, deleted, or outside the session's scopes.

getDocument<Id extends BrandedDocumentId<Registry>>(params: {
        docId: Id;
        type?: undefined;
        skipOptimistic?: boolean;
    }): DatadataDocument<TypedDocumentData<Registry, DocumentTypeForId<Registry, Id>>> | null;

Branded read: the BrandedDocumentId alone types the result. Null like the typed read.

getDocument(params: {
        docId: string;
        type?: undefined;
        skipOptimistic?: boolean;
    }): DatadataDocument | null;

Untyped read by a plain-string id: data is unknown. Null like the typed read.

getValidator(type: string): JsonValidator | undefined;

Resolve the JsonValidator for a document type (dynamic schemas included), or undefined when no schema is found.

getYjsAwareness(params: {
        docId: string;
        yjsId: string;
        presenceType: string;
    }): Awareness;

A y-protocols Awareness for a live Y.Doc, bridged over the presence channel sys:presence:<presenceType>:<docId> so the cursor renders for every live viewer on it. See Session.getYjsAwareness.

The target must be subscribed first (prepare or create it). One bridge serves each (docId, yjsId) pair; naming a different presenceType for the same pair throws. A session that may not update the target binds the awareness to a read-only view of the Y.Doc.

readonly kind: "live";

Always "live" for a live session; see Session.kind.

onDocumentChange(listener: (docId: string, document: DatadataDocument | null) => void): () => void;

Subscribe to per-document changes: the listener gets (docId, document) with the current view, or null. Returns an unsubscribe function.

A change to a presence channel also fires for this session's presence view of it. On a disposed session this registers nothing and returns a no-op.

prepareDocument(docId: string, opts?: {
        signal?: AbortSignal;
        includeSchema?: boolean;
    }): PreparedDocument;

Make a document id available to this session: subscribe it (and, unless includeSchema is false, its sys:schema:<type> document for a custom type) and hold the lease under this session. The schema lease is taken only when the client resolves schemas dynamically (createClient({ dynamicSchemas: true })); a static-schema client has no sys:schema:* documents to subscribe, so includeSchema is a no-op for it regardless of its value. If you also prepare the schema document yourself (e.g. prepareDocument(schemaDocId(type))), pass includeSchema: false here — otherwise this call resolves-and-leases the same sys:schema:<type> doc a second time. Ref-counting keeps that correct (each lease still needs its own balancing release()), but it is a redundant resolve+lease worth avoiding. Returns SYNCHRONOUSLY — the lease is taken at once, the session-level analogue of the client's subscribeToDocument — so the returned handle's release() is valid immediately, even while its settled is still in flight. Await available for the editing precondition updateDocument requires (readable — cache-hydrated counts, so it resolves on an offline boot — and, unless includeSchema is false, schema resolution settled, so an explicit getValidator(type) gate sees the dynamic schema rather than silently missing it); await settled when you need server-confirmed truth. The id need NOT already resolve to a document. What you take out is tenure over the ID — preparing an absent one is legitimate, and is both the normal create path (prepareDocument(id), then createDocument({ docId: id, ... })) and how you watch for a document another client will create: the subscription stands, so a later create — or a restore — is delivered on it. Existence is orthogonal to the lease, and the two promises (not this call) report it: settled RESOLVES for a notFound or deleted document, since that is an ANSWER about a standing subscription — which is what makes "prepare, then check getDocument(...) === null" a valid existence probe — while available REJECTS with a DocumentUnavailableError, there being nothing readable to edit. Ref-counted: preparing the same doc again returns a fresh handle holding its own balancing release, and the subscription drops only when every handle (and any consumer lease) is released or the session is disposed. release() is the per-document antonym of prepare and the targeted counterpart to Session.dispose (in a live session it also tears down the doc's awareness bridges once the last holder releases). Honours signal for the bounded settle. Does not touch a staging session's changeset — staged work survives a release until commit/discard.

prepareDocuments(ids: string[], opts?: {
        signal?: AbortSignal;
        includeSchema?: boolean;
    }): PreparedDocuments;

Batch Session.prepareDocument over several ids with the same opts, returning one PreparedDocuments handle whose release() (and Symbol.dispose) balances all of them and whose settled resolves once every one has settled. Use it for the common "keep these N docs prepared for a scope's lifetime, then release all" shape so a call site needs neither a handle array nor a release loop — using docs = session.prepareDocuments(ids) releases them when the block exits.

readonly presenceId: string;

The presence id this session publishes its own cell under (one session = one participant/cursor). Minted per session (config-overridable); the server stamps subject on top, so several sessions over one client group under one subject.

purgeDocuments(params: {
        docIds: string[];
    }): void;

Hard-delete a batch of soft-deleted documents and release their ids. Needs the docTypes' explicit access.purge rule (the default allows nobody), is all-or-nothing and irreversible, and is neither optimistic nor buffered offline: it throws when the client is not connected. Each target's type resolves from sys:trash.

purgeDocumentsAndWait(params: {
        docIds: string[];
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<PurgeResult>;

LiveSession.purgeDocuments, resolving once the server purged the whole batch. Every failure rejects, and a rejected purge destroys nothing.

renameDocument(params: {
        docId: string;
        name: string | null;
    }): void;

Set (or, with null, clear) a document's index-level name, sent at once. The document's type must be resolvable from the subscribed document or the sys:index / sys:trash listing; throws otherwise, and when the rename is out of scope or predicted unauthorized.

renameDocumentAndWait(params: {
        docId: string;
        name: string | null;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<RenameResult>;

LiveSession.renameDocument, resolving once the server applied it. Every failure, a local refusal included, rejects.

restoreDocument(params: {
        docId: string;
    }): void;

Restore a soft-deleted document, sent at once. Live sessions only: a staging session cannot stage a restore. The type resolves from the subscribed document or sys:trash; throws when it cannot, or when the restore is out of scope or predicted unauthorized.

restoreDocumentAndWait(params: {
        docId: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<RestoreResult>;

LiveSession.restoreDocument, resolving once the server restored it. Every failure, a local refusal included, rejects.

updateDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        callback: (doc: DatadataDocument<TypedDocumentMutable<Registry, Type>>, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
    }): void;

Mutate a document; the diff of the callback's draft is sent at once as an optimistic update. With type, docId is branded to that docType and doc.data is typed. Throws when the target is not loaded (prepare it first), the write is outside the session's scopes or predicted unauthorized, or the session is disposed.

updateDocument(params: {
        docId: string;
        type?: undefined;
        callback: (doc: DatadataDocument, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
    }): void;

Untyped update by a plain-string id: the callback's doc.data is unknown.

updateDocumentAndWait<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        callback: (doc: DatadataDocument<TypedDocumentMutable<Registry, Type>>, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
        guard?: GuardMode;
        eventId?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<UpdateResult>;

LiveSession.updateDocument, resolving with the server's answer. guard makes the write conditional: "sequence" on the document still being at the base the patch was authored against, "patch" on the patch's test operations still holding; a failed guard resolves committed: false, and any other failure rejects. A local refusal rejects rather than throws. eventId pre-stamps the write's event id.

updateDocumentAndWait(params: {
        docId: string;
        type?: undefined;
        callback: (doc: DatadataDocument, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
        guard?: GuardMode;
        eventId?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<UpdateResult>;

Untyped LiveSession.updateDocumentAndWait.

waitForSchema(type: string, opts?: {
        signal?: AbortSignal;
    }): Promise<SchemaResolution>;

Wait until the schema of type is resolved, and learn where it resolves from. This asks about the TYPE only — no document of the type need exist — so it is the wait before a plain createDocument on a dynamic-schema client (createClient({ dynamicSchemas: true })), which refuses the write until then. createDocumentAndWait / updateDocumentAndWait wait on their own, and a prepared document's available includes this wait for the document's type.

Resolves once the answer can no longer flip from "not synced yet": the type's sys:schema:<type> document is readable (dynamic), or the client knows there is none (static, or none when the static registry has no entry either). A staging session answers staged for a type whose schema document it has staged. Immediate on a static-schema client and for sys: types. See SchemaResolution.

Readiness is not server confirmation: a schema hydrated from the offline cache resolves (confirmed: false), so an offline boot that has the schema cached can author. Only a COLD offline boot on a dynamic-schema client stays pending — there is no index to consult — so pass signal for a bounded wait.

A write issued when this resolves, with anything but none, is not refused for its schema being unresolved; its data is still validated against that schema. The answer is of that moment. A type can become unresolved again later: when a schema document is created for a type that had none, until it has synced, and across a reconnect for a listed schema document that was not found. Wait again before a later plain write, or use the awaited writes, which hold through it.

Rejects with the abort reason when signal aborts, with a SchemaUnavailableError when sys:index or the type's schema document could not be read, or when the client is destroyed before the answer. An absent schema is not a failure: that is the static fallback.

StagingSession

export declare class StagingSession<Registry extends ValidatorRegistry = ValidatorRegistry> implements Session<Registry>

The front-end overlay: a decorator over the live client. Reads come back overlaid (live server-truth-plus-optimistic view with the staged stack folded on top), writes get staged and held until commit. The staged work lives in a pluggable store: a host-document field (durable) when hostDocId is given, else in memory. (session.allium: surface StagingSession.)

[Symbol.dispose](): void;

Symbol.dispose alias for StagingSession.dispose.

constructor(params: StagingSessionParams);

Constructs a new instance of the StagingSession class

amendStagedChange(docId: string, changeId: string, callback: (doc: DatadataDocument) => void): Promise<void>;

Replace a staged op's content — the CONTENT move (merge values, re-key, fix a schema violation). The callback receives the document as of base + the ops BEFORE this one and mutates it; the replacement patch is recomputed from that. It does NOT move the base, so on its own amend cannot clear a SAME-PATH physical conflict — amend the value, then rebaseStage to re-assert it over head. An amend that cancels the op (no net change) drops it.

Async because the interim needs the stage's BASE, which is resolved, not persisted: instant whenever it is resident (always, for the client that staged in-session), an online-only event-log replay on a cold client whose target advanced unseen. (session.allium: rule AmendStagedChange.)

can(params: {
        kind: AccessKind;
        docId: string;
    }): boolean | null;

Predict whether an operation of kind on the document docId would be permitted: a boolean once its type resolves and the authz facts are synced, else null. See Session.can.

A document that exists only as a stage here (a staged create) resolves to its staged type, and types whose schema is staged in this session are predicted against that schema.

commit(opts?: {
        signal?: AbortSignal;
    }): Promise<CommitResult>;

Accept the session: collapse each dirty stage's patch stack into one base-to-final diff against live head and write it through the live client, releasing staged Yjs updates into the live Y.Doc. Stages drain INDEPENDENTLY on the write's ack, so a stage whose write fails (e.g. a staged op no longer applies — the apply-based conflict gate) keeps its staged work for retry while the others proceed. CommitResult.status is committed when every stage drained, else staging; the session itself stays reusable either way (auto-clear-on-drain). (session.allium: rule CommitChangeset.)

commit may SUBSCRIBE-AND-WAIT for staged targets the caller hasn't subscribed (CommitSubscribesItsTargets), so pass opts.signal with a deadline to bound that wait — otherwise a target that never reaches a synced state would hang the commit.

Abort contract with opts.signal: an abort DURING a stage's drain resolves normally — the aborted stage lands in CommitResult.failed and its staged work is kept. An abort during the pre-drain phases (the target subscribe-and-sync wait, or awaiting a prior commit's intent reconciliation — a retry commit found journaled writes still in flight) REJECTS with the abort error instead, since there is no per-stage outcome to report yet. Handle both when passing a signal.

createDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        data: TypedDocumentData<Registry, Type>;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }): void;

Stage the CREATION of a new document — held, not sent, like every staged edit. The docId must not already exist or be staged. Until commit the new document is visible only through this session's overlaid getDocument; further updateDocument calls append to it. At commit it is created (not updated), ordered after any schema document it depends on — commit() subscribes the new id itself so the create can be acked. Its content is schema-validated like any stage — including against a schema staged in the same session, which is what makes "new type + instances in one session" work.

createDocument(params: {
        docId: string;
        type: string;
        data: unknown;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }): void;

Untyped create: data is validated at runtime against the schema of type.

deleteDocument(params: {
        docId: string;
    }): void;

Stage the DELETION of a document — held, not sent, like every staged edit. A delete SUPERSEDES the doc's other staged work (its changes/yjsCopies are dropped) and reads as null through this session's overlaid getDocument until commit, which soft-deletes it (deleteDocumentAndWait). Deleting a not-yet-committed staged CREATE instead cancels the create outright — the document was never going to exist, so there is no deletion to commit. Idempotent: re-deleting an already-staged delete is a no-op. The target must be available live (or be a staged create); subscribe-and-wait first otherwise.

discard(): void;

Abandon the staged work without committing. Nothing ever reached the target documents (staged edits are never sent), so discard just clears the changeset back to an empty staging state. Like a fully-drained commit, discard does NOT terminate the session: the host returns to fresh staging and can stage again. Releasing the session's resources is dispose(). (session.allium: DiscardSession.)

dispose(): void;

Release the session's resources — its host subscription and live/store change listeners — WITHOUT committing, discarding, or otherwise touching the changeset. Unlike commit/discard, this does not terminate the session: the persisted changeset is left intact, so a fresh client.openStagingSession(...) over the same host RESUMES the staged work (for an in-memory session the work is ephemeral and goes with the disposed instance). Use this to stop displaying / driving a session that still has pending changes — e.g. a UI unmounting, or a server releasing a per-turn session — without throwing the staged work away. After dispose the session is inert: further staging, commit or discard throw. Idempotent.

dropStage(docId: string): void;

Drop a whole stage — every piece of staged work on docId: its staged changes and Yjs copies, and a staged rename, delete or create. The resolution for a stage no single dropped change clears — one holding only a delete, a rename or Yjs edits, or a targetReplaced stage, which nothing else ever un-blocks — without discarding the session's other stages. A no-op when docId holds no staged work. Refused while a commit is in flight, like discard. (session.allium: rule DropStage.)

dropStagedChange(docId: string, changeId: string): void;

Drop one staged op out of a stage's stack — the YIELD move. Because evaluation and commit fold the stack change by change, dropping the op that overlapped upstream removes the overlap (and its guards); the conflict clears if the remaining guarded stack folds onto head. A stage left with no staged work is removed. (session.allium: rule DropStagedChange.)

getCapabilities(params: {
        docId: string;
    }): AccessCapabilities;

The per-kind Session.can verdicts for docId, in one call (all null when the type is unresolvable).

getConflictPreview(docId: string): Promise<ConflictPreview | null>;

The 3-way preview for resolving a stage's conflict: base (what the stack was authored against), ours (base with the stack applied), and theirs (live head). Null when the stage has no conflict, and when its target was replaced (targetReplaced): the base belongs to a document that no longer exists, so there is nothing to merge — drop the stage. Derived, not stored — hand it to the AI or user to resolve as conversation content.

Async because the base is RESOLVED, not persisted: instant whenever it is resident (the author's own session, a watcher's pin, a target still at its baseSequence), an event-log replay round-trip on a cold client whose target advanced unseen. That replay is online-only — offline it rejects; retry on reconnect. (session.allium: Conflict's derived base/ours/theirs preview.)

getDocument(params: {
        docId: typeof CLIENT_DOCUMENTS_STATUS_DOCUMENT_ID;
        type?: typeof CLIENT_DOCUMENTS_STATUS_DOCUMENT_TYPE;
        skipOptimistic?: boolean;
        skipStaged?: boolean;
    }): DatadataDocument<ClientDocumentsStatusDocument>;

Read a document through the session: the live view with this session's staged changes folded on top. skipOptimistic drops the client's pending writes, skipStaged ignores the staged changes. This overload reads the client's documents-status document. See Session.getDocument.

getDocument(params: {
        docId: string;
        type: typeof SESSION_DOCUMENT_TYPE;
        skipOptimistic?: boolean;
        skipStaged?: boolean;
    }): DatadataDocument<SessionDocumentData> | null;

Read the synthesized sys:session document, the summary of this session's staged work, by passing type: SESSION_DOCUMENT_TYPE. Derived on read and read-only.

getDocument(params: {
        docId: string;
        type: typeof SESSION_STAGE_DOCUMENT_TYPE;
        skipOptimistic?: boolean;
        skipStaged?: boolean;
    }): DatadataDocument<SessionStageDocumentData> | null;

Read the synthesized sys:stage:<docId> document, one stage's detail, by passing type: SESSION_STAGE_DOCUMENT_TYPE. Null when the target has no staged work, and with skipStaged.

getDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        skipOptimistic?: boolean;
        skipStaged?: boolean;
    }): DatadataDocument<TypedDocumentData<Registry, Type>> | null;

Typed read: type names the docType and docId is a TypedDocumentId of it. Null when the document is absent, deleted (or staged for deletion), or outside the session's scopes.

getDocument<Id extends BrandedDocumentId<Registry>>(params: {
        docId: Id;
        type?: undefined;
        skipOptimistic?: boolean;
        skipStaged?: boolean;
    }): DatadataDocument<TypedDocumentData<Registry, DocumentTypeForId<Registry, Id>>> | null;

Branded read: the BrandedDocumentId alone types the result. Null like the typed read.

getDocument(params: {
        docId: string;
        type?: undefined;
        skipOptimistic?: boolean;
        skipStaged?: boolean;
    }): DatadataDocument | null;

Untyped read by a plain-string id: data is unknown. Null like the typed read.

getValidator(type: string): JsonValidator | undefined;

Resolve the JsonValidator for a document type as THIS SESSION sees it: a schema staged in this session wins (so a brand-new type defined this session is usable immediately), otherwise the live client's resolution (synced sys:schema doc, else static registry). Returns undefined when no schema is found, or when a staged schema is itself malformed.

This is the public way to obtain a validator. Do NOT reach for the schema-layer resolveValidator helper — it is internal plumbing that knows nothing about staged schemas and leaves the caller to wire the type-to-docId convention. When you only need to check data, prefer validate({ type, data }); use this when you need the JsonValidator object itself. (session.allium: GetValidatorIsStagedAware.)

getYjsAwareness(params: {
        docId: string;
        yjsId: string;
        presenceType: string;
    }): Awareness;

The y-protocols Awareness for a staged copy — cursors and selections for every editor bound to it. Hand it to y-prosemirror/Tiptap alongside getYDoc's doc. For a PERSISTED session it rides THIS session's own cell in the TARGET's presence view (the shared sys:awareness field, keyed by the copy's copyRef), so other session clients' cursors flow in ephemerally. Only session participants (who hold the copy) resolve the copyRef-keyed entries; a live-target viewer reads the same field by plain yjsId and never sees them. An in-memory session stays local-only. Torn down on discard/dispose.

Calling this without a prior getYDoc acquires the same copy doc (deferred if unwritten) — awareness is per copy, so one cannot exist without the other. Like getYDoc it does not stage on its own; the first content edit promotes the copy.

readonly hostDocId: string | undefined;

The host document the changeset is persisted in, or undefined for in-memory.

readonly kind: "staging";

Discriminates this staging session from a live Session ("live"): a consumer narrows on kind === "staging" to reach the commit/discard/conflict surface that a live session has nothing to drive.

onDocumentChange(listener: (docId: string, document: DatadataDocument | null) => void): () => void;

Subscribe to PER-DOCUMENT changes, matching the live client's onDocumentChange — the callback gets (docId, document) where document is the OVERLAID view (or null). Fires for: any document the live client reports changed (forwarded so a document-driven consumer sees every doc, staged or not); each staged document when the changeset changes; and the synthesized sys:session document whenever the staged-work summary changes. This makes the StagingSession a drop-in observable decorator: point a per-docId reactive binding at it and overlaid content, the system documents and the staged-work summary all flow through one pipeline. Returns an unsubscribe function; a no-op on a disposed session.

get onUpstreamAdvance(): UpstreamAdvancePolicy;

The changeset's policy for upstream advances to its staged docs.

preflightCommit(): {
        docId: string;
        kind: OperationKind;
        authorized: boolean | null;
    }[];

ADVISORY authorization pre-flight for a commit: the predicted verdict per (dirty stage, operation kind) the drain would perform, from the SESSION's capability prediction — so a stage whose type is defined by a schema staged here is previewed against that schema, the one the drain will commit first. null = unknown (no principal / unsynced facts). Purely a UX affordance — commit's write boundary is the authority; a denied stage still lands in CommitResult.failed with category unauthorized if committed anyway.

prepareDocument(docId: string, opts?: {
        signal?: AbortSignal;
        includeSchema?: boolean;
    }): PreparedDocument;

Make an EXISTING document available to this session: subscribe it (and, unless includeSchema is false, its sys:schema:<type> document for a custom type) and hold the lease under the session. Returns SYNCHRONOUSLY — the lease is taken at once — so the returned PreparedDocument's release() is valid immediately. Await its available for the editing precondition (staging requires the target READABLE — which a cache-hydrated document satisfies on an offline boot), or its settled when you need confirmed live truth in place first (settled honestly stays pending offline until the server answers). The read surface (getDocument, the overlay) stays synchronous; the awaits are the async step that gets truth in place — the same shape on the client (subscribe at route mount) and the server (prepare on demand), letting a server-side actor work with a document without pre-subscribing the whole folder. Independent of subscribeStaged; ref-counted; honours signal (forwarded to the bounded settle). release() drops the session lease (its schema doc was taken under its own id and may be shared, so it too is balanced by this release) but leaves the changeset untouched — staged work for docId survives until commit/discard. For the host document this is a no-op handle (already subscribed by the session, never released here). (session.allium: PrepareDocumentSubscribesUntilDispose, ReleaseDocumentDropsLease.)

prepareDocuments(ids: string[], opts?: {
        signal?: AbortSignal;
        includeSchema?: boolean;
    }): PreparedDocuments;

StagingSession.prepareDocument over several ids with the same opts, as one PreparedDocuments handle. See Session.prepareDocuments.

readonly presenceId: string;

The presence id this session publishes its own cell under (one session = one participant/cursor). Minted per session (config-overridable); the server stamps subject on top, so several sessions over one client group under one subject.

rebaseStage(docId: string): void;

Advance the stage's base to live head — the RE-ASSERT move. Requires the stack still re-applies onto head (else drop/amend the offending op first). The conflict guards are the per-change test ops embedded in the staged patches, so rebase REWRITES each change against the new base (interim-by-interim, the same recompute amendStagedChange does for one change): the guards then match head and the staged ops overwrite it. Only the paths the stack touches are overridden — disjoint upstream edits are preserved. A change whose ops net to nothing on the new base (upstream already holds the staged value) is dropped, like an amend that cancels. A DELIBERATE, reviewed override, not an automatic merge. (session.allium: rule RebaseStage.)

rekeyStagedMigration(docId: string, from: string, to: string): void;

Give a staged migration another key — the way out of a migrationKeyRefused conflict, where the key it was staged under sorts at or before one committed meanwhile, or was taken by another author's migration. docId is the sys:schema:<type> document the migration is staged for.

One write moves the key everywhere the changeset names it: in the schema stage's patches, and in the position of every change of the type staged after the migration (stagedMigrations). The second is what editing the schema stage by hand leaves out: a change naming a key found nowhere is read as owed the migration again under its new key, and a patch already in the migrated shape is carried through it twice. to has to pass where from did not: it is refused when the log already holds it or it does not sort after every committed key. Staged migrations replay in key order, so a key that moves past another staged one changes their order too.

The changes lane is a flat record other authors write, as with rebaseStage: a change an author stages under the old key while this write is in flight still names it. (session.allium: rule RekeyStagedMigration.)

renameDocument(params: {
        docId: string;
        name: string | null;
    }): void;

Stage a rename — set a document's index-level name (see sys:index). A name is metadata about the document, not content, so the rename lives on the stage (never the changes patch lane) and the document's data is untouched. Works on a not-yet-committed staged create (the name folds into that create) and on an already-committed live document (commit emits a doc:rename). null clears the name. The session's sys:index overlay reflects the new name immediately.

get stages(): DocumentStage[];

A read-only snapshot of the current stages, each with its derived conflict.

updateDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        callback: (doc: DatadataDocument<TypedDocumentMutable<Registry, Type>>, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
    }): void;

Stage an update: the diff of the callback's draft is appended to the document's stage, held until commit and never sent. Edits build on the overlaid view, so consecutive updates stack. Yjs edits through the callback's accessor go to the staged copies. Throws when the document is not available in the session (prepare it and await available first), when the write is outside the session's scopes, or once the session is disposed. Writing to a presence view document publishes this session's cell directly instead of staging.

updateDocument(params: {
        docId: string;
        type?: undefined;
        callback: (doc: DatadataDocument, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
    }): void;

Untyped update by a plain-string id: the callback's doc.data is unknown.

validate(params: {
        type: string;
        data: unknown;
    }): string | null;

Validate data for type against the schema THIS session sees — WITHOUT staging anything — returning the validation error message, or null when it conforms. Schema resolution matches the commit gate: a schema staged in this same session (the overlay) wins, else the synced sys:schema:<type> document, else the static registry; an unrecognised type reports "No schema found". Shape + intra-document references only — cross-document (toType) references stay the server's authority at the commit boundary.

Use it to PRE-CHECK a proposed write (e.g. an AI tool's create/update) and hand the message back to fix it, instead of re-implementing schema resolution. It is the same verdict the session derives for an already-staged document — that one is read after the fact from the sys:session projection (each stage's schemaError, and a schemaInvalid conflict when the merged-against-head result is invalid). Staging itself never throws on invalid content: an invalid stage is representable and simply blocks commit until resolved (drop/amend or a corrected stage).

waitForSchema(type: string, opts?: {
        signal?: AbortSignal;
    }): Promise<SchemaResolution>;

The schema of type as THIS SESSION sees it: staged while the session edits the type's sys:schema:<type> document (a brand-new type staged here resolves at once, nothing having synced), otherwise the live client's resolution. A staged schema that is itself malformed validates nothing, so it answers none. A wait the client has not answered resolves when the session stages the type's schema meanwhile.

Functions

emptySessionChangeset

export declare function emptySessionChangeset(options?: {
    onUpstreamAdvance?: UpstreamAdvancePolicy;
}): SessionChangeset;

An empty changeset under onUpstreamAdvance (default autoMerge), for creating a host document whose sessions apply a policy other than the field's default — every session hosted on the document reads the policy from here. E.g. createDocument({ docId, type: "conversation", data: { title, session: emptySessionChangeset({ onUpstreamAdvance: "block" }) } }). A host created without the field gets the autoMerge default.

isStagingSession

export declare function isStagingSession(session: Session): session is StagingSession;

Narrow an Session to a staging StagingSession: true when it stages edits into a changeset (so stages, commit, discard, getConflictPreview and the sys:session projection are available), false for a live editing session (which has none of those — its writes land on the document immediately). Use it to conditionally reach the staging surface, e.g. to render commit/conflict UI only when there is staged work to drive.

sessionChangesetReferences

export declare function sessionChangesetReferences(field?: string): Record<string, ReferenceRule>;

The referential-integrity rules for an embedded changeset, to spread into the HOST schema's references (reference paths are document-rooted, so they must be prefixed with the host field name — pass it if you store the changeset somewhere other than the default "session"): { conversation: { fields: { session: SESSION_CHANGESET_FIELD }, references: sessionChangesetReferences() } }.

The rule asserts that a commit intent names an existing stages key, with onDelete: "cascade" — so an intent whose stage is gone is dropped by the write-boundary repair pass (mirroring what normalizeChangeset does on read), rather than rejecting the write. Cascade rules only ever REPAIR, never block, so they're safe under the convergent record-set merge.

The changes/yjsCopies lanes deliberately carry NO such rule: a lane entry whose stage is gone is staged work that raced the stage's removal by another author, and it rebuilds its stage from what it recorded (stage) instead of being dropped — which a server-side cascade would do before any client could read it.

sessionStageDocId

export declare function sessionStageDocId(docId: string): string;

Id of the stage detail document for docId (sys:stage:<docId>): the full staged changes of one target document, synthesized by a staging session on read. Reads null when the target has no staged work, and always on the live client.

Interfaces

CapabilityPredictor

export interface CapabilityPredictor

The narrow capability surface Session.can and Session.getCapabilities run their verdicts through: the live client itself, or a predictor bound to a session's own schema resolution (see ClientSessionBridge.authzPredictorWithValidators).

can(params: {
        kind: AccessKind;
        docType: string;
        docId?: string;
    }): boolean | null;

Predict whether an operation of kind on docType is permitted: true/false once the authz facts are synced, null when no verdict can be predicted. Same contract as DatadataClient.can.

ClientSessionBridge

export interface ClientSessionBridge

The client surface the editing sessions (LiveSession / StagingSession) and the per-session presence view consume — the SUPERSET of the public client (it includes the session-scoped read/write/subscribe/presence methods that DatadataClientInterface deliberately omits). Sessions depend on this narrow bridge rather than on the concrete client class, so they cannot reach arbitrary client internals and the coupling between the two is an explicit, reviewable contract. The concrete client class satisfies it structurally (it is passed as this to the session constructors).

Signatures here mirror the client's untyped internal forms — the registry-typed reads and writes are a session-level affordance (the sessions own those overloads and cast down to these forms), so the bridge stays schema-agnostic and needs no Registry parameter.

_presenceCells(presenceDocId: string): Map<string, {
        subject: string | null;
        state: Record<string, JsonValue>;
        stale: boolean;
    }>;

The cells of a presence aggregate keyed by presence id, with liveness applied: stale marks a cell from an earlier presence generation, and expired cells are left out. Internal, like ClientSessionBridge._publishPresenceCell.

_presenceEpoch(presenceDocId: string): string | undefined;

The epoch the local copy of a presence channel counts in (see PresenceOverlay.epoch).

_publishPresenceCell(params: {
        presenceDocId: string;
        presenceId: string;
        state: Record<string, JsonValue> | null;
    }): void;

Publish (or, with state: null, clear) this session's cell on a presence aggregate, as an optimistic cell-scoped update. Throws when the presence document is not subscribed, the write is predicted unauthorized, or the state exceeds the presence size limit. Internal: a session's presence view is the public path.

assertWriteAuthorized(params: {
        kind: WriteKind;
        docId: string;
        docType: string;
    }): void;

Run the write-authorization prediction for an explicit docType and throw the predicted WriteRejectedError if it would be denied. A session uses this for rename/delete/restore, whose target type it resolves itself — the write path's own prediction keys off the local copy and skips a target not synced locally, so this closes that gap. A null (unknown) verdict passes through; only a definite denial throws.

authzPredictorWithValidators(getValidator: (type: string) => JsonValidator | undefined): CapabilityPredictor;

A capability predictor over this client's authz facts but with a caller-supplied schema resolution — the seam a staging session uses to predict against schemas staged in it. See the client method's docs.

can(params: {
        kind: AccessKind;
        docType: string;
        docId?: string;
    }): boolean | null;

The client's authorization prediction for an operation; see DatadataClient.can.

command(params: {
        docId: string;
        type?: string;
        name: string;
        args: unknown;
    }): void;

Run a domain command on a document: its predicted effect is staged as an optimistic update and the doc:command sent for the server to run authoritatively. Throws when the target is not loaded or (with type) not of that docType, the command is not defined for its docType, the args fail its shape, or the command is predicted unauthorized.

commandAndWait(params: {
        docId: string;
        type?: string;
        name: string;
        args: unknown;
        eventId?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<CommandResult>;

ClientSessionBridge.command, resolving with the server's answer: a refusal rejects with a CommandRefusedError, an "exact" command's guard miss resolves committed: false.

createDocument(params: {
        docId: string;
        type: string;
        data: unknown;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }): void;

Stage an optimistic create and send it (buffered while offline). Throws when the create is refused locally: invalid data, a predicted denial, or a dynamic schema not yet resolved.

createDocumentAndWait(params: {
        docId: string;
        type: string;
        data: unknown;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
        eventId?: string;
        onUnsent?: () => void;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<CreateResult>;

ClientSessionBridge.createDocument, resolving with the server's answer: an existing document resolves created: false, any other failure rejects. Holds the create until its type's schema resolves; eventId pre-stamps the write's event id.

createReadOnlyMirror(params: {
        source: Y.Doc;
        docId: string;
        yjsId: string;
    }): {
        view: Y.Doc;
        detach: () => void;
    };

A read-only mirror of an arbitrary source Y.Doc, kept current one way (its edits never reach source). The caller owns view and must call detach() when done. docId and yjsId only label the warning logged when the view is written to.

deleteDocument(params: {
        docId: string;
        guard?: DeleteGuardMode;
    }): void;

Soft-delete a document optimistically. With guard: "sequence" the delete applies only if the document is still at the sequence this client holds.

deleteDocumentAndWait(params: {
        docId: string;
        guard?: DeleteGuardMode;
        eventId?: string;
        generation?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<DeleteResult>;

ClientSessionBridge.deleteDocument, resolving with the server's answer: a failed guard resolves deleted: false, any other failure rejects. generation names the document generation the delete targets.

dropDeferredYDoc(params: {
        yjsId: string;
        docId: string;
    }): void;

Drop a still-deferred (held) staged copy; a no-op for a doc that is not held.

readonly dynamicSchemas: boolean;

Whether the client resolves schemas dynamically. When false there are no sys:schema:* documents on the server, so a session's prepareDocument skips the schema lease.

flushYjsUpdates(params: {
        docId: string;
    }): void;

Send the document's pending Y.Doc updates now instead of after the auto-sync debounce.

forgetWriteOutcome(eventId: string): void;

Stop keeping the outcome ClientSessionBridge.rememberWriteOutcome asked for.

getAllDocumentEvents(params: {
        docId: string;
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<DocumentEventHistory>;

A staging session resolves a stage's BASE (the target's data at its baseSequence) by replaying the event log when no fresher source is resident — see StagingSession's base resolution. Same authoritative read as the public client method (online-only, read-gated).

getDeferredYDoc(params: {
        yjsId: string;
        docId: string;
        seed: Uint8Array;
        onFirstEdit: () => boolean;
    }): Y.Doc;

A Y.Doc seeded from seed and held from auto-sync until its first local edit, so it can be read without persisting anything. The first edit calls onFirstEdit once, synchronously, and releases the hold; returning false refuses the promotion and re-holds it. A Y.Doc that already exists for the pair is returned as-is, unseeded and unheld.

getDocument(params: {
        docId: string;
        type?: string;
        skipOptimistic?: boolean;
    }): DatadataDocument | null;

The client's untyped read of docId: its optimistic view (the synced copy with skipOptimistic), or null when the document is absent, deleted or not held locally.

getDocumentUnavailability(docId: string): DocumentUnavailableError | null;

Why the document is terminally unusable (null while usable or undecided) — the verdict prepareDocument turns into its settled rejection. See the client method's docs.

getHeldDocumentType(docId: string): string | undefined;

The docType the client holds for docId BEHIND its optimistic overlay — the synced copy, its own unconfirmed create, or the tombstone of a deleted document it held the copy of — or undefined when it holds none. A session's target-type resolution falls back to it where getDocument answers null though the type was never unknown to the client: a pending delete hides the document (an unconfirmed create under it has no synced base either), and a landed tombstone drops the copy outright.

getReadOnlyYDoc(params: {
        yjsId: string;
        docId: string;
    }): Y.Doc;

A read-only view of a document's sub-doc: it follows the live content, but its own edits never flush or reach the shared doc. The caller owns it and passes it to ClientSessionBridge.releaseReadOnlyYDoc when done.

getValidator(type: string): JsonValidator | undefined;

The validator for a document type (dynamic schemas included), or undefined when none.

getYDoc(params: {
        yjsId: string;
        docId: string;
    }): Y.Doc;

The shared, editable Y.Doc for a document's sub-doc. Local edits auto-sync after the client's Yjs debounce. It lives while the document is subscribed: the last release destroys it, and a later call mints a new one — sessions hand it out only through a PreparedDocument, under a lease.

isLandedWrite(params: {
        docId: string;
        eventId: string;
    }): boolean;

Whether the write under eventId — this client's or ANY other's — is in the confirmed copy of docId this client holds: its broadcast doc:patch was seen, and the copy has reached the sequence it committed at. False for a write whose frame this client never saw (a copy taken by a doc:init after it, or a frame older than the client's bounded memory of them).

isRenderedWrite(params: {
        docId: string;
        eventId: string;
    }): boolean;

Whether the write this client tracks under eventId is in its optimistic read of docId. A tracked update is not always: one whose patch no longer applies over the base (a value it replaces was overwritten by another author first) is skipped by the read until the server answers it. False for a write this client does not track.

isSubscribed(ref: {
        docId: string;
    }): boolean;

Whether any lease currently holds docId subscribed.

isTrackedWrite(eventId: string): boolean;

Whether a pending optimistic write is tracked under this event id.

mintEventId(): string;

Mint a fresh client event id, for pre-stamping a durable commit intent.

onDocumentChange(callback: DocumentChangeCallback): () => void;

Listen for changes to any document the client holds. Returns the unsubscribe function.

readonly principal: Principal | null;

The configured principal (null when none) — sessions stamp authorship from it.

purgeDocuments(params: {
        docIds: string[];
    }): void;

Hard-delete a batch of soft-deleted documents and release their ids — requires the docTypes' explicit access.purge rule (the default is nobody, admins included). All-or-nothing and irreversible; deliberately NOT optimistic and NOT offline-buffered: both methods throw when the client is not connected rather than queueing destruction for later.

purgeDocumentsAndWait(params: {
        docIds: string[];
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<PurgeResult>;

ClientSessionBridge.purgeDocuments, resolving once the server purged the whole batch; failures reject.

releaseReadOnlyYDoc(params: {
        yjsId: string;
        docId: string;
        doc: Y.Doc;
    }): void;

Tear down a view from ClientSessionBridge.getReadOnlyYDoc. Idempotent.

rememberWriteOutcome(eventId: string): void;

Keep the outcome of the write under this event id once it settles, for ClientSessionBridge.settledWriteOutcome, until ClientSessionBridge.forgetWriteOutcome. Call before issuing the write: an outcome that settled earlier is not recovered.

renameDocument(params: {
        docId: string;
        name: string | null;
    }): void;

Set (or, with null, clear) a document's index-level name optimistically.

renameDocumentAndWait(params: {
        docId: string;
        name: string | null;
        eventId?: string;
        generation?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<RenameResult>;

ClientSessionBridge.renameDocument, resolving once the server applied it; failures reject. generation names the document generation the rename targets.

reportWriteError(error: {
        docId: string;
        clientEventId: null;
        kind: "update";
        category: DocumentErrorCategory;
        message: string;
    }): void;

Report a write the session refused to send on the client's ambient write-error channel (onWriteError) — the documented destination for a FIRE-AND-FORGET write's failure, so a session can degrade instead of throwing into a caller (a cursor timer, a render effect) that has no reason to try/catch. The parameter is the client's WriteError narrowed to what a session mints; clientEventId is null because no event was ever minted.

restoreDocument(params: {
        docId: string;
    }): void;

Restore a soft-deleted document optimistically.

restoreDocumentAndWait(params: {
        docId: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<RestoreResult>;

ClientSessionBridge.restoreDocument, resolving once the server restored it; failures reject.

settledWriteOutcome(eventId: string): SettledWriteOutcome | null;

How a remembered write settled — applied when the server applied it, rejected when it settled any other way — or null while it is pending (or not remembered). Recorded as the write settles, ahead of the document notification the settling frame sends, so a listener can tell an ack from a rollback synchronously: after either, ClientSessionBridge.isTrackedWrite is false and the …AndWait promise settles a microtask later. An abort or timeout of the awaited promise records rejected while the write itself may still be pending; the server's eventual answer replaces it. A staging session reads it behind ClientSessionBridge.isTrackedWrite, which a pending write answers first.

subscribeToDocument(ref: {
        docId: string;
    }): DocumentSubscriptionLease;

Take a ref-counted subscription lease on docId, balanced by ClientSessionBridge.unsubscribeFromDocument. Throws once the client is destroyed.

unsubscribeFromDocument(ref: DocumentSubscriptionLease): void;

Release a lease taken by ClientSessionBridge.subscribeToDocument. The last release unsubscribes the document and drops its local state.

updateDocument(params: {
        docId: string;
        type?: string;
        callback: (doc: DatadataDocument, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
    }): void;

Run callback on a draft of the document and stage the diff as an optimistic update. Throws when the target is not loaded or the write is refused locally.

updateDocumentAndWait(params: {
        docId: string;
        type?: string;
        callback: (doc: DatadataDocument, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
        guard?: GuardMode;
        eventId?: string;
    }, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): Promise<UpdateResult>;

ClientSessionBridge.updateDocument, resolving with the server's answer. guard makes the write conditional on its base (a failed guard resolves committed: false); eventId pre-stamps the write's event id.

waitForAvailable(docId: string, options?: {
        signal?: AbortSignal;
    }): Promise<void>;

The client's availability wait; see DatadataClient.waitForAvailable.

waitForSchema(type: string, options?: {
        signal?: AbortSignal;
    }): Promise<SchemaResolution>;

Where type's schema resolves from, once that can no longer flip from "not synced yet". Rejects with a SchemaUnavailableError when the schema could not be read. The wait behind a session's waitForSchema and a prepared document's available.

waitForSettled(docId?: string, options?: {
        signal?: AbortSignal;
    }): Promise<void>;

The client's settle wait; see DatadataClient.waitForSettled.

waitForWrite(eventId: string, opts?: {
        signal?: AbortSignal;
        timeoutMs?: number | null;
    }): {
        kind: "create" | "update" | "delete" | "restore" | "rename";
        outcome: Promise<WriteResult>;
    } | null;

Attach an outcome awaiter to an already-tracked (e.g. rehydrated) write; null when no tracked write holds the event id. See the client method's docs.

CommitResult

export interface CommitResult

The outcome of a commit: which stages drained, which failed, and the new status.

committed: string[];

docIds whose stage drained (its write was acknowledged).

failed: {
        docId: string;
        error: Error;
        category?: DocumentErrorCategory;
    }[];

docIds whose stage did not commit — its staged work is left intact for retry. category carries the wire error category when the rejection had one (from the awaited write's WriteRejectedError, or the benign result), so review UIs can branch structurally — unauthorized in particular.

status: ChangesetStatus;

committed when every stage drained, else staging.

Conflict

export interface Conflict

A surfaced blocker on one stage. headSequence names the upstream sequence the clash was detected at. message carries human/AI-readable detail — for a schemaInvalid conflict, the validation error explaining WHY (a missing field, wrong type, …) so the resolver knows what to fix; absent for a physical conflict. migrationKey names the key a migrationKeyRefused conflict is about (the first the server's rule refuses), and message then carries the refusal the server would give. Conflicts are DERIVED (recomputed from the stage's base and stack vs live head), not stored. The behaviour-driving fields — reason and headSequence — converge: two clients computing them reach the same result, which is what makes automatic surfacing convergent. message is advisory only (no sync decision hangs on it) and may differ between clients — e.g. one validating against a staged schema and another against the live registry — so it is deliberately excluded from that convergence guarantee. (session.allium: entity Conflict.)

headSequence: number;

The live head sequence the conflict was detected at.

message?: string;

Readable detail: the validation error for schemaInvalid, the server's refusal for migrationKeyRefused. Advisory, and may differ between clients.

migrationKey?: string;

For migrationKeyRefused, the first staged migration key the server would refuse.

reason: ConflictReason;

Why the stage is blocked; see ConflictReason.

ConflictPreview

export interface ConflictPreview

A 3-way preview for resolving a conflict: the base the stack was authored against, ours (base with the stack applied), and theirs (live head). Derived, not stored. (session.allium: Conflict's derived base/ours/theirs preview.)

base: unknown;

The target's data at the stage's base sequence, the state the stack was authored against.

ours: unknown;

base with the stage's staged changes applied: what this session intends.

theirs: unknown;

The target's current live head data.

DocumentStage

export interface DocumentStage

One target document's staged work — the per-document unit of staging, conflict and commit. baseSequence is the source sequence the stack was first opened against, conflict its current blocker (or null). A stage holds a PATCH lane and a YJS lane, each a record set keyed by change id and ordered by fractional index; the accessor returns them already ordered. (session.allium: entity DocumentStage.)

readonly baseSequence: number;

The target's sequence when the stage was opened: the base its changes were authored against.

readonly conflict: Conflict | null;

The stage's current conflict, or null when it is not blocked.

readonly docId: string;

The target document's id.

readonly isCreate: boolean;

True iff this stage CREATES the document (it does not exist live yet).

readonly isDelete: boolean;

True iff this stage DELETES the document (it commits as a soft delete).

readonly stagedChanges: readonly StagedChange[];

The staged JSON changes, in commit order.

readonly stagedYjsCopies: readonly StagedYjsCopy[];

The staged Yjs copies, one per touched target Y.Doc.

readonly type: string;

The staged document's docType (the create type, or the live document's type).

PreparedDocument

export interface PreparedDocument

The handle Session.prepareDocument returns — the session-level analogue of the client's DocumentSubscriptionLease. Holding it keeps the document subscribed under the session; release() balances exactly this acquisition. Leases ref-count, so the underlying subscription survives while any other holder (a sibling handle, the consumer's own lease, another session) remains, and only the last release tears the document down.

[Symbol.asyncDispose](): Promise<void>;

Symbol.asyncDispose — the drain-then-release scope exit: await using doc = session.prepareDocument(id) runs PreparedDocument.releaseWhenSettled when the block exits, so a scope that WROTE to the document gets the safe teardown by syntax alone (using gets the bare release). Bounded: a document that never settles must not hang the scope exit, so the drain is capped (30s) and falls back to the bare PreparedDocument.release — whose warning reports any dropped tracking. Never throws (a throwing async disposer would surface as a SuppressedError clobbering the scope's own outcome).

[Symbol.dispose](): void;

Symbol.dispose alias for PreparedDocument.release, so the handle can be scoped with using: using doc = session.prepareDocument(id) releases the lease when the block exits, removing the manual release-loop boilerplate (and the leak-on-missed-release footgun).

readonly available: Promise<void>;

Resolves once the prepared document is AVAILABLE — readable through the client, whether server-confirmed or hydrated from the persisted offline cache — and (unless includeSchema was false) its schema resolution is decided (Session.waitForSchema): the type's sys:schema:<type> document is readable too, or the client knows there is none and getValidator falls back to the static registry (immediate on a static-schema client). Availability is the EDITING precondition (updateDocument, staging, and an explicit getValidator(type) validation gate all rely on it), so offline-capable flows await THIS before editing: on an offline boot a cached document — and its cache-pinned schema — becomes available while PreparedDocument.settled stays pending. Only a COLD offline boot on a dynamic-schema client leaves the schema leg pending (there is no index to consult); pass includeSchema: false when byte-readability alone is enough there. This asserts the DOCUMENT is readable, which a wait for the schema alone does not: to wait for a type's schema whether or not a document of it exists (before a create, or a create-or-update), await Session.waitForSchema. Rejects with a DocumentUnavailableError when the DOCUMENT is terminally unreadable (notFound / error / deleted — the error carries the server's own doc:error category, so a denial is diagnosable at the await point), with a SchemaUnavailableError when its schema could not be READ (a missing schema document is the static-fallback outcome, not a rejection), or with the abort reason when the prepare's signal aborts; a caller that never consumes it is safe (the rejection is pre-handled, so it cannot surface as an unhandled rejection).

readonly docId: string;

The document id this handle prepared.

getReadOnlyYDoc(yjsId: string): Y.Doc;

A READ-ONLY Y.Doc for one of this document's sub-docs — the read counterpart of PreparedDocument.getYDoc. It mirrors the collaborative content but its edits never flush, promote/stage, or reach the shared live doc / another session. Gated on read (not update): use it to VIEW collaborative content from a session that may read the target but not update it. Same lifetime as PreparedDocument.getYDoc: valid while a lease on the document is held, and throws once this handle is released. In a staging session it mirrors the staged copy, so it also ends with that copy: once the copy is dropped or reaped the next call returns a fresh view over the re-forked copy, and the old view is destroyed.

getYDoc(yjsId: string): Y.Doc;

The collaborative Y.Doc for one of this document's sub-docs (its yjsRef id). A live session returns the LIVE target doc, whose local edits auto-sync to every viewer; a staging session returns a forked staged copy. Gated on update (an editable Y.Doc is a write capability) — a read-only session uses PreparedDocument.getReadOnlyYDoc instead.

The doc is only good while a lease on the document is held: when the LAST lease is released the client destroys it, and a later prepare mints a NEW Y.Doc for the same id. Taking it from the handle ties it to a lease you hold, so drop the doc when you release the handle (a React hook should prepare and read it in an effect or a store subscription, never during render). Throws once this handle is released or its session disposed. Editing a destroyed doc logs a warning, since the edit never syncs. Cheap to call repeatedly; returns the same doc while the document stays subscribed.

A staging session's copy has a second, shorter lifetime: the STAGE's. Its own commit or discard drops the copy, and a concurrent session committing or discarding the same target reaps the shared copy, destroying the instance an editor bound — holding the handle does not prevent that. The next call forks a fresh copy (so it is a NEW doc, not the same one), and never returns the destroyed one; a holder rebinds on the session's change notification.

release(): void;

Drop this acquisition's lease (and, in a live session, the doc's awareness bridges once the last holder releases). Idempotent, and safe to call while PreparedDocument.settled is still in flight — a release mid-settle also cancels taking the schema lease.

NOTE: a bare release() on the LAST lease tears the document's local state down at once. A debounced Y.Doc edit that hasn't synced yet is flushed first, but an in-flight optimistic write (a doc:update/create/delete whose server ack hasn't arrived) loses its local tracking — its outcome and any rollback go unobserved. A scope that wrote to the document and then stops caring about it should prefer PreparedDocument.releaseWhenSettled.

releaseWhenSettled(opts?: {
        signal?: AbortSignal;
    }): Promise<void>;

Await this document's pending optimistic writes to drain (the same settle Session.prepareDocument's settled and the client's waitForSettled report), then PreparedDocument.release. This is the safe "I wrote to this doc and now I'm done with it" teardown: it closes the window where a bare PreparedDocument.release drops still-in-flight writes.

Pass signal for a BOUNDED wait — without one, a document that never settles leaves the promise pending forever. On abort/reject the lease is NOT dropped (so the caller can retry or fall back to a bare release()); only a successful settle releases it, exactly once. Idempotent: a no-op once released.

readonly settled: Promise<void>;

Resolves once the prepared document (and, unless includeSchema was false, its schema document) has synced — the server has answered and no writes are pending. Await this when you need CONFIRMED live head (e.g. before pinning a base you don't want stale). NOTE: on an offline boot this honestly stays pending until the server answers — for "may I edit yet?", await PreparedDocument.available instead.

Rejects if the prepare's signal aborts the bounded settle, or with a DocumentUnavailableError when a SUBSCRIBE ITSELF was refused — answered doc:error (an undeclared presence channel, a read-scope denial, a read failure), which can drop the registration server-side and leaves this handle holding a lease over nothing. A caller that reads settle-resolution as "safe to use now" is exactly the caller that must hear about that, so it is a rejection rather than a quiet resolve. This covers BOTH legs: a refused schema subscribe fails the prepare too, since settled claims the schema synced and a caller that did not pass includeSchema: false asked for it.

A notFound or deleted document still RESOLVES: those are answers about a document whose subscription stands (a later create is delivered on it), so "prepare, then check getDocument(...) === null" remains a valid existence probe. An ABSENT schema document resolves for the same reason — that is the static-registry fallback, not a failure to reach anything. And the DRAIN waits keep resolving on any terminal status, error included: a permanently errored document must not wedge a folder-wide waitForSettled(), nor strand its lease in PreparedDocument.releaseWhenSettled.

PreparedDocuments

export interface PreparedDocuments

The handle Session.prepareDocuments returns — a single batch handle over several PreparedDocument acquisitions. release() (and Symbol.dispose) balances every one of them at once, and settled resolves only when all have settled, so a "keep these N docs prepared for a scope, then release all" site needs neither a handle array nor a release loop.

[Symbol.asyncDispose](): Promise<void>;

Symbol.asyncDispose — drain every document then release each (PreparedDocuments.releaseWhenSettled) on await using scope exit, with the same bounded-drain / bare-release fallback contract as the per-document handle.

[Symbol.dispose](): void;

Symbol.dispose alias for PreparedDocuments.release — enables using docs = session.prepareDocuments(ids).

readonly available: Promise<void>;

Resolves once every document in the batch is available (PreparedDocument.available — readable, cache-hydrated included, schema resolution settled; the editing precondition). Same Promise.all semantics and no-auto-release contract as PreparedDocuments.settled; pre-handled like the per-document promise.

release(): void;

Balance every acquisition in the batch. Idempotent; safe mid-settle.

releaseWhenSettled(opts?: {
        signal?: AbortSignal;
    }): Promise<void>;

Await every document's pending writes to drain (PreparedDocument.releaseWhenSettled), then release each. Same Promise.all semantics as PreparedDocuments.settled: a rejection releases the documents that did settle and leaves the rest held (recover with a bare PreparedDocuments.release).

readonly settled: Promise<void>;

Resolves once every document in the batch has settled — same semantics as Promise.all over the individual PreparedDocument.settled, so it rejects if any one's bounded settle aborts or any one's subscribe was refused (the first such rejection wins). A rejection does NOT auto-release: leases already taken stay held until release() runs, so consume this with using (or a try/finally that calls release()).

PresenceViewData

export interface PresenceViewData

The synthesized sys:presence-view:<presenceType>:<docId> data: this session's lens over the aggregate.

peers: RemotePresenceEntry[];

Every OTHER cell, stale-flagged from the liveness overlay; grace-expired ones hidden.

self: {
        subject: string | null;
        state: Record<string, JsonValue>;
    } | null;

This session's own cell ({ subject, state }), or null when it has published none.

Session

export interface Session<Registry extends ValidatorRegistry = ValidatorRegistry>

The document-editing surface shared by a live editing session and a staging StagingSession. Everything here is mode-agnostic: a live session writes straight to the document, a staging session stages the same calls into a changeset. Staging-only operations (commit/discard/stages/conflicts) live on StagingSession, not here — narrow on Session.kind to reach them.

[Symbol.dispose](): void;

Symbol.dispose alias for Session.dispose, so an owned session can be scoped with using session = client.createLiveSession() / using staging = client.openStagingSession(...).

can(params: {
        kind: AccessKind;
        docId: string;
    }): boolean | null;

Predict whether an operation of kind on the EXISTING document docId would be permitted — the capability query for a document you hold only by id (a delete/rename/restore target). It resolves the document's docType the same way those ops do (the subscribed doc, else the sys:index / sys:trash listing) and runs the client's docType-keyed prediction, so a UI can gray out a delete/rename button without knowing the type. boolean when the type resolves and the authz facts are synced; null when the verdict is unknown — the type could not be resolved (subscribe the doc or its listing) or the facts have not synced. A false predicts the same local rejection the write would raise; the server stays authoritative. For create, whose type is not carried by an existing document, ask the client's can({ kind: "create", docType }).

createDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        data: TypedDocumentData<Registry, Type>;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }): void;

Create a document (optionally with an initial index name). yjsCallback seeds the document's embedded Y.Docs at creation — for a live session the new doc's initial Yjs state; for a staging session the staged copies, which merge in on commit.

createDocument(params: {
        docId: string;
        type: string;
        data: unknown;
        yjsCallback?: (yjsDocs: YjsDocsAccessor) => void;
        name?: string;
    }): void;

Untyped create: data is validated at runtime against the schema of type.

deleteDocument(params: {
        docId: string;
    }): void;

Delete a document. A live session soft-deletes it immediately; a staging session stages the deletion — the document reads as null through the session and drops from the overlaid index, committing as a soft delete (and deleting a not-yet-committed staged create cancels it). A live session also accepts a CAS guard (narrow to isStagingSession to know the kind); restoring a deleted document is live-only — reach it via a LiveSession.

dispose(): void;

Release this session's resources — its held subscriptions and awareness bridges. For a staging session this does not touch the persisted changeset; for a live session there is none.

getCapabilities(params: {
        docId: string;
    }): AccessCapabilities;

The per-kind Session.can verdicts for docId, in one call (all null when the type is unresolvable).

getDocument(params: {
        docId: typeof CLIENT_DOCUMENTS_STATUS_DOCUMENT_ID;
        type?: typeof CLIENT_DOCUMENTS_STATUS_DOCUMENT_TYPE;
        skipOptimistic?: boolean;
    }): DatadataDocument<ClientDocumentsStatusDocument>;

Read a document's current value (overlaid in a staging session; live head here). Typed reads: pass a docType type with a typed id, OR pass a branded id alone (its brand identifies the docType). A plain-string docId reads unknown. This overload stack is the authoritative typed read surface — the client's own getDocument is an untyped internal.

getDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        skipOptimistic?: boolean;
    }): DatadataDocument<TypedDocumentData<Registry, Type>> | null;

Typed read: type names the docType and docId is a TypedDocumentId of it, so data is typed. Null when the document is absent, deleted, or outside the session's scopes.

getDocument<Id extends BrandedDocumentId<Registry>>(params: {
        docId: Id;
        type?: undefined;
        skipOptimistic?: boolean;
    }): DatadataDocument<TypedDocumentData<Registry, DocumentTypeForId<Registry, Id>>> | null;

Branded read: a BrandedDocumentId identifies its own docType, so the result is typed from the id alone, with no type. Null like the typed read.

getDocument(params: {
        docId: string;
        type?: undefined;
        skipOptimistic?: boolean;
    }): DatadataDocument | null;

Untyped read by a plain-string id: data is unknown. Null like the typed read. A call that passes type must use the typed overload, with an id of that type.

getValidator(type: string): JsonValidator | undefined;

Resolve the JsonValidator for a document type (dynamic schemas included), or undefined when no schema is found.

getYjsAwareness(params: {
        docId: string;
        yjsId: string;
        presenceType: string;
    }): Awareness;

A y-protocols Awareness for a Y.Doc, so an edit cursor broadcasts. presenceType names the presence CHANNEL the cursors ride (its presence:<presenceType> schema must be declared, and every participant must use the same name to see each other — the channel is app convention, like a deterministic docId). A live session bridges over sys:presence:<presenceType>:<docId> (cursors render for live viewers on that channel); a staging session bridges over the host-scoped staged channel.

readonly kind: "live" | "staging";

Discriminates a live editing session ("live") from a staging StagingSession ("staging"). Switch on it to conditionally render staging UI: a live session has no commit/conflict surface to drive.

onDocumentChange(listener: (docId: string, document: DatadataDocument | null) => void): () => void;

Subscribe to per-document changes — the callback gets (docId, document) where document is the current view (or null). Returns an unsubscribe function.

prepareDocument(docId: string, opts?: {
        signal?: AbortSignal;
        includeSchema?: boolean;
    }): PreparedDocument;

Make a document id available to this session: subscribe it (and, unless includeSchema is false, its sys:schema:<type> document for a custom type) and hold the lease under this session. The schema lease is taken only when the client resolves schemas dynamically (createClient({ dynamicSchemas: true })); a static-schema client has no sys:schema:* documents to subscribe, so includeSchema is a no-op for it regardless of its value. If you also prepare the schema document yourself (e.g. prepareDocument(schemaDocId(type))), pass includeSchema: false here — otherwise this call resolves-and-leases the same sys:schema:<type> doc a second time. Ref-counting keeps that correct (each lease still needs its own balancing release()), but it is a redundant resolve+lease worth avoiding. Returns SYNCHRONOUSLY — the lease is taken at once, the session-level analogue of the client's subscribeToDocument — so the returned handle's release() is valid immediately, even while its settled is still in flight. Await available for the editing precondition updateDocument requires (readable — cache-hydrated counts, so it resolves on an offline boot — and, unless includeSchema is false, schema resolution settled, so an explicit getValidator(type) gate sees the dynamic schema rather than silently missing it); await settled when you need server-confirmed truth. The id need NOT already resolve to a document. What you take out is tenure over the ID — preparing an absent one is legitimate, and is both the normal create path (prepareDocument(id), then createDocument({ docId: id, ... })) and how you watch for a document another client will create: the subscription stands, so a later create — or a restore — is delivered on it. Existence is orthogonal to the lease, and the two promises (not this call) report it: settled RESOLVES for a notFound or deleted document, since that is an ANSWER about a standing subscription — which is what makes "prepare, then check getDocument(...) === null" a valid existence probe — while available REJECTS with a DocumentUnavailableError, there being nothing readable to edit. Ref-counted: preparing the same doc again returns a fresh handle holding its own balancing release, and the subscription drops only when every handle (and any consumer lease) is released or the session is disposed. release() is the per-document antonym of prepare and the targeted counterpart to Session.dispose (in a live session it also tears down the doc's awareness bridges once the last holder releases). Honours signal for the bounded settle. Does not touch a staging session's changeset — staged work survives a release until commit/discard.

prepareDocuments(ids: string[], opts?: {
        signal?: AbortSignal;
        includeSchema?: boolean;
    }): PreparedDocuments;

Batch Session.prepareDocument over several ids with the same opts, returning one PreparedDocuments handle whose release() (and Symbol.dispose) balances all of them and whose settled resolves once every one has settled. Use it for the common "keep these N docs prepared for a scope's lifetime, then release all" shape so a call site needs neither a handle array nor a release loop — using docs = session.prepareDocuments(ids) releases them when the block exits.

readonly presenceId: string;

The presence id this session publishes its own cell under (one session = one participant/cursor). Minted per session (config-overridable); the server stamps subject on top, so several sessions over one client group under one subject.

renameDocument(params: {
        docId: string;
        name: string | null;
    }): void;

Set a document's index-level name (see sys:index) — null clears it. A live session renames immediately; a staging session stages the rename (it folds into a staged create, or commits as a doc:rename). Document content is untouched either way.

updateDocument<Type extends keyof Registry & string>(params: {
        docId: TypedDocumentId<Registry, Type>;
        type: Type;
        callback: (doc: DatadataDocument<TypedDocumentMutable<Registry, Type>>, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
    }): void;

Mutate a document in place; the diff of the callback's before/after is the write. A live session sends it immediately; a staging session stages it. With type, docId is branded to that docType and the callback's doc.data is typed; without it the document is untyped.

updateDocument(params: {
        docId: string;
        type?: undefined;
        callback: (doc: DatadataDocument, yjsDocs: YjsDocsAccessor, ops: DocumentUpdateOps) => void;
    }): void;

Untyped update by a plain-string id: the callback's doc.data is unknown.

waitForSchema(type: string, opts?: {
        signal?: AbortSignal;
    }): Promise<SchemaResolution>;

Wait until the schema of type is resolved, and learn where it resolves from. This asks about the TYPE only — no document of the type need exist — so it is the wait before a plain createDocument on a dynamic-schema client (createClient({ dynamicSchemas: true })), which refuses the write until then. createDocumentAndWait / updateDocumentAndWait wait on their own, and a prepared document's available includes this wait for the document's type.

Resolves once the answer can no longer flip from "not synced yet": the type's sys:schema:<type> document is readable (dynamic), or the client knows there is none (static, or none when the static registry has no entry either). A staging session answers staged for a type whose schema document it has staged. Immediate on a static-schema client and for sys: types. See SchemaResolution.

Readiness is not server confirmation: a schema hydrated from the offline cache resolves (confirmed: false), so an offline boot that has the schema cached can author. Only a COLD offline boot on a dynamic-schema client stays pending — there is no index to consult — so pass signal for a bounded wait.

A write issued when this resolves, with anything but none, is not refused for its schema being unresolved; its data is still validated against that schema. The answer is of that moment. A type can become unresolved again later: when a schema document is created for a type that had none, until it has synced, and across a reconnect for a listed schema document that was not found. Wait again before a later plain write, or use the awaited writes, which hold through it.

Rejects with the abort reason when signal aborts, with a SchemaUnavailableError when sys:index or the type's schema document could not be read, or when the client is destroyed before the answer. An absent schema is not a failure: that is the static fallback.

SessionDocumentData

export interface SessionDocumentData

The data of the synthesized sys:session document: the changeset-level summary plus a per-target-document entry. Derived on read from the changeset (never separately persisted), exposed through the StagingSession's ordinary getDocument/onDocumentChange surface so a document-driven consumer discovers staged work without a bespoke channel.

When session scopes or the connection principal's read visibility hide some target documents, stages contains only the visible stage entries, but the top-level aggregate fields still describe the whole changeset. The hidden* fields tell a UI that commit-affecting work exists without leaking those target documents' ids, types, staged data, or conflicts.

hasHiddenBlockedStages: boolean;

Whether at least one redacted stage is blocked.

hasHiddenDirtyStages: boolean;

Whether at least one redacted stage is dirty.

hiddenStageCount: number;

Count of stage entries redacted from stages by read visibility.

isBlocked: boolean;

Whether any visible or hidden stage is blocked.

isDirty: boolean;

Whether any visible or hidden stage is dirty.

stages: Record<string, SessionStageProjection>;

Per-stage summaries for targets readable through this session.

SessionStageChange

export interface SessionStageChange

One staged JSON-patch edit, as projected into the sys:stage:<docId> detail document. id is the stable handle (the same id dropStagedChange/amendStagedChange take), and the changes array is already ordered by fractional index — so position conveys order and the internal index key is deliberately not exposed (a read-only surface offers no reorder; that stays a typed command on the changeset).

id: string;

The change's stable id; pass it to drop or amend the change.

patch: Operation[];

The change's RFC 6902 patch.

SessionStageDocumentData

export interface SessionStageDocumentData extends SessionStageProjection

The data of a synthesized sys:stage:<docId> document: the per-stage SUMMARY (the same fields as the matching sys:session entry) plus the DETAIL the summary omits — the base sequence and the ordered patch/yjs lanes. Derived on read from the changeset (never separately persisted), exposed through the StagingSession's ordinary getDocument/onDocumentChange surface so a document-driven consumer can inspect one document's staged changes without the imperative session.stages getter. A target with no staged work reads as null (no document), like any absent document.

baseSequence: number;

The target's sequence when the stage was opened: the base its changes were authored against.

changes: SessionStageChange[];

The staged JSON changes, in commit order.

yjsCopies: SessionStageYjsCopy[];

The staged Yjs copies, one per touched target Y.Doc.

SessionStageProjection

export interface SessionStageProjection

One stage's entry in the synthesized sys:session document — the staged-work summary for a single target document. (session.allium: the sys:session projection.)

authorized: boolean | null;

Whether the committer's principal is predicted to be allowed to commit this stage (each staged kind: create, update, delete, and rename when a name is staged); null when unknown (no principal or unsynced facts). Commit's write boundary stays the authority. A target that becomes unreadable mid-session disappears from the projection instead of flipping this.

baseError: string | null;

Why the stage's base could not be resolved (offline, or the target soft-deleted), or null while it is resident or still resolving. While set, the overlaid read serves live head, schemaError stays null and there is no conflict preview. Advisory and self-healing: a later read retries after a backoff, and nothing is blocked by it. Only reported while the stage has a staged JSON change.

conflict: Conflict | null;

The stage's current conflict, or null when it is not blocked.

docId: string;

The target document's id.

isBlocked: boolean;

Whether the stage has a conflict, which blocks committing it.

isCreate: boolean;

Whether the stage creates the document, which does not exist live yet.

isDelete: boolean;

Whether the stage deletes the document; it commits as a soft delete.

isDirty: boolean;

Whether the stage holds work a commit would write.

isRenamed: boolean;

Whether a name was staged this session, so a null name means it was cleared.

name: string | null;

The staged index-level name (a create's initial name or a rename), or null when none is staged or a name was staged and then cleared; SessionStageProjection.isRenamed tells the two apart.

schemaError: string | null;

The validation error of the staged intent (base plus staged changes), or null when it conforms or there are no JSON changes. Reported even while a physical conflict also blocks the stage. Null does not promise a clean commit: commit validates against live head, which a schemaInvalid conflict reports.

type: string;

The target's docType (the create type, or the live document's type).

SessionStageYjsCopy

export interface SessionStageYjsCopy

One staged Yjs copy, as projected into the sys:stage:<docId> detail document. The copy's content is NOT projected — it is a live collaborative Y.Doc reached via PreparedDocument.getYDoc; the target Y.Doc id and the host copy id are.

copyRef: string;

The host Y.Doc id holding the forked copy.

yjsId: string;

The id of the target Y.Doc the copy was forked from.

StagedChange

export interface StagedChange

One staged edit to one target document — the JSON-patch lane. patch is the RFC-6902 diff, computed with test ops for guarded replace/remove — these guards are what the conflict gate executes when folding the stack onto live head, and what rebaseStage rewrites to re-assert the stack over an advanced head. id is the stable handle (pass it to drop/amend); index is the fractional index the lane is ordered by — the changeset is a RECORD SET keyed by id, not an array, so concurrent appends merge convergently when the changeset rides host-doc sync. (session.allium: entity StagedChange.)

id: string;

The change's stable id; pass it to drop or amend the change.

index: string;

The fractional index the stage's changes are ordered by.

patch: Operation[];

The RFC 6902 patch, with test operations guarding each replace and remove.

StagedYjsCopy

export interface StagedYjsCopy

One staged Yjs copy for one target Y.Doc. The session forks the target into a host sub-document (copyRef) that editors collaborate on natively — live sharing, cursors, offline two-way editing and state-vector catch-up all ride the host document's ordinary Yjs sync — and merges it back into the live target at commit. The bindable copy is obtained via PreparedDocument.getYDoc; this projection records which targets have a staged copy and the host id holding it. (session.allium: entity StagedYjsCopy.)

copyRef: string;

The host Y.Doc id of the forked copy (a yjsRef on the changeset).

yjsId: string;

The id of the target Y.Doc the copy was forked from.

StagingSessionParams

export interface StagingSessionParams

Options for a StagingSession. An app passes them, without client, to DatadataClient.openStagingSession.

client: ClientSessionBridge;

The live client the session decorates — the single source of server truth.

field?: string;

The host-document field the structured changeset lives in. Defaults to "session"; the app's host schema must declare it with SESSION_CHANGESET_FIELD, whose defaults give every host document an empty changeset — a session refuses to stage into a host without one. Ignored for an in-memory session.

generateId?: () => string;

Mints the stable id (record-set key) for each staged change / yjs update. Defaults to crypto.randomUUID(); apps that standardise on nanoid pass () => nanoid(). Must return a fresh, collision-free string per call.

hostDocId?: string;

The host document the changeset is persisted in. When given, staged work is serialized into field on this document and rides ordinary doc sync (durable: survives reload, reaches every device, replays on a fresh client). The document must be available — the StagingSession subscribes it itself. OMIT this for a purely in-memory (non-persisted) session — zero setup, no host schema, no per-stage write — for ephemeral or test usage.

The changeset persistence rides the CLIENT's capabilities, so hosting requires the principal to hold live write access to this document — a grant the session facade does NOT scope back down by itself. When the session's editor must not touch the host directly (an agent staging into the conversation that drives it), pass scopes excluding the host's docType: scopes: [{ excludeDocTypes: [hostType] }] hides host documents from the session's reads/listings and rejects session-routed writes targeting them, while the internal changeset writes keep riding the client.

onUpstreamAdvance?: UpstreamAdvancePolicy;

What happens when a staged doc advances upstream, for an IN-MEMORY session. Defaults to autoMerge. A hosted session takes the policy its host's changeset was created with (emptySessionChangeset({ onUpstreamAdvance })), so every session on the host agrees on it; passing this with hostDocId throws.

presenceId?: string;

The presence id this session publishes its own cell under (one session = one participant/cursor). Defaults to a fresh crypto.randomUUID(); override for deterministic ids (tests) or to correlate presence across systems. Follows the user-id grammar (as a docId does) and throws otherwise: the id is the cell's key in the presence document.

presencePublishDebounceMs?: number;

Debounce for publishing staged-awareness (getYjsAwareness) cursor states onto host-document presence, forwarded to the awareness bridge. Default 50.

scopes?: readonly AccessScope[];

Advisory session-level scope attenuation. When set, a staged write outside these scopes is rejected LOCALLY (a WriteRejectedError, before it enters the changeset) — the same client-side guardrail the client's own principal-scope gate applies. It INTERSECTS the client principal's scopes (both gates run), so a session can only narrow what its client may write, never widen it. undefined = no session attenuation; [] denies every staged write. Enforcement remains server-side against the connection principal, so this is a cooperative guardrail, not a security boundary. Reads through the session facade are also attenuated: ordinary documents outside the session's read scope read as absent, and sys:index / sys:trash / staged-work projections are filtered to the documents still readable through the session. excludeDocTypes (the scope's deny axis) is how an app hides the HOST docType from its own hosted session — see StagingSessionParams.hostDocId. CONSUMER TRAP: the filtering is a DROP, not a redaction — resolving a docId's type through the session's sys:index / sys:trash answers undefined for an out-of-scope document, indistinguishable from "not created yet". Correct for non-disclosure, but an app-level gate keyed on that resolution degrades silently — resolve through the CLIENT (whose index this advisory attenuation does not touch) when the gate needs the truth about excluded documents. See AccessScope. (session.allium: OpenStagingSession — session-scope attenuation.)

subscribeStaged?: boolean | {
        includeSchemas?: boolean;
    };

Keep the session's staged documents subscribed for as long as they are staged (and, with { includeSchemas: true }, their sys:schema:<type> documents too). OFF by default — a session only subscribes its host document. Turn it ON for a client that OBSERVES a session it did not author (the browser showing a server-side session, or a resumed device): the staged targets must be subscribed for the sys:session projection to surface physical conflicts and (for custom types) schema validity, and to read a staged update's overlaid content. It is an ongoing mode — documents are subscribed as they enter the changeset (via sync or local staging) and released as they leave (commit/discard) or on dispose — and the leases ref-count, so it never disturbs the consumer's own subscriptions. It does NOT change the staging precondition: editing a document still requires it subscribed-and-synced first.

SILENT SYMPTOM if you forget it on an observer: the changeset itself rides host-doc sync, so the stages still LIST (docId/type/isDirty) — but with the targets unsubscribed the projection reports conflict: null for every stage and the overlaid reads come back empty. It does not error; it reads as "everything is clean, no conflicts ever." This is a DISPLAY gap, not a safety hole: commit() subscribes its targets, syncs heads and re-gates conflicts before draining (CommitSubscribesItsTargets), so an observer that commits without this still never blindly applies a conflicted stage.

Types

ChangesetStatus

export type ChangesetStatus = "staging" | "committed" | "discarded";

A commit BATCH's outcome (CommitResult.status): committed when every stage drained, else staging. Never persisted — auto-clear-on-drain keeps the stored changeset permanently in staging shape, so the terminal values exist only as this per-batch report. discarded is unused by the shipped client and kept for the spec's vocabulary. (session.allium: enum ChangesetStatus.)

ConflictReason

export type ConflictReason = "physicalConflict" | "schemaInvalid" | "upstreamAdvance" | "targetReplaced" | "migrationKeyRefused";

Why a stage is blocked. physicalConflict: the guarded stack no longer folds onto live head. schemaInvalid: it applies cleanly but the merged result fails validation (shape + intra-document references). upstreamAdvance: nothing clashes — the stack folds and validates — but the stage's document advanced upstream and the session's policy is block, so the advance is surfaced for review. The gates are orthogonal — a stage is committable only when it both applies and validates — and a real overlap outranks a bare advance: under block, a stage whose stack no longer folds reports physicalConflict, not upstreamAdvance. targetReplaced: the stage was opened against a document that no longer exists — a purge released its id and a create reused it, so live head is ANOTHER generation (DatadataDocument.generation). It outranks every other reason and applies to every non-create stage, content or not: nothing staged belongs to the new document, so the only resolution is dropping the stage. migrationKeyRefused: a schema document's stage folds and validates, but the migration log it would commit does not extend the live one — a staged key sorts at or before a key committed meanwhile, or is a key another author took for a different migration — so the server would refuse the write (schema/migration-log.ts). Both are read off the live log every client holds. Resolved by giving the staged migration another key (rekeyStagedMigration). It outranks a bare upstreamAdvance, which the same upstream commit trips under block: the refusal is a defect and names its fix. (session.allium: enum ConflictReason, ConflictDetection.SchemaGateIsSeparate.)

LiveSessionWithoutCommands

export type LiveSessionWithoutCommands<Registry extends ValidatorRegistry = ValidatorRegistry> = Omit<LiveSession<Registry>, "command" | "commandAndWait">;

A LiveSession without its command methods: what the schema-agnostic DatadataClient holder's live sessions are. A holder that has erased the client's command map has nothing to check a command's name or args against, so rather than accept any it sends none.

SessionChangeset

export type SessionChangeset = InferField<typeof SESSION_CHANGESET_FIELD>;

The inferred shape of the persisted changeset, as described by SESSION_CHANGESET_FIELD.

UpstreamAdvancePolicy

export type UpstreamAdvancePolicy = "autoMerge" | "block";

What the session does when a doc it has staged edits for advances upstream. - autoMerge (default): absorb an upstream advance that the guarded stack still folds onto; surface a failed fold as a physicalConflict. - block: surface whenever a staged document's JSON lane advanced upstream at all, even if the stack still folds cleanly, as an upstreamAdvance.

Named for the trigger, not the detection: an "advance" is a JSON-lane sequence advance, so another author's Yjs-only deltas — which never move sequence — are outside BOTH policies by construction, and the rich-text lane stays conflict-exempt. (session.allium: enum UpstreamAdvancePolicy.)

Variables

DEFAULT_SESSION_FIELD

DEFAULT_SESSION_FIELD = "session"

The default host-document field the changeset is stored in.

SESSION_CHANGESET_FIELD

SESSION_CHANGESET_FIELD: {
    readonly type: "object";
    readonly default: {
        readonly onUpstreamAdvance: "autoMerge";
        readonly stages: {};
        readonly changes: {};
        readonly yjsCopies: {};
        readonly commitIntents: {};
    };
    readonly fields: {
        readonly onUpstreamAdvance: {
            readonly type: "enum";
            readonly values: readonly ["autoMerge", "block"];
        };
        readonly stages: {
            readonly type: "record";
            readonly default: {};
            readonly values: {
                readonly type: "object";
                readonly fields: {
                    readonly baseSequence: {
                        readonly type: "number";
                        readonly int: true;
                    };
                    readonly generation: {
                        readonly type: "string";
                        readonly optional: true;
                    };
                    readonly schemaSequence: {
                        readonly type: "number";
                        readonly int: true;
                        readonly optional: true;
                    };
                    readonly type: {
                        readonly type: "string";
                    };
                    readonly kind: {
                        readonly type: "enum";
                        readonly values: readonly ["create", "delete"];
                        readonly optional: true;
                    };
                    readonly rename: {
                        readonly type: "object";
                        readonly optional: true;
                        readonly fields: {
                            readonly name: {
                                readonly type: "string";
                                readonly nullable: true;
                            };
                        };
                    };
                };
            };
        };
        readonly changes: {
            readonly type: "record";
            readonly default: {};
            readonly values: {
                readonly type: "object";
                readonly fields: {
                    readonly docId: {
                        readonly type: "string";
                    };
                    readonly index: {
                        readonly type: "string";
                        readonly format: "fractionalIndex";
                    };
                    readonly patch: {
                        readonly type: "array";
                        readonly items: {
                            readonly type: "union";
                            readonly discriminator: "op";
                            readonly variants: {
                                readonly add: {
                                    readonly path: {
                                        readonly type: "string";
                                    };
                                    readonly value: {
                                        readonly type: "json";
                                    };
                                };
                                readonly remove: {
                                    readonly path: {
                                        readonly type: "string";
                                    };
                                };
                                readonly replace: {
                                    readonly path: {
                                        readonly type: "string";
                                    };
                                    readonly value: {
                                        readonly type: "json";
                                    };
                                };
                                readonly move: {
                                    readonly path: {
                                        readonly type: "string";
                                    };
                                    readonly from: {
                                        readonly type: "string";
                                    };
                                };
                                readonly copy: {
                                    readonly path: {
                                        readonly type: "string";
                                    };
                                    readonly from: {
                                        readonly type: "string";
                                    };
                                };
                                readonly test: {
                                    readonly path: {
                                        readonly type: "string";
                                    };
                                    readonly value: {
                                        readonly type: "json";
                                    };
                                };
                            };
                        };
                    };
                    readonly generation: {
                        readonly type: "string";
                        readonly optional: true;
                    };
                    readonly schemaSequence: {
                        readonly type: "number";
                        readonly int: true;
                        readonly optional: true;
                    };
                    readonly stagedMigrations: {
                        readonly type: "array";
                        readonly optional: true;
                        readonly items: {
                            readonly type: "string";
                        };
                    };
                    readonly stage: {
                        readonly type: "object";
                        readonly fields: {
                            readonly type: {
                                readonly type: "string";
                            };
                            readonly baseSequence: {
                                readonly type: "number";
                                readonly int: true;
                            };
                            readonly schemaSequence: {
                                readonly type: "number";
                                readonly int: true;
                                readonly optional: true;
                            };
                        };
                    };
                };
            };
        };
        readonly yjsCopies: {
            readonly type: "record";
            readonly default: {};
            readonly values: {
                readonly type: "object";
                readonly fields: {
                    readonly docId: {
                        readonly type: "string";
                    };
                    readonly yjsId: {
                        readonly type: "string";
                    };
                    readonly copyRef: {
                        readonly type: "yjsRef";
                    };
                    readonly stage: {
                        readonly type: "object";
                        readonly fields: {
                            readonly type: {
                                readonly type: "string";
                            };
                            readonly baseSequence: {
                                readonly type: "number";
                                readonly int: true;
                            };
                            readonly schemaSequence: {
                                readonly type: "number";
                                readonly int: true;
                                readonly optional: true;
                            };
                        };
                    };
                };
            };
        };
        readonly commitIntents: {
            readonly type: "record";
            readonly default: {};
            readonly values: {
                readonly type: "object";
                readonly fields: {
                    readonly contentEventId: {
                        readonly type: "string";
                        readonly optional: true;
                    };
                    readonly rename: {
                        readonly type: "object";
                        readonly optional: true;
                        readonly fields: {
                            readonly eventId: {
                                readonly type: "string";
                            };
                            readonly name: {
                                readonly type: "string";
                                readonly nullable: true;
                            };
                        };
                    };
                    readonly changeIds: {
                        readonly type: "array";
                        readonly items: {
                            readonly type: "string";
                        };
                    };
                    readonly copyKeys: {
                        readonly type: "array";
                        readonly items: {
                            readonly type: "string";
                        };
                    };
                    readonly migrationKeys: {
                        readonly type: "array";
                        readonly optional: true;
                        readonly items: {
                            readonly type: "string";
                        };
                    };
                };
            };
        };
    };
}

The schema a host document's session field must use: an object field that holds the structured changeset (its onUpstreamAdvance policy, the per-doc stages base metadata, and the flat changes/yjsCopies/commitIntents record-set lanes), DEFAULTING to an empty changeset under autoMerge. Spread it into the host type, e.g. { conversation: { fields: { session: SESSION_CHANGESET_FIELD } } }, and add the matching integrity rules with sessionChangesetReferences() (below) on the host schema's references. A host created without the field gets the default; pass emptySessionChangeset({ onUpstreamAdvance: "block" }) to create one under the other policy.

Stored structurally (not a serialized string) so the sync layer merges the flat changes record field-by-field. Because the field is schema-described (not json), the host document's write gate validates the changeset's SHAPE on every staging write — a malformed changeset can no longer be written. The read-time normalizeChangeset still runs as the convergence net for data that arrives by other routes (a server-side merge of two authors' disjoint edits, or legacy data persisted under an older/looser schema): repair runs on write, so it cannot heal a merge-produced dangling reference until the next write touches the doc, whereas the read-time net cleans it on every read.

Why the field and every lane in it default rather than being created by the first staging write: a write can only merge with another author's if it lands on paths that already exist. An author whose view of the host held no changeset would write the field WHOLE (an add /session), replacing whatever another author had staged meanwhile — silently, and including the stale-generation work that keeps a replaced target's stage blocked. With defaults the server fills the changeset in when the host is created, and backfills it (persisted, like any newly-added default) the first time it reads a host stored before the field or a lane existed — so every client's view holds every lane, and every staging write is a fine-grained op under it. A session refuses to stage into a host that still has none (a host type that does not declare the field this way).

SESSION_DOCUMENT_ID

SESSION_DOCUMENT_ID = "sys:session"

Id of a staging session's staged-work summary: a document the session synthesizes on read, with one entry per staged document. The live client returns null for it.

SESSION_DOCUMENT_TYPE

SESSION_DOCUMENT_TYPE = "sys:session"

Document type of the SESSION_DOCUMENT_ID summary.

SESSION_STAGE_DOC_ID_PREFIX

SESSION_STAGE_DOC_ID_PREFIX = "sys:stage:"

Prefix of every stage detail document id: sys:stage:.

SESSION_STAGE_DOCUMENT_TYPE

SESSION_STAGE_DOCUMENT_TYPE = "sys:stage"

Document type of a per-document stage detail document (see sessionStageDocId).