@kyneta/yjs-schema 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +109 -147
- package/dist/index.js +321 -210
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/bind-constraints.test.ts +333 -0
- package/src/__tests__/bind-yjs.test.ts +53 -55
- package/src/__tests__/create.test.ts +71 -62
- package/src/__tests__/{store-reader.test.ts → reader.test.ts} +64 -90
- package/src/__tests__/record-text-spike.test.ts +38 -31
- package/src/__tests__/structural-merge.test.ts +362 -0
- package/src/__tests__/substrate.test.ts +65 -84
- package/src/__tests__/version.test.ts +82 -16
- package/src/bind-yjs.ts +115 -64
- package/src/change-mapping.ts +60 -84
- package/src/create.ts +33 -28
- package/src/index.ts +32 -51
- package/src/populate.ts +87 -92
- package/src/{store-reader.ts → reader.ts} +7 -12
- package/src/substrate.ts +186 -42
- package/src/sync.ts +26 -26
- package/src/version.ts +57 -4
- package/src/yjs-resolve.ts +5 -21
- package/src/yjs-escape.ts +0 -100
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { change, Schema, subscribe } from "@kyneta/schema"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
2
3
|
import * as Y from "yjs"
|
|
3
|
-
import {
|
|
4
|
-
import { createYjsDoc,
|
|
5
|
-
import { version, exportSnapshot, exportSince, importDelta } from "../sync.js"
|
|
6
|
-
import { YjsVersion } from "../version.js"
|
|
4
|
+
import { yjs } from "../bind-yjs.js"
|
|
5
|
+
import { createYjsDoc, createYjsDocFromEntirety } from "../create.js"
|
|
7
6
|
import { ensureContainers } from "../populate.js"
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { exportEntirety, exportSince, merge, version } from "../sync.js"
|
|
8
|
+
import { YjsVersion } from "../version.js"
|
|
10
9
|
|
|
11
10
|
// ===========================================================================
|
|
12
11
|
// Schemas used across tests
|
|
13
12
|
// ===========================================================================
|
|
14
13
|
|
|
15
|
-
const SimpleSchema = Schema.
|
|
16
|
-
title: Schema.
|
|
14
|
+
const SimpleSchema = Schema.struct({
|
|
15
|
+
title: Schema.text(),
|
|
17
16
|
count: Schema.number(),
|
|
18
17
|
items: Schema.list(Schema.string()),
|
|
19
18
|
})
|
|
20
19
|
|
|
21
|
-
const StructListSchema = Schema.
|
|
20
|
+
const StructListSchema = Schema.struct({
|
|
22
21
|
tasks: Schema.list(
|
|
23
22
|
Schema.struct({
|
|
24
23
|
name: Schema.string(),
|
|
@@ -27,8 +26,8 @@ const StructListSchema = Schema.doc({
|
|
|
27
26
|
),
|
|
28
27
|
})
|
|
29
28
|
|
|
30
|
-
const NestedSchema = Schema.
|
|
31
|
-
title: Schema.
|
|
29
|
+
const NestedSchema = Schema.struct({
|
|
30
|
+
title: Schema.text(),
|
|
32
31
|
meta: Schema.struct({
|
|
33
32
|
author: Schema.string(),
|
|
34
33
|
tags: Schema.list(Schema.string()),
|
|
@@ -179,12 +178,12 @@ describe("createYjsDoc", () => {
|
|
|
179
178
|
expect(doc.count()).toBe(77)
|
|
180
179
|
})
|
|
181
180
|
|
|
182
|
-
it("yjs() escape hatch returns the same Y.Doc", () => {
|
|
181
|
+
it("yjs.unwrap() escape hatch returns the same Y.Doc", () => {
|
|
183
182
|
const yjsDoc = new Y.Doc()
|
|
184
183
|
ensureContainers(yjsDoc, SimpleSchema)
|
|
185
184
|
|
|
186
185
|
const doc = createYjsDoc(SimpleSchema, yjsDoc)
|
|
187
|
-
const escaped = yjs(doc)
|
|
186
|
+
const escaped = yjs.unwrap(doc)
|
|
188
187
|
|
|
189
188
|
expect(escaped).toBe(yjsDoc)
|
|
190
189
|
})
|
|
@@ -192,10 +191,10 @@ describe("createYjsDoc", () => {
|
|
|
192
191
|
})
|
|
193
192
|
|
|
194
193
|
// ===========================================================================
|
|
195
|
-
//
|
|
194
|
+
// createYjsDocFromEntirety
|
|
196
195
|
// ===========================================================================
|
|
197
196
|
|
|
198
|
-
describe("
|
|
197
|
+
describe("createYjsDocFromEntirety", () => {
|
|
199
198
|
it("reconstructs state from a snapshot", () => {
|
|
200
199
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
201
200
|
change(doc1, (d: any) => {
|
|
@@ -205,8 +204,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
205
204
|
change(doc1, (d: any) => d.items.push("a"))
|
|
206
205
|
change(doc1, (d: any) => d.items.push("b"))
|
|
207
206
|
|
|
208
|
-
const payload =
|
|
209
|
-
const doc2 =
|
|
207
|
+
const payload = exportEntirety(doc1)
|
|
208
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
210
209
|
|
|
211
210
|
expect(doc2.title()).toBe("Snapshot")
|
|
212
211
|
expect(doc2.count()).toBe(42)
|
|
@@ -225,8 +224,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
225
224
|
d.items.push("x")
|
|
226
225
|
})
|
|
227
226
|
|
|
228
|
-
const payload =
|
|
229
|
-
const doc2 =
|
|
227
|
+
const payload = exportEntirety(doc1)
|
|
228
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
230
229
|
|
|
231
230
|
expect(doc2.title()).toBe("Start End")
|
|
232
231
|
expect(doc2.count()).toBe(99)
|
|
@@ -243,8 +242,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
243
242
|
change(doc1, (d: any) => d.meta.tags.push("v1"))
|
|
244
243
|
change(doc1, (d: any) => d.meta.tags.push("v2"))
|
|
245
244
|
|
|
246
|
-
const payload =
|
|
247
|
-
const doc2 =
|
|
245
|
+
const payload = exportEntirety(doc1)
|
|
246
|
+
const doc2 = createYjsDocFromEntirety(NestedSchema, payload)
|
|
248
247
|
|
|
249
248
|
expect(doc2.title()).toBe("Nested")
|
|
250
249
|
expect(doc2.meta.author()).toBe("Alice")
|
|
@@ -258,8 +257,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
258
257
|
change(doc1, (d: any) => d.tasks.push({ name: "Task A", done: false }))
|
|
259
258
|
change(doc1, (d: any) => d.tasks.push({ name: "Task B", done: true }))
|
|
260
259
|
|
|
261
|
-
const payload =
|
|
262
|
-
const doc2 =
|
|
260
|
+
const payload = exportEntirety(doc1)
|
|
261
|
+
const doc2 = createYjsDocFromEntirety(StructListSchema, payload)
|
|
263
262
|
|
|
264
263
|
expect(doc2.tasks.length).toBe(2)
|
|
265
264
|
expect((doc2.tasks.at(0) as any).name()).toBe("Task A")
|
|
@@ -271,8 +270,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
271
270
|
change(doc1, (d: any) => {
|
|
272
271
|
d.title.insert(0, "Original")
|
|
273
272
|
})
|
|
274
|
-
const payload =
|
|
275
|
-
const doc2 =
|
|
273
|
+
const payload = exportEntirety(doc1)
|
|
274
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
276
275
|
|
|
277
276
|
change(doc2, (d: any) => {
|
|
278
277
|
d.title.insert(8, " Copy")
|
|
@@ -288,8 +287,8 @@ describe("createYjsDocFromSnapshot", () => {
|
|
|
288
287
|
change(doc1, (d: any) => {
|
|
289
288
|
d.title.insert(0, "Original")
|
|
290
289
|
})
|
|
291
|
-
const payload =
|
|
292
|
-
const doc2 =
|
|
290
|
+
const payload = exportEntirety(doc1)
|
|
291
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, payload)
|
|
293
292
|
|
|
294
293
|
const received: any[] = []
|
|
295
294
|
subscribe(doc2, (changeset: any) => {
|
|
@@ -330,7 +329,9 @@ describe("sync primitives", () => {
|
|
|
330
329
|
|
|
331
330
|
it("serialize/parse round-trips", () => {
|
|
332
331
|
const doc = createYjsDoc(SimpleSchema)
|
|
333
|
-
change(doc, (d: any) => {
|
|
332
|
+
change(doc, (d: any) => {
|
|
333
|
+
d.title.insert(0, "Test")
|
|
334
|
+
})
|
|
334
335
|
const v = version(doc)
|
|
335
336
|
const serialized = v.serialize()
|
|
336
337
|
const parsed = YjsVersion.parse(serialized)
|
|
@@ -338,22 +339,26 @@ describe("sync primitives", () => {
|
|
|
338
339
|
})
|
|
339
340
|
})
|
|
340
341
|
|
|
341
|
-
describe("
|
|
342
|
+
describe("exportEntirety", () => {
|
|
342
343
|
it("returns a binary payload", () => {
|
|
343
344
|
const doc = createYjsDoc(SimpleSchema)
|
|
344
|
-
change(doc, (d: any) => {
|
|
345
|
-
|
|
345
|
+
change(doc, (d: any) => {
|
|
346
|
+
d.title.insert(0, "Snap")
|
|
347
|
+
})
|
|
348
|
+
const payload = exportEntirety(doc)
|
|
346
349
|
expect(payload.encoding).toBe("binary")
|
|
347
350
|
expect(payload.data).toBeInstanceOf(Uint8Array)
|
|
348
351
|
expect((payload.data as Uint8Array).byteLength).toBeGreaterThan(0)
|
|
349
352
|
})
|
|
350
353
|
})
|
|
351
354
|
|
|
352
|
-
describe("exportSince +
|
|
355
|
+
describe("exportSince + merge", () => {
|
|
353
356
|
it("syncs incremental changes between two docs", () => {
|
|
354
357
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
355
|
-
change(doc1, (d: any) => {
|
|
356
|
-
|
|
358
|
+
change(doc1, (d: any) => {
|
|
359
|
+
d.title.insert(0, "Start")
|
|
360
|
+
})
|
|
361
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
357
362
|
|
|
358
363
|
const v2Before = version(doc2)
|
|
359
364
|
|
|
@@ -367,9 +372,9 @@ describe("sync primitives", () => {
|
|
|
367
372
|
// Export delta and apply to doc2
|
|
368
373
|
const delta = exportSince(doc1, v2Before)
|
|
369
374
|
expect(delta).not.toBeNull()
|
|
370
|
-
expect(delta
|
|
375
|
+
expect(delta?.encoding).toBe("binary")
|
|
371
376
|
|
|
372
|
-
|
|
377
|
+
merge(doc2, delta!)
|
|
373
378
|
|
|
374
379
|
expect(doc2.title()).toBe("Start Edited")
|
|
375
380
|
expect(doc2.count()).toBe(42)
|
|
@@ -378,37 +383,39 @@ describe("sync primitives", () => {
|
|
|
378
383
|
|
|
379
384
|
it("syncs multiple incremental deltas", () => {
|
|
380
385
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
381
|
-
const doc2 =
|
|
386
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
382
387
|
|
|
383
388
|
// First round
|
|
384
389
|
let vBefore = version(doc2)
|
|
385
390
|
change(doc1, (d: any) => {
|
|
386
391
|
d.title.insert(0, "A")
|
|
387
392
|
})
|
|
388
|
-
|
|
393
|
+
merge(doc2, exportSince(doc1, vBefore)!)
|
|
389
394
|
|
|
390
395
|
// Second round
|
|
391
396
|
vBefore = version(doc2)
|
|
392
397
|
change(doc1, (d: any) => {
|
|
393
398
|
d.title.insert(1, "B")
|
|
394
399
|
})
|
|
395
|
-
|
|
400
|
+
merge(doc2, exportSince(doc1, vBefore)!)
|
|
396
401
|
|
|
397
402
|
// Third round
|
|
398
403
|
vBefore = version(doc2)
|
|
399
404
|
change(doc1, (d: any) => {
|
|
400
405
|
d.count.set(3)
|
|
401
406
|
})
|
|
402
|
-
|
|
407
|
+
merge(doc2, exportSince(doc1, vBefore)!)
|
|
403
408
|
|
|
404
409
|
expect(doc2.title()).toBe("AB")
|
|
405
410
|
expect(doc2.count()).toBe(3)
|
|
406
411
|
})
|
|
407
412
|
|
|
408
|
-
it("changefeed fires on
|
|
413
|
+
it("changefeed fires on merge", () => {
|
|
409
414
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
410
|
-
change(doc1, (d: any) => {
|
|
411
|
-
|
|
415
|
+
change(doc1, (d: any) => {
|
|
416
|
+
d.title.insert(0, "Source")
|
|
417
|
+
})
|
|
418
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
412
419
|
|
|
413
420
|
const v2Before = version(doc2)
|
|
414
421
|
|
|
@@ -423,15 +430,15 @@ describe("sync primitives", () => {
|
|
|
423
430
|
received.push(changeset)
|
|
424
431
|
})
|
|
425
432
|
|
|
426
|
-
|
|
433
|
+
merge(doc2, delta!)
|
|
427
434
|
|
|
428
435
|
expect(received.length).toBeGreaterThanOrEqual(1)
|
|
429
436
|
expect(doc2.count()).toBe(77)
|
|
430
437
|
})
|
|
431
438
|
|
|
432
|
-
it("
|
|
439
|
+
it("merge passes origin to changefeed", () => {
|
|
433
440
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
434
|
-
const doc2 =
|
|
441
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
435
442
|
|
|
436
443
|
const v2Before = version(doc2)
|
|
437
444
|
change(doc1, (d: any) => {
|
|
@@ -443,7 +450,7 @@ describe("sync primitives", () => {
|
|
|
443
450
|
receivedOrigins.push(changeset.origin)
|
|
444
451
|
})
|
|
445
452
|
|
|
446
|
-
|
|
453
|
+
merge(doc2, exportSince(doc1, v2Before)!, "my-sync-origin")
|
|
447
454
|
|
|
448
455
|
expect(receivedOrigins).toContain("my-sync-origin")
|
|
449
456
|
})
|
|
@@ -452,19 +459,21 @@ describe("sync primitives", () => {
|
|
|
452
459
|
describe("versions equal after sync", () => {
|
|
453
460
|
it("versions equal after full snapshot sync", () => {
|
|
454
461
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
455
|
-
change(doc1, (d: any) => {
|
|
462
|
+
change(doc1, (d: any) => {
|
|
463
|
+
d.title.insert(0, "Same")
|
|
464
|
+
})
|
|
456
465
|
change(doc1, (d: any) => {
|
|
457
466
|
d.count.set(42)
|
|
458
467
|
})
|
|
459
468
|
|
|
460
|
-
const doc2 =
|
|
469
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
461
470
|
|
|
462
471
|
expect(version(doc1).compare(version(doc2))).toBe("equal")
|
|
463
472
|
})
|
|
464
473
|
|
|
465
474
|
it("versions equal after bidirectional delta sync", () => {
|
|
466
475
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
467
|
-
const doc2 =
|
|
476
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
468
477
|
|
|
469
478
|
const v1Before = version(doc1)
|
|
470
479
|
const v2Before = version(doc2)
|
|
@@ -480,8 +489,8 @@ describe("sync primitives", () => {
|
|
|
480
489
|
// Bidirectional sync
|
|
481
490
|
const d1to2 = exportSince(doc1, v2Before)
|
|
482
491
|
const d2to1 = exportSince(doc2, v1Before)
|
|
483
|
-
|
|
484
|
-
|
|
492
|
+
merge(doc2, d1to2!)
|
|
493
|
+
merge(doc1, d2to1!)
|
|
485
494
|
|
|
486
495
|
expect(version(doc1).compare(version(doc2))).toBe("equal")
|
|
487
496
|
})
|
|
@@ -496,9 +505,9 @@ describe("full workflow", () => {
|
|
|
496
505
|
it("create → mutate → sync → observe", () => {
|
|
497
506
|
// 1. Create two docs
|
|
498
507
|
const doc1 = createYjsDoc(StructListSchema)
|
|
499
|
-
const doc2 =
|
|
508
|
+
const doc2 = createYjsDocFromEntirety(
|
|
500
509
|
StructListSchema,
|
|
501
|
-
|
|
510
|
+
exportEntirety(doc1),
|
|
502
511
|
)
|
|
503
512
|
|
|
504
513
|
// 2. Set up observer on doc2
|
|
@@ -520,7 +529,7 @@ describe("full workflow", () => {
|
|
|
520
529
|
|
|
521
530
|
// 4. Sync doc1 → doc2
|
|
522
531
|
const delta = exportSince(doc1, vBefore)
|
|
523
|
-
|
|
532
|
+
merge(doc2, delta!)
|
|
524
533
|
|
|
525
534
|
// 5. Verify state converged
|
|
526
535
|
expect(doc2.tasks.length).toBe(2)
|
|
@@ -541,7 +550,7 @@ describe("full workflow", () => {
|
|
|
541
550
|
})
|
|
542
551
|
|
|
543
552
|
const delta2 = exportSince(doc2, v1Before)
|
|
544
|
-
|
|
553
|
+
merge(doc1, delta2!)
|
|
545
554
|
|
|
546
555
|
expect(doc1.tasks.length).toBe(3)
|
|
547
556
|
expect((doc1.tasks.at(2) as any).name()).toBe("Read book")
|
|
@@ -561,10 +570,10 @@ describe("full workflow", () => {
|
|
|
561
570
|
})
|
|
562
571
|
|
|
563
572
|
// 2. Snapshot
|
|
564
|
-
const snapshot =
|
|
573
|
+
const snapshot = exportEntirety(doc1)
|
|
565
574
|
|
|
566
575
|
// 3. Reconstruct
|
|
567
|
-
const doc2 =
|
|
576
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, snapshot)
|
|
568
577
|
expect(doc2.title()).toBe("Start Middle")
|
|
569
578
|
expect(doc2.count()).toBe(10)
|
|
570
579
|
expect(doc2.items()).toEqual(["first"])
|
|
@@ -590,7 +599,7 @@ describe("full workflow", () => {
|
|
|
590
599
|
it("concurrent edits converge correctly", () => {
|
|
591
600
|
// 1. Create two peers from the same initial state
|
|
592
601
|
const doc1 = createYjsDoc(SimpleSchema)
|
|
593
|
-
const doc2 =
|
|
602
|
+
const doc2 = createYjsDocFromEntirety(SimpleSchema, exportEntirety(doc1))
|
|
594
603
|
|
|
595
604
|
const v1Before = version(doc1)
|
|
596
605
|
const v2Before = version(doc2)
|
|
@@ -611,8 +620,8 @@ describe("full workflow", () => {
|
|
|
611
620
|
// 4. Bidirectional sync
|
|
612
621
|
const d1to2 = exportSince(doc1, v2Before)
|
|
613
622
|
const d2to1 = exportSince(doc2, v1Before)
|
|
614
|
-
|
|
615
|
-
|
|
623
|
+
merge(doc2, d1to2!)
|
|
624
|
+
merge(doc1, d2to1!)
|
|
616
625
|
|
|
617
626
|
// 5. Versions converge
|
|
618
627
|
expect(version(doc1).compare(version(doc2))).toBe("equal")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { KIND, 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
|
|
@@ -16,7 +16,7 @@ import { ensureContainers } from "../populate.js"
|
|
|
16
16
|
* values. We populate values via raw Yjs API within a single transact.
|
|
17
17
|
*/
|
|
18
18
|
function setup(
|
|
19
|
-
schema:
|
|
19
|
+
schema: any,
|
|
20
20
|
seed?: Record<string, unknown>,
|
|
21
21
|
) {
|
|
22
22
|
const doc = new Y.Doc()
|
|
@@ -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
|
|
|
@@ -42,11 +42,11 @@ function setup(
|
|
|
42
42
|
*/
|
|
43
43
|
function populateSeed(
|
|
44
44
|
ymap: Y.Map<unknown>,
|
|
45
|
-
schema:
|
|
45
|
+
schema: any,
|
|
46
46
|
seed: Record<string, unknown>,
|
|
47
47
|
) {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
if (schema[KIND] !== "product") return
|
|
49
|
+
const rootProduct = schema
|
|
50
50
|
|
|
51
51
|
for (const [key, value] of Object.entries(seed)) {
|
|
52
52
|
if (value === undefined) continue
|
|
@@ -63,20 +63,15 @@ function populateField(
|
|
|
63
63
|
fieldSchema: any,
|
|
64
64
|
value: unknown,
|
|
65
65
|
) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
switch (fieldSchema[KIND]) {
|
|
67
|
+
case "text": {
|
|
68
|
+
// Text field — the Y.Text was already created by ensureContainers
|
|
69
|
+
const text = ymap.get(key) as Y.Text
|
|
70
|
+
if (text && typeof value === "string" && value.length > 0) {
|
|
71
|
+
text.insert(0, value)
|
|
72
|
+
}
|
|
73
|
+
return
|
|
73
74
|
}
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const structural = unwrapAnnotations(fieldSchema)
|
|
78
|
-
|
|
79
|
-
switch (structural._kind) {
|
|
80
75
|
case "product": {
|
|
81
76
|
// Struct — recurse into the existing Y.Map
|
|
82
77
|
const childMap = ymap.get(key) as Y.Map<unknown>
|
|
@@ -84,9 +79,9 @@ function populateField(
|
|
|
84
79
|
for (const [childKey, childValue] of Object.entries(
|
|
85
80
|
value as Record<string, unknown>,
|
|
86
81
|
)) {
|
|
87
|
-
const childFieldSchema = (
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
const childFieldSchema = (fieldSchema.fields as Record<string, any>)[
|
|
83
|
+
childKey
|
|
84
|
+
]
|
|
90
85
|
if (!childFieldSchema) continue
|
|
91
86
|
populateField(childMap, childKey, childFieldSchema, childValue)
|
|
92
87
|
}
|
|
@@ -99,14 +94,11 @@ function populateField(
|
|
|
99
94
|
const arr = ymap.get(key) as Y.Array<unknown>
|
|
100
95
|
if (arr && Array.isArray(value)) {
|
|
101
96
|
for (const item of value) {
|
|
102
|
-
const itemSchema =
|
|
103
|
-
if (
|
|
104
|
-
itemSchema &&
|
|
105
|
-
unwrapAnnotations(itemSchema)._kind === "product"
|
|
106
|
-
) {
|
|
97
|
+
const itemSchema = fieldSchema.item
|
|
98
|
+
if (itemSchema && itemSchema[KIND] === "product") {
|
|
107
99
|
// Struct items: create a Y.Map for each
|
|
108
100
|
const itemMap = buildStructMap(
|
|
109
|
-
|
|
101
|
+
itemSchema,
|
|
110
102
|
item as Record<string, unknown>,
|
|
111
103
|
)
|
|
112
104
|
arr.push([itemMap])
|
|
@@ -153,23 +145,19 @@ function buildStructMap(
|
|
|
153
145
|
const value = seed[key]
|
|
154
146
|
if (value === undefined) continue
|
|
155
147
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
148
|
+
switch (fieldSchema[KIND]) {
|
|
149
|
+
case "text": {
|
|
150
|
+
const text = new Y.Text()
|
|
151
|
+
if (typeof value === "string" && value.length > 0) {
|
|
152
|
+
text.insert(0, value)
|
|
153
|
+
}
|
|
154
|
+
map.set(key, text)
|
|
155
|
+
break
|
|
162
156
|
}
|
|
163
|
-
map.set(key, text)
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const structural = unwrapAnnotations(fieldSchema)
|
|
168
|
-
switch (structural._kind) {
|
|
169
157
|
case "product": {
|
|
170
158
|
map.set(
|
|
171
159
|
key,
|
|
172
|
-
buildStructMap(
|
|
160
|
+
buildStructMap(fieldSchema, value as Record<string, unknown>),
|
|
173
161
|
)
|
|
174
162
|
break
|
|
175
163
|
}
|
|
@@ -177,14 +165,14 @@ function buildStructMap(
|
|
|
177
165
|
const arr = new Y.Array()
|
|
178
166
|
if (Array.isArray(value)) {
|
|
179
167
|
for (const item of value) {
|
|
180
|
-
const itemSchema =
|
|
168
|
+
const itemSchema = fieldSchema.item
|
|
181
169
|
if (
|
|
182
170
|
itemSchema &&
|
|
183
|
-
|
|
171
|
+
itemSchema[KIND] === "product"
|
|
184
172
|
) {
|
|
185
173
|
arr.push([
|
|
186
174
|
buildStructMap(
|
|
187
|
-
|
|
175
|
+
itemSchema,
|
|
188
176
|
item as Record<string, unknown>,
|
|
189
177
|
),
|
|
190
178
|
])
|
|
@@ -216,22 +204,7 @@ function buildStructMap(
|
|
|
216
204
|
return map
|
|
217
205
|
}
|
|
218
206
|
|
|
219
|
-
function unwrapToProduct(schema: any): any {
|
|
220
|
-
let s = schema
|
|
221
|
-
while (s._kind === "annotated" && s.schema !== undefined) {
|
|
222
|
-
s = s.schema
|
|
223
|
-
}
|
|
224
|
-
if (s._kind === "product") return s
|
|
225
|
-
return null
|
|
226
|
-
}
|
|
227
207
|
|
|
228
|
-
function unwrapAnnotations(schema: any): any {
|
|
229
|
-
let s = schema
|
|
230
|
-
while (s._kind === "annotated" && s.schema !== undefined) {
|
|
231
|
-
s = s.schema
|
|
232
|
-
}
|
|
233
|
-
return s
|
|
234
|
-
}
|
|
235
208
|
|
|
236
209
|
/** Build a RawPath from variadic key/index segments. */
|
|
237
210
|
function p(...segs: (string | number)[]): RawPath {
|
|
@@ -246,18 +219,18 @@ function p(...segs: (string | number)[]): RawPath {
|
|
|
246
219
|
// Schemas used across tests
|
|
247
220
|
// ===========================================================================
|
|
248
221
|
|
|
249
|
-
const TextSchema = Schema.
|
|
250
|
-
title: Schema.
|
|
251
|
-
subtitle: Schema.
|
|
222
|
+
const TextSchema = Schema.struct({
|
|
223
|
+
title: Schema.text(),
|
|
224
|
+
subtitle: Schema.text(),
|
|
252
225
|
})
|
|
253
226
|
|
|
254
|
-
const ScalarSchema = Schema.
|
|
227
|
+
const ScalarSchema = Schema.struct({
|
|
255
228
|
name: Schema.string(),
|
|
256
229
|
count: Schema.number(),
|
|
257
230
|
active: Schema.boolean(),
|
|
258
231
|
})
|
|
259
232
|
|
|
260
|
-
const NestedStructSchema = Schema.
|
|
233
|
+
const NestedStructSchema = Schema.struct({
|
|
261
234
|
profile: Schema.struct({
|
|
262
235
|
first: Schema.string(),
|
|
263
236
|
last: Schema.string(),
|
|
@@ -268,7 +241,7 @@ const NestedStructSchema = Schema.doc({
|
|
|
268
241
|
}),
|
|
269
242
|
})
|
|
270
243
|
|
|
271
|
-
const ListSchema = Schema.
|
|
244
|
+
const ListSchema = Schema.struct({
|
|
272
245
|
items: Schema.list(Schema.string()),
|
|
273
246
|
structs: Schema.list(
|
|
274
247
|
Schema.struct({
|
|
@@ -278,12 +251,12 @@ const ListSchema = Schema.doc({
|
|
|
278
251
|
),
|
|
279
252
|
})
|
|
280
253
|
|
|
281
|
-
const MapSchema = Schema.
|
|
254
|
+
const MapSchema = Schema.struct({
|
|
282
255
|
labels: Schema.record(Schema.string()),
|
|
283
256
|
})
|
|
284
257
|
|
|
285
|
-
const MixedSchema = Schema.
|
|
286
|
-
title: Schema.
|
|
258
|
+
const MixedSchema = Schema.struct({
|
|
259
|
+
title: Schema.text(),
|
|
287
260
|
count: Schema.number(),
|
|
288
261
|
items: Schema.list(
|
|
289
262
|
Schema.struct({
|
|
@@ -302,7 +275,7 @@ const MixedSchema = Schema.doc({
|
|
|
302
275
|
// Tests
|
|
303
276
|
// ===========================================================================
|
|
304
277
|
|
|
305
|
-
describe("
|
|
278
|
+
describe("YjsReader", () => {
|
|
306
279
|
// -------------------------------------------------------------------------
|
|
307
280
|
// read
|
|
308
281
|
// -------------------------------------------------------------------------
|
|
@@ -347,12 +320,8 @@ describe("YjsStoreReader", () => {
|
|
|
347
320
|
})
|
|
348
321
|
expect(reader.read(p("profile", "first"))).toBe("Jane")
|
|
349
322
|
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")
|
|
323
|
+
expect(reader.read(p("profile", "address", "city"))).toBe("Portland")
|
|
324
|
+
expect(reader.read(p("profile", "address", "zip"))).toBe("97201")
|
|
356
325
|
})
|
|
357
326
|
|
|
358
327
|
it("reads nested struct as plain object", () => {
|
|
@@ -395,14 +364,9 @@ describe("YjsStoreReader", () => {
|
|
|
395
364
|
{ name: "Task 2", done: true },
|
|
396
365
|
],
|
|
397
366
|
})
|
|
398
|
-
expect(reader.read(p("structs", 0, "name"))).toBe(
|
|
399
|
-
"Task 1",
|
|
400
|
-
)
|
|
367
|
+
expect(reader.read(p("structs", 0, "name"))).toBe("Task 1")
|
|
401
368
|
expect(reader.read(p("structs", 1, "done"))).toBe(true)
|
|
402
|
-
const item = reader.read(p("structs", 0)) as Record<
|
|
403
|
-
string,
|
|
404
|
-
unknown
|
|
405
|
-
>
|
|
369
|
+
const item = reader.read(p("structs", 0)) as Record<string, unknown>
|
|
406
370
|
expect(item.name).toBe("Task 1")
|
|
407
371
|
expect(item.done).toBe(false)
|
|
408
372
|
})
|
|
@@ -468,7 +432,11 @@ describe("YjsStoreReader", () => {
|
|
|
468
432
|
})
|
|
469
433
|
|
|
470
434
|
it("returns 0 for non-list paths", () => {
|
|
471
|
-
const { reader } = setup(ScalarSchema, {
|
|
435
|
+
const { reader } = setup(ScalarSchema, {
|
|
436
|
+
name: "test",
|
|
437
|
+
count: 0,
|
|
438
|
+
active: true,
|
|
439
|
+
})
|
|
472
440
|
expect(reader.arrayLength(p("name"))).toBe(0)
|
|
473
441
|
})
|
|
474
442
|
})
|
|
@@ -516,7 +484,11 @@ describe("YjsStoreReader", () => {
|
|
|
516
484
|
})
|
|
517
485
|
|
|
518
486
|
it("returns empty array for non-map paths", () => {
|
|
519
|
-
const { reader } = setup(ScalarSchema, {
|
|
487
|
+
const { reader } = setup(ScalarSchema, {
|
|
488
|
+
name: "test",
|
|
489
|
+
count: 0,
|
|
490
|
+
active: true,
|
|
491
|
+
})
|
|
520
492
|
expect(reader.keys(p("name"))).toEqual([])
|
|
521
493
|
})
|
|
522
494
|
})
|
|
@@ -564,7 +536,11 @@ describe("YjsStoreReader", () => {
|
|
|
564
536
|
})
|
|
565
537
|
|
|
566
538
|
it("returns false for non-map paths", () => {
|
|
567
|
-
const { reader } = setup(ScalarSchema, {
|
|
539
|
+
const { reader } = setup(ScalarSchema, {
|
|
540
|
+
name: "test",
|
|
541
|
+
count: 0,
|
|
542
|
+
active: true,
|
|
543
|
+
})
|
|
568
544
|
expect(reader.hasKey(p("name"), "anything")).toBe(false)
|
|
569
545
|
})
|
|
570
546
|
})
|
|
@@ -655,9 +631,7 @@ describe("YjsStoreReader", () => {
|
|
|
655
631
|
const profile = rootMap.get("profile") as Y.Map<unknown>
|
|
656
632
|
const address = profile.get("address") as Y.Map<string>
|
|
657
633
|
address.set("city", "Seattle")
|
|
658
|
-
expect(
|
|
659
|
-
reader.read(p("profile", "address", "city")),
|
|
660
|
-
).toBe("Seattle")
|
|
634
|
+
expect(reader.read(p("profile", "address", "city"))).toBe("Seattle")
|
|
661
635
|
})
|
|
662
636
|
|
|
663
637
|
it("list delete + insert mutations are immediately visible", () => {
|
|
@@ -719,4 +693,4 @@ describe("YjsStoreReader", () => {
|
|
|
719
693
|
expect(reader.keys(p("labels"))).toEqual(["priority"])
|
|
720
694
|
})
|
|
721
695
|
})
|
|
722
|
-
})
|
|
696
|
+
})
|