@kyneta/yjs-schema 1.7.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 +6 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +121 -37
- 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 +60 -49
- package/src/__tests__/eager-write-coherence.test.ts +321 -0
- 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 +106 -51
- package/src/bind-yjs.ts +1 -1
- package/src/change-mapping.ts +11 -13
- package/src/index.ts +1 -1
- package/src/populate.ts +13 -1
- package/src/substrate.ts +298 -113
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// 4. Sync: two peers should converge after exchanging deltas
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
|
-
|
|
20
|
+
batch,
|
|
21
21
|
createDoc,
|
|
22
22
|
exportEntirety,
|
|
23
23
|
exportSince,
|
|
@@ -76,7 +76,7 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
76
76
|
it("set a record entry and read it back", () => {
|
|
77
77
|
const doc = createDoc(BoundPlainRecord)
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
batch(doc, (d: any) => {
|
|
80
80
|
d.profiles.set("alice", { displayName: "Alice", age: 30 })
|
|
81
81
|
})
|
|
82
82
|
|
|
@@ -89,7 +89,7 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
89
89
|
it("set multiple entries and read all back", () => {
|
|
90
90
|
const doc = createDoc(BoundPlainRecord)
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
batch(doc, (d: any) => {
|
|
93
93
|
d.profiles.set("alice", { displayName: "Alice", age: 30 })
|
|
94
94
|
d.profiles.set("bob", { displayName: "Bob", age: 25 })
|
|
95
95
|
})
|
|
@@ -104,7 +104,7 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
104
104
|
it("navigate into a record entry via .at()", () => {
|
|
105
105
|
const doc = createDoc(BoundPlainRecord)
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
batch(doc, (d: any) => {
|
|
108
108
|
d.profiles.set("alice", { displayName: "Alice", age: 30 })
|
|
109
109
|
})
|
|
110
110
|
|
|
@@ -117,7 +117,7 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
117
117
|
it("syncs record-of-struct between two peers via snapshot", () => {
|
|
118
118
|
const docA = createDoc(BoundPlainRecord)
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
batch(docA, (d: any) => {
|
|
121
121
|
d.profiles.set("alice", { displayName: "Alice", age: 30 })
|
|
122
122
|
})
|
|
123
123
|
|
|
@@ -132,7 +132,7 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
132
132
|
it("syncs record-of-struct between two peers via delta", () => {
|
|
133
133
|
const docA = createDoc(BoundPlainRecord)
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
batch(docA, (d: any) => {
|
|
136
136
|
d.profiles.set("alice", { displayName: "Alice", age: 30 })
|
|
137
137
|
})
|
|
138
138
|
|
|
@@ -141,7 +141,7 @@ describe("record-of-struct (plain baseline)", () => {
|
|
|
141
141
|
|
|
142
142
|
const v0 = version(docB)
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
batch(docA, (d: any) => {
|
|
145
145
|
d.profiles.set("bob", { displayName: "Bob", age: 25 })
|
|
146
146
|
})
|
|
147
147
|
|
|
@@ -164,7 +164,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
164
164
|
it("set a record entry with text field omitted and read it back", () => {
|
|
165
165
|
const doc = createDoc(BoundProfile)
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
batch(doc, (d: any) => {
|
|
168
168
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
169
169
|
})
|
|
170
170
|
|
|
@@ -177,7 +177,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
177
177
|
it("set a record entry with text field provided and read it back", () => {
|
|
178
178
|
const doc = createDoc(BoundProfile)
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
batch(doc, (d: any) => {
|
|
181
181
|
d.profiles.set("alice", { displayName: "Alice", bio: "Hello world" })
|
|
182
182
|
})
|
|
183
183
|
|
|
@@ -190,7 +190,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
190
190
|
it("navigate into a record entry and read the text", () => {
|
|
191
191
|
const doc = createDoc(BoundProfile)
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
batch(doc, (d: any) => {
|
|
194
194
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
195
195
|
})
|
|
196
196
|
|
|
@@ -203,11 +203,11 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
203
203
|
it("insert text into a text field inside a record entry (field omitted at creation)", () => {
|
|
204
204
|
const doc = createDoc(BoundProfile)
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
batch(doc, (d: any) => {
|
|
207
207
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
208
208
|
})
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
batch(doc, (d: any) => {
|
|
211
211
|
d.profiles.at("alice").bio.insert(0, "Hello world")
|
|
212
212
|
})
|
|
213
213
|
|
|
@@ -217,11 +217,11 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
217
217
|
it("insert text into a text field inside a record entry (field provided at creation)", () => {
|
|
218
218
|
const doc = createDoc(BoundProfile)
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
batch(doc, (d: any) => {
|
|
221
221
|
d.profiles.set("alice", { displayName: "Alice", bio: "Initial" })
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
batch(doc, (d: any) => {
|
|
225
225
|
d.profiles.at("alice").bio.insert(7, " bio")
|
|
226
226
|
})
|
|
227
227
|
|
|
@@ -231,12 +231,12 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
231
231
|
it("set multiple entries and edit text independently", () => {
|
|
232
232
|
const doc = createDoc(BoundProfile)
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
batch(doc, (d: any) => {
|
|
235
235
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
236
236
|
d.profiles.set("bob", { displayName: "Bob" })
|
|
237
237
|
})
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
batch(doc, (d: any) => {
|
|
240
240
|
d.profiles.at("alice").bio.insert(0, "Alice's bio")
|
|
241
241
|
d.profiles.at("bob").bio.insert(0, "Bob's bio")
|
|
242
242
|
})
|
|
@@ -248,7 +248,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
248
248
|
it("subscribe fires on text edit inside record entry", () => {
|
|
249
249
|
const doc = createDoc(BoundProfile)
|
|
250
250
|
|
|
251
|
-
|
|
251
|
+
batch(doc, (d: any) => {
|
|
252
252
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
253
253
|
})
|
|
254
254
|
|
|
@@ -257,7 +257,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
257
257
|
fired = true
|
|
258
258
|
})
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
batch(doc, (d: any) => {
|
|
261
261
|
d.profiles.at("alice").bio.insert(0, "Hello")
|
|
262
262
|
})
|
|
263
263
|
|
|
@@ -267,10 +267,10 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
267
267
|
it("syncs text-inside-record via snapshot", () => {
|
|
268
268
|
const docA = createDoc(BoundProfile)
|
|
269
269
|
|
|
270
|
-
|
|
270
|
+
batch(docA, (d: any) => {
|
|
271
271
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
272
272
|
})
|
|
273
|
-
|
|
273
|
+
batch(docA, (d: any) => {
|
|
274
274
|
d.profiles.at("alice").bio.insert(0, "Collaborative bio")
|
|
275
275
|
})
|
|
276
276
|
|
|
@@ -288,7 +288,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
288
288
|
it("syncs text-inside-record via delta", () => {
|
|
289
289
|
const docA = createDoc(BoundProfile)
|
|
290
290
|
|
|
291
|
-
|
|
291
|
+
batch(docA, (d: any) => {
|
|
292
292
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
293
293
|
})
|
|
294
294
|
|
|
@@ -298,7 +298,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
298
298
|
const docB = createDoc(BoundProfile, exportEntirety(docA))
|
|
299
299
|
const v0 = version(docB)
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
batch(docA, (d: any) => {
|
|
302
302
|
d.profiles.at("alice").bio.insert(0, "Hello from A")
|
|
303
303
|
})
|
|
304
304
|
|
|
@@ -311,7 +311,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
311
311
|
})
|
|
312
312
|
|
|
313
313
|
// Text on B is functional — can insert independently
|
|
314
|
-
|
|
314
|
+
batch(docB, (d: any) => {
|
|
315
315
|
d.profiles.at("alice").bio.insert(12, "!")
|
|
316
316
|
})
|
|
317
317
|
expect((docB as any).profiles.at("alice").bio()).toBe("Hello from A!")
|
|
@@ -321,7 +321,7 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
321
321
|
const docA = createDoc(BoundProfile)
|
|
322
322
|
|
|
323
323
|
// Sync initial state: A creates the entry, B starts from snapshot
|
|
324
|
-
|
|
324
|
+
batch(docA, (d: any) => {
|
|
325
325
|
d.profiles.set("alice", { displayName: "Alice" })
|
|
326
326
|
})
|
|
327
327
|
const docB = createDoc(BoundProfile, exportEntirety(docA))
|
|
@@ -330,10 +330,10 @@ describe("text-inside-struct-inside-record", () => {
|
|
|
330
330
|
const vA = version(docA)
|
|
331
331
|
const vB = version(docB)
|
|
332
332
|
|
|
333
|
-
|
|
333
|
+
batch(docA, (d: any) => {
|
|
334
334
|
d.profiles.at("alice").bio.insert(0, "Hello ")
|
|
335
335
|
})
|
|
336
|
-
|
|
336
|
+
batch(docB, (d: any) => {
|
|
337
337
|
d.profiles.at("alice").bio.insert(0, "World")
|
|
338
338
|
})
|
|
339
339
|
|
|
@@ -360,7 +360,7 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
360
360
|
it("push a struct with text field omitted and read it back", () => {
|
|
361
361
|
const doc = createDoc(BoundListProfile)
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
batch(doc, (d: any) => {
|
|
364
364
|
d.players.push({ name: "Alice" })
|
|
365
365
|
})
|
|
366
366
|
|
|
@@ -372,7 +372,7 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
372
372
|
it("push a struct with text field provided and read it back", () => {
|
|
373
373
|
const doc = createDoc(BoundListProfile)
|
|
374
374
|
|
|
375
|
-
|
|
375
|
+
batch(doc, (d: any) => {
|
|
376
376
|
d.players.push({ name: "Alice", bio: "Hi there" })
|
|
377
377
|
})
|
|
378
378
|
|
|
@@ -384,11 +384,11 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
384
384
|
it("insert text into a text field inside a list item (field omitted at creation)", () => {
|
|
385
385
|
const doc = createDoc(BoundListProfile)
|
|
386
386
|
|
|
387
|
-
|
|
387
|
+
batch(doc, (d: any) => {
|
|
388
388
|
d.players.push({ name: "Alice" })
|
|
389
389
|
})
|
|
390
390
|
|
|
391
|
-
|
|
391
|
+
batch(doc, (d: any) => {
|
|
392
392
|
d.players.at(0).bio.insert(0, "Alice's bio")
|
|
393
393
|
})
|
|
394
394
|
|
|
@@ -398,12 +398,12 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
398
398
|
it("multiple list items with independent text fields", () => {
|
|
399
399
|
const doc = createDoc(BoundListProfile)
|
|
400
400
|
|
|
401
|
-
|
|
401
|
+
batch(doc, (d: any) => {
|
|
402
402
|
d.players.push({ name: "Alice" })
|
|
403
403
|
d.players.push({ name: "Bob" })
|
|
404
404
|
})
|
|
405
405
|
|
|
406
|
-
|
|
406
|
+
batch(doc, (d: any) => {
|
|
407
407
|
d.players.at(0).bio.insert(0, "Alice's bio")
|
|
408
408
|
d.players.at(1).bio.insert(0, "Bob's bio")
|
|
409
409
|
})
|
|
@@ -415,7 +415,7 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
415
415
|
it("syncs list-of-struct-with-text via delta", () => {
|
|
416
416
|
const docA = createDoc(BoundListProfile)
|
|
417
417
|
|
|
418
|
-
|
|
418
|
+
batch(docA, (d: any) => {
|
|
419
419
|
d.players.push({ name: "Alice" })
|
|
420
420
|
})
|
|
421
421
|
|
|
@@ -423,7 +423,7 @@ describe("text-inside-struct-inside-list", () => {
|
|
|
423
423
|
const docB = createDoc(BoundListProfile, exportEntirety(docA))
|
|
424
424
|
const v0 = version(docB)
|
|
425
425
|
|
|
426
|
-
|
|
426
|
+
batch(docA, (d: any) => {
|
|
427
427
|
d.players.at(0).bio.insert(0, "Synced bio")
|
|
428
428
|
})
|
|
429
429
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
batch,
|
|
3
3
|
createDoc,
|
|
4
4
|
createRef,
|
|
5
5
|
exportEntirety,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
RawPath,
|
|
9
9
|
Schema,
|
|
10
10
|
subscribe,
|
|
11
|
+
unwrap,
|
|
11
12
|
version,
|
|
12
13
|
} from "@kyneta/schema"
|
|
13
14
|
import { describe, expect, it } from "vitest"
|
|
@@ -75,16 +76,16 @@ describe("YjsSubstrate", () => {
|
|
|
75
76
|
expect(substrate.reader.read(RawPath.empty.field("items"))).toEqual([])
|
|
76
77
|
})
|
|
77
78
|
|
|
78
|
-
it("creates a substrate and populates via
|
|
79
|
+
it("creates a substrate and populates via batch()", () => {
|
|
79
80
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
80
|
-
|
|
81
|
+
batch(doc, (d: any) => {
|
|
81
82
|
d.title.insert(0, "Hello")
|
|
82
83
|
d.count.set(42)
|
|
83
84
|
})
|
|
84
|
-
// Separate
|
|
85
|
+
// Separate batch() calls for list pushes to preserve order
|
|
85
86
|
// (Yjs reverses order within a single transaction)
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
batch(doc, (d: any) => d.items.push("a"))
|
|
88
|
+
batch(doc, (d: any) => d.items.push("b"))
|
|
88
89
|
expect(doc.title()).toBe("Hello")
|
|
89
90
|
expect(doc.count()).toBe(42)
|
|
90
91
|
expect(doc.items()).toEqual(["a", "b"])
|
|
@@ -92,7 +93,7 @@ describe("YjsSubstrate", () => {
|
|
|
92
93
|
|
|
93
94
|
it("creates a substrate with partial values (unset fields stay empty)", () => {
|
|
94
95
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
95
|
-
|
|
96
|
+
batch(doc, (d: any) => {
|
|
96
97
|
d.title.insert(0, "Partial")
|
|
97
98
|
})
|
|
98
99
|
expect(doc.title()).toBe("Partial")
|
|
@@ -100,19 +101,19 @@ describe("YjsSubstrate", () => {
|
|
|
100
101
|
expect(doc.items()).toEqual([])
|
|
101
102
|
})
|
|
102
103
|
|
|
103
|
-
it("creates a substrate with nested struct values via
|
|
104
|
+
it("creates a substrate with nested struct values via batch()", () => {
|
|
104
105
|
const doc = createDoc(yjs.bind(FullSchema))
|
|
105
|
-
|
|
106
|
+
batch(doc, (d: any) => {
|
|
106
107
|
d.meta.author.set("Alice")
|
|
107
108
|
})
|
|
108
109
|
expect(doc.meta.author()).toBe("Alice")
|
|
109
110
|
})
|
|
110
111
|
|
|
111
|
-
it("creates a substrate with struct list values via
|
|
112
|
+
it("creates a substrate with struct list values via batch()", () => {
|
|
112
113
|
const doc = createDoc(yjs.bind(StructListSchema))
|
|
113
|
-
// Separate
|
|
114
|
-
|
|
115
|
-
|
|
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 }))
|
|
116
117
|
expect((doc.tasks.at(0) as any).name()).toBe("Task 1")
|
|
117
118
|
expect((doc.tasks.at(1) as any).done()).toBe(true)
|
|
118
119
|
})
|
|
@@ -125,7 +126,7 @@ describe("YjsSubstrate", () => {
|
|
|
125
126
|
describe("write round-trip", () => {
|
|
126
127
|
it("text insert round-trips through prepare/flush", () => {
|
|
127
128
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
128
|
-
|
|
129
|
+
batch(doc, (d: any) => {
|
|
129
130
|
d.title.insert(0, "Hello")
|
|
130
131
|
})
|
|
131
132
|
expect(doc.title()).toBe("Hello")
|
|
@@ -133,7 +134,7 @@ describe("YjsSubstrate", () => {
|
|
|
133
134
|
|
|
134
135
|
it("scalar set round-trips through prepare/flush", () => {
|
|
135
136
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
136
|
-
|
|
137
|
+
batch(doc, (d: any) => {
|
|
137
138
|
d.count.set(42)
|
|
138
139
|
})
|
|
139
140
|
expect(doc.count()).toBe(42)
|
|
@@ -141,10 +142,10 @@ describe("YjsSubstrate", () => {
|
|
|
141
142
|
|
|
142
143
|
it("list push round-trips through prepare/flush", () => {
|
|
143
144
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
144
|
-
|
|
145
|
+
batch(doc, (d: any) => {
|
|
145
146
|
d.items.push("a")
|
|
146
147
|
})
|
|
147
|
-
|
|
148
|
+
batch(doc, (d: any) => {
|
|
148
149
|
d.items.push("b")
|
|
149
150
|
})
|
|
150
151
|
expect(doc.items()).toEqual(["a", "b"])
|
|
@@ -161,7 +162,7 @@ describe("YjsSubstrate", () => {
|
|
|
161
162
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
162
163
|
const v1 = version(doc)
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
batch(doc, (d: any) => {
|
|
165
166
|
d.title.insert(0, "Hi")
|
|
166
167
|
})
|
|
167
168
|
const v2 = version(doc)
|
|
@@ -172,7 +173,7 @@ describe("YjsSubstrate", () => {
|
|
|
172
173
|
|
|
173
174
|
it("version serialize/parse round-trips", () => {
|
|
174
175
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
175
|
-
|
|
176
|
+
batch(doc, (d: any) => {
|
|
176
177
|
d.title.insert(0, "Test")
|
|
177
178
|
d.count.set(5)
|
|
178
179
|
})
|
|
@@ -185,12 +186,12 @@ describe("YjsSubstrate", () => {
|
|
|
185
186
|
|
|
186
187
|
it("version changes after a delete-only mutation", () => {
|
|
187
188
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
188
|
-
|
|
189
|
+
batch(doc, (d: any) => {
|
|
189
190
|
d.title.insert(0, "hello")
|
|
190
191
|
})
|
|
191
192
|
const vAfterInsert = version(doc)
|
|
192
193
|
|
|
193
|
-
|
|
194
|
+
batch(doc, (d: any) => {
|
|
194
195
|
d.title.delete(1, 1)
|
|
195
196
|
})
|
|
196
197
|
const vAfterDelete = version(doc)
|
|
@@ -209,7 +210,7 @@ describe("YjsSubstrate", () => {
|
|
|
209
210
|
describe("export/import snapshot", () => {
|
|
210
211
|
it("exports a binary payload", () => {
|
|
211
212
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
212
|
-
|
|
213
|
+
batch(doc, (d: any) => {
|
|
213
214
|
d.title.insert(0, "Snapshot")
|
|
214
215
|
})
|
|
215
216
|
const payload = exportEntirety(doc)
|
|
@@ -219,14 +220,14 @@ describe("YjsSubstrate", () => {
|
|
|
219
220
|
|
|
220
221
|
it("reconstructs equivalent state from snapshot", () => {
|
|
221
222
|
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
222
|
-
|
|
223
|
+
batch(doc1, (d: any) => {
|
|
223
224
|
d.title.insert(0, "Hello")
|
|
224
225
|
d.count.set(42)
|
|
225
226
|
})
|
|
226
|
-
// Separate
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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) => {
|
|
230
231
|
d.title.insert(5, " World")
|
|
231
232
|
})
|
|
232
233
|
|
|
@@ -246,14 +247,14 @@ describe("YjsSubstrate", () => {
|
|
|
246
247
|
describe("delta sync", () => {
|
|
247
248
|
it("exportSince → merge syncs state", () => {
|
|
248
249
|
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
249
|
-
|
|
250
|
+
batch(doc1, (d: any) => {
|
|
250
251
|
d.title.insert(0, "Start")
|
|
251
252
|
})
|
|
252
253
|
const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
|
|
253
254
|
|
|
254
255
|
const v1Before = version(doc1)
|
|
255
256
|
|
|
256
|
-
|
|
257
|
+
batch(doc1, (d: any) => {
|
|
257
258
|
d.title.insert(5, " Edited")
|
|
258
259
|
d.count.set(99)
|
|
259
260
|
})
|
|
@@ -274,10 +275,10 @@ describe("YjsSubstrate", () => {
|
|
|
274
275
|
const v2Before = version(doc2)
|
|
275
276
|
|
|
276
277
|
// Independent mutations
|
|
277
|
-
|
|
278
|
+
batch(doc1, (d: any) => {
|
|
278
279
|
d.title.insert(0, "A")
|
|
279
280
|
})
|
|
280
|
-
|
|
281
|
+
batch(doc2, (d: any) => {
|
|
281
282
|
d.count.set(7)
|
|
282
283
|
})
|
|
283
284
|
|
|
@@ -314,14 +315,14 @@ describe("YjsSubstrate", () => {
|
|
|
314
315
|
describe("changefeed", () => {
|
|
315
316
|
it("fires on merge", () => {
|
|
316
317
|
const doc1 = createDoc(yjs.bind(SimpleSchema))
|
|
317
|
-
|
|
318
|
+
batch(doc1, (d: any) => {
|
|
318
319
|
d.title.insert(0, "A")
|
|
319
320
|
})
|
|
320
321
|
const doc2 = createDoc(yjs.bind(SimpleSchema), exportEntirety(doc1))
|
|
321
322
|
|
|
322
323
|
const v2Before = version(doc2)
|
|
323
324
|
|
|
324
|
-
|
|
325
|
+
batch(doc1, (d: any) => {
|
|
325
326
|
d.count.set(42)
|
|
326
327
|
})
|
|
327
328
|
|
|
@@ -366,7 +367,7 @@ describe("YjsSubstrate", () => {
|
|
|
366
367
|
received.push(changeset)
|
|
367
368
|
})
|
|
368
369
|
|
|
369
|
-
|
|
370
|
+
batch(doc, (d: any) => {
|
|
370
371
|
d.count.set(42)
|
|
371
372
|
})
|
|
372
373
|
|
|
@@ -380,7 +381,7 @@ describe("YjsSubstrate", () => {
|
|
|
380
381
|
const _doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
|
|
381
382
|
|
|
382
383
|
// Add a struct item on doc1, sync to doc2
|
|
383
|
-
|
|
384
|
+
batch(doc1, (d: any) => {
|
|
384
385
|
d.tasks.push({ name: "Buy milk", done: false })
|
|
385
386
|
})
|
|
386
387
|
const snap = exportEntirety(doc1)
|
|
@@ -397,7 +398,7 @@ describe("YjsSubstrate", () => {
|
|
|
397
398
|
const unsub = cf.subscribe((cs: unknown) => fieldChanges.push(cs))
|
|
398
399
|
|
|
399
400
|
// Toggle done on doc1
|
|
400
|
-
|
|
401
|
+
batch(doc1, (d: any) => {
|
|
401
402
|
d.tasks.at(0).done.set(true)
|
|
402
403
|
})
|
|
403
404
|
|
|
@@ -418,7 +419,7 @@ describe("YjsSubstrate", () => {
|
|
|
418
419
|
const doc1 = createDoc(yjs.bind(StructListSchema))
|
|
419
420
|
|
|
420
421
|
// Add a struct item, sync to doc2
|
|
421
|
-
|
|
422
|
+
batch(doc1, (d: any) => {
|
|
422
423
|
d.tasks.push({ name: "Buy milk", done: false })
|
|
423
424
|
})
|
|
424
425
|
const doc2 = createDoc(yjs.bind(StructListSchema), exportEntirety(doc1))
|
|
@@ -434,8 +435,8 @@ describe("YjsSubstrate", () => {
|
|
|
434
435
|
const unsub1 = cfName.subscribe((cs: unknown) => nameChanges.push(cs))
|
|
435
436
|
const unsub2 = cfDone.subscribe((cs: unknown) => doneChanges.push(cs))
|
|
436
437
|
|
|
437
|
-
// Update both fields in a single
|
|
438
|
-
|
|
438
|
+
// Update both fields in a single batch() on doc1
|
|
439
|
+
batch(doc1, (d: any) => {
|
|
439
440
|
const task = d.tasks.at(0)
|
|
440
441
|
task.name.set("Buy oat milk")
|
|
441
442
|
task.done.set(true)
|
|
@@ -462,7 +463,7 @@ describe("YjsSubstrate", () => {
|
|
|
462
463
|
// -------------------------------------------------------------------------
|
|
463
464
|
|
|
464
465
|
describe("transaction support", () => {
|
|
465
|
-
it("multi-op
|
|
466
|
+
it("multi-op batch() is atomic", () => {
|
|
466
467
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
467
468
|
|
|
468
469
|
const received: any[] = []
|
|
@@ -470,7 +471,7 @@ describe("YjsSubstrate", () => {
|
|
|
470
471
|
received.push(changeset)
|
|
471
472
|
})
|
|
472
473
|
|
|
473
|
-
|
|
474
|
+
batch(doc, (d: any) => {
|
|
474
475
|
d.title.insert(0, "Hello")
|
|
475
476
|
d.count.set(42)
|
|
476
477
|
d.items.push("a")
|
|
@@ -501,7 +502,7 @@ describe("YjsSubstrate", () => {
|
|
|
501
502
|
it("push struct into list, read back via navigation", () => {
|
|
502
503
|
const doc = createDoc(yjs.bind(StructListSchema))
|
|
503
504
|
|
|
504
|
-
|
|
505
|
+
batch(doc, (d: any) => {
|
|
505
506
|
d.tasks.push({ name: "Task 1", done: false })
|
|
506
507
|
})
|
|
507
508
|
|
|
@@ -509,7 +510,7 @@ describe("YjsSubstrate", () => {
|
|
|
509
510
|
expect((doc.tasks.at(0) as any).name()).toBe("Task 1")
|
|
510
511
|
expect((doc.tasks.at(0) as any).done()).toBe(false)
|
|
511
512
|
|
|
512
|
-
|
|
513
|
+
batch(doc, (d: any) => {
|
|
513
514
|
d.tasks.push({ name: "Task 2", done: true })
|
|
514
515
|
})
|
|
515
516
|
|
|
@@ -520,12 +521,12 @@ describe("YjsSubstrate", () => {
|
|
|
520
521
|
|
|
521
522
|
it("nested struct write round-trip", () => {
|
|
522
523
|
const doc = createDoc(yjs.bind(FullSchema))
|
|
523
|
-
|
|
524
|
+
batch(doc, (d: any) => {
|
|
524
525
|
d.meta.author.set("Alice")
|
|
525
526
|
})
|
|
526
527
|
expect(doc.meta.author()).toBe("Alice")
|
|
527
528
|
|
|
528
|
-
|
|
529
|
+
batch(doc, (d: any) => {
|
|
529
530
|
d.meta.author.set("Bob")
|
|
530
531
|
})
|
|
531
532
|
|
|
@@ -579,7 +580,7 @@ describe("YjsSubstrate", () => {
|
|
|
579
580
|
|
|
580
581
|
it("reconstructs from snapshot with correct state", () => {
|
|
581
582
|
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
582
|
-
|
|
583
|
+
batch(doc, (d: any) => {
|
|
583
584
|
d.title.insert(0, "Snapshot Test")
|
|
584
585
|
d.count.set(77)
|
|
585
586
|
d.items.push("x")
|
|
@@ -612,17 +613,17 @@ describe("YjsSubstrate", () => {
|
|
|
612
613
|
// Re-entrant write during merge replay
|
|
613
614
|
// -------------------------------------------------------------------------
|
|
614
615
|
//
|
|
615
|
-
// A subscriber that calls `
|
|
616
|
+
// A subscriber that calls `batch(doc, ...)` while delivering a sync
|
|
616
617
|
// merge must reach Yjs — otherwise the substrate stalls and the
|
|
617
618
|
// subscriber loops on stale state until the lease budget trips.
|
|
618
619
|
// Context: jj:qpultxsw.
|
|
619
620
|
|
|
620
621
|
describe("re-entrant write during merge replay", () => {
|
|
621
|
-
it("subscriber's local
|
|
622
|
+
it("subscriber's local batch() inside a merge-replay batch lands in Yjs", () => {
|
|
622
623
|
const docA = createDoc(yjs.bind(SimpleSchema))
|
|
623
624
|
const docB = createDoc(yjs.bind(SimpleSchema))
|
|
624
625
|
|
|
625
|
-
|
|
626
|
+
batch(docA, (d: any) => {
|
|
626
627
|
d.title.insert(0, "seed")
|
|
627
628
|
})
|
|
628
629
|
merge(docB, exportEntirety(docA), { origin: "sync" })
|
|
@@ -634,14 +635,14 @@ describe("YjsSubstrate", () => {
|
|
|
634
635
|
subscribe(docB.title, () => {
|
|
635
636
|
if (writes === 0 && (docB.title() as string) === "seedmore") {
|
|
636
637
|
writes++
|
|
637
|
-
|
|
638
|
+
batch(docB, (d: any) => {
|
|
638
639
|
d.count.set(42)
|
|
639
640
|
})
|
|
640
641
|
}
|
|
641
642
|
})
|
|
642
643
|
|
|
643
644
|
const v0 = version(docB)
|
|
644
|
-
|
|
645
|
+
batch(docA, (d: any) => {
|
|
645
646
|
d.title.insert((d.title() as string).length, "more")
|
|
646
647
|
})
|
|
647
648
|
const delta = exportSince(docA, v0)!
|
|
@@ -652,4 +653,58 @@ describe("YjsSubstrate", () => {
|
|
|
652
653
|
expect(writes).toBe(1)
|
|
653
654
|
})
|
|
654
655
|
})
|
|
656
|
+
|
|
657
|
+
// -------------------------------------------------------------------------
|
|
658
|
+
// Origin-free discriminator tests
|
|
659
|
+
// -------------------------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
describe("origin-free discriminator", () => {
|
|
662
|
+
it("options.origin survives to transaction.origin", () => {
|
|
663
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
664
|
+
const native = unwrap(doc) as Y.Doc
|
|
665
|
+
|
|
666
|
+
let capturedOrigin: string | undefined = "not-called"
|
|
667
|
+
native.on("afterTransaction", tr => {
|
|
668
|
+
capturedOrigin = tr.origin
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
batch(doc, d => d.title.insert(0, "x"), { origin: "undo" })
|
|
672
|
+
expect(capturedOrigin).toBe("undo")
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it("external wrapping kyneta is correctly classified as own", () => {
|
|
676
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
677
|
+
|
|
678
|
+
let kynetaFires = 0
|
|
679
|
+
subscribe(doc, () => {
|
|
680
|
+
kynetaFires++
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
const native = unwrap(doc) as Y.Doc
|
|
684
|
+
native.transact(() => {
|
|
685
|
+
batch(doc, d => d.title.insert(0, "x"))
|
|
686
|
+
}, "external")
|
|
687
|
+
|
|
688
|
+
// Should fire exactly once (captured via wrappedPrepare),
|
|
689
|
+
// and NOT twice (the bridge should skip the external transaction
|
|
690
|
+
// because the inner kyneta transact marked the transaction).
|
|
691
|
+
expect(kynetaFires).toBe(1)
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
it("external raw transact with any string origin is bridged", () => {
|
|
695
|
+
const doc = createDoc(yjs.bind(SimpleSchema))
|
|
696
|
+
|
|
697
|
+
let kynetaFires = 0
|
|
698
|
+
subscribe(doc, () => {
|
|
699
|
+
kynetaFires++
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
const native = unwrap(doc) as Y.Doc
|
|
703
|
+
native.transact(() => {
|
|
704
|
+
native.getMap("root").set("title", "ext")
|
|
705
|
+
}, "kyneta-prepare")
|
|
706
|
+
|
|
707
|
+
expect(kynetaFires).toBe(1)
|
|
708
|
+
})
|
|
709
|
+
})
|
|
655
710
|
})
|
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
|
})
|