@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
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// eager-write-coherence — pins the post-Phase-3 contract for the Yjs
|
|
2
|
+
// substrate's write path:
|
|
3
|
+
//
|
|
4
|
+
// 1. Re-entry: subscriber callbacks may freely `batch()` the doc.
|
|
5
|
+
// Substrate writes land synchronously; both reads (σ via the
|
|
6
|
+
// Reader) AND subsequent writes (λ via applyChangeToYjs) succeed
|
|
7
|
+
// against the new state.
|
|
8
|
+
// 2. Projection law: `σ ≡ Π(λ)` holds at every prepare boundary
|
|
9
|
+
// (asserted via deep-equal between the substrate's reader view
|
|
10
|
+
// and a fresh `materializeYjsShadow` after a non-trivial
|
|
11
|
+
// mutation sequence).
|
|
12
|
+
// 3. Json-boundary storage: `struct.json` / `record.json` subtrees
|
|
13
|
+
// round-trip as plain JSON values in the parent Y.Map entry, not
|
|
14
|
+
// as nested Y.Map containers.
|
|
15
|
+
//
|
|
16
|
+
// Yjs doesn't need a nested-commit semantics test (4.7) because its
|
|
17
|
+
// native `Y.transact` already collapses re-entrant nesting for free.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
batch,
|
|
21
|
+
interpret,
|
|
22
|
+
observation,
|
|
23
|
+
readable,
|
|
24
|
+
Schema,
|
|
25
|
+
subscribe,
|
|
26
|
+
unwrap,
|
|
27
|
+
writable,
|
|
28
|
+
} from "@kyneta/schema"
|
|
29
|
+
import { describe, expect, it } from "vitest"
|
|
30
|
+
import * as Y from "yjs"
|
|
31
|
+
import { materializeYjsShadow } from "../materialize.js"
|
|
32
|
+
import { ensureContainers } from "../populate.js"
|
|
33
|
+
import { createYjsSubstrate, yjsSubstrateFactory } from "../substrate.js"
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Harness
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
function build<S extends ReturnType<typeof Schema.struct>>(schema: S) {
|
|
40
|
+
const substrate = yjsSubstrateFactory.create(schema)
|
|
41
|
+
const doc = interpret(schema, substrate.context())
|
|
42
|
+
.with(readable)
|
|
43
|
+
.with(writable)
|
|
44
|
+
.with(observation)
|
|
45
|
+
.done() as any
|
|
46
|
+
return { substrate, doc }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Unbound variant — bypasses the trivialBinding that identity-keys
|
|
50
|
+
// product fields, so raw-name `materializeYjsShadow(doc, schema)` /
|
|
51
|
+
// `rootMap.get(name)` calls in tests round-trip with the substrate.
|
|
52
|
+
function buildUnbound<S extends ReturnType<typeof Schema.struct>>(schema: S) {
|
|
53
|
+
const doc = new Y.Doc()
|
|
54
|
+
ensureContainers(doc, schema)
|
|
55
|
+
const substrate = createYjsSubstrate(doc, schema)
|
|
56
|
+
const view = interpret(schema, substrate.context())
|
|
57
|
+
.with(readable)
|
|
58
|
+
.with(writable)
|
|
59
|
+
.with(observation)
|
|
60
|
+
.done() as any
|
|
61
|
+
return { substrate, doc: view }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// 1. Re-entry — subscriber writes after subscriber push
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe("Yjs re-entry: subscriber writes after subscriber push", () => {
|
|
69
|
+
it("substrate-write timing: push then set inside the just-pushed item", () => {
|
|
70
|
+
const schema = Schema.struct({
|
|
71
|
+
events: Schema.list(
|
|
72
|
+
Schema.struct({ kind: Schema.string(), body: Schema.string() }),
|
|
73
|
+
),
|
|
74
|
+
})
|
|
75
|
+
const { doc } = build(schema)
|
|
76
|
+
|
|
77
|
+
subscribe(doc.events, () => {
|
|
78
|
+
if ((doc.events as any).length !== 1) return
|
|
79
|
+
batch(doc, (d: any) => {
|
|
80
|
+
d.events.push({ kind: "assistant", body: "" })
|
|
81
|
+
})
|
|
82
|
+
batch(doc, (d: any) => {
|
|
83
|
+
d.events.at(1).body.set("hello")
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(() => {
|
|
88
|
+
batch(doc, (d: any) => {
|
|
89
|
+
d.events.push({ kind: "user", body: "hi" })
|
|
90
|
+
})
|
|
91
|
+
}).not.toThrow()
|
|
92
|
+
|
|
93
|
+
expect((doc.events as any).length).toBe(2)
|
|
94
|
+
expect((doc.events as any).at(1).body()).toBe("hello")
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it("read-your-writes: re-entrant read inside subscriber sees just-pushed item", () => {
|
|
98
|
+
const schema = Schema.struct({
|
|
99
|
+
items: Schema.list(Schema.struct({ name: Schema.string() })),
|
|
100
|
+
})
|
|
101
|
+
const { doc } = build(schema)
|
|
102
|
+
|
|
103
|
+
let observed: string | undefined
|
|
104
|
+
subscribe(doc.items, () => {
|
|
105
|
+
if ((doc.items as any).length !== 1) return
|
|
106
|
+
batch(doc, (d: any) => {
|
|
107
|
+
d.items.push({ name: "synthesised" })
|
|
108
|
+
})
|
|
109
|
+
observed = (doc.items as any).at(1).name()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
batch(doc, (d: any) => {
|
|
113
|
+
d.items.push({ name: "user" })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(observed).toBe("synthesised")
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// 2. Projection law σ ≡ Π(λ)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("Yjs projection law", () => {
|
|
125
|
+
it("shadow equals materialized projection of native doc after a mixed mutation sequence", () => {
|
|
126
|
+
const schema = Schema.struct({
|
|
127
|
+
title: Schema.text(),
|
|
128
|
+
items: Schema.list(
|
|
129
|
+
Schema.struct({ name: Schema.string(), done: Schema.boolean() }),
|
|
130
|
+
),
|
|
131
|
+
meta: Schema.struct.json({
|
|
132
|
+
tags: Schema.string(),
|
|
133
|
+
version: Schema.number(),
|
|
134
|
+
}),
|
|
135
|
+
peers: Schema.record(Schema.boolean()),
|
|
136
|
+
})
|
|
137
|
+
const { doc } = buildUnbound(schema)
|
|
138
|
+
|
|
139
|
+
batch(doc, (d: any) => {
|
|
140
|
+
d.title.insert(0, "Hello")
|
|
141
|
+
d.items.push({ name: "a", done: false })
|
|
142
|
+
})
|
|
143
|
+
batch(doc, (d: any) => {
|
|
144
|
+
d.items.at(0).done.set(true)
|
|
145
|
+
d.items.push({ name: "b", done: false })
|
|
146
|
+
})
|
|
147
|
+
batch(doc, (d: any) => {
|
|
148
|
+
d.meta.set({ tags: "kyneta", version: 2 })
|
|
149
|
+
d.peers.set("alice", true)
|
|
150
|
+
d.peers.set("bob", false)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const nativeDoc = unwrap(doc) as Y.Doc
|
|
154
|
+
const projected = materializeYjsShadow(nativeDoc, schema)
|
|
155
|
+
expect(projected).toEqual({
|
|
156
|
+
title: "Hello",
|
|
157
|
+
items: [
|
|
158
|
+
{ name: "a", done: true },
|
|
159
|
+
{ name: "b", done: false },
|
|
160
|
+
],
|
|
161
|
+
meta: { tags: "kyneta", version: 2 },
|
|
162
|
+
peers: { alice: true, bob: false },
|
|
163
|
+
})
|
|
164
|
+
expect((doc.title as any)()).toBe("Hello")
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// 3. JSON boundary: plain JS values in the parent Y.Map entry
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
describe("Yjs json-boundary storage", () => {
|
|
173
|
+
it("struct.json field stores a plain JS value at the parent boundary key", () => {
|
|
174
|
+
const schema = Schema.struct({
|
|
175
|
+
config: Schema.struct.json({
|
|
176
|
+
tags: Schema.string(),
|
|
177
|
+
retries: Schema.number(),
|
|
178
|
+
}),
|
|
179
|
+
})
|
|
180
|
+
const { doc } = build(schema)
|
|
181
|
+
|
|
182
|
+
batch(doc, (d: any) => {
|
|
183
|
+
d.config.set({ tags: "ci", retries: 3 })
|
|
184
|
+
})
|
|
185
|
+
expect(doc.config()).toEqual({ tags: "ci", retries: 3 })
|
|
186
|
+
|
|
187
|
+
batch(doc, (d: any) => {
|
|
188
|
+
d.config.tags.set("prod")
|
|
189
|
+
})
|
|
190
|
+
expect(doc.config()).toEqual({ tags: "prod", retries: 3 })
|
|
191
|
+
|
|
192
|
+
// Direct CRDT inspection: the boundary slot in the root Y.Map
|
|
193
|
+
// holds a plain JS object — NOT a Y.Map. Yjs Y.Map instances
|
|
194
|
+
// would respond to `instanceof Y.Map`.
|
|
195
|
+
const native = unwrap(doc) as Y.Doc
|
|
196
|
+
const rootMap = native.getMap("root")
|
|
197
|
+
const rootKeys: string[] = []
|
|
198
|
+
rootMap.forEach((_v, k) => {
|
|
199
|
+
rootKeys.push(k)
|
|
200
|
+
})
|
|
201
|
+
expect(rootKeys.length).toBe(1)
|
|
202
|
+
const key = rootKeys[0]
|
|
203
|
+
if (key === undefined) throw new Error("rootKeys[0] missing")
|
|
204
|
+
const value = rootMap.get(key)
|
|
205
|
+
expect(value).toEqual({ tags: "prod", retries: 3 })
|
|
206
|
+
expect(value instanceof Y.Map).toBe(false)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("list.json items round-trip and replace cleanly on field-inside-item writes", () => {
|
|
210
|
+
const schema = Schema.struct({
|
|
211
|
+
todos: Schema.list.json(
|
|
212
|
+
Schema.struct({ title: Schema.string(), done: Schema.boolean() }),
|
|
213
|
+
),
|
|
214
|
+
})
|
|
215
|
+
const { doc } = build(schema)
|
|
216
|
+
|
|
217
|
+
batch(doc, (d: any) => {
|
|
218
|
+
d.todos.push({ title: "first", done: false })
|
|
219
|
+
})
|
|
220
|
+
batch(doc, (d: any) => {
|
|
221
|
+
d.todos.push({ title: "second", done: false })
|
|
222
|
+
})
|
|
223
|
+
expect(doc.todos()).toEqual([
|
|
224
|
+
{ title: "first", done: false },
|
|
225
|
+
{ title: "second", done: false },
|
|
226
|
+
])
|
|
227
|
+
|
|
228
|
+
batch(doc, (d: any) => {
|
|
229
|
+
d.todos.at(0).done.set(true)
|
|
230
|
+
})
|
|
231
|
+
expect(doc.todos()).toEqual([
|
|
232
|
+
{ title: "first", done: true },
|
|
233
|
+
{ title: "second", done: false },
|
|
234
|
+
])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it("record.json entries round-trip through the json-boundary path", () => {
|
|
238
|
+
const schema = Schema.struct({
|
|
239
|
+
profiles: Schema.record.json(Schema.struct({ email: Schema.string() })),
|
|
240
|
+
})
|
|
241
|
+
const { doc } = build(schema)
|
|
242
|
+
|
|
243
|
+
batch(doc, (d: any) => {
|
|
244
|
+
d.profiles.set("alice", { email: "alice@example.com" })
|
|
245
|
+
d.profiles.set("bob", { email: "bob@example.com" })
|
|
246
|
+
})
|
|
247
|
+
expect(doc.profiles()).toEqual({
|
|
248
|
+
alice: { email: "alice@example.com" },
|
|
249
|
+
bob: { email: "bob@example.com" },
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
batch(doc, (d: any) => {
|
|
253
|
+
d.profiles.at("alice").email.set("alice@new.example.com")
|
|
254
|
+
})
|
|
255
|
+
expect(doc.profiles()).toEqual({
|
|
256
|
+
alice: { email: "alice@new.example.com" },
|
|
257
|
+
bob: { email: "bob@example.com" },
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Three-primitive-substrate (jj:ryquprut): read-your-writes carries through
|
|
264
|
+
// to Yjs; abort produces one batched native event with net-zero delta.
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
describe("Yjs three-primitive substrate (jj:ryquprut)", () => {
|
|
268
|
+
it("multi-push in one batch() block appends in order against the CRDT", () => {
|
|
269
|
+
const schema = Schema.struct({
|
|
270
|
+
todos: Schema.list(Schema.string()),
|
|
271
|
+
})
|
|
272
|
+
const { doc } = build(schema)
|
|
273
|
+
|
|
274
|
+
batch(doc, (d: any) => {
|
|
275
|
+
d.todos.push("a")
|
|
276
|
+
d.todos.push("b")
|
|
277
|
+
d.todos.push("c")
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
expect((doc.todos as any)()).toEqual(["a", "b", "c"])
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it("abort restores state on outermost throw", () => {
|
|
284
|
+
const schema = Schema.struct({
|
|
285
|
+
a: Schema.string(),
|
|
286
|
+
b: Schema.string(),
|
|
287
|
+
})
|
|
288
|
+
const { doc } = build(schema)
|
|
289
|
+
|
|
290
|
+
expect(() => {
|
|
291
|
+
batch(doc, (d: any) => {
|
|
292
|
+
d.a.set("set-a")
|
|
293
|
+
d.b.set("set-b")
|
|
294
|
+
throw new Error("abort")
|
|
295
|
+
})
|
|
296
|
+
}).toThrow("abort")
|
|
297
|
+
|
|
298
|
+
expect((doc.a as any)()).toBe("")
|
|
299
|
+
expect((doc.b as any)()).toBe("")
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it("abort fires one Changeset with aborted: true on the Yjs stack", () => {
|
|
303
|
+
const schema = Schema.struct({ a: Schema.string() })
|
|
304
|
+
const { doc } = build(schema)
|
|
305
|
+
|
|
306
|
+
let aborted = false
|
|
307
|
+
subscribe(doc.a, (cs: any) => {
|
|
308
|
+
if (cs.aborted) aborted = true
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
expect(() => {
|
|
312
|
+
batch(doc, (d: any) => {
|
|
313
|
+
d.a.set("hello")
|
|
314
|
+
throw new Error("abort")
|
|
315
|
+
})
|
|
316
|
+
}).toThrow("abort")
|
|
317
|
+
|
|
318
|
+
expect(aborted).toBe(true)
|
|
319
|
+
expect((doc.a as any)()).toBe("")
|
|
320
|
+
})
|
|
321
|
+
})
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { ProductSchema, SchemaBinding } from "@kyneta/schema"
|
|
10
10
|
import {
|
|
11
11
|
BACKING_DOC,
|
|
12
|
-
|
|
12
|
+
batch,
|
|
13
13
|
createDoc,
|
|
14
14
|
deriveSchemaBinding,
|
|
15
15
|
KIND,
|
|
@@ -80,7 +80,7 @@ const FullSchema = Schema.struct({
|
|
|
80
80
|
describe("materializeYjsShadow", () => {
|
|
81
81
|
it("materializes text fields", () => {
|
|
82
82
|
const doc = createDoc(yjs.bind(TextSchema))
|
|
83
|
-
|
|
83
|
+
batch(doc, (d: any) => {
|
|
84
84
|
d.title.insert(0, "hello")
|
|
85
85
|
})
|
|
86
86
|
|
|
@@ -93,7 +93,7 @@ describe("materializeYjsShadow", () => {
|
|
|
93
93
|
|
|
94
94
|
it("materializes scalar fields", () => {
|
|
95
95
|
const doc = createDoc(yjs.bind(ScalarSchema))
|
|
96
|
-
|
|
96
|
+
batch(doc, (d: any) => {
|
|
97
97
|
d.name.set("Alice")
|
|
98
98
|
d.age.set(30)
|
|
99
99
|
d.active.set(true)
|
|
@@ -108,11 +108,11 @@ describe("materializeYjsShadow", () => {
|
|
|
108
108
|
|
|
109
109
|
it("materializes sequence fields", () => {
|
|
110
110
|
const doc = createDoc(yjs.bind(SequenceSchema))
|
|
111
|
-
// Separate
|
|
111
|
+
// Separate batch() calls for list pushes to preserve order
|
|
112
112
|
// (Yjs reverses order within a single transaction)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
batch(doc, (d: any) => d.items.push("alpha"))
|
|
114
|
+
batch(doc, (d: any) => d.items.push("beta"))
|
|
115
|
+
batch(doc, (d: any) => d.items.push("gamma"))
|
|
116
116
|
|
|
117
117
|
const yDoc = getYDoc(doc)
|
|
118
118
|
const binding = trivialBinding(SequenceSchema)
|
|
@@ -123,7 +123,7 @@ describe("materializeYjsShadow", () => {
|
|
|
123
123
|
|
|
124
124
|
it("materializes nested struct fields", () => {
|
|
125
125
|
const doc = createDoc(yjs.bind(NestedSchema))
|
|
126
|
-
|
|
126
|
+
batch(doc, (d: any) => {
|
|
127
127
|
d.meta.author.set("Bob")
|
|
128
128
|
d.meta.version.set(42)
|
|
129
129
|
})
|
|
@@ -154,7 +154,7 @@ describe("materializeYjsShadow", () => {
|
|
|
154
154
|
|
|
155
155
|
it("uses raw field names, not identity hashes", () => {
|
|
156
156
|
const doc = createDoc(yjs.bind(TextSchema))
|
|
157
|
-
|
|
157
|
+
batch(doc, (d: any) => {
|
|
158
158
|
d.title.insert(0, "test")
|
|
159
159
|
})
|
|
160
160
|
|
|
@@ -175,14 +175,14 @@ describe("materializeYjsShadow", () => {
|
|
|
175
175
|
it("materializes a complex document with multiple field types", () => {
|
|
176
176
|
const doc = createDoc(yjs.bind(FullSchema))
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
batch(doc, (d: any) => {
|
|
179
179
|
d.title.insert(0, "My Doc")
|
|
180
180
|
d.theme.set("dark")
|
|
181
181
|
d.settings.darkMode.set(true)
|
|
182
182
|
d.settings.fontSize.set(16)
|
|
183
183
|
})
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
batch(doc, (d: any) => d.tags.push("important"))
|
|
185
|
+
batch(doc, (d: any) => d.tags.push("urgent"))
|
|
186
186
|
|
|
187
187
|
const yDoc = getYDoc(doc)
|
|
188
188
|
const binding = trivialBinding(FullSchema)
|
|
@@ -198,7 +198,7 @@ describe("materializeYjsShadow", () => {
|
|
|
198
198
|
|
|
199
199
|
it("materializes without binding (undefined binding)", () => {
|
|
200
200
|
const doc = createDoc(yjs.bind(TextSchema))
|
|
201
|
-
|
|
201
|
+
batch(doc, (d: any) => {
|
|
202
202
|
d.title.insert(0, "no binding")
|
|
203
203
|
})
|
|
204
204
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// resolve correctly on the converged state.
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
batch,
|
|
12
12
|
createRef,
|
|
13
13
|
hasPosition,
|
|
14
14
|
type Instruction,
|
|
@@ -55,7 +55,7 @@ function createYjsEnv(initialText: string): PositionTestEnv {
|
|
|
55
55
|
|
|
56
56
|
// Seed the initial text content
|
|
57
57
|
if (initialText.length > 0) {
|
|
58
|
-
|
|
58
|
+
batch(ref, (d: any) => {
|
|
59
59
|
d.title.insert(0, initialText)
|
|
60
60
|
})
|
|
61
61
|
}
|
|
@@ -71,7 +71,7 @@ function createYjsEnv(initialText: string): PositionTestEnv {
|
|
|
71
71
|
positions,
|
|
72
72
|
|
|
73
73
|
insert(index: number, text: string): readonly Instruction[] {
|
|
74
|
-
const ops =
|
|
74
|
+
const ops = batch(ref, (d: any) => {
|
|
75
75
|
d.title.insert(index, text)
|
|
76
76
|
})
|
|
77
77
|
const textOp = ops.find(op => isTextChange(op.change))
|
|
@@ -82,7 +82,7 @@ function createYjsEnv(initialText: string): PositionTestEnv {
|
|
|
82
82
|
},
|
|
83
83
|
|
|
84
84
|
delete(index: number, count: number): readonly Instruction[] {
|
|
85
|
-
const ops =
|
|
85
|
+
const ops = batch(ref, (d: any) => {
|
|
86
86
|
d.title.delete(index, count)
|
|
87
87
|
})
|
|
88
88
|
const textOp = ops.find(op => isTextChange(op.change))
|
|
@@ -111,7 +111,7 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
111
111
|
ensureContainers(doc1, TextSchema)
|
|
112
112
|
const substrate1 = createYjsSubstrate(doc1, TextSchema)
|
|
113
113
|
const ref1 = createRef(TextSchema, substrate1) as any
|
|
114
|
-
|
|
114
|
+
batch(ref1, (d: any) => {
|
|
115
115
|
d.title.insert(0, "hello")
|
|
116
116
|
})
|
|
117
117
|
|
|
@@ -136,10 +136,10 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
136
136
|
const pos2 = textRef2[POSITION].createPosition(4, "left") // before "o"
|
|
137
137
|
|
|
138
138
|
// --- Concurrent edits ---
|
|
139
|
-
|
|
139
|
+
batch(ref1, (d: any) => {
|
|
140
140
|
d.title.insert(0, "AA") // doc1: "AAhello"
|
|
141
141
|
})
|
|
142
|
-
|
|
142
|
+
batch(ref2, (d: any) => {
|
|
143
143
|
d.title.insert(5, "BB") // doc2: "helloBBo"
|
|
144
144
|
})
|
|
145
145
|
|
|
@@ -168,7 +168,7 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
168
168
|
ensureContainers(doc1, TextSchema)
|
|
169
169
|
const substrate1 = createYjsSubstrate(doc1, TextSchema)
|
|
170
170
|
const ref1 = createRef(TextSchema, substrate1) as any
|
|
171
|
-
|
|
171
|
+
batch(ref1, (d: any) => {
|
|
172
172
|
d.title.insert(0, "abc")
|
|
173
173
|
})
|
|
174
174
|
|
|
@@ -190,10 +190,10 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
190
190
|
const rightPos = textRef2[POSITION].createPosition(1, "right")
|
|
191
191
|
|
|
192
192
|
// --- Both insert at index 1 concurrently ---
|
|
193
|
-
|
|
193
|
+
batch(ref1, (d: any) => {
|
|
194
194
|
d.title.insert(1, "X")
|
|
195
195
|
})
|
|
196
|
-
|
|
196
|
+
batch(ref2, (d: any) => {
|
|
197
197
|
d.title.insert(1, "Y")
|
|
198
198
|
})
|
|
199
199
|
|
|
@@ -222,7 +222,7 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
222
222
|
ensureContainers(doc1, TextSchema)
|
|
223
223
|
const substrate1 = createYjsSubstrate(doc1, TextSchema)
|
|
224
224
|
const ref1 = createRef(TextSchema, substrate1) as any
|
|
225
|
-
|
|
225
|
+
batch(ref1, (d: any) => {
|
|
226
226
|
d.title.insert(0, "abcde")
|
|
227
227
|
})
|
|
228
228
|
|
|
@@ -240,7 +240,7 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
240
240
|
expect(pos.resolve()).toBe(3)
|
|
241
241
|
|
|
242
242
|
// Doc2 deletes the range covering position 3
|
|
243
|
-
|
|
243
|
+
batch(ref2, (d: any) => {
|
|
244
244
|
d.title.delete(1, 3) // "ae"
|
|
245
245
|
})
|
|
246
246
|
|
|
@@ -255,7 +255,7 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
255
255
|
expect(afterDelete!).toBeLessThanOrEqual(ref1.title().length)
|
|
256
256
|
|
|
257
257
|
// Now insert new content near the collapsed position
|
|
258
|
-
|
|
258
|
+
batch(ref1, (d: any) => {
|
|
259
259
|
d.title.insert(1, "XYZ")
|
|
260
260
|
})
|
|
261
261
|
|
|
@@ -272,7 +272,7 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
272
272
|
ensureContainers(doc1, TextSchema)
|
|
273
273
|
const substrate1 = createYjsSubstrate(doc1, TextSchema)
|
|
274
274
|
const ref1 = createRef(TextSchema, substrate1) as any
|
|
275
|
-
|
|
275
|
+
batch(ref1, (d: any) => {
|
|
276
276
|
d.title.insert(0, "hello world")
|
|
277
277
|
})
|
|
278
278
|
|
|
@@ -304,7 +304,7 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
304
304
|
ensureContainers(doc1, TextSchema)
|
|
305
305
|
const substrate1 = createYjsSubstrate(doc1, TextSchema)
|
|
306
306
|
const ref1 = createRef(TextSchema, substrate1) as any
|
|
307
|
-
|
|
307
|
+
batch(ref1, (d: any) => {
|
|
308
308
|
d.title.insert(0, "0123456789")
|
|
309
309
|
})
|
|
310
310
|
|
|
@@ -334,13 +334,13 @@ describe("YjsPosition: concurrent edits", () => {
|
|
|
334
334
|
const posC = t3[POSITION].createPosition(8, "right")
|
|
335
335
|
|
|
336
336
|
// Concurrent edits from all three peers
|
|
337
|
-
|
|
337
|
+
batch(ref1, (d: any) => {
|
|
338
338
|
d.title.insert(0, "AA")
|
|
339
339
|
})
|
|
340
|
-
|
|
340
|
+
batch(ref2, (d: any) => {
|
|
341
341
|
d.title.insert(5, "BB")
|
|
342
342
|
})
|
|
343
|
-
|
|
343
|
+
batch(ref3, (d: any) => {
|
|
344
344
|
d.title.delete(7, 2)
|
|
345
345
|
})
|
|
346
346
|
|