@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/src/bind-yjs.ts CHANGED
@@ -1,36 +1,46 @@
1
- // bind-yjs — bindYjs() convenience wrapper for Yjs CRDT substrate.
1
+ // bind-yjs — Yjs CRDT substrate namespace and factory.
2
2
  //
3
- // Binds a schema to the Yjs substrate with causal merge strategy.
4
- // The factory builder accepts { peerId } and returns a SubstrateFactory
5
- // that sets doc.clientID on every new Y.Doc, ensuring deterministic
6
- // peer identity across all documents in an exchange.
3
+ // Provides the `yjs` substrate namespace (`yjs.bind()`, `yjs.replica()`,
4
+ // `yjs.unwrap()`) and the internal factory builder that injects a
5
+ // deterministic numeric Yjs clientID derived from the exchange's peerId.
7
6
  //
8
7
  // Yjs clientID is a uint32 number. We use FNV-1a hash truncated to
9
8
  // 32 bits, mirroring the Loro binding's hashPeerId pattern but
10
9
  // targeting Yjs's number type (not Loro's bigint/53-bit PeerID).
11
10
  //
12
11
  // Usage:
13
- // import { bindYjs } from "@kyneta/yjs-schema"
12
+ // import { yjs } from "@kyneta/yjs-schema"
14
13
  //
15
- // const TodoDoc = bindYjs(Schema.doc({
16
- // title: Schema.annotated("text"),
14
+ // const TodoDoc = yjs.bind(Schema.struct({
15
+ // title: Schema.text(),
17
16
  // items: Schema.list(Schema.struct({ name: Schema.string() })),
18
17
  // }))
19
18
  //
20
19
  // const doc = exchange.get("my-doc", TodoDoc)
21
20
 
22
- import { bind } from "@kyneta/schema"
23
- import type { BoundSchema } from "@kyneta/schema"
24
- import type { Schema as SchemaNode } from "@kyneta/schema"
25
21
  import type {
22
+ CrdtStrategy,
23
+ Replica,
24
+ Schema as SchemaNode,
26
25
  Substrate,
27
26
  SubstrateFactory,
27
+ SubstrateNamespace,
28
28
  SubstratePayload,
29
29
  } from "@kyneta/schema"
30
+ import {
31
+ BACKING_DOC,
32
+ createSubstrateNamespace,
33
+ STRUCTURAL_YJS_CLIENT_ID,
34
+ unwrap,
35
+ } from "@kyneta/schema"
30
36
  import * as Y from "yjs"
31
- import { createYjsSubstrate } from "./substrate.js"
32
- import { YjsVersion } from "./version.js"
33
37
  import { ensureContainers } from "./populate.js"
38
+ import {
39
+ createYjsReplica,
40
+ createYjsSubstrate,
41
+ yjsReplicaFactory,
42
+ } from "./substrate.js"
43
+ import { YjsVersion } from "./version.js"
34
44
 
35
45
  // ---------------------------------------------------------------------------
36
46
  // Peer ID hashing — deterministic string → numeric Yjs clientID
@@ -55,7 +65,9 @@ function hashPeerId(peerId: string): number {
55
65
  hash = Math.imul(hash, 0x01000193)
56
66
  }
57
67
  // Ensure unsigned 32-bit integer
58
- return hash >>> 0
68
+ const result = hash >>> 0
69
+ // Reserve 0 for structural ops — real peers never collide
70
+ return result === STRUCTURAL_YJS_CLIENT_ID ? 1 : result
59
71
  }
60
72
 
61
73
  // ---------------------------------------------------------------------------
@@ -67,36 +79,50 @@ function hashPeerId(peerId: string): number {
67
79
  * on every new Y.Doc with a deterministic uint32 clientID derived
68
80
  * from the exchange's string peerId.
69
81
  */
70
- function createYjsFactory(
71
- peerId: string,
72
- ): SubstrateFactory<YjsVersion> {
82
+ function createYjsFactory(peerId: string): SubstrateFactory<YjsVersion> {
73
83
  const numericClientId = hashPeerId(peerId)
74
84
 
75
85
  return {
86
+ replica: yjsReplicaFactory,
87
+
88
+ createReplica(): Replica<YjsVersion> {
89
+ // Default random clientID — safe for hydration (no local writes).
90
+ // Identity is set at upgrade() time, after hydration.
91
+ return createYjsReplica(new Y.Doc())
92
+ },
93
+
94
+ upgrade(
95
+ replica: Replica<YjsVersion>,
96
+ schema: SchemaNode,
97
+ ): Substrate<YjsVersion> {
98
+ const doc = (replica as any)[BACKING_DOC] as Y.Doc
99
+ // Set stable identity AFTER hydration — avoids Yjs clientID
100
+ // conflict detection that would reassign to a random value.
101
+ doc.clientID = numericClientId
102
+ // Conditional ensureContainers: skip fields that already exist
103
+ // from hydrated state (each set() is a CRDT write).
104
+ ensureContainers(doc, schema, true)
105
+ return createYjsSubstrate(doc, schema)
106
+ },
107
+
76
108
  create(schema: SchemaNode): Substrate<YjsVersion> {
109
+ // Fresh doc — set identity immediately, unconditional containers.
77
110
  const doc = new Y.Doc()
78
111
  doc.clientID = numericClientId
79
-
80
112
  ensureContainers(doc, schema)
81
113
  return createYjsSubstrate(doc, schema)
82
114
  },
83
115
 
84
- fromSnapshot(
116
+ fromEntirety(
85
117
  payload: SubstratePayload,
86
118
  schema: SchemaNode,
87
119
  ): Substrate<YjsVersion> {
88
- if (
89
- payload.encoding !== "binary" ||
90
- !(payload.data instanceof Uint8Array)
91
- ) {
92
- throw new Error(
93
- "YjsSubstrateFactory.fromSnapshot only supports binary-encoded payloads",
94
- )
95
- }
96
- const doc = new Y.Doc()
97
- doc.clientID = numericClientId
98
- Y.applyUpdate(doc, payload.data)
99
- return createYjsSubstrate(doc, schema)
120
+ // Two-phase path: createReplica → merge → upgrade
121
+ // Identity is set at upgrade() time, after hydration —
122
+ // avoids Yjs clientID conflict detection.
123
+ const replica = this.createReplica()
124
+ replica.merge(payload)
125
+ return this.upgrade(replica, schema)
100
126
  },
101
127
 
102
128
  parseVersion(serialized: string): YjsVersion {
@@ -106,42 +132,67 @@ function createYjsFactory(
106
132
  }
107
133
 
108
134
  // ---------------------------------------------------------------------------
109
- // bindYjs — the convenience wrapper
135
+ // yjs — the Yjs CRDT substrate namespace
110
136
  // ---------------------------------------------------------------------------
111
137
 
112
138
  /**
113
- * Bind a schema to the Yjs CRDT substrate with causal merge strategy.
114
- *
115
- * This is the recommended way to declare a Yjs-backed document type.
116
- * The factory builder injects a deterministic numeric Yjs clientID derived
117
- * from the exchange's string peerId, ensuring consistent change attribution
118
- * across all documents and sessions.
119
- *
120
- * **Unsupported annotations:** Yjs has no native counter, movable list,
121
- * or tree types. Schemas passed to `bindYjs` must not contain
122
- * `Schema.annotated("counter")`, `Schema.annotated("movable")`, or
123
- * `Schema.annotated("tree")`. These will throw at construction time.
139
+ * The Yjs CRDT substrate namespace.
124
140
  *
125
- * @example
126
- * ```ts
127
- * import { bindYjs } from "@kyneta/yjs-schema"
128
- * import { Schema } from "@kyneta/schema"
141
+ * - `yjs.bind(schema)` — collaborative sync (default)
142
+ * - `yjs.bind(schema, "ephemeral")` — ephemeral/presence broadcast
143
+ * - `yjs.replica()` collaborative replication (default)
144
+ * - `yjs.replica("ephemeral")` ephemeral replication
145
+ * - `yjs.unwrap(ref)` — access the underlying Y.Doc
129
146
  *
130
- * const TodoDoc = bindYjs(Schema.doc({
131
- * title: Schema.annotated("text"),
132
- * items: Schema.list(Schema.struct({
133
- * name: Schema.string(),
134
- * done: Schema.boolean(),
135
- * })),
136
- * }))
137
- *
138
- * const doc = exchange.get("my-todos", TodoDoc)
139
- * ```
147
+ * Strategy is constrained to `CrdtStrategy` (`"collaborative" | "ephemeral"`).
148
+ * Passing `"authoritative"` is a compile error.
140
149
  */
141
- export function bindYjs<S extends SchemaNode>(schema: S): BoundSchema<S> {
142
- return bind({
143
- schema,
144
- factory: (ctx) => createYjsFactory(ctx.peerId),
145
- strategy: "causal",
146
- })
147
- }
150
+ /** The closed set of capability tags that the Yjs substrate supports. */
151
+ export type YjsCaps = "text" | "json"
152
+
153
+ export const yjs: SubstrateNamespace<CrdtStrategy, YjsCaps> & {
154
+ /** Access the underlying `Y.Doc` backing a ref. */
155
+ unwrap(ref: object): Y.Doc
156
+ } = {
157
+ ...createSubstrateNamespace<CrdtStrategy, YjsCaps>({
158
+ strategies: {
159
+ collaborative: {
160
+ factory: ctx => createYjsFactory(ctx.peerId),
161
+ replicaFactory: yjsReplicaFactory,
162
+ },
163
+ ephemeral: {
164
+ factory: ctx => createYjsFactory(ctx.peerId),
165
+ replicaFactory: yjsReplicaFactory,
166
+ },
167
+ },
168
+ defaultStrategy: "collaborative",
169
+ }),
170
+
171
+ unwrap(ref: object): Y.Doc {
172
+ let substrate: any
173
+ try {
174
+ substrate = unwrap(ref)
175
+ } catch {
176
+ throw new Error(
177
+ "yjs.unwrap() requires a ref backed by a Yjs substrate. " +
178
+ "Use a doc created by exchange.get() with a yjs.bind() schema, " +
179
+ "or by createYjsDoc().",
180
+ )
181
+ }
182
+
183
+ const doc = substrate[BACKING_DOC]
184
+ if (
185
+ !doc ||
186
+ typeof doc !== "object" ||
187
+ typeof (doc as any).getMap !== "function" ||
188
+ typeof (doc as any).clientID !== "number"
189
+ ) {
190
+ throw new Error(
191
+ "yjs.unwrap() requires a ref backed by a Yjs substrate. " +
192
+ "The ref has a substrate but it is not a Yjs substrate. " +
193
+ "Use a doc created with a yjs.bind() schema or createYjsDoc().",
194
+ )
195
+ }
196
+ return doc as Y.Doc
197
+ },
198
+ }
@@ -16,7 +16,6 @@
16
16
  // This produces a single observeDeep event with the complete struct,
17
17
  // rather than a cascade of child MapChange events.
18
18
 
19
- import { advanceSchema, expandMapOpsToLeaves } from "@kyneta/schema"
20
19
  import type {
21
20
  ChangeBase,
22
21
  IncrementChange,
@@ -30,7 +29,7 @@ import type {
30
29
  TextChange,
31
30
  TextInstruction,
32
31
  } from "@kyneta/schema"
33
- import { RawPath } from "@kyneta/schema"
32
+ import { advanceSchema, expandMapOpsToLeaves, KIND, RawPath } from "@kyneta/schema"
34
33
  import * as Y from "yjs"
35
34
  import { resolveYjsType } from "./yjs-resolve.js"
36
35
 
@@ -75,15 +74,15 @@ export function applyChangeToYjs(
75
74
 
76
75
  case "increment":
77
76
  throw new Error(
78
- "Yjs substrate does not support counter annotations. " +
79
- "Use Schema.number() with ReplaceChange instead. " +
77
+ `Yjs substrate does not support "${change.type}" changes. ` +
78
+ `Counter requires a CRDT backend that supports counters (e.g. Loro). ` +
80
79
  `Attempted IncrementChange with amount=${(change as IncrementChange).amount} at path [${pathToString(path)}].`,
81
80
  )
82
81
 
83
82
  case "tree":
84
83
  throw new Error(
85
- "Yjs substrate does not support tree annotations. " +
86
- "Yjs has no native tree type. " +
84
+ `Yjs substrate does not support "${change.type}" changes. ` +
85
+ `Tree requires a CRDT backend that supports trees (e.g. Loro). ` +
87
86
  `Attempted TreeChange at path [${pathToString(path)}].`,
88
87
  )
89
88
 
@@ -146,7 +145,7 @@ function applySequenceChange(
146
145
  // cursor stays — deleted items shift remaining items down
147
146
  } else if ("insert" in instruction) {
148
147
  const items = instruction.insert as readonly unknown[]
149
- const yjsItems = items.map((item) =>
148
+ const yjsItems = items.map(item =>
150
149
  maybeCreateSharedType(item, itemSchema),
151
150
  )
152
151
  resolved.insert(cursor, yjsItems)
@@ -251,26 +250,16 @@ function maybeCreateSharedType(
251
250
  ): unknown {
252
251
  if (schema === undefined) return value
253
252
 
254
- const structural = unwrapAnnotations(schema)
255
- const tag = schema._kind === "annotated" ? schema.tag : undefined
256
-
257
- // Annotated text Y.Text
258
- if (tag === "text") {
259
- const text = new Y.Text()
260
- if (typeof value === "string" && value.length > 0) {
261
- text.insert(0, value)
253
+ switch (schema[KIND]) {
254
+ // First-class text Y.Text
255
+ case "text": {
256
+ const text = new Y.Text()
257
+ if (typeof value === "string" && value.length > 0) {
258
+ text.insert(0, value)
259
+ }
260
+ return text
262
261
  }
263
- return text
264
- }
265
262
 
266
- // Annotated counter/movable/tree → should not reach here (thrown earlier)
267
- if (tag === "counter" || tag === "movable" || tag === "tree") {
268
- throw new Error(
269
- `Yjs substrate does not support "${tag}" annotations.`,
270
- )
271
- }
272
-
273
- switch (structural._kind) {
274
263
  case "product": {
275
264
  if (
276
265
  value === null ||
@@ -280,17 +269,14 @@ function maybeCreateSharedType(
280
269
  ) {
281
270
  return value
282
271
  }
283
- return createStructuredMap(
284
- value as Record<string, unknown>,
285
- structural,
286
- )
272
+ return createStructuredMap(value as Record<string, unknown>, schema)
287
273
  }
288
274
 
289
275
  case "sequence": {
290
276
  if (!Array.isArray(value)) return value
291
277
  const arr = new Y.Array()
292
- const itemSchema = structural.item
293
- const items = (value as unknown[]).map((item) =>
278
+ const itemSchema = schema.item
279
+ const items = (value as unknown[]).map(item =>
294
280
  maybeCreateSharedType(item, itemSchema),
295
281
  )
296
282
  arr.insert(0, items)
@@ -307,17 +293,26 @@ function maybeCreateSharedType(
307
293
  return value
308
294
  }
309
295
  const map = new Y.Map()
310
- const valueSchema = structural.item
311
- for (const [k, v] of Object.entries(
312
- value as Record<string, unknown>,
313
- )) {
296
+ const valueSchema = schema.item
297
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
314
298
  map.set(k, maybeCreateSharedType(v, valueSchema))
315
299
  }
316
300
  return map
317
301
  }
318
302
 
303
+ // Unsupported first-class CRDT types — should not reach here
304
+ // (rejected at bind time by caps check)
305
+ case "counter":
306
+ case "set":
307
+ case "tree":
308
+ case "movable":
309
+ throw new Error(
310
+ `Yjs substrate does not support [KIND]="${schema[KIND]}". ` +
311
+ `This should have been caught at bind() time.`,
312
+ )
313
+
319
314
  default:
320
- // Scalar, sum, or other — return as plain value
315
+ // Scalar, sum — return as plain value
321
316
  return value
322
317
  }
323
318
  }
@@ -334,9 +329,8 @@ function createStructuredMap(
334
329
  productSchema: SchemaNode,
335
330
  ): Y.Map<any> {
336
331
  const map = new Y.Map()
337
- const structural = unwrapAnnotations(productSchema)
338
332
 
339
- if (structural._kind !== "product") {
333
+ if (productSchema[KIND] !== "product") {
340
334
  // Fallback: set all values as plain
341
335
  for (const [key, val] of Object.entries(obj)) {
342
336
  map.set(key, val)
@@ -347,28 +341,22 @@ function createStructuredMap(
347
341
  // Process fields present in the value object
348
342
  for (const [key, val] of Object.entries(obj)) {
349
343
  if (val === undefined) continue
350
- const fieldSchema = structural.fields[key]
351
- const yjsVal = fieldSchema
352
- ? maybeCreateSharedType(val, fieldSchema)
353
- : val
344
+ const fieldSchema = productSchema.fields[key]
345
+ const yjsVal = fieldSchema ? maybeCreateSharedType(val, fieldSchema) : val
354
346
  map.set(key, yjsVal)
355
347
  }
356
348
 
357
- // Create shared types for annotated fields declared in the schema
349
+ // Create shared types for first-class CRDT fields declared in the schema
358
350
  // but missing from the value object. This ensures Yjs containers
359
351
  // exist for later mutation (e.g. .insert() on a text field inside
360
352
  // a struct inside a record/list).
361
353
  for (const [key, fieldSchema] of Object.entries(
362
- structural.fields as Record<string, SchemaNode>,
354
+ productSchema.fields as Record<string, SchemaNode>,
363
355
  )) {
364
356
  if (key in obj) continue // already processed above
365
- const tag =
366
- fieldSchema._kind === "annotated" ? fieldSchema.tag : undefined
367
- if (tag === "text") {
357
+ if (fieldSchema[KIND] === "text") {
368
358
  map.set(key, new Y.Text())
369
359
  }
370
- // Other annotated container types (counter, movable, tree) are
371
- // unsupported in Yjs and will throw if used elsewhere.
372
360
  }
373
361
 
374
362
  return map
@@ -391,7 +379,7 @@ function createStructuredMap(
391
379
  *
392
380
  * @param events - The events from the `observeDeep` callback
393
381
  */
394
- export function eventsToOps(events: Y.YEvent<any>[]): Op[] {
382
+ export function eventsToOps(events: Y.YEvent<any>[], schema: SchemaNode): Op[] {
395
383
  const ops: Op[] = []
396
384
 
397
385
  for (const event of events) {
@@ -402,7 +390,7 @@ export function eventsToOps(events: Y.YEvent<any>[]): Op[] {
402
390
  }
403
391
  }
404
392
 
405
- return expandMapOpsToLeaves(ops)
393
+ return expandMapOpsToLeaves(ops, schema)
406
394
  }
407
395
 
408
396
  // ---------------------------------------------------------------------------
@@ -512,18 +500,16 @@ function mapEventToChange(event: Y.YEvent<any>): MapChange | null {
512
500
 
513
501
  const target = event.target as Y.Map<any>
514
502
 
515
- event.changes.keys.forEach(
516
- (change: { action: string }, key: string) => {
517
- if (change.action === "add" || change.action === "update") {
518
- const value = target.get(key)
519
- set[key] = extractEventValue(value)
520
- hasSet = true
521
- } else if (change.action === "delete") {
522
- deleteKeys.push(key)
523
- hasDelete = true
524
- }
525
- },
526
- )
503
+ event.changes.keys.forEach((change: { action: string }, key: string) => {
504
+ if (change.action === "add" || change.action === "update") {
505
+ const value = target.get(key)
506
+ set[key] = extractEventValue(value)
507
+ hasSet = true
508
+ } else if (change.action === "delete") {
509
+ deleteKeys.push(key)
510
+ hasDelete = true
511
+ }
512
+ })
527
513
 
528
514
  if (!hasSet && !hasDelete) return null
529
515
 
@@ -554,17 +540,6 @@ function extractEventValue(value: unknown): unknown {
554
540
  // Schema helpers
555
541
  // ---------------------------------------------------------------------------
556
542
 
557
- /**
558
- * Unwrap annotation wrappers to reach the structural schema node.
559
- */
560
- function unwrapAnnotations(schema: SchemaNode): SchemaNode {
561
- let s = schema
562
- while (s._kind === "annotated" && s.schema !== undefined) {
563
- s = s.schema
564
- }
565
- return s
566
- }
567
-
568
543
  /**
569
544
  * Resolve the schema at a given path by walking through advanceSchema.
570
545
  */
@@ -580,8 +555,9 @@ function resolveSchemaAtPath(rootSchema: SchemaNode, path: Path): SchemaNode {
580
555
  * Get the item schema from a sequence schema, if available.
581
556
  */
582
557
  function getItemSchema(schema: SchemaNode): SchemaNode | undefined {
583
- const structural = unwrapAnnotations(schema)
584
- return structural._kind === "sequence" ? structural.item : undefined
558
+ if (schema[KIND] === "sequence") return schema.item
559
+ if (schema[KIND] === "movable") return schema.item
560
+ return undefined
585
561
  }
586
562
 
587
563
  /**
@@ -591,12 +567,14 @@ function getFieldSchema(
591
567
  schema: SchemaNode,
592
568
  key: string,
593
569
  ): SchemaNode | undefined {
594
- const structural = unwrapAnnotations(schema)
595
- if (structural._kind === "product") {
596
- return structural.fields[key]
570
+ if (schema[KIND] === "product") {
571
+ return schema.fields[key]
572
+ }
573
+ if (schema[KIND] === "map") {
574
+ return schema.item
597
575
  }
598
- if (structural._kind === "map") {
599
- return structural.item
576
+ if (schema[KIND] === "set") {
577
+ return schema.item
600
578
  }
601
579
  return undefined
602
580
  }
@@ -606,7 +584,5 @@ function getFieldSchema(
606
584
  // ---------------------------------------------------------------------------
607
585
 
608
586
  function pathToString(path: Path): string {
609
- return path.segments
610
- .map((seg) => String(seg.resolve()))
611
- .join(".")
587
+ return path.segments.map(seg => String(seg.resolve())).join(".")
612
588
  }
package/src/create.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  // create — batteries-included document construction backed by YjsSubstrate.
2
2
  //
3
- // Provides `createYjsDoc` and `createYjsDocFromSnapshot` functions that
3
+ // Provides `createYjsDoc` and `createYjsDocFromEntirety` functions that
4
4
  // hide the interpret pipeline and layer composition behind a single call.
5
5
  //
6
6
  // Internally tracks substrates via a module-scoped WeakMap so that sync
7
- // primitives (`version`, `exportSnapshot`, `importDelta` in sync.ts)
7
+ // primitives (`version`, `exportEntirety`, `merge` in sync.ts)
8
8
  // can retrieve the substrate from just a doc ref.
9
9
  //
10
10
  // `getSubstrate` is exported for use by `sync.ts` but is NOT re-exported
@@ -14,14 +14,22 @@
14
14
  // createYjsDoc(schema, yjsDoc) — "bring your own doc" (wrap existing)
15
15
  // createYjsDoc(schema) — create a fresh empty Y.Doc
16
16
 
17
- import { interpret, registerSubstrate } from "@kyneta/schema"
18
- import { changefeed, readable, writable } from "@kyneta/schema"
19
- import type { Ref } from "@kyneta/schema"
20
- import type { Schema as SchemaType } from "@kyneta/schema"
21
- import type { Substrate, SubstratePayload } from "@kyneta/schema"
22
- import * as Y from "yjs"
23
- import { YjsVersion } from "./version.js"
17
+ import type {
18
+ Ref,
19
+ Schema as SchemaType,
20
+ Substrate,
21
+ SubstratePayload,
22
+ } from "@kyneta/schema"
23
+ import {
24
+ interpret,
25
+ observation,
26
+ readable,
27
+ registerSubstrate,
28
+ writable,
29
+ } from "@kyneta/schema"
30
+ import type * as Y from "yjs"
24
31
  import { createYjsSubstrate, yjsSubstrateFactory } from "./substrate.js"
32
+ import type { YjsVersion } from "./version.js"
25
33
 
26
34
  // ---------------------------------------------------------------------------
27
35
  // Substrate tracking (module-scoped)
@@ -31,18 +39,18 @@ const substrates = new WeakMap<object, Substrate<YjsVersion>>()
31
39
 
32
40
  /**
33
41
  * Retrieve the substrate associated with a doc created by `createYjsDoc`
34
- * or `createYjsDocFromSnapshot`.
42
+ * or `createYjsDocFromEntirety`.
35
43
  *
36
44
  * Exported for `sync.ts` — NOT re-exported from the barrel.
37
45
  *
38
- * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromSnapshot`.
46
+ * @throws If `doc` was not created by `createYjsDoc` / `createYjsDocFromEntirety`.
39
47
  */
40
48
  export function getSubstrate(doc: object): Substrate<YjsVersion> {
41
49
  const s = substrates.get(doc)
42
50
  if (!s) {
43
51
  throw new Error(
44
- "version/exportSnapshot/importDelta called on an object without a YjsSubstrate. " +
45
- "Use a doc created by createYjsDoc() or createYjsDocFromSnapshot().",
52
+ "version/exportEntirety/merge called on an object without a YjsSubstrate. " +
53
+ "Use a doc created by createYjsDoc() or createYjsDocFromEntirety().",
46
54
  )
47
55
  }
48
56
  return s
@@ -58,12 +66,12 @@ function registerDoc(
58
66
  ): any {
59
67
  // The `as any` on the builder avoids TS2589 — interpret's fluent API
60
68
  // produces deeply recursive types when S is the abstract SchemaType.
61
- // The public createYjsDoc/createYjsDocFromSnapshot signatures provide
69
+ // The public createYjsDoc/createYjsDocFromEntirety signatures provide
62
70
  // the correct Ref<S> return type via interface call signature patterns.
63
71
  const doc: any = (interpret as any)(schema, substrate.context())
64
72
  .with(readable)
65
73
  .with(writable)
66
- .with(changefeed)
74
+ .with(observation)
67
75
  .done()
68
76
  substrates.set(doc, substrate)
69
77
  // Also register in the general unwrap() registry so that the
@@ -123,17 +131,14 @@ function isYDoc(value: unknown): value is Y.Doc {
123
131
  * CRDT collaboration support.
124
132
  *
125
133
  * The returned ref observes **all** mutations to the underlying Y.Doc,
126
- * regardless of source (local kyneta writes, importDelta, external
134
+ * regardless of source (local kyneta writes, merge, external
127
135
  * `Y.applyUpdate()`, external raw Yjs API mutations).
128
136
  *
129
137
  * @param schema - The schema describing the document structure.
130
138
  * @param doc - Optional `Y.Doc` instance to wrap. If omitted, a fresh
131
139
  * empty Y.Doc is created with containers matching the schema.
132
140
  */
133
- type CreateYjsDoc = <S extends SchemaType>(
134
- schema: S,
135
- doc?: Y.Doc,
136
- ) => Ref<S>
141
+ type CreateYjsDoc = <S extends SchemaType>(schema: S, doc?: Y.Doc) => Ref<S>
137
142
 
138
143
  export const createYjsDoc: CreateYjsDoc = (schema, doc) => {
139
144
  if (doc !== undefined && isYDoc(doc)) {
@@ -145,28 +150,28 @@ export const createYjsDoc: CreateYjsDoc = (schema, doc) => {
145
150
  }
146
151
 
147
152
  // ---------------------------------------------------------------------------
148
- // createYjsDocFromSnapshot
153
+ // createYjsDocFromEntirety
149
154
  // ---------------------------------------------------------------------------
150
155
 
151
- type CreateYjsDocFromSnapshot = <S extends SchemaType>(
156
+ type CreateYjsDocFromEntirety = <S extends SchemaType>(
152
157
  schema: S,
153
158
  payload: SubstratePayload,
154
159
  ) => Ref<S>
155
160
 
156
161
  /**
157
- * Reconstruct a live Yjs-backed document from a substrate snapshot payload.
162
+ * Reconstruct a live Yjs-backed document from a substrate entirety payload.
158
163
  *
159
- * The payload must have been produced by `exportSnapshot()` on a
164
+ * The payload must have been produced by `exportEntirety()` on a
160
165
  * compatible document. This is the entry point for SSR hydration
161
166
  * and reconnection past log compaction.
162
167
  *
163
168
  * ```ts
164
- * const payload = exportSnapshot(docA)
165
- * const docB = createYjsDocFromSnapshot(MySchema, payload)
169
+ * const payload = exportEntirety(docA)
170
+ * const docB = createYjsDocFromEntirety(MySchema, payload)
166
171
  * // docB has the same state as docA at the time of export
167
172
  * ```
168
173
  */
169
- export const createYjsDocFromSnapshot: CreateYjsDocFromSnapshot = (
174
+ export const createYjsDocFromEntirety: CreateYjsDocFromEntirety = (
170
175
  schema,
171
176
  payload,
172
- ) => registerDoc(schema, yjsSubstrateFactory.fromSnapshot(payload, schema))
177
+ ) => registerDoc(schema, yjsSubstrateFactory.fromEntirety(payload, schema))