@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.
@@ -0,0 +1,362 @@
1
+ // structural-merge — Yjs structural merge protocol tests.
2
+ //
3
+ // Validates the core invariant: all peers using the same schema produce
4
+ // byte-identical structural ops via STRUCTURAL_YJS_CLIENT_ID (0),
5
+ // which Yjs deduplicates on merge instead of conflicting.
6
+ //
7
+ // Context: jj:ptyzqoul (structural merge protocol)
8
+
9
+ import { BACKING_DOC, Schema, STRUCTURAL_YJS_CLIENT_ID } from "@kyneta/schema"
10
+ import { describe, expect, it } from "vitest"
11
+ import * as Y from "yjs"
12
+ import { bindYjs } from "../bind-yjs.js"
13
+ import { ensureContainers } from "../populate.js"
14
+ import { createYjsSubstrate, yjsSubstrateFactory } from "../substrate.js"
15
+
16
+ // ===========================================================================
17
+ // Schemas used across tests
18
+ // ===========================================================================
19
+
20
+ const TestSchema = Schema.doc({
21
+ title: Schema.annotated("text"),
22
+ count: Schema.number(),
23
+ items: Schema.list(Schema.string()),
24
+ })
25
+
26
+ // ===========================================================================
27
+ // Tests
28
+ // ===========================================================================
29
+
30
+ describe("structural merge protocol (Yjs)", () => {
31
+ // ── Core invariant: two peers independently create, sync, no data loss ──
32
+
33
+ it("two peers independently create same schema, sync, data preserved", () => {
34
+ // Peer A creates doc and writes text
35
+ const docA = new Y.Doc()
36
+ docA.clientID = 100
37
+ ensureContainers(docA, TestSchema)
38
+ docA.transact(() => {
39
+ const root = docA.getMap("root")
40
+ ;(root.get("title") as Y.Text).insert(0, "Hello from A")
41
+ root.set("count", 42)
42
+ })
43
+
44
+ // Peer B independently creates same doc and writes different text
45
+ const docB = new Y.Doc()
46
+ docB.clientID = 200
47
+ ensureContainers(docB, TestSchema)
48
+ docB.transact(() => {
49
+ const root = docB.getMap("root")
50
+ ;(root.get("title") as Y.Text).insert(0, "Hello from B")
51
+ root.set("count", 99)
52
+ })
53
+
54
+ // Sync bidirectionally
55
+ const updateA = Y.encodeStateAsUpdate(docA)
56
+ const updateB = Y.encodeStateAsUpdate(docB)
57
+ Y.applyUpdate(docB, updateA)
58
+ Y.applyUpdate(docA, updateB)
59
+
60
+ // Both see the same state — no data loss
61
+ const rootA = docA.getMap("root")
62
+ const rootB = docB.getMap("root")
63
+
64
+ // Text should contain content from both peers (no orphaned containers)
65
+ const titleA = (rootA.get("title") as Y.Text).toString()
66
+ const titleB = (rootB.get("title") as Y.Text).toString()
67
+ expect(titleA).toBe(titleB)
68
+ // Both texts should be present (merged, not lost)
69
+ expect(titleA).toContain("Hello from A")
70
+ expect(titleA).toContain("Hello from B")
71
+
72
+ // Count: last writer wins — both converge to the same value
73
+ expect(rootA.get("count")).toBe(rootB.get("count"))
74
+ })
75
+
76
+ it("three peers independently create, sync, all converge", () => {
77
+ const docs = [100, 200, 300].map(id => {
78
+ const doc = new Y.Doc()
79
+ doc.clientID = id
80
+ ensureContainers(doc, TestSchema)
81
+ doc.transact(() => {
82
+ const root = doc.getMap("root")
83
+ ;(root.get("title") as Y.Text).insert(0, `Peer${id}`)
84
+ })
85
+ return doc
86
+ })
87
+
88
+ // Full mesh sync
89
+ for (let i = 0; i < docs.length; i++) {
90
+ for (let j = 0; j < docs.length; j++) {
91
+ if (i !== j) {
92
+ Y.applyUpdate(docs[j]!, Y.encodeStateAsUpdate(docs[i]!))
93
+ }
94
+ }
95
+ }
96
+
97
+ // All three converge to the same text
98
+ const texts = docs.map(d =>
99
+ (d.getMap("root").get("title") as Y.Text).toString(),
100
+ )
101
+ expect(texts[0]).toBe(texts[1])
102
+ expect(texts[1]).toBe(texts[2])
103
+ // All three peer contributions present
104
+ expect(texts[0]).toContain("Peer100")
105
+ expect(texts[0]).toContain("Peer200")
106
+ expect(texts[0]).toContain("Peer300")
107
+ })
108
+
109
+ // ── Persistence round-trip ──
110
+
111
+ it("persist → hydrate → data preserved", () => {
112
+ // Create and write
113
+ const doc1 = new Y.Doc()
114
+ doc1.clientID = 42
115
+ ensureContainers(doc1, TestSchema)
116
+ doc1.transact(() => {
117
+ const root = doc1.getMap("root")
118
+ ;(root.get("title") as Y.Text).insert(0, "Persistent")
119
+ root.set("count", 7)
120
+ })
121
+
122
+ // Export
123
+ const snapshot = Y.encodeStateAsUpdate(doc1)
124
+
125
+ // Hydrate into fresh doc
126
+ const doc2 = new Y.Doc()
127
+ doc2.clientID = 42
128
+ Y.applyUpdate(doc2, snapshot)
129
+ ensureContainers(doc2, TestSchema, true) // conditional
130
+
131
+ // Data preserved
132
+ const root2 = doc2.getMap("root")
133
+ expect((root2.get("title") as Y.Text).toString()).toBe("Persistent")
134
+ expect(root2.get("count")).toBe(7)
135
+ })
136
+
137
+ // ── Determinism: alphabetical sort ──
138
+
139
+ it("field reordering in source doesn't affect structural ops", () => {
140
+ // Schema A: fields in one order
141
+ const schemaA = Schema.doc({
142
+ alpha: Schema.string(),
143
+ beta: Schema.number(),
144
+ gamma: Schema.annotated("text"),
145
+ })
146
+
147
+ // Schema B: same fields, different insertion order
148
+ // JavaScript objects preserve insertion order, so we construct
149
+ // with a different order to verify alphabetical sort overrides it.
150
+ const fields: Record<string, any> = {}
151
+ fields.gamma = Schema.annotated("text")
152
+ fields.alpha = Schema.string()
153
+ fields.beta = Schema.number()
154
+ const schemaB = Schema.doc(fields)
155
+
156
+ const docA = new Y.Doc()
157
+ ensureContainers(docA, schemaA)
158
+
159
+ const docB = new Y.Doc()
160
+ ensureContainers(docB, schemaB)
161
+
162
+ // Both should produce byte-identical structural state
163
+ const stateA = Y.encodeStateAsUpdate(docA)
164
+ const stateB = Y.encodeStateAsUpdate(docB)
165
+ expect(stateA).toEqual(stateB)
166
+ })
167
+
168
+ // ── Schema evolution ──
169
+
170
+ it("add field after hydration, structural ops extend correctly", () => {
171
+ const v1Schema = Schema.doc({
172
+ title: Schema.annotated("text"),
173
+ count: Schema.number(),
174
+ })
175
+
176
+ const v2Schema = Schema.doc({
177
+ count: Schema.number(),
178
+ notes: Schema.annotated("text"), // new field
179
+ title: Schema.annotated("text"),
180
+ })
181
+
182
+ // Peer A: create v1, write data, export
183
+ const docA = new Y.Doc()
184
+ docA.clientID = 100
185
+ ensureContainers(docA, v1Schema)
186
+ docA.transact(() => {
187
+ const root = docA.getMap("root")
188
+ ;(root.get("title") as Y.Text).insert(0, "Title")
189
+ root.set("count", 5)
190
+ })
191
+ const v1State = Y.encodeStateAsUpdate(docA)
192
+
193
+ // Peer B: independently create v2, hydrate v1 data, conditional containers
194
+ const docB = new Y.Doc()
195
+ docB.clientID = 200
196
+ Y.applyUpdate(docB, v1State)
197
+ ensureContainers(docB, v2Schema, true) // conditional — only creates "notes"
198
+
199
+ // Another peer C: same thing independently
200
+ const docC = new Y.Doc()
201
+ docC.clientID = 300
202
+ Y.applyUpdate(docC, v1State)
203
+ ensureContainers(docC, v2Schema, true)
204
+
205
+ // B and C's structural ops for "notes" should be identical (both at clientID 0)
206
+ const stateB = Y.encodeStateAsUpdate(docB)
207
+ const stateC = Y.encodeStateAsUpdate(docC)
208
+
209
+ // Merge B into C and C into B — should converge without conflict
210
+ Y.applyUpdate(docC, stateB)
211
+ Y.applyUpdate(docB, stateC)
212
+
213
+ const rootB = docB.getMap("root")
214
+ const rootC = docC.getMap("root")
215
+
216
+ // Original data preserved
217
+ expect((rootB.get("title") as Y.Text).toString()).toBe("Title")
218
+ expect(rootB.get("count")).toBe(5)
219
+ expect((rootC.get("title") as Y.Text).toString()).toBe("Title")
220
+ expect(rootC.get("count")).toBe(5)
221
+
222
+ // New field exists on both
223
+ expect(rootB.get("notes")).toBeInstanceOf(Y.Text)
224
+ expect(rootC.get("notes")).toBeInstanceOf(Y.Text)
225
+ })
226
+
227
+ // ── Structural identity is clientID 0 ──
228
+
229
+ it("ensureContainers uses clientID 0 for structural ops", () => {
230
+ const doc = new Y.Doc()
231
+ doc.clientID = 999
232
+ ensureContainers(doc, TestSchema)
233
+
234
+ // clientID should be restored
235
+ expect(doc.clientID).toBe(999)
236
+
237
+ // Structural ops should be at clientID 0
238
+ const sv = Y.decodeStateVector(Y.encodeStateVector(doc))
239
+ expect(sv.get(STRUCTURAL_YJS_CLIENT_ID)).toBeGreaterThan(0)
240
+ })
241
+
242
+ it("ensureContainers does not leak caller clientID into structural ops", () => {
243
+ const doc = new Y.Doc()
244
+ doc.clientID = 777
245
+ ensureContainers(doc, TestSchema)
246
+
247
+ // The state vector should NOT contain the caller's clientID —
248
+ // only STRUCTURAL_YJS_CLIENT_ID (0) should have produced ops.
249
+ const sv = Y.decodeStateVector(Y.encodeStateVector(doc))
250
+ expect(sv.has(STRUCTURAL_YJS_CLIENT_ID)).toBe(true)
251
+ expect(sv.has(777)).toBe(false)
252
+ })
253
+
254
+ // ── SubstrateFactory integration ──
255
+
256
+ it("yjsSubstrateFactory.create produces deterministic structural ops", () => {
257
+ const sub1 = yjsSubstrateFactory.create(TestSchema)
258
+ const sub2 = yjsSubstrateFactory.create(TestSchema)
259
+
260
+ const state1 = sub1.exportEntirety()
261
+ const state2 = sub2.exportEntirety()
262
+
263
+ // Byte-identical structural state
264
+ expect(state1.data).toEqual(state2.data)
265
+ })
266
+
267
+ it("yjsSubstrateFactory.fromEntirety preserves data through round-trip", () => {
268
+ const sub1 = yjsSubstrateFactory.create(TestSchema)
269
+ const doc1 = (sub1 as any)[BACKING_DOC] as Y.Doc
270
+ doc1.transact(() => {
271
+ const root = doc1.getMap("root")
272
+ ;(root.get("title") as Y.Text).insert(0, "Round-trip")
273
+ root.set("count", 123)
274
+ })
275
+
276
+ const payload = sub1.exportEntirety()
277
+ const sub2 = yjsSubstrateFactory.fromEntirety(payload, TestSchema)
278
+ const doc2 = (sub2 as any)[BACKING_DOC] as Y.Doc
279
+ const root2 = doc2.getMap("root")
280
+
281
+ expect((root2.get("title") as Y.Text).toString()).toBe("Round-trip")
282
+ expect(root2.get("count")).toBe(123)
283
+ })
284
+
285
+ // ── bindYjs integration ──
286
+
287
+ it("bindYjs factory produces deterministic structural ops across peers", () => {
288
+ const bound = bindYjs(TestSchema)
289
+
290
+ const factoryA = bound.factory({ peerId: "alice" })
291
+ const factoryB = bound.factory({ peerId: "bob" })
292
+
293
+ const subA = factoryA.create(TestSchema)
294
+ const subB = factoryB.create(TestSchema)
295
+
296
+ // Different peerIds but same structural ops
297
+ const stateA = subA.exportEntirety()
298
+ const stateB = subB.exportEntirety()
299
+
300
+ // Structural state is byte-identical (same schema → same containers at clientID 0)
301
+ expect(stateA.data).toEqual(stateB.data)
302
+ })
303
+
304
+ it("bindYjs peers merge without structural conflict", () => {
305
+ const bound = bindYjs(TestSchema)
306
+
307
+ const factoryA = bound.factory({ peerId: "alice" })
308
+ const factoryB = bound.factory({ peerId: "bob" })
309
+
310
+ const subA = factoryA.create(TestSchema)
311
+ const subB = factoryB.create(TestSchema)
312
+
313
+ // Write different data on each peer
314
+ const docA = (subA as any)[BACKING_DOC] as Y.Doc
315
+ docA.transact(() => {
316
+ const root = docA.getMap("root")
317
+ ;(root.get("title") as Y.Text).insert(0, "Alice's text")
318
+ root.set("count", 10)
319
+ })
320
+
321
+ const docB = (subB as any)[BACKING_DOC] as Y.Doc
322
+ docB.transact(() => {
323
+ const root = docB.getMap("root")
324
+ ;(root.get("title") as Y.Text).insert(0, "Bob's text")
325
+ root.set("count", 20)
326
+ })
327
+
328
+ // Bidirectional merge — should not throw
329
+ const payloadA = subA.exportEntirety()
330
+ const payloadB = subB.exportEntirety()
331
+ subA.merge(payloadB, "sync")
332
+ subB.merge(payloadA, "sync")
333
+
334
+ // Both converge
335
+ const rootA = docA.getMap("root")
336
+ const rootB = docB.getMap("root")
337
+
338
+ const titleA = (rootA.get("title") as Y.Text).toString()
339
+ const titleB = (rootB.get("title") as Y.Text).toString()
340
+ expect(titleA).toBe(titleB)
341
+ expect(titleA).toContain("Alice's text")
342
+ expect(titleA).toContain("Bob's text")
343
+
344
+ expect(rootA.get("count")).toBe(rootB.get("count"))
345
+ })
346
+
347
+ // ── Conditional ensureContainers is idempotent ──
348
+
349
+ it("conditional ensureContainers is idempotent on hydrated doc", () => {
350
+ const doc = new Y.Doc()
351
+ doc.clientID = 50
352
+ ensureContainers(doc, TestSchema)
353
+
354
+ const stateBefore = Y.encodeStateAsUpdate(doc)
355
+
356
+ // Conditional call should not create new ops (everything already exists)
357
+ ensureContainers(doc, TestSchema, true)
358
+
359
+ const stateAfter = Y.encodeStateAsUpdate(doc)
360
+ expect(stateAfter).toEqual(stateBefore)
361
+ })
362
+ })
@@ -1,23 +1,16 @@
1
+ import { change, RawPath, Schema, subscribe } from "@kyneta/schema"
1
2
  import { describe, expect, it, vi } from "vitest"
2
3
  import * as Y from "yjs"
3
- import { Schema, change, subscribe, RawPath } from "@kyneta/schema"
4
+ import { createYjsDoc, createYjsDocFromEntirety } from "../create.js"
5
+ import { ensureContainers } from "../populate.js"
4
6
  import { createYjsSubstrate, yjsSubstrateFactory } from "../substrate.js"
7
+ import { exportEntirety, exportSince, merge, version } from "../sync.js"
5
8
  import { YjsVersion } from "../version.js"
6
- import { createYjsDoc, createYjsDocFromSnapshot } from "../create.js"
7
- import {
8
- version,
9
- exportSnapshot,
10
- exportSince,
11
- importDelta,
12
- } from "../sync.js"
13
- import { ensureContainers } from "../populate.js"
14
9
 
15
10
  // ===========================================================================
16
11
  // Helpers
17
12
  // ===========================================================================
18
13
 
19
-
20
-
21
14
  // ===========================================================================
22
15
  // Schemas used across tests
23
16
  // ===========================================================================
@@ -66,10 +59,10 @@ describe("YjsSubstrate", () => {
66
59
  describe("factory create", () => {
67
60
  it("creates a substrate with empty containers", () => {
68
61
  const substrate = yjsSubstrateFactory.create(SimpleSchema)
69
- expect(substrate.store.read(RawPath.empty.field("title"))).toBe("")
62
+ expect(substrate.reader.read(RawPath.empty.field("title"))).toBe("")
70
63
  // Plain scalars return structural zeros
71
- expect(substrate.store.read(RawPath.empty.field("count"))).toBe(0)
72
- expect(substrate.store.read(RawPath.empty.field("items"))).toEqual([])
64
+ expect(substrate.reader.read(RawPath.empty.field("count"))).toBe(0)
65
+ expect(substrate.reader.read(RawPath.empty.field("items"))).toEqual([])
73
66
  })
74
67
 
75
68
  it("creates a substrate and populates via change()", () => {
@@ -188,8 +181,10 @@ describe("YjsSubstrate", () => {
188
181
  describe("export/import snapshot", () => {
189
182
  it("exports a binary payload", () => {
190
183
  const doc = createYjsDoc(SimpleSchema)
191
- change(doc, (d: any) => { d.title.insert(0, "Snapshot") })
192
- const payload = exportSnapshot(doc)
184
+ change(doc, (d: any) => {
185
+ d.title.insert(0, "Snapshot")
186
+ })
187
+ const payload = exportEntirety(doc)
193
188
  expect(payload.encoding).toBe("binary")
194
189
  expect(payload.data).toBeInstanceOf(Uint8Array)
195
190
  })
@@ -207,8 +202,8 @@ describe("YjsSubstrate", () => {
207
202
  d.title.insert(5, " World")
208
203
  })
209
204
 
210
- const payload = exportSnapshot(doc1)
211
- const doc2 = createYjsDocFromSnapshot(SimpleSchema, payload)
205
+ const payload = exportEntirety(doc1)
206
+ const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
212
207
 
213
208
  expect(doc2.title()).toBe("Hello World")
214
209
  expect(doc2.count()).toBe(42)
@@ -221,13 +216,12 @@ describe("YjsSubstrate", () => {
221
216
  // -------------------------------------------------------------------------
222
217
 
223
218
  describe("delta sync", () => {
224
- it("exportSince → importDelta syncs state", () => {
219
+ it("exportSince → merge syncs state", () => {
225
220
  const doc1 = createYjsDoc(SimpleSchema)
226
- change(doc1, (d: any) => { d.title.insert(0, "Start") })
227
- const doc2 = createYjsDocFromSnapshot(
228
- SimpleSchema,
229
- exportSnapshot(doc1),
230
- )
221
+ change(doc1, (d: any) => {
222
+ d.title.insert(0, "Start")
223
+ })
224
+ const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
231
225
 
232
226
  const v1Before = version(doc1)
233
227
 
@@ -239,17 +233,14 @@ describe("YjsSubstrate", () => {
239
233
  const delta = exportSince(doc1, v1Before)
240
234
  expect(delta).not.toBeNull()
241
235
 
242
- importDelta(doc2, delta!)
236
+ merge(doc2, delta!)
243
237
  expect(doc2.title()).toBe("Start Edited")
244
238
  expect(doc2.count()).toBe(99)
245
239
  })
246
240
 
247
241
  it("concurrent sync — two substrates converge after bidirectional sync", () => {
248
242
  const doc1 = createYjsDoc(SimpleSchema)
249
- const doc2 = createYjsDocFromSnapshot(
250
- SimpleSchema,
251
- exportSnapshot(doc1),
252
- )
243
+ const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
253
244
 
254
245
  const v1Before = version(doc1)
255
246
  const v2Before = version(doc2)
@@ -271,8 +262,8 @@ describe("YjsSubstrate", () => {
271
262
  const d1to2 = exportSince(doc1, v2Before)
272
263
  const d2to1 = exportSince(doc2, v1Before)
273
264
 
274
- importDelta(doc2, d1to2!)
275
- importDelta(doc1, d2to1!)
265
+ merge(doc2, d1to2!)
266
+ merge(doc1, d2to1!)
276
267
 
277
268
  // Should now be equal
278
269
  expect(version(doc1).compare(version(doc2))).toBe("equal")
@@ -293,13 +284,12 @@ describe("YjsSubstrate", () => {
293
284
  // -------------------------------------------------------------------------
294
285
 
295
286
  describe("changefeed", () => {
296
- it("fires on importDelta", () => {
287
+ it("fires on merge", () => {
297
288
  const doc1 = createYjsDoc(SimpleSchema)
298
- change(doc1, (d: any) => { d.title.insert(0, "A") })
299
- const doc2 = createYjsDocFromSnapshot(
300
- SimpleSchema,
301
- exportSnapshot(doc1),
302
- )
289
+ change(doc1, (d: any) => {
290
+ d.title.insert(0, "A")
291
+ })
292
+ const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
303
293
 
304
294
  const v2Before = version(doc2)
305
295
 
@@ -313,7 +303,7 @@ describe("YjsSubstrate", () => {
313
303
  })
314
304
 
315
305
  const delta = exportSince(doc1, v2Before)
316
- importDelta(doc2, delta!)
306
+ merge(doc2, delta!)
317
307
 
318
308
  expect(received.length).toBeGreaterThanOrEqual(1)
319
309
  expect(doc2.count()).toBe(42)
@@ -354,19 +344,19 @@ describe("YjsSubstrate", () => {
354
344
  expect(received.length).toBe(1)
355
345
  })
356
346
 
357
- it("nested struct field changefeed fires on importDelta", () => {
347
+ it("nested struct field changefeed fires on merge", () => {
358
348
  const doc1 = createYjsDoc(StructListSchema)
359
- const doc2 = createYjsDocFromSnapshot(
349
+ const doc2 = createYjsDocFromEntirety(
360
350
  StructListSchema,
361
- exportSnapshot(doc1),
351
+ exportEntirety(doc1),
362
352
  )
363
353
 
364
354
  // Add a struct item on doc1, sync to doc2
365
355
  change(doc1, (d: any) => {
366
356
  d.tasks.push({ name: "Buy milk", done: false })
367
357
  })
368
- const snap = exportSnapshot(doc1)
369
- const doc2b = createYjsDocFromSnapshot(StructListSchema, snap)
358
+ const snap = exportEntirety(doc1)
359
+ const doc2b = createYjsDocFromEntirety(StructListSchema, snap)
370
360
 
371
361
  const taskB = [...doc2b.tasks][0] as any
372
362
  expect(taskB.done()).toBe(false)
@@ -385,7 +375,7 @@ describe("YjsSubstrate", () => {
385
375
 
386
376
  // Sync the toggle to doc2b
387
377
  const delta = exportSince(doc1, v2)!
388
- importDelta(doc2b, delta)
378
+ merge(doc2b, delta)
389
379
 
390
380
  // Value should be updated
391
381
  expect(taskB.done()).toBe(true)
@@ -396,16 +386,16 @@ describe("YjsSubstrate", () => {
396
386
  unsub()
397
387
  })
398
388
 
399
- it("multi-key struct update fires per-field changefeeds on importDelta", () => {
389
+ it("multi-key struct update fires per-field changefeeds on merge", () => {
400
390
  const doc1 = createYjsDoc(StructListSchema)
401
391
 
402
392
  // Add a struct item, sync to doc2
403
393
  change(doc1, (d: any) => {
404
394
  d.tasks.push({ name: "Buy milk", done: false })
405
395
  })
406
- const doc2 = createYjsDocFromSnapshot(
396
+ const doc2 = createYjsDocFromEntirety(
407
397
  StructListSchema,
408
- exportSnapshot(doc1),
398
+ exportEntirety(doc1),
409
399
  )
410
400
 
411
401
  const taskB = [...doc2.tasks][0] as any
@@ -428,7 +418,7 @@ describe("YjsSubstrate", () => {
428
418
 
429
419
  // Sync to doc2
430
420
  const delta = exportSince(doc1, v2)!
431
- importDelta(doc2, delta)
421
+ merge(doc2, delta)
432
422
 
433
423
  // Both field-level changefeeds should have fired
434
424
  expect(nameChanges.length).toBeGreaterThanOrEqual(1)
@@ -528,9 +518,7 @@ describe("YjsSubstrate", () => {
528
518
  count: Schema.annotated("counter"),
529
519
  })
530
520
 
531
- expect(() =>
532
- yjsSubstrateFactory.create(CounterSchema),
533
- ).toThrow("counter")
521
+ expect(() => yjsSubstrateFactory.create(CounterSchema)).toThrow("counter")
534
522
  })
535
523
 
536
524
  it("movable annotation throws clear error at construction", () => {
@@ -538,9 +526,7 @@ describe("YjsSubstrate", () => {
538
526
  items: Schema.annotated("movable", Schema.list(Schema.string())),
539
527
  })
540
528
 
541
- expect(() =>
542
- yjsSubstrateFactory.create(MovableSchema),
543
- ).toThrow("movable")
529
+ expect(() => yjsSubstrateFactory.create(MovableSchema)).toThrow("movable")
544
530
  })
545
531
 
546
532
  it("tree annotation throws clear error at construction", () => {
@@ -551,21 +537,19 @@ describe("YjsSubstrate", () => {
551
537
  ),
552
538
  })
553
539
 
554
- expect(() =>
555
- yjsSubstrateFactory.create(TreeSchema),
556
- ).toThrow("tree")
540
+ expect(() => yjsSubstrateFactory.create(TreeSchema)).toThrow("tree")
557
541
  })
558
542
  })
559
543
 
560
544
  // -------------------------------------------------------------------------
561
- // fromSnapshot
545
+ // fromEntirety
562
546
  // -------------------------------------------------------------------------
563
547
 
564
- describe("fromSnapshot", () => {
548
+ describe("fromEntirety", () => {
565
549
  it("rejects non-binary payloads", () => {
566
550
  expect(() =>
567
- yjsSubstrateFactory.fromSnapshot(
568
- { encoding: "json", data: "{}" },
551
+ yjsSubstrateFactory.fromEntirety(
552
+ { kind: "entirety", encoding: "json", data: "{}" },
569
553
  SimpleSchema,
570
554
  ),
571
555
  ).toThrow("binary")
@@ -579,8 +563,8 @@ describe("YjsSubstrate", () => {
579
563
  d.items.push("x")
580
564
  })
581
565
 
582
- const payload = exportSnapshot(doc)
583
- const doc2 = createYjsDocFromSnapshot(SimpleSchema, payload)
566
+ const payload = exportEntirety(doc)
567
+ const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
584
568
 
585
569
  expect(doc2.title()).toBe("Snapshot Test")
586
570
  expect(doc2.count()).toBe(77)
@@ -601,4 +585,4 @@ describe("YjsSubstrate", () => {
601
585
  expect(parsed.compare(v)).toBe("equal")
602
586
  })
603
587
  })
604
- })
588
+ })
@@ -36,7 +36,7 @@ describe("YjsVersion", () => {
36
36
  })
37
37
 
38
38
  it("round-trips a version vector with one peer", () => {
39
- const v = versionAfterOps((doc) => {
39
+ const v = versionAfterOps(doc => {
40
40
  doc.getMap("root").set("title", "Hello")
41
41
  })
42
42
  const serialized = v.serialize()
@@ -66,7 +66,7 @@ describe("YjsVersion", () => {
66
66
  })
67
67
 
68
68
  it("serialized form is a non-empty string", () => {
69
- const v = versionAfterOps((doc) => {
69
+ const v = versionAfterOps(doc => {
70
70
  doc.getMap("root").set("count", 42)
71
71
  })
72
72
  const s = v.serialize()
@@ -89,7 +89,7 @@ describe("YjsVersion", () => {
89
89
 
90
90
  describe("compare", () => {
91
91
  it("returns 'equal' for the same version vector", () => {
92
- const v = versionAfterOps((doc) => {
92
+ const v = versionAfterOps(doc => {
93
93
  doc.getMap("root").set("t", "hi")
94
94
  })
95
95
  expect(v.compare(v)).toBe("equal")
@@ -152,10 +152,7 @@ describe("YjsVersion", () => {
152
152
  doc2.getMap("root").set("t", "B")
153
153
 
154
154
  // Sync doc1 → doc2 only (doc2 knows about both, doc1 only knows itself)
155
- const update = Y.encodeStateAsUpdate(
156
- doc1,
157
- Y.encodeStateVector(doc2),
158
- )
155
+ const update = Y.encodeStateAsUpdate(doc1, Y.encodeStateVector(doc2))
159
156
  Y.applyUpdate(doc2, update)
160
157
 
161
158
  const v1 = new YjsVersion(Y.encodeStateVector(doc1))
@@ -175,14 +172,8 @@ describe("YjsVersion", () => {
175
172
  doc2.getMap("root").set("t", "B")
176
173
 
177
174
  // Bidirectional sync
178
- const u1to2 = Y.encodeStateAsUpdate(
179
- doc1,
180
- Y.encodeStateVector(doc2),
181
- )
182
- const u2to1 = Y.encodeStateAsUpdate(
183
- doc2,
184
- Y.encodeStateVector(doc1),
185
- )
175
+ const u1to2 = Y.encodeStateAsUpdate(doc1, Y.encodeStateVector(doc2))
176
+ const u2to1 = Y.encodeStateAsUpdate(doc2, Y.encodeStateVector(doc1))
186
177
  Y.applyUpdate(doc2, u1to2)
187
178
  Y.applyUpdate(doc1, u2to1)
188
179
 
@@ -224,4 +215,4 @@ describe("YjsVersion", () => {
224
215
  expect(late.compare(earlyParsed)).toBe("ahead")
225
216
  })
226
217
  })
227
- })
218
+ })