@loro-extended/change 4.0.0 → 5.1.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 +173 -149
- package/dist/index.d.ts +962 -335
- package/dist/index.js +1040 -598
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/change.test.ts +51 -52
- package/src/functional-helpers.test.ts +316 -4
- package/src/functional-helpers.ts +96 -6
- package/src/grand-unified-api.test.ts +35 -29
- package/src/index.ts +25 -1
- package/src/json-patch.test.ts +46 -27
- package/src/loro.test.ts +449 -0
- package/src/loro.ts +273 -0
- package/src/overlay-recursion.test.ts +1 -1
- package/src/path-evaluator.ts +1 -1
- package/src/path-selector.test.ts +94 -1
- package/src/shape.ts +47 -15
- package/src/typed-doc.ts +99 -98
- package/src/typed-refs/base.ts +126 -35
- package/src/typed-refs/counter-ref-internals.ts +62 -0
- package/src/typed-refs/{counter.test.ts → counter-ref.test.ts} +5 -4
- package/src/typed-refs/counter-ref.ts +45 -0
- package/src/typed-refs/{doc.ts → doc-ref-internals.ts} +33 -38
- package/src/typed-refs/doc-ref.ts +47 -0
- package/src/typed-refs/encapsulation.test.ts +226 -0
- package/src/typed-refs/list-ref-base-internals.ts +280 -0
- package/src/typed-refs/{list-base.ts → list-ref-base.ts} +255 -160
- package/src/typed-refs/list-ref-internals.ts +21 -0
- package/src/typed-refs/{list.ts → list-ref.ts} +10 -11
- package/src/typed-refs/movable-list-ref-internals.ts +38 -0
- package/src/typed-refs/movable-list-ref.ts +31 -0
- package/src/typed-refs/proxy-handlers.ts +13 -4
- package/src/typed-refs/{record.ts → record-ref-internals.ts} +78 -79
- package/src/typed-refs/{record.test.ts → record-ref.test.ts} +21 -16
- package/src/typed-refs/record-ref.ts +80 -0
- package/src/typed-refs/struct-ref-internals.ts +195 -0
- package/src/typed-refs/{struct-value-updates.test.ts → struct-ref.test.ts} +5 -3
- package/src/typed-refs/struct-ref.ts +257 -0
- package/src/typed-refs/text-ref-internals.ts +100 -0
- package/src/typed-refs/text-ref.ts +72 -0
- package/src/typed-refs/tree-node-ref-internals.ts +111 -0
- package/src/typed-refs/{tree-node.ts → tree-node-ref.ts} +58 -94
- package/src/typed-refs/tree-ref-internals.ts +110 -0
- package/src/typed-refs/tree-ref.ts +194 -0
- package/src/typed-refs/utils.ts +21 -23
- package/src/typed-refs/counter.ts +0 -62
- package/src/typed-refs/movable-list.ts +0 -32
- package/src/typed-refs/struct.ts +0 -201
- package/src/typed-refs/text.ts +0 -91
- package/src/typed-refs/tree.ts +0 -268
- /package/src/typed-refs/{list-value-updates.test.ts → list-ref-value-updates.test.ts} +0 -0
- /package/src/typed-refs/{list.test.ts → list-ref.test.ts} +0 -0
- /package/src/typed-refs/{movable-list.test.ts → movable-list-ref.test.ts} +0 -0
- /package/src/typed-refs/{record-value-updates.test.ts → record-ref-value-updates.test.ts} +0 -0
- /package/src/typed-refs/{tree-node-value-updates.test.ts → tree-node-ref.test.ts} +0 -0
- /package/src/typed-refs/{tree.test.ts → tree-node.test.ts} +0 -0
package/src/loro.test.ts
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the loro() escape hatch function and doc.change() method.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { LoroCounter, LoroDoc, LoroList, LoroMap, LoroText } from "loro-crdt"
|
|
6
|
+
import { describe, expect, it } from "vitest"
|
|
7
|
+
import { change, createTypedDoc, loro, Shape } from "./index.js"
|
|
8
|
+
|
|
9
|
+
describe("loro() function", () => {
|
|
10
|
+
describe("with TypedDoc", () => {
|
|
11
|
+
const schema = Shape.doc({
|
|
12
|
+
title: Shape.text(),
|
|
13
|
+
count: Shape.counter(),
|
|
14
|
+
items: Shape.list(Shape.plain.string()),
|
|
15
|
+
settings: Shape.struct({
|
|
16
|
+
darkMode: Shape.plain.boolean().placeholder(false),
|
|
17
|
+
}),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("should access the underlying LoroDoc", () => {
|
|
21
|
+
const doc = createTypedDoc(schema)
|
|
22
|
+
|
|
23
|
+
const loroDoc = loro(doc).doc
|
|
24
|
+
expect(loroDoc).toBeInstanceOf(LoroDoc)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("should access the container (same as doc for TypedDoc)", () => {
|
|
28
|
+
const doc = createTypedDoc(schema)
|
|
29
|
+
|
|
30
|
+
const container = loro(doc).container
|
|
31
|
+
expect(container).toBeInstanceOf(LoroDoc)
|
|
32
|
+
expect(container).toBe(loro(doc).doc)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("should subscribe to doc-level changes", () => {
|
|
36
|
+
const doc = createTypedDoc(schema)
|
|
37
|
+
const events: unknown[] = []
|
|
38
|
+
|
|
39
|
+
const subscription = loro(doc).subscribe(event => {
|
|
40
|
+
events.push(event)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
doc.title.insert(0, "Hello")
|
|
44
|
+
|
|
45
|
+
expect(events.length).toBeGreaterThan(0)
|
|
46
|
+
subscription()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("should access docShape", () => {
|
|
50
|
+
const doc = createTypedDoc(schema)
|
|
51
|
+
|
|
52
|
+
const docShape = loro(doc).docShape
|
|
53
|
+
expect(docShape).toBe(schema)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("should access rawValue", () => {
|
|
57
|
+
const doc = createTypedDoc(schema)
|
|
58
|
+
doc.title.insert(0, "Hello")
|
|
59
|
+
|
|
60
|
+
const rawValue = loro(doc).rawValue
|
|
61
|
+
expect(rawValue).toHaveProperty("title", "Hello")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should apply JSON patches", () => {
|
|
65
|
+
const doc = createTypedDoc(schema)
|
|
66
|
+
|
|
67
|
+
// Use add operation for counter since it's a container
|
|
68
|
+
loro(doc).applyPatch([
|
|
69
|
+
{ op: "add", path: ["items", 0], value: "test-item" },
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
expect(doc.items.toJSON()).toContain("test-item")
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe("with TextRef", () => {
|
|
77
|
+
const schema = Shape.doc({
|
|
78
|
+
title: Shape.text(),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("should access the underlying LoroDoc", () => {
|
|
82
|
+
const doc = createTypedDoc(schema)
|
|
83
|
+
|
|
84
|
+
const loroDoc = loro(doc.title).doc
|
|
85
|
+
expect(loroDoc).toBeInstanceOf(LoroDoc)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("should access the underlying LoroText container", () => {
|
|
89
|
+
const doc = createTypedDoc(schema)
|
|
90
|
+
|
|
91
|
+
const container = loro(doc.title).container
|
|
92
|
+
expect(container).toBeInstanceOf(LoroText)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should subscribe to text changes", () => {
|
|
96
|
+
const doc = createTypedDoc(schema)
|
|
97
|
+
const events: unknown[] = []
|
|
98
|
+
|
|
99
|
+
const subscription = loro(doc.title).subscribe(event => {
|
|
100
|
+
events.push(event)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
doc.title.insert(0, "Hello")
|
|
104
|
+
|
|
105
|
+
expect(events.length).toBeGreaterThan(0)
|
|
106
|
+
subscription()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe("with CounterRef", () => {
|
|
111
|
+
const schema = Shape.doc({
|
|
112
|
+
count: Shape.counter(),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("should access the underlying LoroDoc", () => {
|
|
116
|
+
const doc = createTypedDoc(schema)
|
|
117
|
+
|
|
118
|
+
const loroDoc = loro(doc.count).doc
|
|
119
|
+
expect(loroDoc).toBeInstanceOf(LoroDoc)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("should access the underlying LoroCounter container", () => {
|
|
123
|
+
const doc = createTypedDoc(schema)
|
|
124
|
+
|
|
125
|
+
const container = loro(doc.count).container
|
|
126
|
+
expect(container).toBeInstanceOf(LoroCounter)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("should subscribe to counter changes", () => {
|
|
130
|
+
const doc = createTypedDoc(schema)
|
|
131
|
+
const events: unknown[] = []
|
|
132
|
+
|
|
133
|
+
const subscription = loro(doc.count).subscribe(event => {
|
|
134
|
+
events.push(event)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
doc.count.increment(5)
|
|
138
|
+
|
|
139
|
+
expect(events.length).toBeGreaterThan(0)
|
|
140
|
+
subscription()
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("with ListRef", () => {
|
|
145
|
+
const schema = Shape.doc({
|
|
146
|
+
items: Shape.list(Shape.plain.string()),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("should access the underlying LoroDoc", () => {
|
|
150
|
+
const doc = createTypedDoc(schema)
|
|
151
|
+
|
|
152
|
+
const loroDoc = loro(doc.items).doc
|
|
153
|
+
expect(loroDoc).toBeInstanceOf(LoroDoc)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("should access the underlying LoroList container", () => {
|
|
157
|
+
const doc = createTypedDoc(schema)
|
|
158
|
+
|
|
159
|
+
const container = loro(doc.items).container
|
|
160
|
+
expect(container).toBeInstanceOf(LoroList)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("should subscribe to list changes", () => {
|
|
164
|
+
const doc = createTypedDoc(schema)
|
|
165
|
+
const events: unknown[] = []
|
|
166
|
+
|
|
167
|
+
const subscription = loro(doc.items).subscribe(event => {
|
|
168
|
+
events.push(event)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
doc.items.push("item1")
|
|
172
|
+
|
|
173
|
+
expect(events.length).toBeGreaterThan(0)
|
|
174
|
+
subscription()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe("with StructRef", () => {
|
|
179
|
+
const schema = Shape.doc({
|
|
180
|
+
settings: Shape.struct({
|
|
181
|
+
darkMode: Shape.plain.boolean().placeholder(false),
|
|
182
|
+
fontSize: Shape.plain.number().placeholder(14),
|
|
183
|
+
}),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should access the underlying LoroDoc", () => {
|
|
187
|
+
const doc = createTypedDoc(schema)
|
|
188
|
+
|
|
189
|
+
const loroDoc = loro(doc.settings).doc
|
|
190
|
+
expect(loroDoc).toBeInstanceOf(LoroDoc)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it("should access the underlying LoroMap container", () => {
|
|
194
|
+
const doc = createTypedDoc(schema)
|
|
195
|
+
|
|
196
|
+
const container = loro(doc.settings).container
|
|
197
|
+
expect(container).toBeInstanceOf(LoroMap)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it("should subscribe to struct changes", () => {
|
|
201
|
+
const doc = createTypedDoc(schema)
|
|
202
|
+
const events: unknown[] = []
|
|
203
|
+
|
|
204
|
+
const subscription = loro(doc.settings).subscribe(event => {
|
|
205
|
+
events.push(event)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Use change() to ensure the subscription fires
|
|
209
|
+
change(doc, draft => {
|
|
210
|
+
draft.settings.darkMode = true
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(events.length).toBeGreaterThan(0)
|
|
214
|
+
subscription()
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe("with RecordRef", () => {
|
|
219
|
+
const schema = Shape.doc({
|
|
220
|
+
users: Shape.record(
|
|
221
|
+
Shape.struct({
|
|
222
|
+
name: Shape.plain.string().placeholder(""),
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it("should access the underlying LoroDoc", () => {
|
|
228
|
+
const doc = createTypedDoc(schema)
|
|
229
|
+
|
|
230
|
+
const loroDoc = loro(doc.users).doc
|
|
231
|
+
expect(loroDoc).toBeInstanceOf(LoroDoc)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("should access the underlying LoroMap container", () => {
|
|
235
|
+
const doc = createTypedDoc(schema)
|
|
236
|
+
|
|
237
|
+
const container = loro(doc.users).container
|
|
238
|
+
expect(container).toBeInstanceOf(LoroMap)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("should subscribe to record changes", () => {
|
|
242
|
+
const doc = createTypedDoc(schema)
|
|
243
|
+
const events: unknown[] = []
|
|
244
|
+
|
|
245
|
+
const subscription = loro(doc.users).subscribe(event => {
|
|
246
|
+
events.push(event)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
change(doc, draft => {
|
|
250
|
+
draft.users.set("alice", { name: "Alice" })
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
expect(events.length).toBeGreaterThan(0)
|
|
254
|
+
subscription()
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe("backward compatibility with $", () => {
|
|
259
|
+
const schema = Shape.doc({
|
|
260
|
+
title: Shape.text(),
|
|
261
|
+
count: Shape.counter(),
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("$ and loro() should access the same LoroDoc", () => {
|
|
265
|
+
const doc = createTypedDoc(schema)
|
|
266
|
+
|
|
267
|
+
expect(loro(doc).doc).toBe(loro(doc).doc)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it("$ and loro() should access the same container for refs", () => {
|
|
271
|
+
const doc = createTypedDoc(schema)
|
|
272
|
+
|
|
273
|
+
expect(loro(doc.title).container).toBe(loro(doc.title).container)
|
|
274
|
+
expect(loro(doc.count).container).toBe(loro(doc.count).container)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe("container operations via loro()", () => {
|
|
279
|
+
describe("ListRef container operations", () => {
|
|
280
|
+
const schema = Shape.doc({
|
|
281
|
+
items: Shape.list(
|
|
282
|
+
Shape.struct({
|
|
283
|
+
name: Shape.plain.string().placeholder(""),
|
|
284
|
+
}),
|
|
285
|
+
),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it("should pushContainer via loro()", () => {
|
|
289
|
+
const doc = createTypedDoc(schema)
|
|
290
|
+
const { LoroMap } = require("loro-crdt")
|
|
291
|
+
|
|
292
|
+
const newMap = new LoroMap()
|
|
293
|
+
newMap.set("name", "pushed-via-loro")
|
|
294
|
+
|
|
295
|
+
// Use loro() to push a container
|
|
296
|
+
const loroList = loro(doc.items) as any
|
|
297
|
+
loroList.pushContainer(newMap)
|
|
298
|
+
|
|
299
|
+
expect(doc.items.length).toBe(1)
|
|
300
|
+
expect(doc.items.toJSON()[0].name).toBe("pushed-via-loro")
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it("should insertContainer via loro()", () => {
|
|
304
|
+
const doc = createTypedDoc(schema)
|
|
305
|
+
const { LoroMap } = require("loro-crdt")
|
|
306
|
+
|
|
307
|
+
// First add an item normally
|
|
308
|
+
change(doc, draft => {
|
|
309
|
+
draft.items.push({ name: "first" })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const newMap = new LoroMap()
|
|
313
|
+
newMap.set("name", "inserted-via-loro")
|
|
314
|
+
|
|
315
|
+
// Use loro() to insert a container at index 0
|
|
316
|
+
const loroList = loro(doc.items) as any
|
|
317
|
+
loroList.insertContainer(0, newMap)
|
|
318
|
+
|
|
319
|
+
expect(doc.items.length).toBe(2)
|
|
320
|
+
expect(doc.items.toJSON()[0].name).toBe("inserted-via-loro")
|
|
321
|
+
expect(doc.items.toJSON()[1].name).toBe("first")
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
describe("StructRef container operations", () => {
|
|
326
|
+
const schema = Shape.doc({
|
|
327
|
+
settings: Shape.struct({
|
|
328
|
+
nested: Shape.struct({
|
|
329
|
+
value: Shape.plain.number().placeholder(0),
|
|
330
|
+
}),
|
|
331
|
+
}),
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it("should setContainer via loro()", () => {
|
|
335
|
+
const doc = createTypedDoc(schema)
|
|
336
|
+
const { LoroMap } = require("loro-crdt")
|
|
337
|
+
|
|
338
|
+
const newMap = new LoroMap()
|
|
339
|
+
newMap.set("value", 42)
|
|
340
|
+
|
|
341
|
+
// Use loro() to set a container
|
|
342
|
+
const loroStruct = loro(doc.settings) as any
|
|
343
|
+
loroStruct.setContainer("nested", newMap)
|
|
344
|
+
|
|
345
|
+
expect(doc.settings.nested.value).toBe(42)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe("doc.change() method", () => {
|
|
349
|
+
const schema = Shape.doc({
|
|
350
|
+
title: Shape.text(),
|
|
351
|
+
count: Shape.counter(),
|
|
352
|
+
items: Shape.list(Shape.plain.string()),
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it("should batch mutations via doc.change()", () => {
|
|
356
|
+
const doc = createTypedDoc(schema)
|
|
357
|
+
|
|
358
|
+
doc.change(draft => {
|
|
359
|
+
draft.title.insert(0, "Hello")
|
|
360
|
+
draft.count.increment(5)
|
|
361
|
+
draft.items.push("item1")
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
expect(doc.title.toString()).toBe("Hello")
|
|
365
|
+
expect(doc.count.value).toBe(5)
|
|
366
|
+
expect(doc.items.toJSON()).toEqual(["item1"])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it("should return the doc for chaining", () => {
|
|
370
|
+
const doc = createTypedDoc(schema)
|
|
371
|
+
|
|
372
|
+
const result = doc.change(draft => {
|
|
373
|
+
draft.count.increment(1)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
expect(result).toBe(doc)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it("should support chained change() calls", () => {
|
|
380
|
+
const doc = createTypedDoc(schema)
|
|
381
|
+
|
|
382
|
+
doc
|
|
383
|
+
.change(draft => {
|
|
384
|
+
draft.count.increment(1)
|
|
385
|
+
})
|
|
386
|
+
.change(draft => {
|
|
387
|
+
draft.count.increment(2)
|
|
388
|
+
})
|
|
389
|
+
.change(draft => {
|
|
390
|
+
draft.count.increment(3)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
expect(doc.count.value).toBe(6)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it("doc.change() and doc.change() should be equivalent", () => {
|
|
397
|
+
const doc1 = createTypedDoc(schema)
|
|
398
|
+
const doc2 = createTypedDoc(schema)
|
|
399
|
+
|
|
400
|
+
doc1.change(draft => {
|
|
401
|
+
draft.count.increment(5)
|
|
402
|
+
draft.title.insert(0, "Test")
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
doc2.change(draft => {
|
|
406
|
+
draft.count.increment(5)
|
|
407
|
+
draft.title.insert(0, "Test")
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
expect(doc1.toJSON()).toEqual(doc2.toJSON())
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it("change() helper should use doc.change() internally", () => {
|
|
414
|
+
const doc = createTypedDoc(schema)
|
|
415
|
+
|
|
416
|
+
change(doc, draft => {
|
|
417
|
+
draft.count.increment(10)
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
expect(doc.count.value).toBe(10)
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
describe("RecordRef container operations", () => {
|
|
426
|
+
const schema = Shape.doc({
|
|
427
|
+
users: Shape.record(
|
|
428
|
+
Shape.struct({
|
|
429
|
+
name: Shape.plain.string().placeholder(""),
|
|
430
|
+
}),
|
|
431
|
+
),
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it("should setContainer via loro()", () => {
|
|
435
|
+
const doc = createTypedDoc(schema)
|
|
436
|
+
const { LoroMap } = require("loro-crdt")
|
|
437
|
+
|
|
438
|
+
const newMap = new LoroMap()
|
|
439
|
+
newMap.set("name", "Alice via loro")
|
|
440
|
+
|
|
441
|
+
// Use loro() to set a container
|
|
442
|
+
const loroRecord = loro(doc.users) as any
|
|
443
|
+
loroRecord.setContainer("alice", newMap)
|
|
444
|
+
|
|
445
|
+
expect(doc.users.get("alice")?.name).toBe("Alice via loro")
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
})
|
package/src/loro.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `loro()` function - single escape hatch for CRDT internals.
|
|
3
|
+
*
|
|
4
|
+
* Design Principle:
|
|
5
|
+
* > If it takes a plain JavaScript value, keep it on the ref.
|
|
6
|
+
* > If it takes a Loro container or exposes CRDT internals, move to `loro()`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { loro } from "@loro-extended/change"
|
|
11
|
+
*
|
|
12
|
+
* // Access underlying LoroDoc
|
|
13
|
+
* loro(ref).doc
|
|
14
|
+
*
|
|
15
|
+
* // Access underlying Loro container (correctly typed)
|
|
16
|
+
* loro(ref).container // LoroList, LoroMap, LoroText, etc.
|
|
17
|
+
*
|
|
18
|
+
* // Subscribe to changes
|
|
19
|
+
* loro(ref).subscribe(callback)
|
|
20
|
+
*
|
|
21
|
+
* // Container operations
|
|
22
|
+
* loro(list).pushContainer(loroMap)
|
|
23
|
+
* loro(struct).setContainer('key', loroMap)
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
Container,
|
|
29
|
+
LoroCounter,
|
|
30
|
+
LoroDoc,
|
|
31
|
+
LoroList,
|
|
32
|
+
LoroMap,
|
|
33
|
+
LoroMovableList,
|
|
34
|
+
LoroText,
|
|
35
|
+
LoroTree,
|
|
36
|
+
Subscription,
|
|
37
|
+
} from "loro-crdt"
|
|
38
|
+
import type { JsonPatch } from "./json-patch.js"
|
|
39
|
+
import type {
|
|
40
|
+
ContainerOrValueShape,
|
|
41
|
+
ContainerShape,
|
|
42
|
+
DocShape,
|
|
43
|
+
StructContainerShape,
|
|
44
|
+
} from "./shape.js"
|
|
45
|
+
import type { TypedDoc } from "./typed-doc.js"
|
|
46
|
+
import type { TypedRef } from "./typed-refs/base.js"
|
|
47
|
+
import type { CounterRef } from "./typed-refs/counter-ref.js"
|
|
48
|
+
import type { ListRef } from "./typed-refs/list-ref.js"
|
|
49
|
+
import type { MovableListRef } from "./typed-refs/movable-list-ref.js"
|
|
50
|
+
import type { RecordRef } from "./typed-refs/record-ref.js"
|
|
51
|
+
import type { StructRef } from "./typed-refs/struct-ref.js"
|
|
52
|
+
import type { TextRef } from "./typed-refs/text-ref.js"
|
|
53
|
+
import type { TreeRef } from "./typed-refs/tree-ref.js"
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Symbol for loro() access
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Well-known Symbol for loro() access.
|
|
61
|
+
* This is exported so advanced users can access it directly if needed.
|
|
62
|
+
*/
|
|
63
|
+
export const LORO_SYMBOL = Symbol.for("loro-extended:loro")
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Interface definitions for loro() return types
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Base interface for all loro() return types.
|
|
71
|
+
* Provides access to the underlying LoroDoc, container, and subscription.
|
|
72
|
+
*/
|
|
73
|
+
export interface LoroRefBase {
|
|
74
|
+
/** The underlying LoroDoc */
|
|
75
|
+
readonly doc: LoroDoc
|
|
76
|
+
|
|
77
|
+
/** The underlying Loro container */
|
|
78
|
+
readonly container: unknown
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Subscribe to container-level changes.
|
|
82
|
+
* @param callback - Function called when the container changes
|
|
83
|
+
* @returns Subscription that can be used to unsubscribe
|
|
84
|
+
*/
|
|
85
|
+
subscribe(callback: (event: unknown) => void): Subscription
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* loro() return type for ListRef and MovableListRef.
|
|
90
|
+
* Provides container operations that take Loro containers.
|
|
91
|
+
*/
|
|
92
|
+
export interface LoroListRef extends LoroRefBase {
|
|
93
|
+
/** The underlying LoroList or LoroMovableList */
|
|
94
|
+
readonly container: LoroList | LoroMovableList
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Push a Loro container to the end of the list.
|
|
98
|
+
* Use this when you need to add a pre-existing container.
|
|
99
|
+
*/
|
|
100
|
+
pushContainer(container: Container): Container
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Insert a Loro container at the specified index.
|
|
104
|
+
* Use this when you need to insert a pre-existing container.
|
|
105
|
+
*/
|
|
106
|
+
insertContainer(index: number, container: Container): Container
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* loro() return type for StructRef and RecordRef.
|
|
111
|
+
* Provides container operations that take Loro containers.
|
|
112
|
+
*/
|
|
113
|
+
export interface LoroMapRef extends LoroRefBase {
|
|
114
|
+
/** The underlying LoroMap */
|
|
115
|
+
readonly container: LoroMap
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Set a Loro container at the specified key.
|
|
119
|
+
* Use this when you need to set a pre-existing container.
|
|
120
|
+
*/
|
|
121
|
+
setContainer(key: string, container: Container): Container
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* loro() return type for TextRef.
|
|
126
|
+
*/
|
|
127
|
+
export interface LoroTextRef extends LoroRefBase {
|
|
128
|
+
/** The underlying LoroText */
|
|
129
|
+
readonly container: LoroText
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* loro() return type for CounterRef.
|
|
134
|
+
*/
|
|
135
|
+
export interface LoroCounterRef extends LoroRefBase {
|
|
136
|
+
/** The underlying LoroCounter */
|
|
137
|
+
readonly container: LoroCounter
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* loro() return type for TreeRef.
|
|
142
|
+
*/
|
|
143
|
+
export interface LoroTreeRef extends LoroRefBase {
|
|
144
|
+
/** The underlying LoroTree */
|
|
145
|
+
readonly container: LoroTree
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* loro() return type for TypedDoc.
|
|
150
|
+
* Provides access to doc-level operations.
|
|
151
|
+
*/
|
|
152
|
+
export interface LoroTypedDocRef extends LoroRefBase {
|
|
153
|
+
/** The underlying LoroDoc (same as doc for TypedDoc) */
|
|
154
|
+
readonly container: LoroDoc
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Apply JSON Patch operations to the document.
|
|
158
|
+
* @param patch - Array of JSON Patch operations (RFC 6902)
|
|
159
|
+
* @param pathPrefix - Optional path prefix for scoped operations
|
|
160
|
+
*/
|
|
161
|
+
applyPatch(patch: JsonPatch, pathPrefix?: (string | number)[]): void
|
|
162
|
+
|
|
163
|
+
/** Access the document schema shape */
|
|
164
|
+
readonly docShape: DocShape
|
|
165
|
+
|
|
166
|
+
/** Get raw CRDT value without placeholder overlay */
|
|
167
|
+
readonly rawValue: unknown
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// loro() function overloads
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Access CRDT internals for a ListRef.
|
|
176
|
+
*/
|
|
177
|
+
export function loro<NestedShape extends ContainerShape>(
|
|
178
|
+
ref: ListRef<NestedShape>,
|
|
179
|
+
): LoroListRef
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Access CRDT internals for a MovableListRef.
|
|
183
|
+
*/
|
|
184
|
+
export function loro<NestedShape extends ContainerShape>(
|
|
185
|
+
ref: MovableListRef<NestedShape>,
|
|
186
|
+
): LoroListRef
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Access CRDT internals for a StructRef.
|
|
190
|
+
*/
|
|
191
|
+
export function loro<
|
|
192
|
+
NestedShapes extends Record<string, ContainerOrValueShape>,
|
|
193
|
+
>(ref: StructRef<NestedShapes>): LoroMapRef
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Access CRDT internals for a RecordRef.
|
|
197
|
+
*/
|
|
198
|
+
export function loro<NestedShape extends ContainerShape>(
|
|
199
|
+
ref: RecordRef<NestedShape>,
|
|
200
|
+
): LoroMapRef
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Access CRDT internals for a TextRef.
|
|
204
|
+
*/
|
|
205
|
+
export function loro(ref: TextRef): LoroTextRef
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Access CRDT internals for a CounterRef.
|
|
209
|
+
*/
|
|
210
|
+
export function loro(ref: CounterRef): LoroCounterRef
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Access CRDT internals for a TreeRef.
|
|
214
|
+
*/
|
|
215
|
+
export function loro<DataShape extends StructContainerShape>(
|
|
216
|
+
ref: TreeRef<DataShape>,
|
|
217
|
+
): LoroTreeRef
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Access CRDT internals for a TypedDoc.
|
|
221
|
+
*/
|
|
222
|
+
export function loro<Shape extends DocShape>(
|
|
223
|
+
doc: TypedDoc<Shape>,
|
|
224
|
+
): LoroTypedDocRef
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Access CRDT internals for any TypedRef.
|
|
228
|
+
*/
|
|
229
|
+
export function loro<Shape extends ContainerShape>(
|
|
230
|
+
ref: TypedRef<Shape>,
|
|
231
|
+
): LoroRefBase
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* The `loro()` function - single escape hatch for CRDT internals.
|
|
235
|
+
*
|
|
236
|
+
* Use this to access:
|
|
237
|
+
* - The underlying LoroDoc
|
|
238
|
+
* - The underlying Loro container (correctly typed)
|
|
239
|
+
* - Container-level subscriptions
|
|
240
|
+
* - Container operations that take Loro containers (pushContainer, setContainer, etc.)
|
|
241
|
+
*
|
|
242
|
+
* @param refOrDoc - A TypedRef or TypedDoc
|
|
243
|
+
* @returns An object with CRDT internals and operations
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* import { loro } from "@loro-extended/change"
|
|
248
|
+
*
|
|
249
|
+
* // Access underlying LoroDoc
|
|
250
|
+
* loro(doc.settings).doc
|
|
251
|
+
*
|
|
252
|
+
* // Access underlying Loro container
|
|
253
|
+
* loro(doc.items).container // LoroList
|
|
254
|
+
*
|
|
255
|
+
* // Subscribe to changes
|
|
256
|
+
* loro(doc.settings).subscribe(event => { ... })
|
|
257
|
+
*
|
|
258
|
+
* // Container operations
|
|
259
|
+
* loro(doc.items).pushContainer(loroMap)
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export function loro(
|
|
263
|
+
refOrDoc: TypedRef<any> | TypedDoc<any> | TreeRef<any> | StructRef<any>,
|
|
264
|
+
): LoroRefBase {
|
|
265
|
+
// Access the loro namespace via the well-known symbol
|
|
266
|
+
const loroNamespace = (refOrDoc as any)[LORO_SYMBOL]
|
|
267
|
+
if (!loroNamespace) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"Invalid argument: expected TypedRef, TreeRef, or TypedDoc with loro() support",
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
return loroNamespace
|
|
273
|
+
}
|