@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,10 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
change,
|
|
3
|
+
createDoc,
|
|
4
|
+
createRef,
|
|
5
|
+
exportEntirety,
|
|
6
|
+
exportSince,
|
|
7
|
+
merge,
|
|
8
|
+
RawPath,
|
|
9
|
+
Schema,
|
|
10
|
+
subscribe,
|
|
11
|
+
version,
|
|
12
|
+
} from "@kyneta/schema"
|
|
13
|
+
import { describe, expect, it } from "vitest"
|
|
3
14
|
import * as Y from "yjs"
|
|
4
|
-
import {
|
|
15
|
+
import { yjs } from "../bind-yjs.js"
|
|
5
16
|
import { ensureContainers } from "../populate.js"
|
|
6
17
|
import { createYjsSubstrate, yjsSubstrateFactory } from "../substrate.js"
|
|
7
|
-
import { exportEntirety, exportSince, merge, version } from "../sync.js"
|
|
8
18
|
import { YjsVersion } from "../version.js"
|
|
9
19
|
|
|
10
20
|
// ===========================================================================
|
|
@@ -15,13 +25,13 @@ import { YjsVersion } from "../version.js"
|
|
|
15
25
|
// Schemas used across tests
|
|
16
26
|
// ===========================================================================
|
|
17
27
|
|
|
18
|
-
const SimpleSchema = Schema.
|
|
19
|
-
title: Schema.
|
|
28
|
+
const SimpleSchema = Schema.struct({
|
|
29
|
+
title: Schema.text(),
|
|
20
30
|
count: Schema.number(),
|
|
21
31
|
items: Schema.list(Schema.string()),
|
|
22
32
|
})
|
|
23
33
|
|
|
24
|
-
const StructListSchema = Schema.
|
|
34
|
+
const StructListSchema = Schema.struct({
|
|
25
35
|
tasks: Schema.list(
|
|
26
36
|
Schema.struct({
|
|
27
37
|
name: Schema.string(),
|
|
@@ -30,8 +40,8 @@ const StructListSchema = Schema.doc({
|
|
|
30
40
|
),
|
|
31
41
|
})
|
|
32
42
|
|
|
33
|
-
const FullSchema = Schema.
|
|
34
|
-
title: Schema.
|
|
43
|
+
const FullSchema = Schema.struct({
|
|
44
|
+
title: Schema.text(),
|
|
35
45
|
count: Schema.number(),
|
|
36
46
|
active: Schema.boolean(),
|
|
37
47
|
items: Schema.list(Schema.string()),
|
|
@@ -66,7 +76,7 @@ describe("YjsSubstrate", () => {
|
|
|
66
76
|
})
|
|
67
77
|
|
|
68
78
|
it("creates a substrate and populates via change()", () => {
|
|
69
|
-
const doc =
|
|
79
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
70
80
|
change(doc, (d: any) => {
|
|
71
81
|
d.title.insert(0, "Hello")
|
|
72
82
|
d.count.set(42)
|
|
@@ -81,7 +91,7 @@ describe("YjsSubstrate", () => {
|
|
|
81
91
|
})
|
|
82
92
|
|
|
83
93
|
it("creates a substrate with partial values (unset fields stay empty)", () => {
|
|
84
|
-
const doc =
|
|
94
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
85
95
|
change(doc, (d: any) => {
|
|
86
96
|
d.title.insert(0, "Partial")
|
|
87
97
|
})
|
|
@@ -91,7 +101,7 @@ describe("YjsSubstrate", () => {
|
|
|
91
101
|
})
|
|
92
102
|
|
|
93
103
|
it("creates a substrate with nested struct values via change()", () => {
|
|
94
|
-
const doc =
|
|
104
|
+
const doc = createDoc(yjs.bind(FullSchema))
|
|
95
105
|
change(doc, (d: any) => {
|
|
96
106
|
d.meta.author.set("Alice")
|
|
97
107
|
})
|
|
@@ -99,7 +109,7 @@ describe("YjsSubstrate", () => {
|
|
|
99
109
|
})
|
|
100
110
|
|
|
101
111
|
it("creates a substrate with struct list values via change()", () => {
|
|
102
|
-
const doc =
|
|
112
|
+
const doc = createDoc(yjs.bind(StructListSchema))
|
|
103
113
|
// Separate change() calls for list pushes to preserve order
|
|
104
114
|
change(doc, (d: any) => d.tasks.push({ name: "Task 1", done: false }))
|
|
105
115
|
change(doc, (d: any) => d.tasks.push({ name: "Task 2", done: true }))
|
|
@@ -114,7 +124,7 @@ describe("YjsSubstrate", () => {
|
|
|
114
124
|
|
|
115
125
|
describe("write round-trip", () => {
|
|
116
126
|
it("text insert round-trips through prepare/flush", () => {
|
|
117
|
-
const doc =
|
|
127
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
118
128
|
change(doc, (d: any) => {
|
|
119
129
|
d.title.insert(0, "Hello")
|
|
120
130
|
})
|
|
@@ -122,7 +132,7 @@ describe("YjsSubstrate", () => {
|
|
|
122
132
|
})
|
|
123
133
|
|
|
124
134
|
it("scalar set round-trips through prepare/flush", () => {
|
|
125
|
-
const doc =
|
|
135
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
126
136
|
change(doc, (d: any) => {
|
|
127
137
|
d.count.set(42)
|
|
128
138
|
})
|
|
@@ -130,7 +140,7 @@ describe("YjsSubstrate", () => {
|
|
|
130
140
|
})
|
|
131
141
|
|
|
132
142
|
it("list push round-trips through prepare/flush", () => {
|
|
133
|
-
const doc =
|
|
143
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
134
144
|
change(doc, (d: any) => {
|
|
135
145
|
d.items.push("a")
|
|
136
146
|
})
|
|
@@ -148,7 +158,7 @@ describe("YjsSubstrate", () => {
|
|
|
148
158
|
|
|
149
159
|
describe("version tracking", () => {
|
|
150
160
|
it("version advances after mutations", () => {
|
|
151
|
-
const doc =
|
|
161
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
152
162
|
const v1 = version(doc)
|
|
153
163
|
|
|
154
164
|
change(doc, (d: any) => {
|
|
@@ -161,7 +171,7 @@ describe("YjsSubstrate", () => {
|
|
|
161
171
|
})
|
|
162
172
|
|
|
163
173
|
it("version serialize/parse round-trips", () => {
|
|
164
|
-
const doc =
|
|
174
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
165
175
|
change(doc, (d: any) => {
|
|
166
176
|
d.title.insert(0, "Test")
|
|
167
177
|
d.count.set(5)
|
|
@@ -180,7 +190,7 @@ describe("YjsSubstrate", () => {
|
|
|
180
190
|
|
|
181
191
|
describe("export/import snapshot", () => {
|
|
182
192
|
it("exports a binary payload", () => {
|
|
183
|
-
const doc =
|
|
193
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
184
194
|
change(doc, (d: any) => {
|
|
185
195
|
d.title.insert(0, "Snapshot")
|
|
186
196
|
})
|
|
@@ -190,7 +200,7 @@ describe("YjsSubstrate", () => {
|
|
|
190
200
|
})
|
|
191
201
|
|
|
192
202
|
it("reconstructs equivalent state from snapshot", () => {
|
|
193
|
-
const doc1 =
|
|
203
|
+
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
194
204
|
change(doc1, (d: any) => {
|
|
195
205
|
d.title.insert(0, "Hello")
|
|
196
206
|
d.count.set(42)
|
|
@@ -203,7 +213,7 @@ describe("YjsSubstrate", () => {
|
|
|
203
213
|
})
|
|
204
214
|
|
|
205
215
|
const payload = exportEntirety(doc1)
|
|
206
|
-
const doc2 =
|
|
216
|
+
const doc2 = createDoc(yjs.bind(SimpleSchema), payload)
|
|
207
217
|
|
|
208
218
|
expect(doc2.title()).toBe("Hello World")
|
|
209
219
|
expect(doc2.count()).toBe(42)
|
|
@@ -217,11 +227,11 @@ describe("YjsSubstrate", () => {
|
|
|
217
227
|
|
|
218
228
|
describe("delta sync", () => {
|
|
219
229
|
it("exportSince → merge syncs state", () => {
|
|
220
|
-
const doc1 =
|
|
230
|
+
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
221
231
|
change(doc1, (d: any) => {
|
|
222
232
|
d.title.insert(0, "Start")
|
|
223
233
|
})
|
|
224
|
-
const doc2 =
|
|
234
|
+
const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
|
|
225
235
|
|
|
226
236
|
const v1Before = version(doc1)
|
|
227
237
|
|
|
@@ -239,8 +249,8 @@ describe("YjsSubstrate", () => {
|
|
|
239
249
|
})
|
|
240
250
|
|
|
241
251
|
it("concurrent sync — two substrates converge after bidirectional sync", () => {
|
|
242
|
-
const doc1 =
|
|
243
|
-
const doc2 =
|
|
252
|
+
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
253
|
+
const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
|
|
244
254
|
|
|
245
255
|
const v1Before = version(doc1)
|
|
246
256
|
const v2Before = version(doc2)
|
|
@@ -285,11 +295,11 @@ describe("YjsSubstrate", () => {
|
|
|
285
295
|
|
|
286
296
|
describe("changefeed", () => {
|
|
287
297
|
it("fires on merge", () => {
|
|
288
|
-
const doc1 =
|
|
298
|
+
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
289
299
|
change(doc1, (d: any) => {
|
|
290
300
|
d.title.insert(0, "A")
|
|
291
301
|
})
|
|
292
|
-
const doc2 =
|
|
302
|
+
const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
|
|
293
303
|
|
|
294
304
|
const v2Before = version(doc2)
|
|
295
305
|
|
|
@@ -312,7 +322,10 @@ describe("YjsSubstrate", () => {
|
|
|
312
322
|
it("fires on external Y.Doc mutation (raw Yjs API)", () => {
|
|
313
323
|
const yjsDoc = new Y.Doc()
|
|
314
324
|
ensureContainers(yjsDoc, SimpleSchema)
|
|
315
|
-
const doc =
|
|
325
|
+
const doc = createRef(
|
|
326
|
+
SimpleSchema,
|
|
327
|
+
createYjsSubstrate(yjsDoc, SimpleSchema),
|
|
328
|
+
)
|
|
316
329
|
|
|
317
330
|
const received: any[] = []
|
|
318
331
|
subscribe(doc, (changeset: any) => {
|
|
@@ -328,7 +341,7 @@ describe("YjsSubstrate", () => {
|
|
|
328
341
|
})
|
|
329
342
|
|
|
330
343
|
it("no double-fire on kyneta local writes", () => {
|
|
331
|
-
const doc =
|
|
344
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
332
345
|
|
|
333
346
|
const received: any[] = []
|
|
334
347
|
subscribe(doc, (changeset: any) => {
|
|
@@ -345,18 +358,15 @@ describe("YjsSubstrate", () => {
|
|
|
345
358
|
})
|
|
346
359
|
|
|
347
360
|
it("nested struct field changefeed fires on merge", () => {
|
|
348
|
-
const doc1 =
|
|
349
|
-
const
|
|
350
|
-
StructListSchema,
|
|
351
|
-
exportEntirety(doc1),
|
|
352
|
-
)
|
|
361
|
+
const doc1 = createDoc(yjs.bind(StructListSchema))
|
|
362
|
+
const _doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
|
|
353
363
|
|
|
354
364
|
// Add a struct item on doc1, sync to doc2
|
|
355
365
|
change(doc1, (d: any) => {
|
|
356
366
|
d.tasks.push({ name: "Buy milk", done: false })
|
|
357
367
|
})
|
|
358
368
|
const snap = exportEntirety(doc1)
|
|
359
|
-
const doc2b =
|
|
369
|
+
const doc2b = createDoc(yjs.bind(StructListSchema), snap)
|
|
360
370
|
|
|
361
371
|
const taskB = [...doc2b.tasks][0] as any
|
|
362
372
|
expect(taskB.done()).toBe(false)
|
|
@@ -387,16 +397,13 @@ describe("YjsSubstrate", () => {
|
|
|
387
397
|
})
|
|
388
398
|
|
|
389
399
|
it("multi-key struct update fires per-field changefeeds on merge", () => {
|
|
390
|
-
const doc1 =
|
|
400
|
+
const doc1 = createDoc(yjs.bind(StructListSchema))
|
|
391
401
|
|
|
392
402
|
// Add a struct item, sync to doc2
|
|
393
403
|
change(doc1, (d: any) => {
|
|
394
404
|
d.tasks.push({ name: "Buy milk", done: false })
|
|
395
405
|
})
|
|
396
|
-
const doc2 =
|
|
397
|
-
StructListSchema,
|
|
398
|
-
exportEntirety(doc1),
|
|
399
|
-
)
|
|
406
|
+
const doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
|
|
400
407
|
|
|
401
408
|
const taskB = [...doc2.tasks][0] as any
|
|
402
409
|
const v2 = version(doc2)
|
|
@@ -438,7 +445,7 @@ describe("YjsSubstrate", () => {
|
|
|
438
445
|
|
|
439
446
|
describe("transaction support", () => {
|
|
440
447
|
it("multi-op change() is atomic", () => {
|
|
441
|
-
const doc =
|
|
448
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
442
449
|
|
|
443
450
|
const received: any[] = []
|
|
444
451
|
subscribe(doc, (changeset: any) => {
|
|
@@ -474,7 +481,7 @@ describe("YjsSubstrate", () => {
|
|
|
474
481
|
|
|
475
482
|
describe("nested structure", () => {
|
|
476
483
|
it("push struct into list, read back via navigation", () => {
|
|
477
|
-
const doc =
|
|
484
|
+
const doc = createDoc(yjs.bind(StructListSchema))
|
|
478
485
|
|
|
479
486
|
change(doc, (d: any) => {
|
|
480
487
|
d.tasks.push({ name: "Task 1", done: false })
|
|
@@ -494,7 +501,7 @@ describe("YjsSubstrate", () => {
|
|
|
494
501
|
})
|
|
495
502
|
|
|
496
503
|
it("nested struct write round-trip", () => {
|
|
497
|
-
const doc =
|
|
504
|
+
const doc = createDoc(yjs.bind(FullSchema))
|
|
498
505
|
change(doc, (d: any) => {
|
|
499
506
|
d.meta.author.set("Alice")
|
|
500
507
|
})
|
|
@@ -512,29 +519,26 @@ describe("YjsSubstrate", () => {
|
|
|
512
519
|
// Counter annotation throws
|
|
513
520
|
// -------------------------------------------------------------------------
|
|
514
521
|
|
|
515
|
-
describe("unsupported
|
|
516
|
-
it("counter
|
|
517
|
-
const CounterSchema = Schema.
|
|
518
|
-
count: Schema.
|
|
522
|
+
describe("unsupported kinds", () => {
|
|
523
|
+
it("counter throws clear error at construction", () => {
|
|
524
|
+
const CounterSchema = Schema.struct({
|
|
525
|
+
count: Schema.counter(),
|
|
519
526
|
})
|
|
520
527
|
|
|
521
528
|
expect(() => yjsSubstrateFactory.create(CounterSchema)).toThrow("counter")
|
|
522
529
|
})
|
|
523
530
|
|
|
524
|
-
it("
|
|
525
|
-
const MovableSchema = Schema.
|
|
526
|
-
items: Schema.
|
|
531
|
+
it("movableList throws clear error at construction", () => {
|
|
532
|
+
const MovableSchema = Schema.struct({
|
|
533
|
+
items: Schema.movableList(Schema.string()),
|
|
527
534
|
})
|
|
528
535
|
|
|
529
536
|
expect(() => yjsSubstrateFactory.create(MovableSchema)).toThrow("movable")
|
|
530
537
|
})
|
|
531
538
|
|
|
532
|
-
it("tree
|
|
533
|
-
const TreeSchema = Schema.
|
|
534
|
-
tree: Schema.
|
|
535
|
-
"tree",
|
|
536
|
-
Schema.struct({ label: Schema.string() }),
|
|
537
|
-
),
|
|
539
|
+
it("tree throws clear error at construction", () => {
|
|
540
|
+
const TreeSchema = Schema.struct({
|
|
541
|
+
tree: Schema.tree(Schema.struct({ label: Schema.string() })),
|
|
538
542
|
})
|
|
539
543
|
|
|
540
544
|
expect(() => yjsSubstrateFactory.create(TreeSchema)).toThrow("tree")
|
|
@@ -556,7 +560,7 @@ describe("YjsSubstrate", () => {
|
|
|
556
560
|
})
|
|
557
561
|
|
|
558
562
|
it("reconstructs from snapshot with correct state", () => {
|
|
559
|
-
const doc =
|
|
563
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
560
564
|
change(doc, (d: any) => {
|
|
561
565
|
d.title.insert(0, "Snapshot Test")
|
|
562
566
|
d.count.set(77)
|
|
@@ -564,7 +568,7 @@ describe("YjsSubstrate", () => {
|
|
|
564
568
|
})
|
|
565
569
|
|
|
566
570
|
const payload = exportEntirety(doc)
|
|
567
|
-
const doc2 =
|
|
571
|
+
const doc2 = createDoc(yjs.bind(SimpleSchema), payload)
|
|
568
572
|
|
|
569
573
|
expect(doc2.title()).toBe("Snapshot Test")
|
|
570
574
|
expect(doc2.count()).toBe(77)
|
|
@@ -187,6 +187,7 @@ describe("YjsVersion", () => {
|
|
|
187
187
|
const fake = {
|
|
188
188
|
serialize: () => "fake",
|
|
189
189
|
compare: () => "equal" as const,
|
|
190
|
+
meet: () => fake,
|
|
190
191
|
}
|
|
191
192
|
expect(() => v.compare(fake)).toThrow(
|
|
192
193
|
"YjsVersion can only be compared with another YjsVersion",
|
|
@@ -215,4 +216,78 @@ describe("YjsVersion", () => {
|
|
|
215
216
|
expect(late.compare(earlyParsed)).toBe("ahead")
|
|
216
217
|
})
|
|
217
218
|
})
|
|
219
|
+
|
|
220
|
+
// -------------------------------------------------------------------------
|
|
221
|
+
// meet
|
|
222
|
+
// -------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
describe("YjsVersion.meet()", () => {
|
|
225
|
+
it("meet of concurrent versions produces component-wise minimum", () => {
|
|
226
|
+
// Create two docs with independent edits
|
|
227
|
+
const doc1 = new Y.Doc()
|
|
228
|
+
const doc2 = new Y.Doc()
|
|
229
|
+
|
|
230
|
+
doc1.getMap("root").set("a", 1)
|
|
231
|
+
doc1.getMap("root").set("b", 2)
|
|
232
|
+
doc2.getMap("root").set("c", 3)
|
|
233
|
+
|
|
234
|
+
const v1 = new YjsVersion(Y.encodeStateVector(doc1))
|
|
235
|
+
const v2 = new YjsVersion(Y.encodeStateVector(doc2))
|
|
236
|
+
|
|
237
|
+
// meet of concurrent versions — result ≤ both
|
|
238
|
+
const meet = v1.meet(v2) as YjsVersion
|
|
239
|
+
expect(meet.compare(v1)).not.toBe("ahead")
|
|
240
|
+
expect(meet.compare(v2)).not.toBe("ahead")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("meet of identical versions returns an equal version", () => {
|
|
244
|
+
const doc = new Y.Doc()
|
|
245
|
+
doc.getMap("root").set("x", 1)
|
|
246
|
+
const v = new YjsVersion(Y.encodeStateVector(doc))
|
|
247
|
+
|
|
248
|
+
const meet = v.meet(v) as YjsVersion
|
|
249
|
+
expect(meet.compare(v)).toBe("equal")
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("meet round-trips through Yjs decode correctly", () => {
|
|
253
|
+
// The custom encodeStateVector must produce bytes that Yjs can decode
|
|
254
|
+
const doc1 = new Y.Doc()
|
|
255
|
+
const doc2 = new Y.Doc()
|
|
256
|
+
|
|
257
|
+
doc1.getMap("root").set("x", 1)
|
|
258
|
+
doc1.getMap("root").set("y", 2)
|
|
259
|
+
|
|
260
|
+
// Sync doc1 → doc2, then doc2 makes independent edits
|
|
261
|
+
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))
|
|
262
|
+
doc2.getMap("root").set("z", 3)
|
|
263
|
+
|
|
264
|
+
const v1 = new YjsVersion(Y.encodeStateVector(doc1))
|
|
265
|
+
const v2 = new YjsVersion(Y.encodeStateVector(doc2))
|
|
266
|
+
|
|
267
|
+
// v1 is behind v2 (v2 has all of v1's ops plus its own)
|
|
268
|
+
expect(v1.compare(v2)).toBe("behind")
|
|
269
|
+
|
|
270
|
+
// meet(v1, v2) should equal v1 (the behind one)
|
|
271
|
+
const meet = v1.meet(v2) as YjsVersion
|
|
272
|
+
expect(meet.compare(v1)).toBe("equal")
|
|
273
|
+
|
|
274
|
+
// The meet's state vector bytes can be decoded by Yjs
|
|
275
|
+
const decoded = Y.decodeStateVector(meet.sv)
|
|
276
|
+
expect(decoded.size).toBeGreaterThan(0)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("meet of two behind-ahead versions gives the behind one", () => {
|
|
280
|
+
const doc = new Y.Doc()
|
|
281
|
+
doc.getMap("root").set("a", 1)
|
|
282
|
+
const early = new YjsVersion(Y.encodeStateVector(doc))
|
|
283
|
+
|
|
284
|
+
doc.getMap("root").set("b", 2)
|
|
285
|
+
const late = new YjsVersion(Y.encodeStateVector(doc))
|
|
286
|
+
|
|
287
|
+
expect(early.compare(late)).toBe("behind")
|
|
288
|
+
|
|
289
|
+
const meet = early.meet(late) as YjsVersion
|
|
290
|
+
expect(meet.compare(early)).toBe("equal")
|
|
291
|
+
})
|
|
292
|
+
})
|
|
218
293
|
})
|
package/src/bind-yjs.ts
CHANGED
|
@@ -1,34 +1,39 @@
|
|
|
1
|
-
// bind-yjs —
|
|
1
|
+
// bind-yjs — Yjs CRDT substrate namespace and factory.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// peer identity across all documents in an exchange.
|
|
3
|
+
// Provides the `yjs` substrate namespace (`yjs.bind()`, `yjs.replica()`)
|
|
4
|
+
// and the internal factory builder that injects a deterministic numeric
|
|
5
|
+
// Yjs clientID derived from the exchange's peerId.
|
|
7
6
|
//
|
|
8
7
|
// Yjs clientID is a uint32 number. We use FNV-1a hash truncated to
|
|
9
8
|
// 32 bits, mirroring the Loro binding's hashPeerId pattern but
|
|
10
9
|
// targeting Yjs's number type (not Loro's bigint/53-bit PeerID).
|
|
11
10
|
//
|
|
12
11
|
// Usage:
|
|
13
|
-
// import {
|
|
12
|
+
// import { yjs } from "@kyneta/yjs-schema"
|
|
14
13
|
//
|
|
15
|
-
// const TodoDoc =
|
|
16
|
-
// title: Schema.
|
|
14
|
+
// const TodoDoc = yjs.bind(Schema.struct({
|
|
15
|
+
// title: Schema.text(),
|
|
17
16
|
// items: Schema.list(Schema.struct({ name: Schema.string() })),
|
|
18
17
|
// }))
|
|
19
18
|
//
|
|
20
19
|
// const doc = exchange.get("my-doc", TodoDoc)
|
|
21
20
|
|
|
22
21
|
import type {
|
|
23
|
-
|
|
22
|
+
CrdtStrategy,
|
|
24
23
|
Replica,
|
|
25
24
|
Schema as SchemaNode,
|
|
26
25
|
Substrate,
|
|
27
26
|
SubstrateFactory,
|
|
27
|
+
SubstrateNamespace,
|
|
28
28
|
SubstratePayload,
|
|
29
29
|
} from "@kyneta/schema"
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
BACKING_DOC,
|
|
32
|
+
createSubstrateNamespace,
|
|
33
|
+
STRUCTURAL_YJS_CLIENT_ID,
|
|
34
|
+
} from "@kyneta/schema"
|
|
31
35
|
import * as Y from "yjs"
|
|
36
|
+
import type { YjsNativeMap } from "./native-map.js"
|
|
32
37
|
import { ensureContainers } from "./populate.js"
|
|
33
38
|
import {
|
|
34
39
|
createYjsReplica,
|
|
@@ -127,42 +132,36 @@ function createYjsFactory(peerId: string): SubstrateFactory<YjsVersion> {
|
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
// ---------------------------------------------------------------------------
|
|
130
|
-
//
|
|
135
|
+
// yjs — the Yjs CRDT substrate namespace
|
|
131
136
|
// ---------------------------------------------------------------------------
|
|
132
137
|
|
|
133
138
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* This is the recommended way to declare a Yjs-backed document type.
|
|
137
|
-
* The factory builder injects a deterministic numeric Yjs clientID derived
|
|
138
|
-
* from the exchange's string peerId, ensuring consistent change attribution
|
|
139
|
-
* across all documents and sessions.
|
|
140
|
-
*
|
|
141
|
-
* **Unsupported annotations:** Yjs has no native counter, movable list,
|
|
142
|
-
* or tree types. Schemas passed to `bindYjs` must not contain
|
|
143
|
-
* `Schema.annotated("counter")`, `Schema.annotated("movable")`, or
|
|
144
|
-
* `Schema.annotated("tree")`. These will throw at construction time.
|
|
139
|
+
* The Yjs CRDT substrate namespace.
|
|
145
140
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
141
|
+
* - `yjs.bind(schema)` — collaborative sync (default)
|
|
142
|
+
* - `yjs.bind(schema, "ephemeral")` — ephemeral/presence broadcast
|
|
143
|
+
* - `yjs.replica()` — collaborative replication (default)
|
|
144
|
+
* - `yjs.replica("ephemeral")` — ephemeral replication
|
|
150
145
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* items: Schema.list(Schema.struct({
|
|
154
|
-
* name: Schema.string(),
|
|
155
|
-
* done: Schema.boolean(),
|
|
156
|
-
* })),
|
|
157
|
-
* }))
|
|
146
|
+
* Strategy is constrained to `CrdtStrategy` (`"collaborative" | "ephemeral"`).
|
|
147
|
+
* Passing `"authoritative"` is a compile error.
|
|
158
148
|
*
|
|
159
|
-
*
|
|
160
|
-
* ```
|
|
149
|
+
* To access the underlying Y.Doc, use `unwrap(ref)` from `@kyneta/schema`.
|
|
161
150
|
*/
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
151
|
+
/** The closed set of capability tags that the Yjs substrate supports. */
|
|
152
|
+
export type YjsCaps = "text" | "json"
|
|
153
|
+
|
|
154
|
+
export const yjs: SubstrateNamespace<CrdtStrategy, YjsCaps, YjsNativeMap> =
|
|
155
|
+
createSubstrateNamespace<CrdtStrategy, YjsCaps, YjsNativeMap>({
|
|
156
|
+
strategies: {
|
|
157
|
+
collaborative: {
|
|
158
|
+
factory: ctx => createYjsFactory(ctx.peerId),
|
|
159
|
+
replicaFactory: yjsReplicaFactory,
|
|
160
|
+
},
|
|
161
|
+
ephemeral: {
|
|
162
|
+
factory: ctx => createYjsFactory(ctx.peerId),
|
|
163
|
+
replicaFactory: yjsReplicaFactory,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
defaultStrategy: "collaborative",
|
|
167
167
|
})
|
|
168
|
-
}
|