@kyneta/yjs-schema 1.0.0 → 1.1.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,4 +1,4 @@
1
- import { Version, Schema, Ref, SubstratePayload, StoreReader, Path, Segment, ChangeBase, Op, Substrate, SubstrateFactory, BoundSchema, AnnotatedSchema } from '@kyneta/schema';
1
+ import { Version, Schema, Ref, SubstratePayload, BoundSchema, Path, ChangeBase, Op, Reader, Substrate, SubstrateFactory, Segment, AnnotatedSchema } from '@kyneta/schema';
2
2
  export { Changeset, Op, Ref, Schema, SubstratePayload, applyChanges, change, subscribe, subscribeNode } from '@kyneta/schema';
3
3
  import * as Y from 'yjs';
4
4
  import { Doc } from 'yjs';
@@ -74,7 +74,7 @@ declare class YjsVersion implements Version {
74
74
  * CRDT collaboration support.
75
75
  *
76
76
  * The returned ref observes **all** mutations to the underlying Y.Doc,
77
- * regardless of source (local kyneta writes, importDelta, external
77
+ * regardless of source (local kyneta writes, merge, external
78
78
  * `Y.applyUpdate()`, external raw Yjs API mutations).
79
79
  *
80
80
  * @param schema - The schema describing the document structure.
@@ -83,21 +83,21 @@ declare class YjsVersion implements Version {
83
83
  */
84
84
  type CreateYjsDoc = <S extends Schema>(schema: S, doc?: Y.Doc) => Ref<S>;
85
85
  declare const createYjsDoc: CreateYjsDoc;
86
- type CreateYjsDocFromSnapshot = <S extends Schema>(schema: S, payload: SubstratePayload) => Ref<S>;
86
+ type CreateYjsDocFromEntirety = <S extends Schema>(schema: S, payload: SubstratePayload) => Ref<S>;
87
87
  /**
88
- * Reconstruct a live Yjs-backed document from a substrate snapshot payload.
88
+ * Reconstruct a live Yjs-backed document from a substrate entirety payload.
89
89
  *
90
- * The payload must have been produced by `exportSnapshot()` on a
90
+ * The payload must have been produced by `exportEntirety()` on a
91
91
  * compatible document. This is the entry point for SSR hydration
92
92
  * and reconnection past log compaction.
93
93
  *
94
94
  * ```ts
95
- * const payload = exportSnapshot(docA)
96
- * const docB = createYjsDocFromSnapshot(MySchema, payload)
95
+ * const payload = exportEntirety(docA)
96
+ * const docB = createYjsDocFromEntirety(MySchema, payload)
97
97
  * // docB has the same state as docA at the time of export
98
98
  * ```
99
99
  */
100
- declare const createYjsDocFromSnapshot: CreateYjsDocFromSnapshot;
100
+ declare const createYjsDocFromEntirety: CreateYjsDocFromEntirety;
101
101
 
102
102
  /**
103
103
  * Current version as a `YjsVersion` (wrapping a Yjs state vector).
@@ -105,20 +105,20 @@ declare const createYjsDocFromSnapshot: CreateYjsDocFromSnapshot;
105
105
  * Use `.serialize()` to get a text-safe string for embedding in HTML
106
106
  * meta tags, URL parameters, etc.
107
107
  *
108
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
109
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
108
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
109
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
110
110
  */
111
111
  declare function version(doc: object): YjsVersion;
112
112
  /**
113
- * Export the full substrate snapshot — sufficient for a new peer to
114
- * reconstruct an equivalent document via `createYjsDocFromSnapshot()`.
113
+ * Export the full substrate entirety — sufficient for a new peer to
114
+ * reconstruct an equivalent document via `createYjsDocFromEntirety()`.
115
115
  *
116
116
  * Returns a binary `SubstratePayload` (Yjs state-as-update bytes).
117
117
  *
118
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
119
- * @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`.
120
120
  */
121
- declare function exportSnapshot(doc: object): SubstratePayload;
121
+ declare function exportEntirety(doc: object): SubstratePayload;
122
122
  /**
123
123
  * Export a delta payload containing all changes since the given version.
124
124
  *
@@ -129,78 +129,65 @@ declare function exportSnapshot(doc: object): SubstratePayload;
129
129
  * const v0 = version(docA)
130
130
  * change(docA, d => d.title.insert(0, "Hi"))
131
131
  * const delta = exportSince(docA, v0)
132
- * importDelta(docB, delta!)
132
+ * merge(docB, delta!)
133
133
  * ```
134
134
  *
135
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
135
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
136
136
  * @param since - The version to diff from.
137
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
137
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
138
138
  */
139
139
  declare function exportSince(doc: object, since: YjsVersion): SubstratePayload | null;
140
140
  /**
141
141
  * Import a delta payload into a live document.
142
142
  *
143
143
  * The payload must have been produced by `exportSince()` or
144
- * `exportSnapshot()` on a compatible document.
144
+ * `exportEntirety()` on a compatible document.
145
145
  *
146
146
  * After import, the changefeed fires for all subscribers — the event
147
147
  * bridge handles this automatically.
148
148
  *
149
149
  * ```ts
150
150
  * const delta = exportSince(docA, sinceVersion)
151
- * importDelta(docB, delta!, "sync")
151
+ * merge(docB, delta!, "sync")
152
152
  * ```
153
153
  *
154
- * @param doc - A document created by `createYjsDoc` or `createYjsDocFromSnapshot`.
155
- * @param payload - The delta or snapshot payload to import.
154
+ * @param doc - A document created by `createYjsDoc` or `createYjsDocFromEntirety`.
155
+ * @param payload - The delta or entirety payload to merge.
156
156
  * @param origin - Optional provenance tag for the changeset.
157
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
157
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
158
158
  */
159
- declare function importDelta(doc: object, payload: SubstratePayload, origin?: string): void;
159
+ declare function merge(doc: object, payload: SubstratePayload, origin?: string): void;
160
160
 
161
161
  /**
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.
165
- *
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.
169
- *
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.
174
- */
175
- declare function yjsStoreReader(doc: Y.Doc, schema: Schema): StoreReader;
176
-
177
- /**
178
- * Navigate one step deeper into the Yjs shared type tree.
162
+ * Bind a schema to the Yjs CRDT substrate with causal merge strategy.
179
163
  *
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`)
164
+ * This is the recommended way to declare a Yjs-backed document type.
165
+ * The factory builder injects a deterministic numeric Yjs clientID derived
166
+ * from the exchange's string peerId, ensuring consistent change attribution
167
+ * across all documents and sessions.
185
168
  *
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.
169
+ * **Unsupported annotations:** Yjs has no native counter, movable list,
170
+ * or tree types. Schemas passed to `bindYjs` must not contain
171
+ * `Schema.annotated("counter")`, `Schema.annotated("movable")`, or
172
+ * `Schema.annotated("tree")`. These will throw at construction time.
192
173
  *
193
- * Left-folds over path segments using `advanceSchema` for pure schema
194
- * descent and `stepIntoYjs` for Yjs-specific navigation.
174
+ * @example
175
+ * ```ts
176
+ * import { bindYjs } from "@kyneta/yjs-schema"
177
+ * import { Schema } from "@kyneta/schema"
195
178
  *
196
- * Returns the Yjs shared type or plain value at the terminal position.
197
- * For an empty path, returns the root map itself.
179
+ * const TodoDoc = bindYjs(Schema.doc({
180
+ * title: Schema.annotated("text"),
181
+ * items: Schema.list(Schema.struct({
182
+ * name: Schema.string(),
183
+ * done: Schema.boolean(),
184
+ * })),
185
+ * }))
198
186
  *
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
187
+ * const doc = exchange.get("my-todos", TodoDoc)
188
+ * ```
202
189
  */
203
- declare function resolveYjsType(rootMap: Y.Map<any>, rootSchema: Schema, path: Path): unknown;
190
+ declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
204
191
 
205
192
  /**
206
193
  * Apply a kyneta Change to the Yjs shared type tree imperatively.
@@ -238,14 +225,42 @@ declare function eventsToOps(events: Y.YEvent<any>[]): Op[];
238
225
  * schema, and creates empty containers for each field within a single
239
226
  * `doc.transact()` call for atomicity.
240
227
  *
241
- * No values are written the containers are empty after this call.
242
- * Initial content should be applied via `change()` after substrate
243
- * construction.
228
+ * When `conditional` is true, fields that already exist in the root map
229
+ * are skipped. This is the correct mode after hydration — containers
230
+ * present from stored state must not be overwritten (each `rootMap.set()`
231
+ * is a CRDT write that advances the version vector and may conflict
232
+ * with stored operations).
233
+ *
234
+ * When `conditional` is false (default), all fields are created
235
+ * unconditionally. This is the correct mode for fresh documents.
236
+ *
237
+ * **Structural identity:** This function temporarily sets `doc.clientID`
238
+ * to `STRUCTURAL_YJS_CLIENT_ID` (0) for the duration of container creation,
239
+ * then restores the caller's clientID. This produces byte-identical
240
+ * structural ops across all peers, enabling Yjs deduplication on merge.
244
241
  *
245
242
  * @param doc - The Y.Doc to prepare
246
243
  * @param schema - The root document schema (typically annotated("doc", product))
244
+ * @param conditional - If true, skip fields that already exist in the root map.
245
+ * Context: jj:smmulzkm (two-phase substrate construction)
246
+ */
247
+ declare function ensureContainers(doc: Y.Doc, schema: Schema, conditional?: boolean): void;
248
+
249
+ /**
250
+ * Creates a Reader that navigates the Yjs shared type tree live,
251
+ * using the schema as a type witness to determine navigation at each
252
+ * path segment.
253
+ *
254
+ * The reader is a live view — mutations to the underlying Y.Doc
255
+ * (via `doc.transact()`, or `Y.applyUpdate()`) are immediately
256
+ * visible through the reader.
257
+ *
258
+ * Internally obtains the root map via `doc.getMap("root")`.
259
+ *
260
+ * @param doc - The Y.Doc to read from.
261
+ * @param schema - The root schema for the document.
247
262
  */
248
- declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
263
+ declare function yjsReader(doc: Y.Doc, schema: Schema): Reader;
249
264
 
250
265
  /**
251
266
  * Creates a `Substrate<YjsVersion>` wrapping a user-provided Y.Doc.
@@ -253,11 +268,11 @@ declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
253
268
  * This is the "bring your own doc" entry point. The user creates and
254
269
  * manages the Y.Doc (possibly via a Yjs provider); this function wraps
255
270
  * it with a schema-aware overlay providing typed reads, writes,
256
- * versioning, and export/import through the standard Substrate interface.
271
+ * versioning, and export/merge through the standard Substrate interface.
257
272
  *
258
273
  * **Event bridge contract:** A persistent `observeDeep` handler is
259
274
  * registered on the root Y.Map at construction time. All non-kyneta
260
- * mutations to the Y.Doc (imports, external local writes) are bridged
275
+ * mutations to the Y.Doc (merges, external local writes) are bridged
261
276
  * to the kyneta changefeed. Subscribing to the kyneta doc observes all
262
277
  * mutations regardless of source.
263
278
  *
@@ -266,49 +281,8 @@ declare function ensureContainers(doc: Y.Doc, schema: Schema): void;
266
281
  * @param schema - The root schema for the document.
267
282
  */
268
283
  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
- */
279
284
  declare const yjsSubstrateFactory: SubstrateFactory<YjsVersion>;
280
285
 
281
- /**
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.
288
- *
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.
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
- * ```
309
- */
310
- declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
311
-
312
286
  /**
313
287
  * Returns the `Y.Doc` backing the given ref.
314
288
  *
@@ -336,6 +310,34 @@ declare function bindYjs<S extends Schema>(schema: S): BoundSchema<S>;
336
310
  */
337
311
  declare function yjs(ref: object): Doc;
338
312
 
313
+ /**
314
+ * Navigate one step deeper into the Yjs shared type tree.
315
+ *
316
+ * Uses `instanceof` for runtime type discrimination:
317
+ * - `Y.Map` → `.get(key)`
318
+ * - `Y.Array` → `.get(index)`
319
+ * - `Y.Text` → terminal (cannot step further)
320
+ * - Plain value → terminal (return `undefined`)
321
+ *
322
+ * @param current - The current position (a Yjs shared type or plain value)
323
+ * @param segment - The path segment to follow
324
+ */
325
+ declare function stepIntoYjs(current: unknown, segment: Segment): unknown;
326
+ /**
327
+ * Resolve a Yjs shared type (or plain value) at the given path.
328
+ *
329
+ * Left-folds over path segments using `advanceSchema` for pure schema
330
+ * descent and `stepIntoYjs` for Yjs-specific navigation.
331
+ *
332
+ * Returns the Yjs shared type or plain value at the terminal position.
333
+ * For an empty path, returns the root map itself.
334
+ *
335
+ * @param rootMap - The root `Y.Map` obtained via `doc.getMap("root")`
336
+ * @param rootSchema - The root document schema
337
+ * @param path - The path to resolve
338
+ */
339
+ declare function resolveYjsType(rootMap: Y.Map<any>, rootSchema: Schema, path: Path): unknown;
340
+
339
341
  /**
340
342
  * Collaborative text (CRDT). Produces `annotated("text")`.
341
343
  *
@@ -348,4 +350,4 @@ declare function yjs(ref: object): Doc;
348
350
  */
349
351
  declare function text(): AnnotatedSchema<"text", undefined>;
350
352
 
351
- export { YjsVersion, applyChangeToYjs, bindYjs, createYjsDoc, createYjsDocFromSnapshot, createYjsSubstrate, ensureContainers, eventsToOps, exportSince, exportSnapshot, importDelta, resolveYjsType, stepIntoYjs, text, version, yjs, yjsStoreReader, yjsSubstrateFactory };
353
+ export { YjsVersion, applyChangeToYjs, bindYjs, createYjsDoc, createYjsDocFromEntirety, createYjsSubstrate, ensureContainers, eventsToOps, exportEntirety, exportSince, merge, resolveYjsType, stepIntoYjs, text, version, yjs, yjsReader, yjsSubstrateFactory };