@repo/datadata/yjs
Yjs integration: awareness (cursors and selections) bridged into datadata presence.
Functions
composeYjsAwarenessPresence
export declare function composeYjsAwarenessPresence<S extends SchemaRegistry>(schemas: S): S;Normalize a schema registry by composing the Yjs awareness field into every presence schema (the presence:<type> docTypes), leaving all other schemas untouched. Applied wherever the app's schemas are turned into stored schema documents (the seed paths) and wherever they are introspected (describe_schemas), so the reserved cursor field is present uniformly — enforced by the server, carried in the stored schema, and visible to schema introspection — without the app having to declare it.
createYjsAwareness
export declare function createYjsAwareness(params: YjsAwarenessParams): YjsAwarenessBridge;Create a y-protocols Awareness for one Y.Doc, backed by datadata presence. Requires an active subscription to docId (presence is scoped to one); set the bridge up after subscribing and destroy it before unsubscribing.
withYjsAwarenessField
export declare function withYjsAwarenessField(schema: DocumentSchema): DocumentSchema;Compose the engine-owned Yjs awareness field into one presence schema. The field is an optional record keyed by yjsId (or a staged copyRef) whose values are opaque y-protocols state (json): datadata carries the cursor blobs but says nothing about their shape, which is the bridge's / y-protocols' to own. A no-op on a non-object root (presence schemas are objects). Idempotent — re-composing overwrites the same field with the same shape.
Interfaces
YjsAwarenessBridge
export interface YjsAwarenessBridgeA y-protocols Awareness bridged onto datadata presence, returned by createYjsAwareness. Call destroy when the editor unbinds.
awareness: Awareness;A real y-protocols Awareness — hand it to y-prosemirror et al. as-is.
destroy(): void;Unhook everything, clear this bridge's field from the published presence.
YjsAwarenessParams
export interface YjsAwarenessParamsParameters for createYjsAwareness.
awareness?: Awareness;Bind the protocol to an EXISTING Awareness instead of creating one — the session's staged bridge owns one shared Awareness per staged doc and upgrades it with transport when the session is persisted.
port: YjsAwarenessPresencePort;The session's presence-view port — read/write this session's cell + peers.
publishDebounceMs?: number;Debounce for publishing local awareness changes (cursor moves fire per keystroke). Positive: trailing debounce in ms; 0: publish synchronously. Default: 50.
ydoc: Y.Doc;The Y.Doc the editor binds to; supplies the local numeric clientID.
yjsId: string;The Y.Doc's id within the document — namespaces this bridge's field.
YjsAwarenessPresencePort
export interface YjsAwarenessPresencePortThe presence-VIEW port the bridge consumes — the session's lens over one target's presence aggregate, scoped to the session's own cell. The session supplies it (LiveSession / StagingSession): getSelfState / setSelfState read-modify-write THIS session's own cell state, getPeers reads the OTHER cells (stale-flagged), and onChange fires when the view (cells or liveness overlay) moves. Keeping the bridge to this narrow port means it neither knows about presence docIds nor the client's subscription state — the session owns both.
getPeers(): RemotePresenceEntry[];The OTHER cells in the view — each peer's presenceId, state and stale flag.
getSelfState(): Record<string, JsonValue> | null;This session's own published cell state (the whole state object), or null.
onChange(listener: () => void): () => void;Subscribe to view changes (cells or overlay moved). Returns an unsubscribe.
setSelfState(state: Record<string, JsonValue> | null): void;Publish (or, with null, clear) this session's own cell state. A no-op when unpublishable.
Variables
AWARENESS_PRESENCE_FIELD
AWARENESS_PRESENCE_FIELD = "sys:awareness"The single reserved presence field carrying Yjs awareness (cursors/selections). A live editor publishes under it keyed by the target's yjsId; a staging session's staged editors publish under the SAME field on the session's own cell, keyed by the copy's copyRef (sys:session:copy:<docId>:<yjsId>). The two never collide — a reader only ever looks up the one key it bound the bridge with (a live yjsId, never the sys:session:copy:-prefixed copyRef) — so live and staged cursors stay isolated without a separate field.