@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.
@@ -1,5 +1,5 @@
1
1
  import {
2
- change,
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 change()", () => {
79
+ it("creates a substrate and populates via batch()", () => {
80
80
  const doc = createDoc(yjs.bind(SimpleSchema))
81
- change(doc, (d: any) => {
81
+ batch(doc, (d: any) => {
82
82
  d.title.insert(0, "Hello")
83
83
  d.count.set(42)
84
84
  })
85
- // Separate change() calls for list pushes to preserve order
85
+ // Separate batch() calls for list pushes to preserve order
86
86
  // (Yjs reverses order within a single transaction)
87
- change(doc, (d: any) => d.items.push("a"))
88
- change(doc, (d: any) => d.items.push("b"))
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
- change(doc, (d: any) => {
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 change()", () => {
104
+ it("creates a substrate with nested struct values via batch()", () => {
105
105
  const doc = createDoc(yjs.bind(FullSchema))
106
- change(doc, (d: any) => {
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 change()", () => {
112
+ it("creates a substrate with struct list values via batch()", () => {
113
113
  const doc = createDoc(yjs.bind(StructListSchema))
114
- // Separate change() calls for list pushes to preserve order
115
- change(doc, (d: any) => d.tasks.push({ name: "Task 1", done: false }))
116
- change(doc, (d: any) => d.tasks.push({ name: "Task 2", done: true }))
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
145
+ batch(doc, (d: any) => {
146
146
  d.items.push("a")
147
147
  })
148
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
189
+ batch(doc, (d: any) => {
190
190
  d.title.insert(0, "hello")
191
191
  })
192
192
  const vAfterInsert = version(doc)
193
193
 
194
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc1, (d: any) => {
223
+ batch(doc1, (d: any) => {
224
224
  d.title.insert(0, "Hello")
225
225
  d.count.set(42)
226
226
  })
227
- // Separate change() calls for list pushes to preserve order
228
- change(doc1, (d: any) => d.items.push("a"))
229
- change(doc1, (d: any) => d.items.push("b"))
230
- change(doc1, (d: any) => {
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
- change(doc1, (d: any) => {
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
- change(doc1, (d: any) => {
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
- change(doc1, (d: any) => {
278
+ batch(doc1, (d: any) => {
279
279
  d.title.insert(0, "A")
280
280
  })
281
- change(doc2, (d: any) => {
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
- change(doc1, (d: any) => {
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
- change(doc1, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc1, (d: any) => {
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
- change(doc1, (d: any) => {
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
- change(doc1, (d: any) => {
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 change() on doc1
439
- change(doc1, (d: any) => {
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 change() is atomic", () => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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
- change(doc, (d: any) => {
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 `change(doc, ...)` while delivering a sync
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 change() inside a merge-replay batch lands in Yjs", () => {
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
- change(docA, (d: any) => {
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
- change(docB, (d: any) => {
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
- change(docA, (d: any) => {
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
- change(doc, d => d.title.insert(0, "x"), { origin: "undo" })
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
- change(doc, d => d.title.insert(0, "x"))
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
- syncProtocol: SYNC_COLLABORATIVE,
171
+ syncMode: SYNC_COLLABORATIVE,
172
172
  })
package/src/index.ts CHANGED
@@ -21,7 +21,7 @@ export type { DocRef, Op, Ref, SubstratePayload } from "@kyneta/schema"
21
21
  // Sync primitives (generic — work for any substrate)
22
22
  export {
23
23
  applyChanges,
24
- change,
24
+ batch,
25
25
  createDoc,
26
26
  createRef,
27
27
  exportEntirety,
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 `change(doc, fn)`.
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 `change(doc, fn)` in its own `Y.transact` (Yjs's nested-
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 `change()` in
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 `change()` after construction.
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)