@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 +110 -108
- package/dist/index.js +171 -109
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/bind-yjs.test.ts +19 -19
- package/src/__tests__/create.test.ts +61 -51
- package/src/__tests__/{store-reader.test.ts → reader.test.ts} +30 -33
- package/src/__tests__/record-text-spike.test.ts +29 -21
- package/src/__tests__/structural-merge.test.ts +362 -0
- package/src/__tests__/substrate.test.ts +48 -64
- package/src/__tests__/version.test.ts +7 -16
- package/src/bind-yjs.ts +46 -25
- package/src/change-mapping.ts +20 -35
- package/src/create.ts +32 -27
- package/src/index.ts +24 -30
- package/src/populate.ts +42 -14
- package/src/{store-reader.ts → reader.ts} +7 -12
- package/src/substrate.ts +139 -40
- package/src/sync.ts +26 -26
- package/src/version.ts +2 -4
- package/src/yjs-escape.ts +19 -35
- package/src/yjs-resolve.ts +4 -10
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { change, Schema, subscribe } from "@kyneta/schema"
|
|
1
2
|
import { describe, expect, it, vi } from "vitest"
|
|
2
3
|
import * as Y from "yjs"
|
|
3
|
-
import {
|
|
4
|
-
import { createYjsDoc, createYjsDocFromSnapshot } from "../create.js"
|
|
5
|
-
import { version, exportSnapshot, exportSince, importDelta } from "../sync.js"
|
|
6
|
-
import { YjsVersion } from "../version.js"
|
|
4
|
+
import { createYjsDoc, createYjsDocFromEntirety } from "../create.js"
|
|
7
5
|
import { ensureContainers } from "../populate.js"
|
|
8
6
|
import { yjsSubstrateFactory } from "../substrate.js"
|
|
7
|
+
import { exportEntirety, exportSince, merge, version } from "../sync.js"
|
|
8
|
+
import { YjsVersion } from "../version.js"
|
|
9
9
|
import { yjs } from "../yjs-escape.js"
|
|
10
10
|
|
|
11
11
|
// ===========================================================================
|
|
@@ -192,10 +192,10 @@ describe("createYjsDoc", () => {
|
|
|
192
192
|
})
|
|
193
193
|
|
|
194
194
|
// ===========================================================================
|
|
195
|
-
//
|
|
195
|
+
// createYjsDocFromEntirety
|
|
196
196
|
// ===========================================================================
|
|
197
197
|
|
|
198
|
-
describe("
|
|
198
|
+
describe("createYjsDocFromEntirety", () => {
|
|
199
199
|
it("reconstructs state from a snapshot", () => {
|
|
200
200
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
201
201
|
change(doc1, (d: any) => {
|
|
@@ -205,8 +205,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
205
205
|
change(doc1, (d: any) => d.items.push("a"))
|
|
206
206
|
change(doc1, (d: any) => d.items.push("b"))
|
|
207
207
|
|
|
208
|
-
const payload =
|
|
209
|
-
const doc2 =
|
|
208
|
+
const payload = exportEntirety(doc1)
|
|
209
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
210
210
|
|
|
211
211
|
expect(doc2.title()).toBe("Snapshot")
|
|
212
212
|
expect(doc2.count()).toBe(42)
|
|
@@ -225,8 +225,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
225
225
|
d.items.push("x")
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
-
const payload =
|
|
229
|
-
const doc2 =
|
|
228
|
+
const payload = exportEntirety(doc1)
|
|
229
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
230
230
|
|
|
231
231
|
expect(doc2.title()).toBe("Start End")
|
|
232
232
|
expect(doc2.count()).toBe(99)
|
|
@@ -243,8 +243,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
243
243
|
change(doc1, (d: any) => d.meta.tags.push("v1"))
|
|
244
244
|
change(doc1, (d: any) => d.meta.tags.push("v2"))
|
|
245
245
|
|
|
246
|
-
const payload =
|
|
247
|
-
const doc2 =
|
|
246
|
+
const payload = exportEntirety(doc1)
|
|
247
|
+
const doc2 = createYjsDocFromEntirety(NestedSchema, payload)
|
|
248
248
|
|
|
249
249
|
expect(doc2.title()).toBe("Nested")
|
|
250
250
|
expect(doc2.meta.author()).toBe("Alice")
|
|
@@ -258,8 +258,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
258
258
|
change(doc1, (d: any) => d.tasks.push({ name: "Task A", done: false }))
|
|
259
259
|
change(doc1, (d: any) => d.tasks.push({ name: "Task B", done: true }))
|
|
260
260
|
|
|
261
|
-
const payload =
|
|
262
|
-
const doc2 =
|
|
261
|
+
const payload = exportEntirety(doc1)
|
|
262
|
+
const doc2 = createYjsDocFromEntirety(StructListSchema, payload)
|
|
263
263
|
|
|
264
264
|
expect(doc2.tasks.length).toBe(2)
|
|
265
265
|
expect((doc2.tasks.at(0) as any).name()).toBe("Task A")
|
|
@@ -271,8 +271,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
271
271
|
change(doc1, (d: any) => {
|
|
272
272
|
d.title.insert(0, "Original")
|
|
273
273
|
})
|
|
274
|
-
const payload =
|
|
275
|
-
const doc2 =
|
|
274
|
+
const payload = exportEntirety(doc1)
|
|
275
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
276
276
|
|
|
277
277
|
change(doc2, (d: any) => {
|
|
278
278
|
d.title.insert(8, " Copy")
|
|
@@ -288,8 +288,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
288
288
|
change(doc1, (d: any) => {
|
|
289
289
|
d.title.insert(0, "Original")
|
|
290
290
|
})
|
|
291
|
-
const payload =
|
|
292
|
-
const doc2 =
|
|
291
|
+
const payload = exportEntirety(doc1)
|
|
292
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
293
293
|
|
|
294
294
|
const received: any[] = []
|
|
295
295
|
subscribe(doc2, (changeset: any) => {
|
|
@@ -330,7 +330,9 @@ describe("sync primitives", () => {
|
|
|
330
330
|
|
|
331
331
|
it("serialize/parse round-trips", () => {
|
|
332
332
|
const doc = createYjsDoc(SimpleSchema)
|
|
333
|
-
change(doc, (d: any) => {
|
|
333
|
+
change(doc, (d: any) => {
|
|
334
|
+
d.title.insert(0, "Test")
|
|
335
|
+
})
|
|
334
336
|
const v = version(doc)
|
|
335
337
|
const serialized = v.serialize()
|
|
336
338
|
const parsed = YjsVersion.parse(serialized)
|
|
@@ -338,22 +340,26 @@ describe("sync primitives", () => {
|
|
|
338
340
|
})
|
|
339
341
|
})
|
|
340
342
|
|
|
341
|
-
describe("
|
|
343
|
+
describe("exportEntirety", () => {
|
|
342
344
|
it("returns a binary payload", () => {
|
|
343
345
|
const doc = createYjsDoc(SimpleSchema)
|
|
344
|
-
change(doc, (d: any) => {
|
|
345
|
-
|
|
346
|
+
change(doc, (d: any) => {
|
|
347
|
+
d.title.insert(0, "Snap")
|
|
348
|
+
})
|
|
349
|
+
const payload = exportEntirety(doc)
|
|
346
350
|
expect(payload.encoding).toBe("binary")
|
|
347
351
|
expect(payload.data).toBeInstanceOf(Uint8Array)
|
|
348
352
|
expect((payload.data as Uint8Array).byteLength).toBeGreaterThan(0)
|
|
349
353
|
})
|
|
350
354
|
})
|
|
351
355
|
|
|
352
|
-
describe("exportSince +
|
|
356
|
+
describe("exportSince + merge", () => {
|
|
353
357
|
it("syncs incremental changes between two docs", () => {
|
|
354
358
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
355
|
-
change(doc1, (d: any) => {
|
|
356
|
-
|
|
359
|
+
change(doc1, (d: any) => {
|
|
360
|
+
d.title.insert(0, "Start")
|
|
361
|
+
})
|
|
362
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
357
363
|
|
|
358
364
|
const v2Before = version(doc2)
|
|
359
365
|
|
|
@@ -369,7 +375,7 @@ describe("sync primitives", () => {
|
|
|
369
375
|
expect(delta).not.toBeNull()
|
|
370
376
|
expect(delta!.encoding).toBe("binary")
|
|
371
377
|
|
|
372
|
-
|
|
378
|
+
merge(doc2, delta!)
|
|
373
379
|
|
|
374
380
|
expect(doc2.title()).toBe("Start Edited")
|
|
375
381
|
expect(doc2.count()).toBe(42)
|
|
@@ -378,37 +384,39 @@ describe("sync primitives", () => {
|
|
|
378
384
|
|
|
379
385
|
it("syncs multiple incremental deltas", () => {
|
|
380
386
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
381
|
-
const doc2 =
|
|
387
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
382
388
|
|
|
383
389
|
// First round
|
|
384
390
|
let vBefore = version(doc2)
|
|
385
391
|
change(doc1, (d: any) => {
|
|
386
392
|
d.title.insert(0, "A")
|
|
387
393
|
})
|
|
388
|
-
|
|
394
|
+
merge(doc2, exportSince(doc1, vBefore)!)
|
|
389
395
|
|
|
390
396
|
// Second round
|
|
391
397
|
vBefore = version(doc2)
|
|
392
398
|
change(doc1, (d: any) => {
|
|
393
399
|
d.title.insert(1, "B")
|
|
394
400
|
})
|
|
395
|
-
|
|
401
|
+
merge(doc2, exportSince(doc1, vBefore)!)
|
|
396
402
|
|
|
397
403
|
// Third round
|
|
398
404
|
vBefore = version(doc2)
|
|
399
405
|
change(doc1, (d: any) => {
|
|
400
406
|
d.count.set(3)
|
|
401
407
|
})
|
|
402
|
-
|
|
408
|
+
merge(doc2, exportSince(doc1, vBefore)!)
|
|
403
409
|
|
|
404
410
|
expect(doc2.title()).toBe("AB")
|
|
405
411
|
expect(doc2.count()).toBe(3)
|
|
406
412
|
})
|
|
407
413
|
|
|
408
|
-
it("changefeed fires on
|
|
414
|
+
it("changefeed fires on merge", () => {
|
|
409
415
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
410
|
-
change(doc1, (d: any) => {
|
|
411
|
-
|
|
416
|
+
change(doc1, (d: any) => {
|
|
417
|
+
d.title.insert(0, "Source")
|
|
418
|
+
})
|
|
419
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
412
420
|
|
|
413
421
|
const v2Before = version(doc2)
|
|
414
422
|
|
|
@@ -423,15 +431,15 @@ describe("sync primitives", () => {
|
|
|
423
431
|
received.push(changeset)
|
|
424
432
|
})
|
|
425
433
|
|
|
426
|
-
|
|
434
|
+
merge(doc2, delta!)
|
|
427
435
|
|
|
428
436
|
expect(received.length).toBeGreaterThanOrEqual(1)
|
|
429
437
|
expect(doc2.count()).toBe(77)
|
|
430
438
|
})
|
|
431
439
|
|
|
432
|
-
it("
|
|
440
|
+
it("merge passes origin to changefeed", () => {
|
|
433
441
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
434
|
-
const doc2 =
|
|
442
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
435
443
|
|
|
436
444
|
const v2Before = version(doc2)
|
|
437
445
|
change(doc1, (d: any) => {
|
|
@@ -443,7 +451,7 @@ describe("sync primitives", () => {
|
|
|
443
451
|
receivedOrigins.push(changeset.origin)
|
|
444
452
|
})
|
|
445
453
|
|
|
446
|
-
|
|
454
|
+
merge(doc2, exportSince(doc1, v2Before)!, "my-sync-origin")
|
|
447
455
|
|
|
448
456
|
expect(receivedOrigins).toContain("my-sync-origin")
|
|
449
457
|
})
|
|
@@ -452,19 +460,21 @@ describe("sync primitives", () => {
|
|
|
452
460
|
describe("versions equal after sync", () => {
|
|
453
461
|
it("versions equal after full snapshot sync", () => {
|
|
454
462
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
455
|
-
change(doc1, (d: any) => {
|
|
463
|
+
change(doc1, (d: any) => {
|
|
464
|
+
d.title.insert(0, "Same")
|
|
465
|
+
})
|
|
456
466
|
change(doc1, (d: any) => {
|
|
457
467
|
d.count.set(42)
|
|
458
468
|
})
|
|
459
469
|
|
|
460
|
-
const doc2 =
|
|
470
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
461
471
|
|
|
462
472
|
expect(version(doc1).compare(version(doc2))).toBe("equal")
|
|
463
473
|
})
|
|
464
474
|
|
|
465
475
|
it("versions equal after bidirectional delta sync", () => {
|
|
466
476
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
467
|
-
const doc2 =
|
|
477
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
468
478
|
|
|
469
479
|
const v1Before = version(doc1)
|
|
470
480
|
const v2Before = version(doc2)
|
|
@@ -480,8 +490,8 @@ describe("sync primitives", () => {
|
|
|
480
490
|
// Bidirectional sync
|
|
481
491
|
const d1to2 = exportSince(doc1, v2Before)
|
|
482
492
|
const d2to1 = exportSince(doc2, v1Before)
|
|
483
|
-
|
|
484
|
-
|
|
493
|
+
merge(doc2, d1to2!)
|
|
494
|
+
merge(doc1, d2to1!)
|
|
485
495
|
|
|
486
496
|
expect(version(doc1).compare(version(doc2))).toBe("equal")
|
|
487
497
|
})
|
|
@@ -496,9 +506,9 @@ describe("full workflow", () => {
|
|
|
496
506
|
it("create → mutate → sync → observe", () => {
|
|
497
507
|
// 1. Create two docs
|
|
498
508
|
const doc1 = createYjsDoc(StructListSchema)
|
|
499
|
-
const doc2 =
|
|
509
|
+
const doc2 = createYjsDocFromEntirety(
|
|
500
510
|
StructListSchema,
|
|
501
|
-
|
|
511
|
+
exportEntirety(doc1),
|
|
502
512
|
)
|
|
503
513
|
|
|
504
514
|
// 2. Set up observer on doc2
|
|
@@ -520,7 +530,7 @@ describe("full workflow", () => {
|
|
|
520
530
|
|
|
521
531
|
// 4. Sync doc1 → doc2
|
|
522
532
|
const delta = exportSince(doc1, vBefore)
|
|
523
|
-
|
|
533
|
+
merge(doc2, delta!)
|
|
524
534
|
|
|
525
535
|
// 5. Verify state converged
|
|
526
536
|
expect(doc2.tasks.length).toBe(2)
|
|
@@ -541,7 +551,7 @@ describe("full workflow", () => {
|
|
|
541
551
|
})
|
|
542
552
|
|
|
543
553
|
const delta2 = exportSince(doc2, v1Before)
|
|
544
|
-
|
|
554
|
+
merge(doc1, delta2!)
|
|
545
555
|
|
|
546
556
|
expect(doc1.tasks.length).toBe(3)
|
|
547
557
|
expect((doc1.tasks.at(2) as any).name()).toBe("Read book")
|
|
@@ -561,10 +571,10 @@ describe("full workflow", () => {
|
|
|
561
571
|
})
|
|
562
572
|
|
|
563
573
|
// 2. Snapshot
|
|
564
|
-
const snapshot =
|
|
574
|
+
const snapshot = exportEntirety(doc1)
|
|
565
575
|
|
|
566
576
|
// 3. Reconstruct
|
|
567
|
-
const doc2 =
|
|
577
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, snapshot)
|
|
568
578
|
expect(doc2.title()).toBe("Start Middle")
|
|
569
579
|
expect(doc2.count()).toBe(10)
|
|
570
580
|
expect(doc2.items()).toEqual(["first"])
|
|
@@ -590,7 +600,7 @@ describe("full workflow", () => {
|
|
|
590
600
|
it("concurrent edits converge correctly", () => {
|
|
591
601
|
// 1. Create two peers from the same initial state
|
|
592
602
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
593
|
-
const doc2 =
|
|
603
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
594
604
|
|
|
595
605
|
const v1Before = version(doc1)
|
|
596
606
|
const v2Before = version(doc2)
|
|
@@ -611,8 +621,8 @@ describe("full workflow", () => {
|
|
|
611
621
|
// 4. Bidirectional sync
|
|
612
622
|
const d1to2 = exportSince(doc1, v2Before)
|
|
613
623
|
const d2to1 = exportSince(doc2, v1Before)
|
|
614
|
-
|
|
615
|
-
|
|
624
|
+
merge(doc2, d1to2!)
|
|
625
|
+
merge(doc1, d2to1!)
|
|
616
626
|
|
|
617
627
|
// 5. Versions converge
|
|
618
628
|
expect(version(doc1).compare(version(doc2))).toBe("equal")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { RawPath, Schema } from "@kyneta/schema"
|
|
1
2
|
import { describe, expect, it } from "vitest"
|
|
2
3
|
import * as Y from "yjs"
|
|
3
|
-
import { RawPath, Schema } from "@kyneta/schema"
|
|
4
|
-
import { yjsStoreReader } from "../store-reader.js"
|
|
5
4
|
import { ensureContainers } from "../populate.js"
|
|
5
|
+
import { yjsReader } from "../reader.js"
|
|
6
6
|
|
|
7
7
|
// ===========================================================================
|
|
8
8
|
// Helpers
|
|
@@ -27,7 +27,7 @@ function setup(
|
|
|
27
27
|
populateSeed(rootMap, schema, seed)
|
|
28
28
|
})
|
|
29
29
|
}
|
|
30
|
-
const reader =
|
|
30
|
+
const reader = yjsReader(doc, schema)
|
|
31
31
|
return { doc, reader }
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -84,9 +84,9 @@ function populateField(
|
|
|
84
84
|
for (const [childKey, childValue] of Object.entries(
|
|
85
85
|
value as Record<string, unknown>,
|
|
86
86
|
)) {
|
|
87
|
-
const childFieldSchema = (
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const childFieldSchema = (structural.fields as Record<string, any>)[
|
|
88
|
+
childKey
|
|
89
|
+
]
|
|
90
90
|
if (!childFieldSchema) continue
|
|
91
91
|
populateField(childMap, childKey, childFieldSchema, childValue)
|
|
92
92
|
}
|
|
@@ -100,10 +100,7 @@ function populateField(
|
|
|
100
100
|
if (arr && Array.isArray(value)) {
|
|
101
101
|
for (const item of value) {
|
|
102
102
|
const itemSchema = structural.item
|
|
103
|
-
if (
|
|
104
|
-
itemSchema &&
|
|
105
|
-
unwrapAnnotations(itemSchema)._kind === "product"
|
|
106
|
-
) {
|
|
103
|
+
if (itemSchema && unwrapAnnotations(itemSchema)._kind === "product") {
|
|
107
104
|
// Struct items: create a Y.Map for each
|
|
108
105
|
const itemMap = buildStructMap(
|
|
109
106
|
unwrapAnnotations(itemSchema),
|
|
@@ -153,8 +150,7 @@ function buildStructMap(
|
|
|
153
150
|
const value = seed[key]
|
|
154
151
|
if (value === undefined) continue
|
|
155
152
|
|
|
156
|
-
const tag =
|
|
157
|
-
fieldSchema._kind === "annotated" ? fieldSchema.tag : undefined
|
|
153
|
+
const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : undefined
|
|
158
154
|
if (tag === "text") {
|
|
159
155
|
const text = new Y.Text()
|
|
160
156
|
if (typeof value === "string" && value.length > 0) {
|
|
@@ -302,7 +298,7 @@ const MixedSchema = Schema.doc({
|
|
|
302
298
|
// Tests
|
|
303
299
|
// ===========================================================================
|
|
304
300
|
|
|
305
|
-
describe("
|
|
301
|
+
describe("YjsReader", () => {
|
|
306
302
|
// -------------------------------------------------------------------------
|
|
307
303
|
// read
|
|
308
304
|
// -------------------------------------------------------------------------
|
|
@@ -347,12 +343,8 @@ describe("YjsStoreReader", () => {
|
|
|
347
343
|
})
|
|
348
344
|
expect(reader.read(p("profile", "first"))).toBe("Jane")
|
|
349
345
|
expect(reader.read(p("profile", "last"))).toBe("Doe")
|
|
350
|
-
expect(
|
|
351
|
-
|
|
352
|
-
).toBe("Portland")
|
|
353
|
-
expect(
|
|
354
|
-
reader.read(p("profile", "address", "zip")),
|
|
355
|
-
).toBe("97201")
|
|
346
|
+
expect(reader.read(p("profile", "address", "city"))).toBe("Portland")
|
|
347
|
+
expect(reader.read(p("profile", "address", "zip"))).toBe("97201")
|
|
356
348
|
})
|
|
357
349
|
|
|
358
350
|
it("reads nested struct as plain object", () => {
|
|
@@ -395,14 +387,9 @@ describe("YjsStoreReader", () => {
|
|
|
395
387
|
{ name: "Task 2", done: true },
|
|
396
388
|
],
|
|
397
389
|
})
|
|
398
|
-
expect(reader.read(p("structs", 0, "name"))).toBe(
|
|
399
|
-
"Task 1",
|
|
400
|
-
)
|
|
390
|
+
expect(reader.read(p("structs", 0, "name"))).toBe("Task 1")
|
|
401
391
|
expect(reader.read(p("structs", 1, "done"))).toBe(true)
|
|
402
|
-
const item = reader.read(p("structs", 0)) as Record<
|
|
403
|
-
string,
|
|
404
|
-
unknown
|
|
405
|
-
>
|
|
392
|
+
const item = reader.read(p("structs", 0)) as Record<string, unknown>
|
|
406
393
|
expect(item.name).toBe("Task 1")
|
|
407
394
|
expect(item.done).toBe(false)
|
|
408
395
|
})
|
|
@@ -468,7 +455,11 @@ describe("YjsStoreReader", () => {
|
|
|
468
455
|
})
|
|
469
456
|
|
|
470
457
|
it("returns 0 for non-list paths", () => {
|
|
471
|
-
const { reader } = setup(ScalarSchema, {
|
|
458
|
+
const { reader } = setup(ScalarSchema, {
|
|
459
|
+
name: "test",
|
|
460
|
+
count: 0,
|
|
461
|
+
active: true,
|
|
462
|
+
})
|
|
472
463
|
expect(reader.arrayLength(p("name"))).toBe(0)
|
|
473
464
|
})
|
|
474
465
|
})
|
|
@@ -516,7 +507,11 @@ describe("YjsStoreReader", () => {
|
|
|
516
507
|
})
|
|
517
508
|
|
|
518
509
|
it("returns empty array for non-map paths", () => {
|
|
519
|
-
const { reader } = setup(ScalarSchema, {
|
|
510
|
+
const { reader } = setup(ScalarSchema, {
|
|
511
|
+
name: "test",
|
|
512
|
+
count: 0,
|
|
513
|
+
active: true,
|
|
514
|
+
})
|
|
520
515
|
expect(reader.keys(p("name"))).toEqual([])
|
|
521
516
|
})
|
|
522
517
|
})
|
|
@@ -564,7 +559,11 @@ describe("YjsStoreReader", () => {
|
|
|
564
559
|
})
|
|
565
560
|
|
|
566
561
|
it("returns false for non-map paths", () => {
|
|
567
|
-
const { reader } = setup(ScalarSchema, {
|
|
562
|
+
const { reader } = setup(ScalarSchema, {
|
|
563
|
+
name: "test",
|
|
564
|
+
count: 0,
|
|
565
|
+
active: true,
|
|
566
|
+
})
|
|
568
567
|
expect(reader.hasKey(p("name"), "anything")).toBe(false)
|
|
569
568
|
})
|
|
570
569
|
})
|
|
@@ -655,9 +654,7 @@ describe("YjsStoreReader", () => {
|
|
|
655
654
|
const profile = rootMap.get("profile") as Y.Map<unknown>
|
|
656
655
|
const address = profile.get("address") as Y.Map<string>
|
|
657
656
|
address.set("city", "Seattle")
|
|
658
|
-
expect(
|
|
659
|
-
reader.read(p("profile", "address", "city")),
|
|
660
|
-
).toBe("Seattle")
|
|
657
|
+
expect(reader.read(p("profile", "address", "city"))).toBe("Seattle")
|
|
661
658
|
})
|
|
662
659
|
|
|
663
660
|
it("list delete + insert mutations are immediately visible", () => {
|
|
@@ -719,4 +716,4 @@ describe("YjsStoreReader", () => {
|
|
|
719
716
|
expect(reader.keys(p("labels"))).toEqual(["priority"])
|
|
720
717
|
})
|
|
721
718
|
})
|
|
722
|
-
})
|
|
719
|
+
})
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
|
|
19
19
|
import { describe, expect, it } from "vitest"
|
|
20
20
|
import {
|
|
21
|
+
change,
|
|
21
22
|
createYjsDoc,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
exportSnapshot,
|
|
23
|
+
createYjsDocFromEntirety,
|
|
24
|
+
exportEntirety,
|
|
25
25
|
exportSince,
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
merge,
|
|
27
|
+
Schema,
|
|
28
28
|
subscribe,
|
|
29
29
|
text,
|
|
30
|
-
|
|
30
|
+
version,
|
|
31
31
|
} from "../index.js"
|
|
32
32
|
|
|
33
33
|
// ===========================================================================
|
|
@@ -114,8 +114,8 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
114
114
|
d.profiles.set("alice", { displayName: "Alice", age: 30 })
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
-
const snapshot =
|
|
118
|
-
const docB =
|
|
117
|
+
const snapshot = exportEntirety(docA)
|
|
118
|
+
const docB = createYjsDocFromEntirety(PlainRecordSchema, snapshot)
|
|
119
119
|
|
|
120
120
|
expect(docB.profiles()).toEqual({
|
|
121
121
|
alice: { displayName: "Alice", age: 30 },
|
|
@@ -130,7 +130,10 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
130
130
|
})
|
|
131
131
|
|
|
132
132
|
// Establish docB from snapshot (avoids Yjs clientID collision)
|
|
133
|
-
const docB =
|
|
133
|
+
const docB = createYjsDocFromEntirety(
|
|
134
|
+
PlainRecordSchema,
|
|
135
|
+
exportEntirety(docA),
|
|
136
|
+
)
|
|
134
137
|
|
|
135
138
|
const v0 = version(docB)
|
|
136
139
|
|
|
@@ -140,7 +143,7 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
140
143
|
|
|
141
144
|
const delta = exportSince(docA, v0)
|
|
142
145
|
expect(delta).not.toBeNull()
|
|
143
|
-
|
|
146
|
+
merge(docB, delta!, "sync")
|
|
144
147
|
|
|
145
148
|
expect(docB.profiles()).toEqual({
|
|
146
149
|
alice: { displayName: "Alice", age: 30 },
|
|
@@ -246,7 +249,9 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
246
249
|
})
|
|
247
250
|
|
|
248
251
|
let fired = false
|
|
249
|
-
subscribe(doc, () => {
|
|
252
|
+
subscribe(doc, () => {
|
|
253
|
+
fired = true
|
|
254
|
+
})
|
|
250
255
|
|
|
251
256
|
change(doc, (d: any) => {
|
|
252
257
|
d.profiles.at("alice").bio.insert(0, "Hello")
|
|
@@ -265,8 +270,8 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
265
270
|
d.profiles.at("alice").bio.insert(0, "Collaborative bio")
|
|
266
271
|
})
|
|
267
272
|
|
|
268
|
-
const snapshot =
|
|
269
|
-
const docB =
|
|
273
|
+
const snapshot = exportEntirety(docA)
|
|
274
|
+
const docB = createYjsDocFromEntirety(ProfileSchema, snapshot)
|
|
270
275
|
|
|
271
276
|
expect(docB.profiles()).toEqual({
|
|
272
277
|
alice: { displayName: "Alice", bio: "Collaborative bio" },
|
|
@@ -286,7 +291,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
286
291
|
// Establish docB from snapshot (consistent with Yjs test patterns —
|
|
287
292
|
// two independently-created Y.Docs may share a clientID, causing
|
|
288
293
|
// silent update drops)
|
|
289
|
-
const docB =
|
|
294
|
+
const docB = createYjsDocFromEntirety(ProfileSchema, exportEntirety(docA))
|
|
290
295
|
const v0 = version(docB)
|
|
291
296
|
|
|
292
297
|
change(docA, (d: any) => {
|
|
@@ -295,7 +300,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
295
300
|
|
|
296
301
|
const delta = exportSince(docA, v0)
|
|
297
302
|
expect(delta).not.toBeNull()
|
|
298
|
-
|
|
303
|
+
merge(docB, delta!, "sync")
|
|
299
304
|
|
|
300
305
|
expect(docB.profiles()).toEqual({
|
|
301
306
|
alice: { displayName: "Alice", bio: "Hello from A" },
|
|
@@ -315,7 +320,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
315
320
|
change(docA, (d: any) => {
|
|
316
321
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
317
322
|
})
|
|
318
|
-
const docB =
|
|
323
|
+
const docB = createYjsDocFromEntirety(ProfileSchema, exportEntirety(docA))
|
|
319
324
|
|
|
320
325
|
// Both peers edit concurrently
|
|
321
326
|
const vA = version(docA)
|
|
@@ -333,8 +338,8 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
333
338
|
const deltaBA = exportSince(docB, vA)
|
|
334
339
|
expect(deltaAB).not.toBeNull()
|
|
335
340
|
expect(deltaBA).not.toBeNull()
|
|
336
|
-
|
|
337
|
-
|
|
341
|
+
merge(docB, deltaAB!, "sync")
|
|
342
|
+
merge(docA, deltaBA!, "sync")
|
|
338
343
|
|
|
339
344
|
// Both converge to the same value (order depends on client IDs)
|
|
340
345
|
expect((docA as any).profiles.at("alice").bio()).toBe(
|
|
@@ -411,7 +416,10 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
411
416
|
})
|
|
412
417
|
|
|
413
418
|
// Establish docB from snapshot (avoids Yjs clientID collision)
|
|
414
|
-
const docB =
|
|
419
|
+
const docB = createYjsDocFromEntirety(
|
|
420
|
+
ListProfileSchema,
|
|
421
|
+
exportEntirety(docA),
|
|
422
|
+
)
|
|
415
423
|
const v0 = version(docB)
|
|
416
424
|
|
|
417
425
|
change(docA, (d: any) => {
|
|
@@ -420,10 +428,10 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
420
428
|
|
|
421
429
|
const delta = exportSince(docA, v0)
|
|
422
430
|
expect(delta).not.toBeNull()
|
|
423
|
-
|
|
431
|
+
merge(docB, delta!, "sync")
|
|
424
432
|
|
|
425
433
|
expect(docB.players.length).toBe(1)
|
|
426
434
|
expect((docB as any).players.at(0).name()).toBe("Alice")
|
|
427
435
|
expect((docB as any).players.at(0).bio()).toBe("Synced bio")
|
|
428
436
|
})
|
|
429
|
-
})
|
|
437
|
+
})
|