@kyneta/yjs-schema 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Version, Schema, Ref, SubstratePayload, StoreReader, Path, Segment, ChangeBase, Op, Substrate, SubstrateFactory, BoundSchema, AnnotatedSchema } from '@kyneta/schema';
2
- export { Changeset, Op, Ref, Schema, SubstratePayload, applyChanges, change, subscribe, subscribeNode } from '@kyneta/schema';
1
+ import { Version, Schema, Ref, SubstratePayload, SubstrateNamespace, CrdtStrategy, Path, ChangeBase, Op, Reader, Substrate, ReplicaFactory, SubstrateFactory, Segment } from '@kyneta/schema';
2
+ export { Op, Ref, Schema, SubstratePayload, applyChanges, change, subscribe, subscribeNode } from '@kyneta/schema';
3
3
  import * as Y from 'yjs';
4
- import { Doc } from 'yjs';
4
+ export { Changeset } from '@kyneta/changefeed';
5
5
 
6
6
  /**
7
7
  * A Version wrapping a Yjs state vector.
@@ -41,6 +41,16 @@ declare class YjsVersion implements Version {
41
41
  * Throws if `other` is not a `YjsVersion`.
42
42
  */
43
43
  compare(other: Version): "behind" | "equal" | "ahead" | "concurrent";
44
+ /**
45
+ * Greatest lower bound (lattice meet) of two Yjs versions.
46
+ *
47
+ * Decodes both state vectors, computes the component-wise minimum
48
+ * via the shared `versionVectorMeet` utility, and encodes the result
49
+ * back to a Yjs state vector.
50
+ *
51
+ * @throws If `other` is not a `YjsVersion`.
52
+ */
53
+ meet(other: Version): YjsVersion;
44
54
  /**
45
55
  * Parse a serialized YjsVersion string back into a YjsVersion.
46
56
  *
@@ -74,7 +84,7 @@ declare class YjsVersion implements Version {
74
84
  * CRDT collaboration support.
75
85
  *
76
86
  * The returned ref observes **all** mutations to the underlying Y.Doc,
77
- * regardless of source (local kyneta writes, importDelta, external
87
+ * regardless of source (local kyneta writes, merge, external
78
88
  * `Y.applyUpdate()`, external raw Yjs API mutations).
79
89
  *
80
90
  * @param schema - The schema describing the document structure.
@@ -83,21 +93,21 @@ declare class YjsVersion implements Version {
83
93
  */
84
94
  type CreateYjsDoc = <S extends Schema>(schema: S, doc?: Y.Doc) => Ref<S>;
85
95
  declare const createYjsDoc: CreateYjsDoc;
86
- type CreateYjsDocFromSnapshot = <S extends Schema>(schema: S, payload: SubstratePayload) => Ref<S>;
96
+ type CreateYjsDocFromEntirety = <S extends Schema>(schema: S, payload: SubstratePayload) => Ref<S>;
87
97
  /**
88
- * Reconstruct a live Yjs-backed document from a substrate snapshot payload.
98
+ * Reconstruct a live Yjs-backed document from a substrate entirety payload.
89
99
  *
90
- * The payload must have been produced by `exportSnapshot()` on a
100
+ * The payload must have been produced by `exportEntirety()` on a
91
101
  * compatible document. This is the entry point for SSR hydration
92
102
  * and reconnection past log compaction.
93
103
  *
94
104
  * ```ts
95
- * const payload = exportSnapshot(docA)
96
- * const docB = createYjsDocFromSnapshot(MySchema, payload)
105
+ * const payload = exportEntirety(docA)
106
+ * const docB = createYjsDocFromEntirety(MySchema, payload)
97
107
  * // docB has the same state as docA at the time of export
98
108
  * ```
99
109
  */
100
- declare const createYjsDocFromSnapshot: CreateYjsDocFromSnapshot;
110
+ declare const createYjsDocFromEntirety: CreateYjsDocFromEntirety;
101
111
 
102
112
  /**
103
113
  * Current version as a `YjsVersion` (wrapping a Yjs state vector).
@@ -105,20 +115,20 @@ declare const createYjsDocFromSnapshot: CreateYjsDocFromSnapshot;
105
115
  * Use `.serialize()` to get a text-safe string for embedding in HTML
106
116
  * meta tags, URL parameters, etc.
107
117
  *
108
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
109
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
118
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
119
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
110
120
  */
111
121
  declare function version(doc: object): YjsVersion;
112
122
  /**
113
- * Export the full substrate snapshot — sufficient for a new peer to
114
- * reconstruct an equivalent document via `createYjsDocFromSnapshot()`.
123
+ * Export the full substrate entirety — sufficient for a new peer to
124
+ * reconstruct an equivalent document via `createYjsDocFromEntirety()`.
115
125
  *
116
126
  * Returns a binary `SubstratePayload` (Yjs state-as-update bytes).
117
127
  *
118
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
119
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
128
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
129
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
120
130
  */
121
- declare function exportSnapshot(doc: object): SubstratePayload;
131
+ declare function exportEntirety(doc: object): SubstratePayload;
122
132
  /**
123
133
  * Export a delta payload containing all changes since the given version.
124
134
  *
@@ -129,78 +139,53 @@ declare function exportSnapshot(doc: object): SubstratePayload;
129
139
  * const v0 = version(docA)
130
140
  * change(docA, d => d.title.insert(0, "Hi"))
131
141
  * const delta = exportSince(docA, v0)
132
- * importDelta(docB, delta!)
142
+ * merge(docB, delta!)
133
143
  * ```
134
144
  *
135
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
145
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
136
146
  * @param since - The version to diff from.
137
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
147
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
138
148
  */
139
149
  declare function exportSince(doc: object, since: YjsVersion): SubstratePayload | null;
140
150
  /**
141
151
  * Import a delta payload into a live document.
142
152
  *
143
153
  * The payload must have been produced by `exportSince()` or
144
- * `exportSnapshot()` on a compatible document.
154
+ * `exportEntirety()` on a compatible document.
145
155
  *
146
156
  * After import, the changefeed fires for all subscribers — the event
147
157
  * bridge handles this automatically.
148
158
  *
149
159
  * ```ts
150
160
  * const delta = exportSince(docA, sinceVersion)
151
- * importDelta(docB, delta!, "sync")
161
+ * merge(docB, delta!, "sync")
152
162
  * ```
153
163
  *
154
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
155
- * @param payload - The delta or snapshot payload to import.
164
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
165
+ * @param payload - The delta or entirety payload to merge.
156
166
  * @param origin - Optional provenance tag for the changeset.
157
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
167
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
158
168
  */
159
- declare function importDelta(doc: object, payload: SubstratePayload, origin?: string): void;
169
+ declare function merge(doc: object, payload: SubstratePayload, origin?: string): void;
160
170
 
161
171
  /**
162
- * Creates a StoreReader that navigates the Yjs shared type tree live,
163
- * using the schema as a type witness to determine navigation at each
164
- * path segment.
172
+ * The Yjs CRDT substrate namespace.
165
173
  *
166
- * The reader is a live view mutations to the underlying Y.Doc
167
- * (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
168
- * visible through the reader.
174
+ * - `yjs.bind(schema)`collaborative sync (default)
175
+ * - `yjs.bind(schema, "ephemeral")` ephemeral/presence broadcast
176
+ * - `yjs.replica()` collaborative replication (default)
177
+ * - `yjs.replica("ephemeral")` — ephemeral replication
178
+ * - `yjs.unwrap(ref)` — access the underlying Y.Doc
169
179
  *
170
- * Internally obtains the root map via `doc.getMap("root")`.
171
- *
172
- * @param doc - The Y.Doc to read from.
173
- * @param schema - The root schema for the document.
180
+ * Strategy is constrained to `CrdtStrategy` (`"collaborative" | "ephemeral"`).
181
+ * Passing `"authoritative"` is a compile error.
174
182
  */
175
- declare function yjsStoreReader(doc: Y.Doc, schema: Schema): StoreReader;
176
-
177
- /**
178
- * Navigate one step deeper into the Yjs shared type tree.
179
- *
180
- * Uses `instanceof` for runtime type discrimination:
181
- * - `Y.Map` → `.get(key)`
182
- * - `Y.Array` → `.get(index)`
183
- * - `Y.Text` → terminal (cannot step further)
184
- * - Plain value → terminal (return `undefined`)
185
- *
186
- * @param current - The current position (a Yjs shared type or plain value)
187
- * @param segment - The path segment to follow
188
- */
189
- declare function stepIntoYjs(current: unknown, segment: Segment): unknown;
190
- /**
191
- * Resolve a Yjs shared type (or plain value) at the given path.
192
- *
193
- * Left-folds over path segments using `advanceSchema` for pure schema
194
- * descent and `stepIntoYjs` for Yjs-specific navigation.
195
- *
196
- * Returns the Yjs shared type or plain value at the terminal position.
197
- * For an empty path, returns the root map itself.
198
- *
199
- * @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
200
- * @param rootSchema - The root document schema
201
- * @param path - The path to resolve
202
- */
203
- declare function resolveYjsType(rootMap: Y.Map<any>, rootSchema: Schema, path: Path): unknown;
183
+ /** The closed set of capability tags that the Yjs substrate supports. */
184
+ type YjsCaps = "text" | "json";
185
+ declare const yjs: SubstrateNamespace<CrdtStrategy, YjsCaps> & {
186
+ /** Access the underlying `Y.Doc` backing a ref. */
187
+ unwrap(ref: object): Y.Doc;
188
+ };
204
189
 
205
190
  /**
206
191
  * Apply a kyneta Change to the Yjs shared type tree imperatively.
@@ -228,24 +213,52 @@ declare function applyChangeToYjs(rootMap: Y.Map<any>, rootSchema: Schema, path:
228
213
  *
229
214
  * @param events - The events from the `observeDeep` callback
230
215
  */
231
- declare function eventsToOps(events: Y.YEvent<any>[]): Op[];
216
+ declare function eventsToOps(events: Y.YEvent<any>[], schema: Schema): Op[];
232
217
 
233
218
  /**
234
219
  * Ensure that a Y.Doc's root map contains the correct Yjs shared types
235
220
  * matching the schema structure.
236
221
  *
237
- * Obtains the root map via `doc.getMap("root")`, unwraps the root product
238
- * schema, and creates empty containers for each field within a single
239
- * `doc.transact()` call for atomicity.
222
+ * Obtains the root map via `doc.getMap("root")`, reads the root product
223
+ * schema's fields, and creates empty containers for each field within a
224
+ * single `doc.transact()` call for atomicity.
225
+ *
226
+ * When `conditional` is true, fields that already exist in the root map
227
+ * are skipped. This is the correct mode after hydration — containers
228
+ * present from stored state must not be overwritten (each `rootMap.set()`
229
+ * is a CRDT write that advances the version vector and may conflict
230
+ * with stored operations).
231
+ *
232
+ * When `conditional` is false (default), all fields are created
233
+ * unconditionally. This is the correct mode for fresh documents.
240
234
  *
241
- * No values are written the containers are empty after this call.
242
- * Initial content should be applied via `change()` after substrate
243
- * construction.
235
+ * **Structural identity:** This function temporarily sets `doc.clientID`
236
+ * to `STRUCTURAL_YJS_CLIENT_ID` (0) for the duration of container creation,
237
+ * then restores the caller's clientID. This produces byte-identical
238
+ * structural ops across all peers, enabling Yjs deduplication on merge.
244
239
  *
245
240
  * @param doc - The Y.Doc to prepare
246
- * @param schema - The root document schema (typically annotated("doc", product))
241
+ * @param schema - The root document schema (a ProductSchema)
242
+ * @param conditional - If true, skip fields that already exist in the root map.
243
+ * Context: jj:smmulzkm (two-phase substrate construction)
244
+ */
245
+ declare function ensureContainers(doc: Y.Doc, schema: Schema, conditional?: boolean): void;
246
+
247
+ /**
248
+ * Creates a Reader that navigates the Yjs shared type tree live,
249
+ * using the schema as a type witness to determine navigation at each
250
+ * path segment.
251
+ *
252
+ * The reader is a live view — mutations to the underlying Y.Doc
253
+ * (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
254
+ * visible through the reader.
255
+ *
256
+ * Internally obtains the root map via `doc.getMap("root")`.
257
+ *
258
+ * @param doc - The Y.Doc to read from.
259
+ * @param schema - The root schema for the document.
247
260
  */
248
- declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
261
+ declare function yjsReader(doc: Y.Doc, schema: Schema): Reader;
249
262
 
250
263
  /**
251
264
  * Creates a `Substrate<YjsVersion>` wrapping a user-provided Y.Doc.
@@ -253,11 +266,11 @@ declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
253
266
  * This is the "bring your own doc" entry point. The user creates and
254
267
  * manages the Y.Doc (possibly via a Yjs provider); this function wraps
255
268
  * it with a schema-aware overlay providing typed reads, writes,
256
- * versioning, and export/import through the standard Substrate interface.
269
+ * versioning, and export/merge through the standard Substrate interface.
257
270
  *
258
271
  * **Event bridge contract:** A persistent `observeDeep` handler is
259
272
  * registered on the root Y.Map at construction time. All non-kyneta
260
- * mutations to the Y.Doc (imports, external local writes) are bridged
273
+ * mutations to the Y.Doc (merges, external local writes) are bridged
261
274
  * to the kyneta changefeed. Subscribing to the kyneta doc observes all
262
275
  * mutations regardless of source.
263
276
  *
@@ -266,86 +279,35 @@ declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
266
279
  * @param schema - The root schema for the document.
267
280
  */
268
281
  declare function createYjsSubstrate(doc: Y.Doc, schema: Schema): Substrate<YjsVersion>;
269
- /**
270
- * Factory for constructing Yjs-backed substrates.
271
- *
272
- * - `create(schema)` — creates a fresh Y.Doc with empty containers
273
- * matching the schema structure. No seed data — initial content
274
- * should be applied via `change()` after construction.
275
- * - `fromSnapshot(payload, schema)` — creates a Y.Doc from a snapshot
276
- * payload, returns a substrate.
277
- * - `parseVersion(serialized)` — deserializes a YjsVersion.
278
- */
282
+ declare const yjsReplicaFactory: ReplicaFactory<YjsVersion>;
279
283
  declare const yjsSubstrateFactory: SubstrateFactory<YjsVersion>;
280
284
 
281
285
  /**
282
- * Bind a schema to the Yjs CRDT substrate with causal merge strategy.
283
- *
284
- * This is the recommended way to declare a Yjs-backed document type.
285
- * The factory builder injects a deterministic numeric Yjs clientID derived
286
- * from the exchange's string peerId, ensuring consistent change attribution
287
- * across all documents and sessions.
286
+ * Navigate one step deeper into the Yjs shared type tree.
288
287
  *
289
- * **Unsupported annotations:** Yjs has no native counter, movable list,
290
- * or tree types. Schemas passed to `bindYjs` must not contain
291
- * `Schema.annotated("counter")`, `Schema.annotated("movable")`, or
292
- * `Schema.annotated("tree")`. These will throw at construction time.
288
+ * Uses `instanceof` for runtime type discrimination:
289
+ * - `Y.Map` `.get(key)`
290
+ * - `Y.Array` → `.get(index)`
291
+ * - `Y.Text` terminal (cannot step further)
292
+ * - Plain value → terminal (return `undefined`)
293
293
  *
294
- * @example
295
- * ```ts
296
- * import { bindYjs } from "@kyneta/yjs-schema"
297
- * import { Schema } from "@kyneta/schema"
298
- *
299
- * const TodoDoc = bindYjs(Schema.doc({
300
- * title: Schema.annotated("text"),
301
- * items: Schema.list(Schema.struct({
302
- * name: Schema.string(),
303
- * done: Schema.boolean(),
304
- * })),
305
- * }))
306
- *
307
- * const doc = exchange.get("my-todos", TodoDoc)
308
- * ```
294
+ * @param current - The current position (a Yjs shared type or plain value)
295
+ * @param segment - The path segment to follow
309
296
  */
310
- declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
311
-
297
+ declare function stepIntoYjs(current: unknown, segment: Segment): unknown;
312
298
  /**
313
- * Returns the `Y.Doc` backing the given ref.
314
- *
315
- * This is the Yjs-specific escape hatch for accessing substrate-level
316
- * capabilities: raw Yjs API, y-prosemirror/y-codemirror bindings,
317
- * undo manager, awareness protocol, Yjs providers (y-websocket,
318
- * y-indexeddb, y-webrtc, Hocuspocus, Liveblocks), etc.
319
- *
320
- * Currently supports root document refs only. Child-level resolution
321
- * (e.g. `yjs(doc.title)` → `Y.Text`) is future work.
322
- *
323
- * @param ref - A root document ref backed by a Yjs substrate
324
- * @returns The `Y.Doc` backing the ref
325
- * @throws If the ref is not backed by a Yjs substrate
326
- *
327
- * @example
328
- * ```ts
329
- * import { yjs } from "@kyneta/yjs-schema"
299
+ * Resolve a Yjs shared type (or plain value) at the given path.
330
300
  *
331
- * const doc = exchange.get("my-doc", TodoDoc)
332
- * const yjsDoc = yjs(doc)
333
- * console.log(yjsDoc.getMap("root").toJSON()) // raw state
334
- * console.log(yjsDoc.clientID) // client ID
335
- * ```
336
- */
337
- declare function yjs(ref: object): Doc;
338
-
339
- /**
340
- * Collaborative text (CRDT). Produces `annotated("text")`.
301
+ * Left-folds over path segments using `advanceSchema` for pure schema
302
+ * descent and `stepIntoYjs` for Yjs-specific navigation.
341
303
  *
342
- * The annotation implies scalar string semantics for reads,
343
- * but the Yjs substrate provides collaborative editing (insert, delete)
344
- * via Y.Text.
304
+ * Returns the Yjs shared type or plain value at the terminal position.
305
+ * For an empty path, returns the root map itself.
345
306
  *
346
- * This is a convenience re-export so that `@kyneta/yjs-schema` users
347
- * don't need to import `LoroSchema` just for `text()`.
307
+ * @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
308
+ * @param rootSchema - The root document schema
309
+ * @param path - The path to resolve
348
310
  */
349
- declare function text(): AnnotatedSchema<"text", undefined>;
311
+ declare function resolveYjsType(rootMap: Y.Map<any>, rootSchema: Schema, path: Path): unknown;
350
312
 
351
- export { YjsVersion, applyChangeToYjs, bindYjs, createYjsDoc, createYjsDocFromSnapshot, createYjsSubstrate, ensureContainers, eventsToOps, exportSince, exportSnapshot, importDelta, resolveYjsType, stepIntoYjs, text, version, yjs, yjsStoreReader, yjsSubstrateFactory };
313
+ export { type YjsCaps, YjsVersion, applyChangeToYjs, createYjsDoc, createYjsDocFromEntirety, createYjsSubstrate, ensureContainers, eventsToOps, exportEntirety, exportSince, merge, resolveYjsType, stepIntoYjs, version, yjs, yjsReader, yjsReplicaFactory, yjsSubstrateFactory };