@kyneta/yjs-schema 1.1.0 → 1.3.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/README.md +3 -3
- package/dist/index.d.ts +97 -225
- package/dist/index.js +281 -316
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/bind-constraints.test.ts +325 -0
- package/src/__tests__/bind-yjs.test.ts +79 -70
- package/src/__tests__/create.test.ts +88 -65
- package/src/__tests__/reader.test.ts +38 -72
- package/src/__tests__/record-text-spike.test.ts +47 -46
- package/src/__tests__/structural-merge.test.ts +18 -18
- package/src/__tests__/substrate.test.ts +62 -58
- package/src/__tests__/version.test.ts +75 -0
- package/src/bind-yjs.ts +40 -41
- package/src/change-mapping.ts +50 -54
- package/src/index.ts +29 -44
- package/src/native-map.ts +37 -0
- package/src/populate.ts +49 -82
- package/src/substrate.ts +68 -8
- package/src/version.ts +54 -52
- package/src/yjs-resolve.ts +0 -10
- package/src/create.ts +0 -177
- package/src/sync.ts +0 -107
- package/src/yjs-escape.ts +0 -84
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
change,
|
|
3
|
+
createDoc,
|
|
4
|
+
createRef,
|
|
5
|
+
Schema,
|
|
6
|
+
subscribe,
|
|
7
|
+
unwrap,
|
|
8
|
+
} from "@kyneta/schema"
|
|
9
|
+
import { describe, expect, it } from "vitest"
|
|
3
10
|
import * as Y from "yjs"
|
|
4
|
-
import {
|
|
11
|
+
import { yjs } from "../bind-yjs.js"
|
|
12
|
+
import { exportEntirety, exportSince, merge, version } from "../index.js"
|
|
5
13
|
import { ensureContainers } from "../populate.js"
|
|
6
|
-
import {
|
|
7
|
-
import { exportEntirety, exportSince, merge, version } from "../sync.js"
|
|
14
|
+
import { createYjsSubstrate } from "../substrate.js"
|
|
8
15
|
import { YjsVersion } from "../version.js"
|
|
9
|
-
import { yjs } from "../yjs-escape.js"
|
|
10
16
|
|
|
11
17
|
// ===========================================================================
|
|
12
18
|
// Schemas used across tests
|
|
13
19
|
// ===========================================================================
|
|
14
20
|
|
|
15
|
-
const SimpleSchema = Schema.
|
|
16
|
-
title: Schema.
|
|
21
|
+
const SimpleSchema = Schema.struct({
|
|
22
|
+
title: Schema.text(),
|
|
17
23
|
count: Schema.number(),
|
|
18
24
|
items: Schema.list(Schema.string()),
|
|
19
25
|
})
|
|
20
26
|
|
|
21
|
-
const StructListSchema = Schema.
|
|
27
|
+
const StructListSchema = Schema.struct({
|
|
22
28
|
tasks: Schema.list(
|
|
23
29
|
Schema.struct({
|
|
24
30
|
name: Schema.string(),
|
|
@@ -27,8 +33,8 @@ const StructListSchema = Schema.doc({
|
|
|
27
33
|
),
|
|
28
34
|
})
|
|
29
35
|
|
|
30
|
-
const NestedSchema = Schema.
|
|
31
|
-
title: Schema.
|
|
36
|
+
const NestedSchema = Schema.struct({
|
|
37
|
+
title: Schema.text(),
|
|
32
38
|
meta: Schema.struct({
|
|
33
39
|
author: Schema.string(),
|
|
34
40
|
tags: Schema.list(Schema.string()),
|
|
@@ -36,18 +42,26 @@ const NestedSchema = Schema.doc({
|
|
|
36
42
|
labels: Schema.record(Schema.string()),
|
|
37
43
|
})
|
|
38
44
|
|
|
45
|
+
// ===========================================================================
|
|
46
|
+
// Helpers
|
|
47
|
+
// ===========================================================================
|
|
48
|
+
|
|
49
|
+
const boundSimple = yjs.bind(SimpleSchema)
|
|
50
|
+
const boundStructList = yjs.bind(StructListSchema)
|
|
51
|
+
const boundNested = yjs.bind(NestedSchema)
|
|
52
|
+
|
|
39
53
|
// ===========================================================================
|
|
40
54
|
// Tests
|
|
41
55
|
// ===========================================================================
|
|
42
56
|
|
|
43
|
-
describe("
|
|
57
|
+
describe("createDoc", () => {
|
|
44
58
|
// -------------------------------------------------------------------------
|
|
45
59
|
// Default values
|
|
46
60
|
// -------------------------------------------------------------------------
|
|
47
61
|
|
|
48
62
|
describe("with defaults", () => {
|
|
49
63
|
it("creates a doc with empty containers for shared types", () => {
|
|
50
|
-
const doc =
|
|
64
|
+
const doc = createDoc(boundSimple)
|
|
51
65
|
// Text annotation returns "" (empty Y.Text)
|
|
52
66
|
expect(doc.title()).toBe("")
|
|
53
67
|
// Plain scalars return structural zeros
|
|
@@ -57,7 +71,7 @@ describe("createYjsDoc", () => {
|
|
|
57
71
|
})
|
|
58
72
|
|
|
59
73
|
it("creates a doc with nested struct empty containers", () => {
|
|
60
|
-
const doc =
|
|
74
|
+
const doc = createDoc(boundNested)
|
|
61
75
|
expect(doc.title()).toBe("")
|
|
62
76
|
// Plain scalar inside struct returns structural zero
|
|
63
77
|
expect(doc.meta.author()).toBe("")
|
|
@@ -66,7 +80,7 @@ describe("createYjsDoc", () => {
|
|
|
66
80
|
})
|
|
67
81
|
|
|
68
82
|
it("creates a doc with struct list defaults", () => {
|
|
69
|
-
const doc =
|
|
83
|
+
const doc = createDoc(boundStructList)
|
|
70
84
|
expect(doc.tasks()).toEqual([])
|
|
71
85
|
expect(doc.tasks.length).toBe(0)
|
|
72
86
|
})
|
|
@@ -78,7 +92,7 @@ describe("createYjsDoc", () => {
|
|
|
78
92
|
|
|
79
93
|
describe("with seeds", () => {
|
|
80
94
|
it("creates a doc with scalar seed values", () => {
|
|
81
|
-
const doc =
|
|
95
|
+
const doc = createDoc(boundSimple)
|
|
82
96
|
change(doc, (d: any) => {
|
|
83
97
|
d.title.insert(0, "Hello")
|
|
84
98
|
d.count.set(42)
|
|
@@ -94,7 +108,7 @@ describe("createYjsDoc", () => {
|
|
|
94
108
|
})
|
|
95
109
|
|
|
96
110
|
it("creates a doc with partial seed (defaults fill gaps)", () => {
|
|
97
|
-
const doc =
|
|
111
|
+
const doc = createDoc(boundSimple)
|
|
98
112
|
change(doc, (d: any) => {
|
|
99
113
|
d.title.insert(0, "Partial")
|
|
100
114
|
})
|
|
@@ -104,7 +118,7 @@ describe("createYjsDoc", () => {
|
|
|
104
118
|
})
|
|
105
119
|
|
|
106
120
|
it("creates a doc with nested struct seed", () => {
|
|
107
|
-
const doc =
|
|
121
|
+
const doc = createDoc(boundNested)
|
|
108
122
|
change(doc, (d: any) => {
|
|
109
123
|
d.title.insert(0, "Doc")
|
|
110
124
|
d.meta.author.set("Alice")
|
|
@@ -119,7 +133,7 @@ describe("createYjsDoc", () => {
|
|
|
119
133
|
})
|
|
120
134
|
|
|
121
135
|
it("creates a doc with struct list seed items", () => {
|
|
122
|
-
const doc =
|
|
136
|
+
const doc = createDoc(boundStructList)
|
|
123
137
|
// Separate change() calls for list pushes to preserve order
|
|
124
138
|
change(doc, (d: any) => d.tasks.push({ name: "Task 1", done: false }))
|
|
125
139
|
change(doc, (d: any) => d.tasks.push({ name: "Task 2", done: true }))
|
|
@@ -146,7 +160,10 @@ describe("createYjsDoc", () => {
|
|
|
146
160
|
;(rootMap.get("items") as Y.Array<string>).push(["x"])
|
|
147
161
|
})
|
|
148
162
|
|
|
149
|
-
const doc =
|
|
163
|
+
const doc = createRef(
|
|
164
|
+
SimpleSchema,
|
|
165
|
+
createYjsSubstrate(yjsDoc, SimpleSchema),
|
|
166
|
+
)
|
|
150
167
|
expect(doc.title()).toBe("External")
|
|
151
168
|
expect(doc.count()).toBe(99)
|
|
152
169
|
expect(doc.items()).toEqual(["x"])
|
|
@@ -156,7 +173,10 @@ describe("createYjsDoc", () => {
|
|
|
156
173
|
const yjsDoc = new Y.Doc()
|
|
157
174
|
ensureContainers(yjsDoc, SimpleSchema)
|
|
158
175
|
|
|
159
|
-
const doc =
|
|
176
|
+
const doc = createRef(
|
|
177
|
+
SimpleSchema,
|
|
178
|
+
createYjsSubstrate(yjsDoc, SimpleSchema),
|
|
179
|
+
)
|
|
160
180
|
change(doc, (d: any) => {
|
|
161
181
|
d.title.insert(0, "Hello")
|
|
162
182
|
d.count.set(42)
|
|
@@ -171,7 +191,10 @@ describe("createYjsDoc", () => {
|
|
|
171
191
|
const yjsDoc = new Y.Doc()
|
|
172
192
|
ensureContainers(yjsDoc, SimpleSchema)
|
|
173
193
|
|
|
174
|
-
const doc =
|
|
194
|
+
const doc = createRef(
|
|
195
|
+
SimpleSchema,
|
|
196
|
+
createYjsSubstrate(yjsDoc, SimpleSchema),
|
|
197
|
+
)
|
|
175
198
|
|
|
176
199
|
const rootMap = yjsDoc.getMap("root")
|
|
177
200
|
rootMap.set("count", 77)
|
|
@@ -179,12 +202,15 @@ describe("createYjsDoc", () => {
|
|
|
179
202
|
expect(doc.count()).toBe(77)
|
|
180
203
|
})
|
|
181
204
|
|
|
182
|
-
it("
|
|
205
|
+
it("unwrap() escape hatch returns the same Y.Doc", () => {
|
|
183
206
|
const yjsDoc = new Y.Doc()
|
|
184
207
|
ensureContainers(yjsDoc, SimpleSchema)
|
|
185
208
|
|
|
186
|
-
const doc =
|
|
187
|
-
|
|
209
|
+
const doc = createRef(
|
|
210
|
+
SimpleSchema,
|
|
211
|
+
createYjsSubstrate(yjsDoc, SimpleSchema),
|
|
212
|
+
)
|
|
213
|
+
const escaped = unwrap(doc)
|
|
188
214
|
|
|
189
215
|
expect(escaped).toBe(yjsDoc)
|
|
190
216
|
})
|
|
@@ -192,12 +218,12 @@ describe("createYjsDoc", () => {
|
|
|
192
218
|
})
|
|
193
219
|
|
|
194
220
|
// ===========================================================================
|
|
195
|
-
//
|
|
221
|
+
// createDoc with payload (fromEntirety)
|
|
196
222
|
// ===========================================================================
|
|
197
223
|
|
|
198
|
-
describe("
|
|
224
|
+
describe("createDoc with payload", () => {
|
|
199
225
|
it("reconstructs state from a snapshot", () => {
|
|
200
|
-
const doc1 =
|
|
226
|
+
const doc1 = createDoc(boundSimple)
|
|
201
227
|
change(doc1, (d: any) => {
|
|
202
228
|
d.title.insert(0, "Snapshot")
|
|
203
229
|
d.count.set(42)
|
|
@@ -206,7 +232,7 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
206
232
|
change(doc1, (d: any) => d.items.push("b"))
|
|
207
233
|
|
|
208
234
|
const payload = exportEntirety(doc1)
|
|
209
|
-
const doc2 =
|
|
235
|
+
const doc2 = createDoc(boundSimple, payload)
|
|
210
236
|
|
|
211
237
|
expect(doc2.title()).toBe("Snapshot")
|
|
212
238
|
expect(doc2.count()).toBe(42)
|
|
@@ -214,7 +240,7 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
214
240
|
})
|
|
215
241
|
|
|
216
242
|
it("reconstructs state after mutations", () => {
|
|
217
|
-
const doc1 =
|
|
243
|
+
const doc1 = createDoc(boundSimple)
|
|
218
244
|
change(doc1, (d: any) => {
|
|
219
245
|
d.title.insert(0, "Start")
|
|
220
246
|
})
|
|
@@ -226,7 +252,7 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
226
252
|
})
|
|
227
253
|
|
|
228
254
|
const payload = exportEntirety(doc1)
|
|
229
|
-
const doc2 =
|
|
255
|
+
const doc2 = createDoc(boundSimple, payload)
|
|
230
256
|
|
|
231
257
|
expect(doc2.title()).toBe("Start End")
|
|
232
258
|
expect(doc2.count()).toBe(99)
|
|
@@ -234,7 +260,7 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
234
260
|
})
|
|
235
261
|
|
|
236
262
|
it("reconstructs nested struct state from snapshot", () => {
|
|
237
|
-
const doc1 =
|
|
263
|
+
const doc1 = createDoc(boundNested)
|
|
238
264
|
change(doc1, (d: any) => {
|
|
239
265
|
d.title.insert(0, "Nested")
|
|
240
266
|
d.meta.author.set("Alice")
|
|
@@ -244,7 +270,7 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
244
270
|
change(doc1, (d: any) => d.meta.tags.push("v2"))
|
|
245
271
|
|
|
246
272
|
const payload = exportEntirety(doc1)
|
|
247
|
-
const doc2 =
|
|
273
|
+
const doc2 = createDoc(boundNested, payload)
|
|
248
274
|
|
|
249
275
|
expect(doc2.title()).toBe("Nested")
|
|
250
276
|
expect(doc2.meta.author()).toBe("Alice")
|
|
@@ -254,12 +280,12 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
254
280
|
})
|
|
255
281
|
|
|
256
282
|
it("reconstructs struct list state from snapshot", () => {
|
|
257
|
-
const doc1 =
|
|
283
|
+
const doc1 = createDoc(boundStructList)
|
|
258
284
|
change(doc1, (d: any) => d.tasks.push({ name: "Task A", done: false }))
|
|
259
285
|
change(doc1, (d: any) => d.tasks.push({ name: "Task B", done: true }))
|
|
260
286
|
|
|
261
287
|
const payload = exportEntirety(doc1)
|
|
262
|
-
const doc2 =
|
|
288
|
+
const doc2 = createDoc(boundStructList, payload)
|
|
263
289
|
|
|
264
290
|
expect(doc2.tasks.length).toBe(2)
|
|
265
291
|
expect((doc2.tasks.at(0) as any).name()).toBe("Task A")
|
|
@@ -267,12 +293,12 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
267
293
|
})
|
|
268
294
|
|
|
269
295
|
it("is writable after reconstruction", () => {
|
|
270
|
-
const doc1 =
|
|
296
|
+
const doc1 = createDoc(boundSimple)
|
|
271
297
|
change(doc1, (d: any) => {
|
|
272
298
|
d.title.insert(0, "Original")
|
|
273
299
|
})
|
|
274
300
|
const payload = exportEntirety(doc1)
|
|
275
|
-
const doc2 =
|
|
301
|
+
const doc2 = createDoc(boundSimple, payload)
|
|
276
302
|
|
|
277
303
|
change(doc2, (d: any) => {
|
|
278
304
|
d.title.insert(8, " Copy")
|
|
@@ -284,12 +310,12 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
284
310
|
})
|
|
285
311
|
|
|
286
312
|
it("is observable after reconstruction", () => {
|
|
287
|
-
const doc1 =
|
|
313
|
+
const doc1 = createDoc(boundSimple)
|
|
288
314
|
change(doc1, (d: any) => {
|
|
289
315
|
d.title.insert(0, "Original")
|
|
290
316
|
})
|
|
291
317
|
const payload = exportEntirety(doc1)
|
|
292
|
-
const doc2 =
|
|
318
|
+
const doc2 = createDoc(boundSimple, payload)
|
|
293
319
|
|
|
294
320
|
const received: any[] = []
|
|
295
321
|
subscribe(doc2, (changeset: any) => {
|
|
@@ -311,13 +337,13 @@ describe("createYjsDocFromEntirety", () => {
|
|
|
311
337
|
describe("sync primitives", () => {
|
|
312
338
|
describe("version", () => {
|
|
313
339
|
it("returns a YjsVersion", () => {
|
|
314
|
-
const doc =
|
|
340
|
+
const doc = createDoc(boundSimple)
|
|
315
341
|
const v = version(doc)
|
|
316
342
|
expect(v).toBeInstanceOf(YjsVersion)
|
|
317
343
|
})
|
|
318
344
|
|
|
319
345
|
it("advances after mutations", () => {
|
|
320
|
-
const doc =
|
|
346
|
+
const doc = createDoc(boundSimple)
|
|
321
347
|
const v1 = version(doc)
|
|
322
348
|
|
|
323
349
|
change(doc, (d: any) => {
|
|
@@ -329,7 +355,7 @@ describe("sync primitives", () => {
|
|
|
329
355
|
})
|
|
330
356
|
|
|
331
357
|
it("serialize/parse round-trips", () => {
|
|
332
|
-
const doc =
|
|
358
|
+
const doc = createDoc(boundSimple)
|
|
333
359
|
change(doc, (d: any) => {
|
|
334
360
|
d.title.insert(0, "Test")
|
|
335
361
|
})
|
|
@@ -342,7 +368,7 @@ describe("sync primitives", () => {
|
|
|
342
368
|
|
|
343
369
|
describe("exportEntirety", () => {
|
|
344
370
|
it("returns a binary payload", () => {
|
|
345
|
-
const doc =
|
|
371
|
+
const doc = createDoc(boundSimple)
|
|
346
372
|
change(doc, (d: any) => {
|
|
347
373
|
d.title.insert(0, "Snap")
|
|
348
374
|
})
|
|
@@ -355,11 +381,11 @@ describe("sync primitives", () => {
|
|
|
355
381
|
|
|
356
382
|
describe("exportSince + merge", () => {
|
|
357
383
|
it("syncs incremental changes between two docs", () => {
|
|
358
|
-
const doc1 =
|
|
384
|
+
const doc1 = createDoc(boundSimple)
|
|
359
385
|
change(doc1, (d: any) => {
|
|
360
386
|
d.title.insert(0, "Start")
|
|
361
387
|
})
|
|
362
|
-
const doc2 =
|
|
388
|
+
const doc2 = createDoc(boundSimple, exportEntirety(doc1))
|
|
363
389
|
|
|
364
390
|
const v2Before = version(doc2)
|
|
365
391
|
|
|
@@ -373,7 +399,7 @@ describe("sync primitives", () => {
|
|
|
373
399
|
// Export delta and apply to doc2
|
|
374
400
|
const delta = exportSince(doc1, v2Before)
|
|
375
401
|
expect(delta).not.toBeNull()
|
|
376
|
-
expect(delta
|
|
402
|
+
expect(delta?.encoding).toBe("binary")
|
|
377
403
|
|
|
378
404
|
merge(doc2, delta!)
|
|
379
405
|
|
|
@@ -383,8 +409,8 @@ describe("sync primitives", () => {
|
|
|
383
409
|
})
|
|
384
410
|
|
|
385
411
|
it("syncs multiple incremental deltas", () => {
|
|
386
|
-
const doc1 =
|
|
387
|
-
const doc2 =
|
|
412
|
+
const doc1 = createDoc(boundSimple)
|
|
413
|
+
const doc2 = createDoc(boundSimple, exportEntirety(doc1))
|
|
388
414
|
|
|
389
415
|
// First round
|
|
390
416
|
let vBefore = version(doc2)
|
|
@@ -412,11 +438,11 @@ describe("sync primitives", () => {
|
|
|
412
438
|
})
|
|
413
439
|
|
|
414
440
|
it("changefeed fires on merge", () => {
|
|
415
|
-
const doc1 =
|
|
441
|
+
const doc1 = createDoc(boundSimple)
|
|
416
442
|
change(doc1, (d: any) => {
|
|
417
443
|
d.title.insert(0, "Source")
|
|
418
444
|
})
|
|
419
|
-
const doc2 =
|
|
445
|
+
const doc2 = createDoc(boundSimple, exportEntirety(doc1))
|
|
420
446
|
|
|
421
447
|
const v2Before = version(doc2)
|
|
422
448
|
|
|
@@ -438,8 +464,8 @@ describe("sync primitives", () => {
|
|
|
438
464
|
})
|
|
439
465
|
|
|
440
466
|
it("merge passes origin to changefeed", () => {
|
|
441
|
-
const doc1 =
|
|
442
|
-
const doc2 =
|
|
467
|
+
const doc1 = createDoc(boundSimple)
|
|
468
|
+
const doc2 = createDoc(boundSimple, exportEntirety(doc1))
|
|
443
469
|
|
|
444
470
|
const v2Before = version(doc2)
|
|
445
471
|
change(doc1, (d: any) => {
|
|
@@ -459,7 +485,7 @@ describe("sync primitives", () => {
|
|
|
459
485
|
|
|
460
486
|
describe("versions equal after sync", () => {
|
|
461
487
|
it("versions equal after full snapshot sync", () => {
|
|
462
|
-
const doc1 =
|
|
488
|
+
const doc1 = createDoc(boundSimple)
|
|
463
489
|
change(doc1, (d: any) => {
|
|
464
490
|
d.title.insert(0, "Same")
|
|
465
491
|
})
|
|
@@ -467,14 +493,14 @@ describe("sync primitives", () => {
|
|
|
467
493
|
d.count.set(42)
|
|
468
494
|
})
|
|
469
495
|
|
|
470
|
-
const doc2 =
|
|
496
|
+
const doc2 = createDoc(boundSimple, exportEntirety(doc1))
|
|
471
497
|
|
|
472
498
|
expect(version(doc1).compare(version(doc2))).toBe("equal")
|
|
473
499
|
})
|
|
474
500
|
|
|
475
501
|
it("versions equal after bidirectional delta sync", () => {
|
|
476
|
-
const doc1 =
|
|
477
|
-
const doc2 =
|
|
502
|
+
const doc1 = createDoc(boundSimple)
|
|
503
|
+
const doc2 = createDoc(boundSimple, exportEntirety(doc1))
|
|
478
504
|
|
|
479
505
|
const v1Before = version(doc1)
|
|
480
506
|
const v2Before = version(doc2)
|
|
@@ -505,11 +531,8 @@ describe("sync primitives", () => {
|
|
|
505
531
|
describe("full workflow", () => {
|
|
506
532
|
it("create → mutate → sync → observe", () => {
|
|
507
533
|
// 1. Create two docs
|
|
508
|
-
const doc1 =
|
|
509
|
-
const doc2 =
|
|
510
|
-
StructListSchema,
|
|
511
|
-
exportEntirety(doc1),
|
|
512
|
-
)
|
|
534
|
+
const doc1 = createDoc(boundStructList)
|
|
535
|
+
const doc2 = createDoc(boundStructList, exportEntirety(doc1))
|
|
513
536
|
|
|
514
537
|
// 2. Set up observer on doc2
|
|
515
538
|
const changes: any[] = []
|
|
@@ -560,7 +583,7 @@ describe("full workflow", () => {
|
|
|
560
583
|
|
|
561
584
|
it("create → mutate → snapshot → reconstruct → continue", () => {
|
|
562
585
|
// 1. Create and mutate
|
|
563
|
-
const doc1 =
|
|
586
|
+
const doc1 = createDoc(boundSimple)
|
|
564
587
|
change(doc1, (d: any) => {
|
|
565
588
|
d.title.insert(0, "Start")
|
|
566
589
|
})
|
|
@@ -574,7 +597,7 @@ describe("full workflow", () => {
|
|
|
574
597
|
const snapshot = exportEntirety(doc1)
|
|
575
598
|
|
|
576
599
|
// 3. Reconstruct
|
|
577
|
-
const doc2 =
|
|
600
|
+
const doc2 = createDoc(boundSimple, snapshot)
|
|
578
601
|
expect(doc2.title()).toBe("Start Middle")
|
|
579
602
|
expect(doc2.count()).toBe(10)
|
|
580
603
|
expect(doc2.items()).toEqual(["first"])
|
|
@@ -599,8 +622,8 @@ describe("full workflow", () => {
|
|
|
599
622
|
|
|
600
623
|
it("concurrent edits converge correctly", () => {
|
|
601
624
|
// 1. Create two peers from the same initial state
|
|
602
|
-
const doc1 =
|
|
603
|
-
const doc2 =
|
|
625
|
+
const doc1 = createDoc(boundSimple)
|
|
626
|
+
const doc2 = createDoc(boundSimple, exportEntirety(doc1))
|
|
604
627
|
|
|
605
628
|
const v1Before = version(doc1)
|
|
606
629
|
const v2Before = version(doc2)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RawPath, Schema } from "@kyneta/schema"
|
|
1
|
+
import { KIND, RawPath, Schema } from "@kyneta/schema"
|
|
2
2
|
import { describe, expect, it } from "vitest"
|
|
3
3
|
import * as Y from "yjs"
|
|
4
4
|
import { ensureContainers } from "../populate.js"
|
|
@@ -15,10 +15,7 @@ import { yjsReader } from "../reader.js"
|
|
|
15
15
|
* After `ensureContainers` the doc has the correct shared types but no
|
|
16
16
|
* values. We populate values via raw Yjs API within a single transact.
|
|
17
17
|
*/
|
|
18
|
-
function setup(
|
|
19
|
-
schema: ReturnType<typeof Schema.doc>,
|
|
20
|
-
seed?: Record<string, unknown>,
|
|
21
|
-
) {
|
|
18
|
+
function setup(schema: any, seed?: Record<string, unknown>) {
|
|
22
19
|
const doc = new Y.Doc()
|
|
23
20
|
ensureContainers(doc, schema)
|
|
24
21
|
if (seed) {
|
|
@@ -42,11 +39,11 @@ function setup(
|
|
|
42
39
|
*/
|
|
43
40
|
function populateSeed(
|
|
44
41
|
ymap: Y.Map<unknown>,
|
|
45
|
-
schema:
|
|
42
|
+
schema: any,
|
|
46
43
|
seed: Record<string, unknown>,
|
|
47
44
|
) {
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
if (schema[KIND] !== "product") return
|
|
46
|
+
const rootProduct = schema
|
|
50
47
|
|
|
51
48
|
for (const [key, value] of Object.entries(seed)) {
|
|
52
49
|
if (value === undefined) continue
|
|
@@ -63,20 +60,15 @@ function populateField(
|
|
|
63
60
|
fieldSchema: any,
|
|
64
61
|
value: unknown,
|
|
65
62
|
) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
switch (fieldSchema[KIND]) {
|
|
64
|
+
case "text": {
|
|
65
|
+
// Text field — the Y.Text was already created by ensureContainers
|
|
66
|
+
const text = ymap.get(key) as Y.Text
|
|
67
|
+
if (text && typeof value === "string" && value.length > 0) {
|
|
68
|
+
text.insert(0, value)
|
|
69
|
+
}
|
|
70
|
+
return
|
|
73
71
|
}
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const structural = unwrapAnnotations(fieldSchema)
|
|
78
|
-
|
|
79
|
-
switch (structural._kind) {
|
|
80
72
|
case "product": {
|
|
81
73
|
// Struct — recurse into the existing Y.Map
|
|
82
74
|
const childMap = ymap.get(key) as Y.Map<unknown>
|
|
@@ -84,7 +76,7 @@ function populateField(
|
|
|
84
76
|
for (const [childKey, childValue] of Object.entries(
|
|
85
77
|
value as Record<string, unknown>,
|
|
86
78
|
)) {
|
|
87
|
-
const childFieldSchema = (
|
|
79
|
+
const childFieldSchema = (fieldSchema.fields as Record<string, any>)[
|
|
88
80
|
childKey
|
|
89
81
|
]
|
|
90
82
|
if (!childFieldSchema) continue
|
|
@@ -99,11 +91,11 @@ function populateField(
|
|
|
99
91
|
const arr = ymap.get(key) as Y.Array<unknown>
|
|
100
92
|
if (arr && Array.isArray(value)) {
|
|
101
93
|
for (const item of value) {
|
|
102
|
-
const itemSchema =
|
|
103
|
-
if (itemSchema &&
|
|
94
|
+
const itemSchema = fieldSchema.item
|
|
95
|
+
if (itemSchema && itemSchema[KIND] === "product") {
|
|
104
96
|
// Struct items: create a Y.Map for each
|
|
105
97
|
const itemMap = buildStructMap(
|
|
106
|
-
|
|
98
|
+
itemSchema,
|
|
107
99
|
item as Record<string, unknown>,
|
|
108
100
|
)
|
|
109
101
|
arr.push([itemMap])
|
|
@@ -150,22 +142,19 @@ function buildStructMap(
|
|
|
150
142
|
const value = seed[key]
|
|
151
143
|
if (value === undefined) continue
|
|
152
144
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
switch (fieldSchema[KIND]) {
|
|
146
|
+
case "text": {
|
|
147
|
+
const text = new Y.Text()
|
|
148
|
+
if (typeof value === "string" && value.length > 0) {
|
|
149
|
+
text.insert(0, value)
|
|
150
|
+
}
|
|
151
|
+
map.set(key, text)
|
|
152
|
+
break
|
|
158
153
|
}
|
|
159
|
-
map.set(key, text)
|
|
160
|
-
continue
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const structural = unwrapAnnotations(fieldSchema)
|
|
164
|
-
switch (structural._kind) {
|
|
165
154
|
case "product": {
|
|
166
155
|
map.set(
|
|
167
156
|
key,
|
|
168
|
-
buildStructMap(
|
|
157
|
+
buildStructMap(fieldSchema, value as Record<string, unknown>),
|
|
169
158
|
)
|
|
170
159
|
break
|
|
171
160
|
}
|
|
@@ -173,16 +162,10 @@ function buildStructMap(
|
|
|
173
162
|
const arr = new Y.Array()
|
|
174
163
|
if (Array.isArray(value)) {
|
|
175
164
|
for (const item of value) {
|
|
176
|
-
const itemSchema =
|
|
177
|
-
if (
|
|
178
|
-
itemSchema &&
|
|
179
|
-
unwrapAnnotations(itemSchema)._kind === "product"
|
|
180
|
-
) {
|
|
165
|
+
const itemSchema = fieldSchema.item
|
|
166
|
+
if (itemSchema && itemSchema[KIND] === "product") {
|
|
181
167
|
arr.push([
|
|
182
|
-
buildStructMap(
|
|
183
|
-
unwrapAnnotations(itemSchema),
|
|
184
|
-
item as Record<string, unknown>,
|
|
185
|
-
),
|
|
168
|
+
buildStructMap(itemSchema, item as Record<string, unknown>),
|
|
186
169
|
])
|
|
187
170
|
} else {
|
|
188
171
|
arr.push([item])
|
|
@@ -212,23 +195,6 @@ function buildStructMap(
|
|
|
212
195
|
return map
|
|
213
196
|
}
|
|
214
197
|
|
|
215
|
-
function unwrapToProduct(schema: any): any {
|
|
216
|
-
let s = schema
|
|
217
|
-
while (s._kind === "annotated" && s.schema !== undefined) {
|
|
218
|
-
s = s.schema
|
|
219
|
-
}
|
|
220
|
-
if (s._kind === "product") return s
|
|
221
|
-
return null
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function unwrapAnnotations(schema: any): any {
|
|
225
|
-
let s = schema
|
|
226
|
-
while (s._kind === "annotated" && s.schema !== undefined) {
|
|
227
|
-
s = s.schema
|
|
228
|
-
}
|
|
229
|
-
return s
|
|
230
|
-
}
|
|
231
|
-
|
|
232
198
|
/** Build a RawPath from variadic key/index segments. */
|
|
233
199
|
function p(...segs: (string | number)[]): RawPath {
|
|
234
200
|
let path = RawPath.empty
|
|
@@ -242,18 +208,18 @@ function p(...segs: (string | number)[]): RawPath {
|
|
|
242
208
|
// Schemas used across tests
|
|
243
209
|
// ===========================================================================
|
|
244
210
|
|
|
245
|
-
const TextSchema = Schema.
|
|
246
|
-
title: Schema.
|
|
247
|
-
subtitle: Schema.
|
|
211
|
+
const TextSchema = Schema.struct({
|
|
212
|
+
title: Schema.text(),
|
|
213
|
+
subtitle: Schema.text(),
|
|
248
214
|
})
|
|
249
215
|
|
|
250
|
-
const ScalarSchema = Schema.
|
|
216
|
+
const ScalarSchema = Schema.struct({
|
|
251
217
|
name: Schema.string(),
|
|
252
218
|
count: Schema.number(),
|
|
253
219
|
active: Schema.boolean(),
|
|
254
220
|
})
|
|
255
221
|
|
|
256
|
-
const NestedStructSchema = Schema.
|
|
222
|
+
const NestedStructSchema = Schema.struct({
|
|
257
223
|
profile: Schema.struct({
|
|
258
224
|
first: Schema.string(),
|
|
259
225
|
last: Schema.string(),
|
|
@@ -264,7 +230,7 @@ const NestedStructSchema = Schema.doc({
|
|
|
264
230
|
}),
|
|
265
231
|
})
|
|
266
232
|
|
|
267
|
-
const ListSchema = Schema.
|
|
233
|
+
const ListSchema = Schema.struct({
|
|
268
234
|
items: Schema.list(Schema.string()),
|
|
269
235
|
structs: Schema.list(
|
|
270
236
|
Schema.struct({
|
|
@@ -274,12 +240,12 @@ const ListSchema = Schema.doc({
|
|
|
274
240
|
),
|
|
275
241
|
})
|
|
276
242
|
|
|
277
|
-
const MapSchema = Schema.
|
|
243
|
+
const MapSchema = Schema.struct({
|
|
278
244
|
labels: Schema.record(Schema.string()),
|
|
279
245
|
})
|
|
280
246
|
|
|
281
|
-
const MixedSchema = Schema.
|
|
282
|
-
title: Schema.
|
|
247
|
+
const MixedSchema = Schema.struct({
|
|
248
|
+
title: Schema.text(),
|
|
283
249
|
count: Schema.number(),
|
|
284
250
|
items: Schema.list(
|
|
285
251
|
Schema.struct({
|