@kyneta/yjs-schema 1.8.0 → 2.0.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 +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -4
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/bind-yjs.test.ts +7 -7
- package/src/__tests__/create.test.ts +50 -50
- package/src/__tests__/eager-write-coherence.test.ts +21 -21
- package/src/__tests__/materialize.test.ts +13 -13
- package/src/__tests__/position.test.ts +18 -18
- package/src/__tests__/record-text-spike.test.ts +34 -34
- package/src/__tests__/substrate.test.ts +53 -53
- package/src/bind-yjs.ts +1 -1
- package/src/index.ts +1 -1
- package/src/substrate.ts +36 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
batch,
|
|
3
3
|
createDoc,
|
|
4
4
|
createRef,
|
|
5
5
|
exportEntirety,
|
|
@@ -76,16 +76,16 @@ describe("YjsSubstrate", () => {
|
|
|
76
76
|
expect(substrate.reader.read(RawPath.empty.field("items"))).toEqual([])
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
it("creates a substrate and populates via
|
|
79
|
+
it("creates a substrate and populates via batch()", () => {
|
|
80
80
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
81
|
-
|
|
81
|
+
batch(doc, (d: any) => {
|
|
82
82
|
d.title.insert(0, "Hello")
|
|
83
83
|
d.count.set(42)
|
|
84
84
|
})
|
|
85
|
-
// Separate
|
|
85
|
+
// Separate batch() calls for list pushes to preserve order
|
|
86
86
|
// (Yjs reverses order within a single transaction)
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
batch(doc, (d: any) => d.items.push("a"))
|
|
88
|
+
batch(doc, (d: any) => d.items.push("b"))
|
|
89
89
|
expect(doc.title()).toBe("Hello")
|
|
90
90
|
expect(doc.count()).toBe(42)
|
|
91
91
|
expect(doc.items()).toEqual(["a", "b"])
|
|
@@ -93,7 +93,7 @@ describe("YjsSubstrate", () => {
|
|
|
93
93
|
|
|
94
94
|
it("creates a substrate with partial values (unset fields stay empty)", () => {
|
|
95
95
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
96
|
-
|
|
96
|
+
batch(doc, (d: any) => {
|
|
97
97
|
d.title.insert(0, "Partial")
|
|
98
98
|
})
|
|
99
99
|
expect(doc.title()).toBe("Partial")
|
|
@@ -101,19 +101,19 @@ describe("YjsSubstrate", () => {
|
|
|
101
101
|
expect(doc.items()).toEqual([])
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
-
it("creates a substrate with nested struct values via
|
|
104
|
+
it("creates a substrate with nested struct values via batch()", () => {
|
|
105
105
|
const doc = createDoc(yjs.bind(FullSchema))
|
|
106
|
-
|
|
106
|
+
batch(doc, (d: any) => {
|
|
107
107
|
d.meta.author.set("Alice")
|
|
108
108
|
})
|
|
109
109
|
expect(doc.meta.author()).toBe("Alice")
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
it("creates a substrate with struct list values via
|
|
112
|
+
it("creates a substrate with struct list values via batch()", () => {
|
|
113
113
|
const doc = createDoc(yjs.bind(StructListSchema))
|
|
114
|
-
// Separate
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
// Separate batch() calls for list pushes to preserve order
|
|
115
|
+
batch(doc, (d: any) => d.tasks.push({ name: "Task 1", done: false }))
|
|
116
|
+
batch(doc, (d: any) => d.tasks.push({ name: "Task 2", done: true }))
|
|
117
117
|
expect((doc.tasks.at(0) as any).name()).toBe("Task 1")
|
|
118
118
|
expect((doc.tasks.at(1) as any).done()).toBe(true)
|
|
119
119
|
})
|
|
@@ -126,7 +126,7 @@ describe("YjsSubstrate", () => {
|
|
|
126
126
|
describe("write round-trip", () => {
|
|
127
127
|
it("text insert round-trips through prepare/flush", () => {
|
|
128
128
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
129
|
-
|
|
129
|
+
batch(doc, (d: any) => {
|
|
130
130
|
d.title.insert(0, "Hello")
|
|
131
131
|
})
|
|
132
132
|
expect(doc.title()).toBe("Hello")
|
|
@@ -134,7 +134,7 @@ describe("YjsSubstrate", () => {
|
|
|
134
134
|
|
|
135
135
|
it("scalar set round-trips through prepare/flush", () => {
|
|
136
136
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
137
|
-
|
|
137
|
+
batch(doc, (d: any) => {
|
|
138
138
|
d.count.set(42)
|
|
139
139
|
})
|
|
140
140
|
expect(doc.count()).toBe(42)
|
|
@@ -142,10 +142,10 @@ describe("YjsSubstrate", () => {
|
|
|
142
142
|
|
|
143
143
|
it("list push round-trips through prepare/flush", () => {
|
|
144
144
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
145
|
-
|
|
145
|
+
batch(doc, (d: any) => {
|
|
146
146
|
d.items.push("a")
|
|
147
147
|
})
|
|
148
|
-
|
|
148
|
+
batch(doc, (d: any) => {
|
|
149
149
|
d.items.push("b")
|
|
150
150
|
})
|
|
151
151
|
expect(doc.items()).toEqual(["a", "b"])
|
|
@@ -162,7 +162,7 @@ describe("YjsSubstrate", () => {
|
|
|
162
162
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
163
163
|
const v1 = version(doc)
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
batch(doc, (d: any) => {
|
|
166
166
|
d.title.insert(0, "Hi")
|
|
167
167
|
})
|
|
168
168
|
const v2 = version(doc)
|
|
@@ -173,7 +173,7 @@ describe("YjsSubstrate", () => {
|
|
|
173
173
|
|
|
174
174
|
it("version serialize/parse round-trips", () => {
|
|
175
175
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
176
|
-
|
|
176
|
+
batch(doc, (d: any) => {
|
|
177
177
|
d.title.insert(0, "Test")
|
|
178
178
|
d.count.set(5)
|
|
179
179
|
})
|
|
@@ -186,12 +186,12 @@ describe("YjsSubstrate", () => {
|
|
|
186
186
|
|
|
187
187
|
it("version changes after a delete-only mutation", () => {
|
|
188
188
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
189
|
-
|
|
189
|
+
batch(doc, (d: any) => {
|
|
190
190
|
d.title.insert(0, "hello")
|
|
191
191
|
})
|
|
192
192
|
const vAfterInsert = version(doc)
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
batch(doc, (d: any) => {
|
|
195
195
|
d.title.delete(1, 1)
|
|
196
196
|
})
|
|
197
197
|
const vAfterDelete = version(doc)
|
|
@@ -210,7 +210,7 @@ describe("YjsSubstrate", () => {
|
|
|
210
210
|
describe("export/import snapshot", () => {
|
|
211
211
|
it("exports a binary payload", () => {
|
|
212
212
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
213
|
-
|
|
213
|
+
batch(doc, (d: any) => {
|
|
214
214
|
d.title.insert(0, "Snapshot")
|
|
215
215
|
})
|
|
216
216
|
const payload = exportEntirety(doc)
|
|
@@ -220,14 +220,14 @@ describe("YjsSubstrate", () => {
|
|
|
220
220
|
|
|
221
221
|
it("reconstructs equivalent state from snapshot", () => {
|
|
222
222
|
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
223
|
-
|
|
223
|
+
batch(doc1, (d: any) => {
|
|
224
224
|
d.title.insert(0, "Hello")
|
|
225
225
|
d.count.set(42)
|
|
226
226
|
})
|
|
227
|
-
// Separate
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
// Separate batch() calls for list pushes to preserve order
|
|
228
|
+
batch(doc1, (d: any) => d.items.push("a"))
|
|
229
|
+
batch(doc1, (d: any) => d.items.push("b"))
|
|
230
|
+
batch(doc1, (d: any) => {
|
|
231
231
|
d.title.insert(5, " World")
|
|
232
232
|
})
|
|
233
233
|
|
|
@@ -247,14 +247,14 @@ describe("YjsSubstrate", () => {
|
|
|
247
247
|
describe("delta sync", () => {
|
|
248
248
|
it("exportSince → merge syncs state", () => {
|
|
249
249
|
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
250
|
-
|
|
250
|
+
batch(doc1, (d: any) => {
|
|
251
251
|
d.title.insert(0, "Start")
|
|
252
252
|
})
|
|
253
253
|
const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
|
|
254
254
|
|
|
255
255
|
const v1Before = version(doc1)
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
batch(doc1, (d: any) => {
|
|
258
258
|
d.title.insert(5, " Edited")
|
|
259
259
|
d.count.set(99)
|
|
260
260
|
})
|
|
@@ -275,10 +275,10 @@ describe("YjsSubstrate", () => {
|
|
|
275
275
|
const v2Before = version(doc2)
|
|
276
276
|
|
|
277
277
|
// Independent mutations
|
|
278
|
-
|
|
278
|
+
batch(doc1, (d: any) => {
|
|
279
279
|
d.title.insert(0, "A")
|
|
280
280
|
})
|
|
281
|
-
|
|
281
|
+
batch(doc2, (d: any) => {
|
|
282
282
|
d.count.set(7)
|
|
283
283
|
})
|
|
284
284
|
|
|
@@ -315,14 +315,14 @@ describe("YjsSubstrate", () => {
|
|
|
315
315
|
describe("changefeed", () => {
|
|
316
316
|
it("fires on merge", () => {
|
|
317
317
|
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
318
|
-
|
|
318
|
+
batch(doc1, (d: any) => {
|
|
319
319
|
d.title.insert(0, "A")
|
|
320
320
|
})
|
|
321
321
|
const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
|
|
322
322
|
|
|
323
323
|
const v2Before = version(doc2)
|
|
324
324
|
|
|
325
|
-
|
|
325
|
+
batch(doc1, (d: any) => {
|
|
326
326
|
d.count.set(42)
|
|
327
327
|
})
|
|
328
328
|
|
|
@@ -367,7 +367,7 @@ describe("YjsSubstrate", () => {
|
|
|
367
367
|
received.push(changeset)
|
|
368
368
|
})
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
batch(doc, (d: any) => {
|
|
371
371
|
d.count.set(42)
|
|
372
372
|
})
|
|
373
373
|
|
|
@@ -381,7 +381,7 @@ describe("YjsSubstrate", () => {
|
|
|
381
381
|
const _doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
|
|
382
382
|
|
|
383
383
|
// Add a struct item on doc1, sync to doc2
|
|
384
|
-
|
|
384
|
+
batch(doc1, (d: any) => {
|
|
385
385
|
d.tasks.push({ name: "Buy milk", done: false })
|
|
386
386
|
})
|
|
387
387
|
const snap = exportEntirety(doc1)
|
|
@@ -398,7 +398,7 @@ describe("YjsSubstrate", () => {
|
|
|
398
398
|
const unsub = cf.subscribe((cs: unknown) => fieldChanges.push(cs))
|
|
399
399
|
|
|
400
400
|
// Toggle done on doc1
|
|
401
|
-
|
|
401
|
+
batch(doc1, (d: any) => {
|
|
402
402
|
d.tasks.at(0).done.set(true)
|
|
403
403
|
})
|
|
404
404
|
|
|
@@ -419,7 +419,7 @@ describe("YjsSubstrate", () => {
|
|
|
419
419
|
const doc1 = createDoc(yjs.bind(StructListSchema))
|
|
420
420
|
|
|
421
421
|
// Add a struct item, sync to doc2
|
|
422
|
-
|
|
422
|
+
batch(doc1, (d: any) => {
|
|
423
423
|
d.tasks.push({ name: "Buy milk", done: false })
|
|
424
424
|
})
|
|
425
425
|
const doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
|
|
@@ -435,8 +435,8 @@ describe("YjsSubstrate", () => {
|
|
|
435
435
|
const unsub1 = cfName.subscribe((cs: unknown) => nameChanges.push(cs))
|
|
436
436
|
const unsub2 = cfDone.subscribe((cs: unknown) => doneChanges.push(cs))
|
|
437
437
|
|
|
438
|
-
// Update both fields in a single
|
|
439
|
-
|
|
438
|
+
// Update both fields in a single batch() on doc1
|
|
439
|
+
batch(doc1, (d: any) => {
|
|
440
440
|
const task = d.tasks.at(0)
|
|
441
441
|
task.name.set("Buy oat milk")
|
|
442
442
|
task.done.set(true)
|
|
@@ -463,7 +463,7 @@ describe("YjsSubstrate", () => {
|
|
|
463
463
|
// -------------------------------------------------------------------------
|
|
464
464
|
|
|
465
465
|
describe("transaction support", () => {
|
|
466
|
-
it("multi-op
|
|
466
|
+
it("multi-op batch() is atomic", () => {
|
|
467
467
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
468
468
|
|
|
469
469
|
const received: any[] = []
|
|
@@ -471,7 +471,7 @@ describe("YjsSubstrate", () => {
|
|
|
471
471
|
received.push(changeset)
|
|
472
472
|
})
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
batch(doc, (d: any) => {
|
|
475
475
|
d.title.insert(0, "Hello")
|
|
476
476
|
d.count.set(42)
|
|
477
477
|
d.items.push("a")
|
|
@@ -502,7 +502,7 @@ describe("YjsSubstrate", () => {
|
|
|
502
502
|
it("push struct into list, read back via navigation", () => {
|
|
503
503
|
const doc = createDoc(yjs.bind(StructListSchema))
|
|
504
504
|
|
|
505
|
-
|
|
505
|
+
batch(doc, (d: any) => {
|
|
506
506
|
d.tasks.push({ name: "Task 1", done: false })
|
|
507
507
|
})
|
|
508
508
|
|
|
@@ -510,7 +510,7 @@ describe("YjsSubstrate", () => {
|
|
|
510
510
|
expect((doc.tasks.at(0) as any).name()).toBe("Task 1")
|
|
511
511
|
expect((doc.tasks.at(0) as any).done()).toBe(false)
|
|
512
512
|
|
|
513
|
-
|
|
513
|
+
batch(doc, (d: any) => {
|
|
514
514
|
d.tasks.push({ name: "Task 2", done: true })
|
|
515
515
|
})
|
|
516
516
|
|
|
@@ -521,12 +521,12 @@ describe("YjsSubstrate", () => {
|
|
|
521
521
|
|
|
522
522
|
it("nested struct write round-trip", () => {
|
|
523
523
|
const doc = createDoc(yjs.bind(FullSchema))
|
|
524
|
-
|
|
524
|
+
batch(doc, (d: any) => {
|
|
525
525
|
d.meta.author.set("Alice")
|
|
526
526
|
})
|
|
527
527
|
expect(doc.meta.author()).toBe("Alice")
|
|
528
528
|
|
|
529
|
-
|
|
529
|
+
batch(doc, (d: any) => {
|
|
530
530
|
d.meta.author.set("Bob")
|
|
531
531
|
})
|
|
532
532
|
|
|
@@ -580,7 +580,7 @@ describe("YjsSubstrate", () => {
|
|
|
580
580
|
|
|
581
581
|
it("reconstructs from snapshot with correct state", () => {
|
|
582
582
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
583
|
-
|
|
583
|
+
batch(doc, (d: any) => {
|
|
584
584
|
d.title.insert(0, "Snapshot Test")
|
|
585
585
|
d.count.set(77)
|
|
586
586
|
d.items.push("x")
|
|
@@ -613,17 +613,17 @@ describe("YjsSubstrate", () => {
|
|
|
613
613
|
// Re-entrant write during merge replay
|
|
614
614
|
// -------------------------------------------------------------------------
|
|
615
615
|
//
|
|
616
|
-
// A subscriber that calls `
|
|
616
|
+
// A subscriber that calls `batch(doc, ...)` while delivering a sync
|
|
617
617
|
// merge must reach Yjs — otherwise the substrate stalls and the
|
|
618
618
|
// subscriber loops on stale state until the lease budget trips.
|
|
619
619
|
// Context: jj:qpultxsw.
|
|
620
620
|
|
|
621
621
|
describe("re-entrant write during merge replay", () => {
|
|
622
|
-
it("subscriber's local
|
|
622
|
+
it("subscriber's local batch() inside a merge-replay batch lands in Yjs", () => {
|
|
623
623
|
const docA = createDoc(yjs.bind(SimpleSchema))
|
|
624
624
|
const docB = createDoc(yjs.bind(SimpleSchema))
|
|
625
625
|
|
|
626
|
-
|
|
626
|
+
batch(docA, (d: any) => {
|
|
627
627
|
d.title.insert(0, "seed")
|
|
628
628
|
})
|
|
629
629
|
merge(docB, exportEntirety(docA), { origin: "sync" })
|
|
@@ -635,14 +635,14 @@ describe("YjsSubstrate", () => {
|
|
|
635
635
|
subscribe(docB.title, () => {
|
|
636
636
|
if (writes === 0 && (docB.title() as string) === "seedmore") {
|
|
637
637
|
writes++
|
|
638
|
-
|
|
638
|
+
batch(docB, (d: any) => {
|
|
639
639
|
d.count.set(42)
|
|
640
640
|
})
|
|
641
641
|
}
|
|
642
642
|
})
|
|
643
643
|
|
|
644
644
|
const v0 = version(docB)
|
|
645
|
-
|
|
645
|
+
batch(docA, (d: any) => {
|
|
646
646
|
d.title.insert((d.title() as string).length, "more")
|
|
647
647
|
})
|
|
648
648
|
const delta = exportSince(docA, v0)!
|
|
@@ -668,7 +668,7 @@ describe("YjsSubstrate", () => {
|
|
|
668
668
|
capturedOrigin = tr.origin
|
|
669
669
|
})
|
|
670
670
|
|
|
671
|
-
|
|
671
|
+
batch(doc, d => d.title.insert(0, "x"), { origin: "undo" })
|
|
672
672
|
expect(capturedOrigin).toBe("undo")
|
|
673
673
|
})
|
|
674
674
|
|
|
@@ -682,7 +682,7 @@ describe("YjsSubstrate", () => {
|
|
|
682
682
|
|
|
683
683
|
const native = unwrap(doc) as Y.Doc
|
|
684
684
|
native.transact(() => {
|
|
685
|
-
|
|
685
|
+
batch(doc, d => d.title.insert(0, "x"))
|
|
686
686
|
}, "external")
|
|
687
687
|
|
|
688
688
|
// Should fire exactly once (captured via wrappedPrepare),
|
package/src/bind-yjs.ts
CHANGED
|
@@ -168,5 +168,5 @@ export const yjs: BindingTarget<YjsLaws, YjsNativeMap> = createBindingTarget<
|
|
|
168
168
|
>({
|
|
169
169
|
factory: ctx => createYjsFactory(ctx.peerId, ctx.binding),
|
|
170
170
|
replicaFactory: yjsReplicaFactory,
|
|
171
|
-
|
|
171
|
+
syncMode: SYNC_COLLABORATIVE,
|
|
172
172
|
})
|
package/src/index.ts
CHANGED
package/src/substrate.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// outermost logical action. Yjs's native transact nesting collapses
|
|
11
11
|
// inner re-entrant transacts into the outer one for free — no depth
|
|
12
12
|
// counter needed (unlike Loro). External `observeDeep` consumers see
|
|
13
|
-
// exactly one batched event per outermost `
|
|
13
|
+
// exactly one batched event per outermost `batch(doc, fn)`.
|
|
14
14
|
// - JSON-boundary writes (struct.json/list.json/record.json subtrees)
|
|
15
15
|
// are buffered in a per-target-key coalescer and flushed in
|
|
16
16
|
// `afterBatch`. Non-boundary writes are applied directly to λ via
|
|
@@ -63,6 +63,9 @@ import {
|
|
|
63
63
|
applyChange,
|
|
64
64
|
BACKING_DOC,
|
|
65
65
|
buildWritableContext,
|
|
66
|
+
DEVTOOLS_HISTORY,
|
|
67
|
+
type DevtoolsHistory,
|
|
68
|
+
type DevtoolsHistorySummary,
|
|
66
69
|
deepClonePreState,
|
|
67
70
|
deriveSchemaBinding,
|
|
68
71
|
executeBatch,
|
|
@@ -90,7 +93,7 @@ import { resolveYjsType } from "./yjs-resolve.js"
|
|
|
90
93
|
// that Yjs hands to observeDeep, regardless of `transaction.origin`.
|
|
91
94
|
// This frees the user-facing `origin` slot for `options.origin`
|
|
92
95
|
// round-trip and correctly handles the case where external code
|
|
93
|
-
// wraps `
|
|
96
|
+
// wraps `batch(doc, fn)` in its own `Y.transact` (Yjs's nested-
|
|
94
97
|
// transact collapse delivers the SAME Transaction object to both
|
|
95
98
|
// outer and inner callbacks; verified by probe — see TECHNICAL.md
|
|
96
99
|
// "Why transaction.meta mark").
|
|
@@ -248,6 +251,7 @@ export function createYjsSubstrate(
|
|
|
248
251
|
|
|
249
252
|
const substrate = {
|
|
250
253
|
[BACKING_DOC]: doc,
|
|
254
|
+
[DEVTOOLS_HISTORY]: yjsDevtoolsHistory(() => doc),
|
|
251
255
|
|
|
252
256
|
reader: reader,
|
|
253
257
|
|
|
@@ -447,7 +451,7 @@ export function createYjsSubstrate(
|
|
|
447
451
|
rootMap.observeDeep((events, transaction) => {
|
|
448
452
|
// Own-commit discriminator: kyneta's runBatch marks the transaction
|
|
449
453
|
// via `tr.meta.set` inside the transact body. The mark survives Yjs's
|
|
450
|
-
// nested-transact collapse, so external code wrapping `
|
|
454
|
+
// nested-transact collapse, so external code wrapping `batch()` in
|
|
451
455
|
// its own Y.transact is correctly classified as own.
|
|
452
456
|
if (transaction.meta.get(KYNETA_MARK)) {
|
|
453
457
|
return
|
|
@@ -486,7 +490,7 @@ export function createYjsSubstrate(
|
|
|
486
490
|
*
|
|
487
491
|
* - `create(schema)` — creates a fresh Y.Doc with empty containers
|
|
488
492
|
* matching the schema structure. No seed data — initial content
|
|
489
|
-
* should be applied via `
|
|
493
|
+
* should be applied via `batch()` after construction.
|
|
490
494
|
* - `fromEntirety(payload, schema)` — creates a Y.Doc from an entirety
|
|
491
495
|
* payload, returns a substrate.
|
|
492
496
|
* - `parseVersion(serialized)` — deserializes a YjsVersion.
|
|
@@ -521,6 +525,33 @@ function trivialBinding(schema: SchemaNode): SchemaBinding {
|
|
|
521
525
|
* that need to accumulate state, compute per-peer deltas, and compact
|
|
522
526
|
* storage without ever interpreting document fields.
|
|
523
527
|
*/
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
// DevTools history capability (pull) — version/op summary.
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Build the `DevtoolsHistory` capability over a Y.Doc accessor.
|
|
534
|
+
*
|
|
535
|
+
* `summary()` only: reliable Yjs time-travel (`valueAt`) requires the doc to
|
|
536
|
+
* be constructed with `gc: false`, which this substrate does not impose (it
|
|
537
|
+
* wraps a user-provided Y.Doc). So `valueAt` is intentionally omitted.
|
|
538
|
+
* Context: jj:qpmkoryn.
|
|
539
|
+
*/
|
|
540
|
+
function yjsDevtoolsHistory(getDoc: () => Y.Doc): DevtoolsHistory {
|
|
541
|
+
return {
|
|
542
|
+
summary(): DevtoolsHistorySummary {
|
|
543
|
+
const sv = Y.encodeStateVector(getDoc())
|
|
544
|
+
const actors: Record<string, number> = {}
|
|
545
|
+
let opCount = 0
|
|
546
|
+
for (const [client, clock] of Y.decodeStateVector(sv)) {
|
|
547
|
+
actors[String(client)] = clock
|
|
548
|
+
opCount += clock
|
|
549
|
+
}
|
|
550
|
+
return { version: new YjsVersion(sv).serialize(), opCount, actors }
|
|
551
|
+
},
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
524
555
|
export function createYjsReplica(doc: Y.Doc): Replica<YjsVersion> {
|
|
525
556
|
let currentDoc = doc
|
|
526
557
|
let currentBase: YjsVersion = new YjsVersion(Y.encodeStateVector(new Y.Doc()))
|
|
@@ -529,6 +560,7 @@ export function createYjsReplica(doc: Y.Doc): Replica<YjsVersion> {
|
|
|
529
560
|
get [BACKING_DOC]() {
|
|
530
561
|
return currentDoc
|
|
531
562
|
},
|
|
563
|
+
[DEVTOOLS_HISTORY]: yjsDevtoolsHistory(() => currentDoc),
|
|
532
564
|
|
|
533
565
|
version(): YjsVersion {
|
|
534
566
|
return YjsVersion.fromDoc(currentDoc)
|