@loro-extended/change 5.1.0 → 5.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loro-extended/change",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "A schema-driven, type-safe wrapper for Loro CRDT that provides natural JavaScript syntax for collaborative data mutations",
5
5
  "author": "Duane Johnson",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
  "./src/*": "./src/*"
22
22
  },
23
23
  "devDependencies": {
24
+ "@typescript/native-preview": "7.0.0-dev.20260103.1",
24
25
  "tsup": "^8.5.0",
25
26
  "tsx": "^4.20.3",
26
27
  "typescript": "^5.9.2",
@@ -31,8 +32,6 @@
31
32
  },
32
33
  "scripts": {
33
34
  "build": "tsup",
34
- "check": "biome check --write .",
35
- "test": "vitest",
36
- "typecheck": "tsc --noEmit --skipLibCheck"
35
+ "verify": "verify"
37
36
  }
38
37
  }
@@ -0,0 +1,260 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { createTypedDoc, forkAt, loro, Shape } from "./index.js"
3
+
4
+ describe("forkAt", () => {
5
+ describe("TypedDoc.forkAt() method", () => {
6
+ it("should fork at a specific version and return correct state", () => {
7
+ const schema = Shape.doc({
8
+ title: Shape.text(),
9
+ count: Shape.counter(),
10
+ })
11
+
12
+ const doc = createTypedDoc(schema)
13
+ doc.title.update("Hello")
14
+ doc.count.increment(5)
15
+
16
+ // Get frontiers at this point
17
+ const frontiers = loro(doc).doc.frontiers()
18
+
19
+ // Make more changes
20
+ doc.title.update("World")
21
+ doc.count.increment(10)
22
+
23
+ // Fork at the earlier version
24
+ const forkedDoc = doc.forkAt(frontiers)
25
+
26
+ // Forked doc should have the earlier state
27
+ expect(forkedDoc.title.toString()).toBe("Hello")
28
+ expect(forkedDoc.count.value).toBe(5)
29
+
30
+ // Original doc should still have the latest state
31
+ expect(doc.title.toString()).toBe("World")
32
+ expect(doc.count.value).toBe(15)
33
+ })
34
+
35
+ it("should preserve type safety on forked doc", () => {
36
+ const schema = Shape.doc({
37
+ items: Shape.list(
38
+ Shape.struct({
39
+ name: Shape.text(),
40
+ done: Shape.plain.boolean(),
41
+ }),
42
+ ),
43
+ })
44
+
45
+ const doc = createTypedDoc(schema)
46
+ doc.items.push({ name: "Task 1", done: false })
47
+
48
+ const frontiers = loro(doc).doc.frontiers()
49
+
50
+ doc.items.push({ name: "Task 2", done: true })
51
+
52
+ const forkedDoc = doc.forkAt(frontiers)
53
+
54
+ // Type safety: forkedDoc.items should have the same type
55
+ expect(forkedDoc.items.length).toBe(1)
56
+ const firstItem = forkedDoc.items[0]
57
+ if (firstItem) {
58
+ expect(firstItem.name.toString()).toBe("Task 1")
59
+ expect(firstItem.done).toBe(false)
60
+ }
61
+
62
+ // Can mutate forked doc independently
63
+ forkedDoc.items.push({ name: "Forked Task", done: true })
64
+ expect(forkedDoc.items.length).toBe(2)
65
+ expect(doc.items.length).toBe(2) // Original unchanged
66
+ })
67
+
68
+ it("should create independent documents (changes don't affect original)", () => {
69
+ const schema = Shape.doc({
70
+ value: Shape.counter(),
71
+ })
72
+
73
+ const doc = createTypedDoc(schema)
74
+ doc.value.increment(10)
75
+
76
+ const frontiers = loro(doc).doc.frontiers()
77
+ const forkedDoc = doc.forkAt(frontiers)
78
+
79
+ // Mutate forked doc
80
+ forkedDoc.value.increment(100)
81
+
82
+ // Original should be unchanged
83
+ expect(doc.value.value).toBe(10)
84
+ expect(forkedDoc.value.value).toBe(110)
85
+
86
+ // Mutate original
87
+ doc.value.increment(5)
88
+
89
+ // Forked should be unchanged
90
+ expect(doc.value.value).toBe(15)
91
+ expect(forkedDoc.value.value).toBe(110)
92
+ })
93
+
94
+ it("should work with complex schemas (maps, trees)", () => {
95
+ const schema = Shape.doc({
96
+ settings: Shape.record(Shape.plain.string()),
97
+ tree: Shape.tree(
98
+ Shape.struct({
99
+ label: Shape.text(),
100
+ }),
101
+ ),
102
+ })
103
+
104
+ const doc = createTypedDoc(schema)
105
+ doc.settings.set("theme", "dark")
106
+ const node = doc.tree.createNode({ label: "Root" })
107
+
108
+ const frontiers = loro(doc).doc.frontiers()
109
+
110
+ doc.settings.set("theme", "light")
111
+ doc.settings.set("lang", "en")
112
+ node.data.label.update("Updated Root")
113
+
114
+ const forkedDoc = doc.forkAt(frontiers)
115
+
116
+ expect(forkedDoc.settings.get("theme")).toBe("dark")
117
+ // "lang" was not set before the fork, so it should not exist
118
+ // Note: Record returns placeholder value (empty string) for missing keys
119
+ expect(forkedDoc.settings.has("lang")).toBe(false)
120
+
121
+ // Tree should have the earlier state
122
+ const forkedRoots = forkedDoc.tree.roots()
123
+ expect(forkedRoots.length).toBe(1)
124
+ expect(forkedRoots[0].data.label.toString()).toBe("Root")
125
+ })
126
+
127
+ it("should have different PeerID from original", () => {
128
+ const schema = Shape.doc({
129
+ text: Shape.text(),
130
+ })
131
+
132
+ const doc = createTypedDoc(schema)
133
+ doc.text.update("Hello")
134
+
135
+ const frontiers = loro(doc).doc.frontiers()
136
+ const forkedDoc = doc.forkAt(frontiers)
137
+
138
+ const originalPeerId = loro(doc).doc.peerId
139
+ const forkedPeerId = loro(forkedDoc).doc.peerId
140
+
141
+ expect(forkedPeerId).not.toBe(originalPeerId)
142
+ })
143
+ })
144
+
145
+ describe("forkAt() functional helper", () => {
146
+ it("should fork at a specific version", () => {
147
+ const schema = Shape.doc({
148
+ title: Shape.text(),
149
+ })
150
+
151
+ const doc = createTypedDoc(schema)
152
+ doc.title.update("Hello")
153
+
154
+ const frontiers = loro(doc).doc.frontiers()
155
+
156
+ doc.title.update("World")
157
+
158
+ // Use functional helper
159
+ const forkedDoc = forkAt(doc, frontiers)
160
+
161
+ expect(forkedDoc.title.toString()).toBe("Hello")
162
+ expect(doc.title.toString()).toBe("World")
163
+ })
164
+
165
+ it("should preserve schema from original doc", () => {
166
+ const schema = Shape.doc({
167
+ count: Shape.counter().placeholder(42),
168
+ })
169
+
170
+ const doc = createTypedDoc(schema)
171
+ // Don't increment - should use placeholder
172
+
173
+ const frontiers = loro(doc).doc.frontiers()
174
+ const forkedDoc = forkAt(doc, frontiers)
175
+
176
+ // Placeholder should be preserved
177
+ expect(forkedDoc.toJSON().count).toBe(42)
178
+ })
179
+ })
180
+
181
+ describe("raw LoroDoc.forkAt() access", () => {
182
+ it("should still be accessible via loro() escape hatch", () => {
183
+ const schema = Shape.doc({
184
+ text: Shape.text(),
185
+ })
186
+
187
+ const doc = createTypedDoc(schema)
188
+ doc.text.update("Hello")
189
+
190
+ const frontiers = loro(doc).doc.frontiers()
191
+ doc.text.update("World")
192
+
193
+ // Raw access returns LoroDoc, not TypedDoc
194
+ const rawForkedDoc = loro(doc).doc.forkAt(frontiers)
195
+
196
+ // It's a plain LoroDoc
197
+ expect(rawForkedDoc.toJSON()).toEqual({ text: "Hello" })
198
+
199
+ // Can wrap it manually if needed
200
+ const typedForkedDoc = createTypedDoc(schema, rawForkedDoc)
201
+ expect(typedForkedDoc.text.toString()).toBe("Hello")
202
+ })
203
+ })
204
+
205
+ describe("edge cases", () => {
206
+ it("should fork at empty frontiers (initial state)", () => {
207
+ const schema = Shape.doc({
208
+ count: Shape.counter().placeholder(0),
209
+ })
210
+
211
+ const doc = createTypedDoc(schema)
212
+ const emptyFrontiers = loro(doc).doc.frontiers()
213
+
214
+ doc.count.increment(10)
215
+
216
+ const forkedDoc = doc.forkAt(emptyFrontiers)
217
+
218
+ // Should be at initial state (placeholder value)
219
+ expect(forkedDoc.count.value).toBe(0)
220
+ })
221
+
222
+ it("should fork at current frontiers (same state)", () => {
223
+ const schema = Shape.doc({
224
+ text: Shape.text(),
225
+ })
226
+
227
+ const doc = createTypedDoc(schema)
228
+ doc.text.update("Hello")
229
+
230
+ const currentFrontiers = loro(doc).doc.frontiers()
231
+ const forkedDoc = doc.forkAt(currentFrontiers)
232
+
233
+ expect(forkedDoc.text.toString()).toBe("Hello")
234
+ })
235
+
236
+ it("should work with change() on forked doc", () => {
237
+ const schema = Shape.doc({
238
+ items: Shape.list(Shape.plain.number()),
239
+ })
240
+
241
+ const doc = createTypedDoc(schema)
242
+ doc.items.push(1)
243
+ doc.items.push(2)
244
+
245
+ const frontiers = loro(doc).doc.frontiers()
246
+ doc.items.push(3)
247
+
248
+ const forkedDoc = doc.forkAt(frontiers)
249
+
250
+ // Use change() on forked doc
251
+ forkedDoc.change(draft => {
252
+ draft.items.push(100)
253
+ draft.items.push(200)
254
+ })
255
+
256
+ expect(forkedDoc.items.toJSON()).toEqual([1, 2, 100, 200])
257
+ expect(doc.items.toJSON()).toEqual([1, 2, 3])
258
+ })
259
+ })
260
+ })