@loro-extended/change 0.9.1 → 1.0.1
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 +201 -93
- package/dist/index.d.ts +361 -169
- package/dist/index.js +516 -235
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +180 -175
- package/src/conversion.test.ts +19 -19
- package/src/conversion.ts +7 -7
- package/src/derive-placeholder.test.ts +14 -14
- package/src/derive-placeholder.ts +3 -3
- package/src/discriminated-union-assignability.test.ts +7 -7
- package/src/discriminated-union-tojson.test.ts +13 -24
- package/src/discriminated-union.test.ts +9 -8
- package/src/equality.test.ts +10 -2
- package/src/functional-helpers.test.ts +149 -0
- package/src/functional-helpers.ts +61 -0
- package/src/grand-unified-api.test.ts +423 -0
- package/src/index.ts +8 -6
- package/src/json-patch.test.ts +64 -56
- package/src/overlay-recursion.test.ts +23 -22
- package/src/overlay.ts +9 -9
- package/src/readonly.test.ts +27 -26
- package/src/shape.ts +103 -21
- package/src/typed-doc.ts +227 -58
- package/src/typed-refs/base.ts +23 -1
- package/src/typed-refs/counter.test.ts +44 -13
- package/src/typed-refs/counter.ts +40 -3
- package/src/typed-refs/doc.ts +12 -6
- package/src/typed-refs/json-compatibility.test.ts +37 -32
- package/src/typed-refs/list-base.ts +26 -22
- package/src/typed-refs/list.test.ts +4 -3
- package/src/typed-refs/movable-list.test.ts +3 -2
- package/src/typed-refs/movable-list.ts +4 -1
- package/src/typed-refs/proxy-handlers.ts +14 -1
- package/src/typed-refs/record.test.ts +107 -42
- package/src/typed-refs/record.ts +37 -19
- package/src/typed-refs/{map.ts → struct.ts} +31 -16
- package/src/typed-refs/text.ts +42 -1
- package/src/typed-refs/utils.ts +28 -6
- package/src/types.test.ts +34 -39
- package/src/types.ts +5 -40
- package/src/utils/type-guards.ts +11 -6
- package/src/validation.ts +10 -10
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { LoroDoc } from "loro-crdt"
|
|
2
|
+
import type { DocShape } from "./shape.js"
|
|
3
|
+
import type { TypedDoc } from "./typed-doc.js"
|
|
4
|
+
import type { Mutable } from "./types.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The primary method of mutating typed documents.
|
|
8
|
+
* Batches multiple mutations into a single transaction.
|
|
9
|
+
* All changes commit together at the end.
|
|
10
|
+
*
|
|
11
|
+
* Use this for:
|
|
12
|
+
* - Find-and-mutate operations (required due to JS limitations)
|
|
13
|
+
* - Performance (fewer commits)
|
|
14
|
+
* - Atomic undo (all changes = one undo step)
|
|
15
|
+
*
|
|
16
|
+
* Returns the doc for chaining.
|
|
17
|
+
*
|
|
18
|
+
* @param doc - The TypedDoc to mutate
|
|
19
|
+
* @param fn - Function that performs mutations on the draft
|
|
20
|
+
* @returns The same TypedDoc for chaining
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { change } from "@loro-extended/change"
|
|
25
|
+
*
|
|
26
|
+
* // Chainable API
|
|
27
|
+
* change(doc, draft => {
|
|
28
|
+
* draft.count.increment(10)
|
|
29
|
+
* draft.title.update("Hello")
|
|
30
|
+
* })
|
|
31
|
+
* .count.increment(5) // Optional: continue mutating
|
|
32
|
+
* .toJSON() // Optional: get last item snapshot when needed
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function change<Shape extends DocShape>(
|
|
36
|
+
doc: TypedDoc<Shape>,
|
|
37
|
+
fn: (draft: Mutable<Shape>) => void,
|
|
38
|
+
): TypedDoc<Shape> {
|
|
39
|
+
return doc.$.change(fn)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Access the underlying LoroDoc for advanced operations.
|
|
44
|
+
*
|
|
45
|
+
* @param doc - The TypedDoc to unwrap
|
|
46
|
+
* @returns The underlying LoroDoc instance
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* import { getLoroDoc } from "@loro-extended/change"
|
|
51
|
+
*
|
|
52
|
+
* const loroDoc = getLoroDoc(doc)
|
|
53
|
+
* const version = loroDoc.version()
|
|
54
|
+
* loroDoc.subscribe(() => console.log("changed"))
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function getLoroDoc<Shape extends DocShape>(
|
|
58
|
+
doc: TypedDoc<Shape>,
|
|
59
|
+
): LoroDoc {
|
|
60
|
+
return doc.$.loroDoc
|
|
61
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { Shape } from "./shape.js"
|
|
3
|
+
import { createTypedDoc } from "./typed-doc.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for Grand Unified API v3 with Proxy-based TypedDoc
|
|
7
|
+
*
|
|
8
|
+
* This API provides direct schema access on the doc object:
|
|
9
|
+
* - doc.count.increment(5) instead of doc.count.increment(5)
|
|
10
|
+
* - doc.$.change() for batched mutations
|
|
11
|
+
* - doc.toJSON() for serialization
|
|
12
|
+
*/
|
|
13
|
+
describe("Grand Unified API v3", () => {
|
|
14
|
+
const schema = Shape.doc({
|
|
15
|
+
title: Shape.text(),
|
|
16
|
+
count: Shape.counter(),
|
|
17
|
+
users: Shape.record(
|
|
18
|
+
Shape.plain.struct({
|
|
19
|
+
name: Shape.plain.string(),
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
items: Shape.list(Shape.plain.string()),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("direct mutations (auto-commit)", () => {
|
|
26
|
+
it("should auto-commit counter increments", () => {
|
|
27
|
+
const doc = createTypedDoc(schema)
|
|
28
|
+
doc.count.increment(5)
|
|
29
|
+
expect(doc.toJSON().count).toBe(5)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("should auto-commit counter decrements", () => {
|
|
33
|
+
const doc = createTypedDoc(schema)
|
|
34
|
+
doc.count.increment(10)
|
|
35
|
+
doc.count.decrement(3)
|
|
36
|
+
expect(doc.toJSON().count).toBe(7)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should auto-commit text inserts", () => {
|
|
40
|
+
const doc = createTypedDoc(schema)
|
|
41
|
+
doc.title.insert(0, "Hello")
|
|
42
|
+
expect(doc.toJSON().title).toBe("Hello")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("should auto-commit text updates", () => {
|
|
46
|
+
const doc = createTypedDoc(schema)
|
|
47
|
+
doc.title.insert(0, "Hello")
|
|
48
|
+
doc.title.update("World")
|
|
49
|
+
expect(doc.toJSON().title).toBe("World")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("should auto-commit text deletes", () => {
|
|
53
|
+
const doc = createTypedDoc(schema)
|
|
54
|
+
doc.title.insert(0, "Hello World")
|
|
55
|
+
doc.title.delete(0, 6)
|
|
56
|
+
expect(doc.toJSON().title).toBe("World")
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("should auto-commit record sets", () => {
|
|
60
|
+
const doc = createTypedDoc(schema)
|
|
61
|
+
doc.users.set("alice", { name: "Alice" })
|
|
62
|
+
expect(doc.toJSON().users.alice).toEqual({ name: "Alice" })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("should auto-commit record deletes", () => {
|
|
66
|
+
const doc = createTypedDoc(schema)
|
|
67
|
+
doc.users.set("alice", { name: "Alice" })
|
|
68
|
+
doc.users.set("bob", { name: "Bob" })
|
|
69
|
+
doc.users.delete("alice")
|
|
70
|
+
expect(doc.toJSON().users.alice).toBeUndefined()
|
|
71
|
+
expect(doc.toJSON().users.bob).toEqual({ name: "Bob" })
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("should auto-commit list pushes", () => {
|
|
75
|
+
const doc = createTypedDoc(schema)
|
|
76
|
+
doc.items.push("first")
|
|
77
|
+
doc.items.push("second")
|
|
78
|
+
expect(doc.toJSON().items).toEqual(["first", "second"])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("should auto-commit list inserts", () => {
|
|
82
|
+
const doc = createTypedDoc(schema)
|
|
83
|
+
doc.items.push("first")
|
|
84
|
+
doc.items.push("third")
|
|
85
|
+
doc.items.insert(1, "second")
|
|
86
|
+
expect(doc.toJSON().items).toEqual(["first", "second", "third"])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("should auto-commit list deletes", () => {
|
|
90
|
+
const doc = createTypedDoc(schema)
|
|
91
|
+
doc.items.push("first")
|
|
92
|
+
doc.items.push("second")
|
|
93
|
+
doc.items.push("third")
|
|
94
|
+
doc.items.delete(1, 1)
|
|
95
|
+
expect(doc.toJSON().items).toEqual(["first", "third"])
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("record has() method and 'in' operator", () => {
|
|
100
|
+
it("should support .has() method", () => {
|
|
101
|
+
const doc = createTypedDoc(schema)
|
|
102
|
+
doc.users.set("alice", { name: "Alice" })
|
|
103
|
+
expect(doc.users.has("alice")).toBe(true)
|
|
104
|
+
expect(doc.users.has("bob")).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("should support 'in' operator for records", () => {
|
|
108
|
+
const doc = createTypedDoc(schema)
|
|
109
|
+
doc.users.set("alice", { name: "Alice" })
|
|
110
|
+
expect("alice" in doc.users).toBe(true)
|
|
111
|
+
expect("bob" in doc.users).toBe(false)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should support 'in' operator after change()", () => {
|
|
115
|
+
const doc = createTypedDoc(schema)
|
|
116
|
+
doc.$.change(draft => {
|
|
117
|
+
draft.users.set("alice", { name: "Alice" })
|
|
118
|
+
draft.users.set("bob", { name: "Bob" })
|
|
119
|
+
})
|
|
120
|
+
expect("alice" in doc.users).toBe(true)
|
|
121
|
+
expect("bob" in doc.users).toBe(true)
|
|
122
|
+
expect("charlie" in doc.users).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe("batched mutations", () => {
|
|
127
|
+
it("should batch all changes into one commit", () => {
|
|
128
|
+
const doc = createTypedDoc(schema)
|
|
129
|
+
|
|
130
|
+
// Track commits by checking version changes
|
|
131
|
+
const versionBefore = doc.$.loroDoc.version()
|
|
132
|
+
|
|
133
|
+
doc.$.change(draft => {
|
|
134
|
+
draft.count.increment(1)
|
|
135
|
+
draft.count.increment(2)
|
|
136
|
+
draft.count.increment(3)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const versionAfter = doc.$.loroDoc.version()
|
|
140
|
+
|
|
141
|
+
// Version should have changed
|
|
142
|
+
expect(versionAfter).not.toEqual(versionBefore)
|
|
143
|
+
expect(doc.toJSON().count).toBe(6)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("should batch multiple different operations", () => {
|
|
147
|
+
const doc = createTypedDoc(schema)
|
|
148
|
+
|
|
149
|
+
doc.$.change(draft => {
|
|
150
|
+
draft.title.insert(0, "Hello World")
|
|
151
|
+
draft.count.increment(42)
|
|
152
|
+
draft.users.set("alice", { name: "Alice" })
|
|
153
|
+
draft.items.push("item1")
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const result = doc.toJSON()
|
|
157
|
+
expect(result.title).toBe("Hello World")
|
|
158
|
+
expect(result.count).toBe(42)
|
|
159
|
+
expect(result.users.alice).toEqual({ name: "Alice" })
|
|
160
|
+
expect(result.items).toEqual(["item1"])
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("should return doc for chaining from change()", () => {
|
|
164
|
+
const doc = createTypedDoc(schema)
|
|
165
|
+
|
|
166
|
+
const result = doc.$.change(draft => {
|
|
167
|
+
draft.title.insert(0, "Test")
|
|
168
|
+
draft.count.increment(5)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// change() returns the doc for chaining
|
|
172
|
+
expect(result).toBe(doc)
|
|
173
|
+
expect(result.toJSON().title).toBe("Test")
|
|
174
|
+
expect(result.toJSON().count).toBe(5)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("should support chaining after change()", () => {
|
|
178
|
+
const doc = createTypedDoc(schema)
|
|
179
|
+
|
|
180
|
+
// Chain mutations after change
|
|
181
|
+
doc.$.change(draft => {
|
|
182
|
+
draft.count.increment(5)
|
|
183
|
+
}).count.increment(3)
|
|
184
|
+
|
|
185
|
+
expect(doc.toJSON().count).toBe(8)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe("API consistency", () => {
|
|
190
|
+
it("should have same methods on doc and draft", () => {
|
|
191
|
+
const doc = createTypedDoc(schema)
|
|
192
|
+
|
|
193
|
+
// Both should have .has()
|
|
194
|
+
expect(typeof doc.users.has).toBe("function")
|
|
195
|
+
doc.$.change(draft => {
|
|
196
|
+
expect(typeof draft.users.has).toBe("function")
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Both should have .keys()
|
|
200
|
+
expect(typeof doc.users.keys).toBe("function")
|
|
201
|
+
doc.$.change(draft => {
|
|
202
|
+
expect(typeof draft.users.keys).toBe("function")
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// Both should have .set()
|
|
206
|
+
expect(typeof doc.users.set).toBe("function")
|
|
207
|
+
doc.$.change(draft => {
|
|
208
|
+
expect(typeof draft.users.set).toBe("function")
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("should allow reading values on doc", () => {
|
|
213
|
+
const doc = createTypedDoc(schema)
|
|
214
|
+
|
|
215
|
+
// Set up some data
|
|
216
|
+
doc.$.change(draft => {
|
|
217
|
+
draft.title.insert(0, "Test Title")
|
|
218
|
+
draft.count.increment(42)
|
|
219
|
+
draft.users.set("alice", { name: "Alice" })
|
|
220
|
+
draft.items.push("item1")
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// Read via doc directly
|
|
224
|
+
expect(doc.title.toString()).toBe("Test Title")
|
|
225
|
+
expect(doc.count.value).toBe(42)
|
|
226
|
+
expect(doc.users.has("alice")).toBe(true)
|
|
227
|
+
expect(doc.items.length).toBe(1)
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe("nested container mutations", () => {
|
|
232
|
+
it("should auto-commit nested map mutations", () => {
|
|
233
|
+
const nestedSchema = Shape.doc({
|
|
234
|
+
article: Shape.struct({
|
|
235
|
+
title: Shape.text(),
|
|
236
|
+
metadata: Shape.struct({
|
|
237
|
+
views: Shape.counter(),
|
|
238
|
+
author: Shape.plain.string(),
|
|
239
|
+
}),
|
|
240
|
+
}),
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const doc = createTypedDoc(nestedSchema)
|
|
244
|
+
|
|
245
|
+
// Direct mutations on nested containers
|
|
246
|
+
doc.article.title.insert(0, "My Article")
|
|
247
|
+
doc.article.metadata.views.increment(100)
|
|
248
|
+
doc.article.metadata.set("author", "John Doe")
|
|
249
|
+
|
|
250
|
+
const result = doc.toJSON()
|
|
251
|
+
expect(result.article.title).toBe("My Article")
|
|
252
|
+
expect(result.article.metadata.views).toBe(100)
|
|
253
|
+
expect(result.article.metadata.author).toBe("John Doe")
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it("should auto-commit list of maps mutations", () => {
|
|
257
|
+
const listMapSchema = Shape.doc({
|
|
258
|
+
articles: Shape.list(
|
|
259
|
+
Shape.struct({
|
|
260
|
+
title: Shape.text(),
|
|
261
|
+
views: Shape.counter(),
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const doc = createTypedDoc(listMapSchema)
|
|
267
|
+
|
|
268
|
+
// Push via batch first to create the structure
|
|
269
|
+
doc.$.change(draft => {
|
|
270
|
+
draft.articles.push({ title: "Article 1", views: 0 })
|
|
271
|
+
draft.articles.push({ title: "Article 2", views: 0 })
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Then mutate directly
|
|
275
|
+
doc.articles.get(0)?.title.update("Updated Article 1")
|
|
276
|
+
doc.articles.get(0)?.views.increment(50)
|
|
277
|
+
|
|
278
|
+
const result = doc.toJSON()
|
|
279
|
+
expect(result.articles[0].title).toBe("Updated Article 1")
|
|
280
|
+
expect(result.articles[0].views).toBe(50)
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
describe("counter and text primitive coercion", () => {
|
|
285
|
+
it("should support valueOf() on CounterRef", () => {
|
|
286
|
+
const doc = createTypedDoc(schema)
|
|
287
|
+
doc.count.increment(42)
|
|
288
|
+
|
|
289
|
+
// valueOf() should return the number
|
|
290
|
+
expect(doc.count.valueOf()).toBe(42)
|
|
291
|
+
|
|
292
|
+
// Arithmetic should work via valueOf()
|
|
293
|
+
expect(+doc.count).toBe(42)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it("should support toString() on TextRef", () => {
|
|
297
|
+
const doc = createTypedDoc(schema)
|
|
298
|
+
doc.title.insert(0, "Hello World")
|
|
299
|
+
|
|
300
|
+
// toString() should return the string
|
|
301
|
+
expect(doc.title.toString()).toBe("Hello World")
|
|
302
|
+
|
|
303
|
+
// String concatenation should work
|
|
304
|
+
expect(`Title: ${doc.title}`).toBe("Title: Hello World")
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
describe("placeholder handling", () => {
|
|
309
|
+
it("should return placeholder for unmaterialized counter", () => {
|
|
310
|
+
const schemaWithPlaceholder = Shape.doc({
|
|
311
|
+
count: Shape.counter(), // default placeholder is 0
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const doc = createTypedDoc(schemaWithPlaceholder)
|
|
315
|
+
|
|
316
|
+
// Before any mutations, should return placeholder
|
|
317
|
+
expect(doc.count.value).toBe(0)
|
|
318
|
+
expect(doc.toJSON().count).toBe(0)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it("should return placeholder for unmaterialized text via toJSON()", () => {
|
|
322
|
+
const schemaWithPlaceholder = Shape.doc({
|
|
323
|
+
title: Shape.text().placeholder("Default Title"),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const doc = createTypedDoc(schemaWithPlaceholder)
|
|
327
|
+
|
|
328
|
+
// Before any container access, toJSON() should return placeholder
|
|
329
|
+
expect(doc.toJSON().title).toBe("Default Title")
|
|
330
|
+
|
|
331
|
+
// Accessing doc.title creates a TextRef but doesn't materialize
|
|
332
|
+
// the container until we actually use it
|
|
333
|
+
const ref = doc.title
|
|
334
|
+
expect(doc.toJSON().title).toBe("Default Title") // Still placeholder
|
|
335
|
+
|
|
336
|
+
// Calling toString() on the ref accesses the container, materializing it
|
|
337
|
+
ref.toString()
|
|
338
|
+
// Now the container exists in the CRDT with empty string
|
|
339
|
+
// The overlay returns the actual CRDT value (empty string) since it exists
|
|
340
|
+
expect(doc.toJSON().title).toBe("")
|
|
341
|
+
|
|
342
|
+
// After mutation, the value changes
|
|
343
|
+
ref.insert(0, "Hello")
|
|
344
|
+
expect(doc.toJSON().title).toBe("Hello")
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it("should return actual value after mutation", () => {
|
|
348
|
+
const schemaWithPlaceholder = Shape.doc({
|
|
349
|
+
count: Shape.counter(),
|
|
350
|
+
title: Shape.text().placeholder("Default"),
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const doc = createTypedDoc(schemaWithPlaceholder)
|
|
354
|
+
|
|
355
|
+
doc.count.increment(10)
|
|
356
|
+
doc.title.update("Custom Title")
|
|
357
|
+
|
|
358
|
+
expect(doc.count.value).toBe(10)
|
|
359
|
+
expect(doc.title.toString()).toBe("Custom Title")
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
describe("multiple sequential mutations", () => {
|
|
364
|
+
it("should handle many sequential auto-commit mutations", () => {
|
|
365
|
+
const doc = createTypedDoc(schema)
|
|
366
|
+
|
|
367
|
+
// Many sequential mutations
|
|
368
|
+
for (let i = 0; i < 10; i++) {
|
|
369
|
+
doc.count.increment(1)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
expect(doc.toJSON().count).toBe(10)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("should handle interleaved reads and writes", () => {
|
|
376
|
+
const doc = createTypedDoc(schema)
|
|
377
|
+
|
|
378
|
+
doc.count.increment(5)
|
|
379
|
+
expect(doc.count.value).toBe(5)
|
|
380
|
+
|
|
381
|
+
doc.count.increment(3)
|
|
382
|
+
expect(doc.count.value).toBe(8)
|
|
383
|
+
|
|
384
|
+
doc.count.decrement(2)
|
|
385
|
+
expect(doc.count.value).toBe(6)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe("$ namespace", () => {
|
|
390
|
+
it("should provide access to meta-operations via $", () => {
|
|
391
|
+
const doc = createTypedDoc(schema)
|
|
392
|
+
|
|
393
|
+
// $ should exist
|
|
394
|
+
expect(doc.$).toBeDefined()
|
|
395
|
+
|
|
396
|
+
// $ should have batch, toJSON, loroDoc, etc.
|
|
397
|
+
expect(typeof doc.$.change).toBe("function")
|
|
398
|
+
expect(typeof doc.toJSON).toBe("function")
|
|
399
|
+
expect(doc.$.loroDoc).toBeDefined()
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it("should not enumerate $ in Object.keys()", () => {
|
|
403
|
+
const doc = createTypedDoc(schema)
|
|
404
|
+
|
|
405
|
+
// $ should not appear in Object.keys()
|
|
406
|
+
const keys = Object.keys(doc)
|
|
407
|
+
expect(keys).not.toContain("$")
|
|
408
|
+
|
|
409
|
+
// But schema keys should appear
|
|
410
|
+
expect(keys).toContain("title")
|
|
411
|
+
expect(keys).toContain("count")
|
|
412
|
+
expect(keys).toContain("users")
|
|
413
|
+
expect(keys).toContain("items")
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it("should support 'in' operator for $", () => {
|
|
417
|
+
const doc = createTypedDoc(schema)
|
|
418
|
+
|
|
419
|
+
// $ should be accessible via 'in'
|
|
420
|
+
expect("$" in doc).toBe(true)
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ export {
|
|
|
4
4
|
derivePlaceholder,
|
|
5
5
|
deriveShapePlaceholder,
|
|
6
6
|
} from "./derive-placeholder.js"
|
|
7
|
+
// Functional helpers (recommended API)
|
|
8
|
+
export { change, getLoroDoc } from "./functional-helpers.js"
|
|
7
9
|
export { mergeValue, overlayPlaceholder } from "./overlay.js"
|
|
8
10
|
export { createPlaceholderProxy } from "./placeholder-proxy.js"
|
|
9
11
|
export type { ObjectValue, PresenceInterface } from "./presence-interface.js"
|
|
@@ -19,11 +21,15 @@ export type {
|
|
|
19
21
|
// Schema node types
|
|
20
22
|
DocShape,
|
|
21
23
|
ListContainerShape,
|
|
24
|
+
/** @deprecated Use StructContainerShape instead */
|
|
22
25
|
MapContainerShape,
|
|
23
26
|
MovableListContainerShape,
|
|
27
|
+
/** @deprecated Use StructValueShape instead */
|
|
24
28
|
ObjectValueShape,
|
|
25
29
|
RecordContainerShape,
|
|
26
30
|
RecordValueShape,
|
|
31
|
+
StructContainerShape,
|
|
32
|
+
StructValueShape,
|
|
27
33
|
TextContainerShape,
|
|
28
34
|
TreeContainerShape,
|
|
29
35
|
UnionValueShape,
|
|
@@ -34,16 +40,12 @@ export type {
|
|
|
34
40
|
} from "./shape.js"
|
|
35
41
|
// Schema and type exports
|
|
36
42
|
export { Shape } from "./shape.js"
|
|
37
|
-
export {
|
|
43
|
+
export type { TypedDoc } from "./typed-doc.js"
|
|
44
|
+
export { createTypedDoc } from "./typed-doc.js"
|
|
38
45
|
export { TypedPresence } from "./typed-presence.js"
|
|
39
46
|
export type {
|
|
40
|
-
DeepReadonly,
|
|
41
|
-
/** @deprecated Use Mutable instead */
|
|
42
|
-
Draft,
|
|
43
47
|
// Type inference - Infer<T> is the recommended unified helper
|
|
44
48
|
Infer,
|
|
45
|
-
/** @deprecated Use InferMutableType instead */
|
|
46
|
-
InferDraftType,
|
|
47
49
|
InferMutableType,
|
|
48
50
|
InferPlaceholderType,
|
|
49
51
|
Mutable,
|