@loro-extended/change 5.3.0 → 5.4.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 +85 -28
- package/dist/index.d.ts +291 -107
- package/dist/index.js +587 -36
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/change.test.ts +1 -1
- package/src/conversion.ts +40 -4
- package/src/diff-overlay.test.ts +95 -0
- package/src/diff-overlay.ts +10 -0
- package/src/discriminated-union-tojson.test.ts +2 -2
- package/src/fork-at.test.ts +1 -1
- package/src/functional-helpers.test.ts +50 -1
- package/src/functional-helpers.ts +152 -8
- package/src/index.ts +46 -18
- package/src/loro.ts +2 -1
- package/src/nested-container-materialization.test.ts +336 -0
- package/src/overlay-recursion.test.ts +8 -8
- package/src/replay-diff.test.ts +389 -0
- package/src/replay-diff.ts +229 -0
- package/src/shallow-fork.test.ts +302 -0
- package/src/shape.ts +7 -7
- package/src/typed-doc-ownkeys.test.ts +116 -0
- package/src/typed-doc.ts +33 -10
- package/src/typed-refs/base.ts +40 -4
- package/src/typed-refs/counter-ref-internals.ts +16 -2
- package/src/typed-refs/doc-ref-internals.ts +1 -0
- package/src/typed-refs/doc-ref-ownkeys.test.ts +78 -0
- package/src/typed-refs/index.ts +17 -0
- package/src/typed-refs/json-compatibility.test.ts +1 -1
- package/src/typed-refs/list-ref-base-internals.ts +2 -1
- package/src/typed-refs/list-ref-base.ts +79 -3
- package/src/typed-refs/record-ref-internals.ts +116 -2
- package/src/typed-refs/record-ref.test.ts +522 -1
- package/src/typed-refs/record-ref.ts +72 -3
- package/src/typed-refs/struct-ref-internals.ts +40 -3
- package/src/typed-refs/text-ref-internals.ts +70 -4
- package/src/typed-refs/tree-node-ref-internals.ts +14 -2
- package/src/typed-refs/tree-ref-internals.ts +2 -1
- package/src/typed-refs/utils.ts +65 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-extended/change",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
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",
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"tsup": "^8.5.0",
|
|
26
26
|
"tsx": "^4.20.3",
|
|
27
27
|
"typescript": "^5.9.2",
|
|
28
|
-
"vitest": "^
|
|
28
|
+
"vitest": "^4.0.17"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"loro-crdt": "^1.10.3"
|
|
32
32
|
},
|
|
33
33
|
"scripts": {
|
|
34
34
|
"build": "tsup",
|
|
35
|
+
"test": "verify logic",
|
|
35
36
|
"verify": "verify"
|
|
36
37
|
}
|
|
37
38
|
}
|
package/src/change.test.ts
CHANGED
|
@@ -2248,7 +2248,7 @@ describe("Edge Cases and Error Handling", () => {
|
|
|
2248
2248
|
// Note: authorColor is NOT set - this should fall back to placeholder default
|
|
2249
2249
|
|
|
2250
2250
|
// Wrap with TypedDoc
|
|
2251
|
-
const typedDoc = createTypedDoc(DocSchema, loroDoc)
|
|
2251
|
+
const typedDoc = createTypedDoc(DocSchema, { doc: loroDoc })
|
|
2252
2252
|
|
|
2253
2253
|
// This should not throw "placeholder required"
|
|
2254
2254
|
expect(() => {
|
package/src/conversion.ts
CHANGED
|
@@ -108,17 +108,53 @@ function convertStructInput(
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
const map = new LoroMap()
|
|
111
|
-
|
|
111
|
+
|
|
112
|
+
// Iterate over schema keys to ensure all nested containers are materialized
|
|
113
|
+
for (const k of Object.keys(shape.shapes)) {
|
|
112
114
|
const nestedSchema = shape.shapes[k]
|
|
113
|
-
|
|
115
|
+
const v = value[k]
|
|
116
|
+
|
|
117
|
+
if (v !== undefined) {
|
|
114
118
|
const convertedValue = convertInputToRef(v, nestedSchema)
|
|
115
119
|
if (isContainer(convertedValue)) {
|
|
116
120
|
map.setContainer(k, convertedValue)
|
|
117
121
|
} else {
|
|
118
122
|
map.set(k, convertedValue)
|
|
119
123
|
}
|
|
120
|
-
} else {
|
|
121
|
-
|
|
124
|
+
} else if (isContainerShape(nestedSchema)) {
|
|
125
|
+
// If value is missing but it's a container shape, create an empty container
|
|
126
|
+
// This ensures deterministic container IDs across peers
|
|
127
|
+
let emptyValue: any
|
|
128
|
+
if (nestedSchema._type === "struct" || nestedSchema._type === "record") {
|
|
129
|
+
emptyValue = {}
|
|
130
|
+
} else if (
|
|
131
|
+
nestedSchema._type === "list" ||
|
|
132
|
+
nestedSchema._type === "movableList"
|
|
133
|
+
) {
|
|
134
|
+
emptyValue = []
|
|
135
|
+
} else if (nestedSchema._type === "text") {
|
|
136
|
+
emptyValue = ""
|
|
137
|
+
} else if (nestedSchema._type === "counter") {
|
|
138
|
+
emptyValue = 0
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (emptyValue !== undefined) {
|
|
142
|
+
const convertedValue = convertInputToRef(emptyValue, nestedSchema)
|
|
143
|
+
if (isContainer(convertedValue)) {
|
|
144
|
+
map.setContainer(k, convertedValue)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Also handle keys present in value but not in schema (if any, though for structs this shouldn't happen ideally)
|
|
151
|
+
// But for backward compatibility or loose typing, we might want to preserve them?
|
|
152
|
+
// The original code did:
|
|
153
|
+
// if (nestedSchema) { ... } else { map.set(k, value) }
|
|
154
|
+
// So it allowed extra keys.
|
|
155
|
+
for (const [k, v] of Object.entries(value)) {
|
|
156
|
+
if (!shape.shapes[k]) {
|
|
157
|
+
map.set(k, v)
|
|
122
158
|
}
|
|
123
159
|
}
|
|
124
160
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { LoroEventBatch } from "loro-crdt"
|
|
2
|
+
import { describe, expect, it } from "vitest"
|
|
3
|
+
import { createDiffOverlay } from "./diff-overlay.js"
|
|
4
|
+
import { createTypedDoc, loro, Shape } from "./index.js"
|
|
5
|
+
|
|
6
|
+
describe("diff overlay", () => {
|
|
7
|
+
it("should read before values via overlay without checkout", () => {
|
|
8
|
+
const schema = Shape.doc({
|
|
9
|
+
counter: Shape.counter(),
|
|
10
|
+
info: Shape.struct({
|
|
11
|
+
name: Shape.plain.string(),
|
|
12
|
+
count: Shape.plain.number(),
|
|
13
|
+
}),
|
|
14
|
+
list: Shape.list(Shape.plain.number()),
|
|
15
|
+
text: Shape.text(),
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const doc = createTypedDoc(schema)
|
|
19
|
+
const loroDoc = loro(doc).doc
|
|
20
|
+
|
|
21
|
+
doc.counter.increment(10)
|
|
22
|
+
doc.info.name = "Alice"
|
|
23
|
+
doc.info.count = 1
|
|
24
|
+
doc.list.push(1)
|
|
25
|
+
doc.text.insert(0, "hello")
|
|
26
|
+
loroDoc.commit()
|
|
27
|
+
|
|
28
|
+
const transitions: Array<{
|
|
29
|
+
before: {
|
|
30
|
+
counter: number
|
|
31
|
+
name: string
|
|
32
|
+
count: number
|
|
33
|
+
list: number[]
|
|
34
|
+
text: string
|
|
35
|
+
}
|
|
36
|
+
after: {
|
|
37
|
+
counter: number
|
|
38
|
+
name: string
|
|
39
|
+
count: number
|
|
40
|
+
list: number[]
|
|
41
|
+
text: string
|
|
42
|
+
}
|
|
43
|
+
}> = []
|
|
44
|
+
|
|
45
|
+
loroDoc.subscribe(event => {
|
|
46
|
+
const batch = event as LoroEventBatch
|
|
47
|
+
if (batch.by === "checkout") return
|
|
48
|
+
|
|
49
|
+
const overlay = createDiffOverlay(loroDoc, batch)
|
|
50
|
+
const beforeDoc = createTypedDoc(schema, { doc: loroDoc, overlay })
|
|
51
|
+
const afterDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
52
|
+
|
|
53
|
+
transitions.push({
|
|
54
|
+
before: {
|
|
55
|
+
counter: beforeDoc.counter.value,
|
|
56
|
+
name: beforeDoc.info.name,
|
|
57
|
+
count: beforeDoc.info.count,
|
|
58
|
+
list: beforeDoc.list.toArray(),
|
|
59
|
+
text: beforeDoc.text.toString(),
|
|
60
|
+
},
|
|
61
|
+
after: {
|
|
62
|
+
counter: afterDoc.counter.value,
|
|
63
|
+
name: afterDoc.info.name,
|
|
64
|
+
count: afterDoc.info.count,
|
|
65
|
+
list: afterDoc.list.toArray(),
|
|
66
|
+
text: afterDoc.text.toString(),
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
doc.change(draft => {
|
|
72
|
+
draft.counter.increment(5)
|
|
73
|
+
draft.info.name = "Bob"
|
|
74
|
+
draft.info.count = 2
|
|
75
|
+
draft.list.push(2)
|
|
76
|
+
draft.text.update("hello world")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
expect(transitions).toHaveLength(1)
|
|
80
|
+
expect(transitions[0].before).toEqual({
|
|
81
|
+
counter: 10,
|
|
82
|
+
name: "Alice",
|
|
83
|
+
count: 1,
|
|
84
|
+
list: [1],
|
|
85
|
+
text: "hello",
|
|
86
|
+
})
|
|
87
|
+
expect(transitions[0].after).toEqual({
|
|
88
|
+
counter: 15,
|
|
89
|
+
name: "Bob",
|
|
90
|
+
count: 2,
|
|
91
|
+
list: [1, 2],
|
|
92
|
+
text: "hello world",
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContainerID, Diff, LoroDoc, LoroEventBatch } from "loro-crdt"
|
|
2
|
+
|
|
3
|
+
export type DiffOverlay = ReadonlyMap<ContainerID, Diff>
|
|
4
|
+
|
|
5
|
+
export function createDiffOverlay(
|
|
6
|
+
doc: LoroDoc,
|
|
7
|
+
batch: LoroEventBatch,
|
|
8
|
+
): DiffOverlay {
|
|
9
|
+
return new Map(doc.diff(batch.to, batch.from, false))
|
|
10
|
+
}
|
|
@@ -64,7 +64,7 @@ describe("Record with Map entries - placeholder required bug", () => {
|
|
|
64
64
|
// Note: authorColor is NOT set - this should fall back to placeholder
|
|
65
65
|
|
|
66
66
|
// Now wrap it with TypedDoc
|
|
67
|
-
const typedDoc = createTypedDoc(AiStateSchema, loroDoc)
|
|
67
|
+
const typedDoc = createTypedDoc(AiStateSchema, { doc: loroDoc })
|
|
68
68
|
|
|
69
69
|
// This should not throw "placeholder required"
|
|
70
70
|
// BUG: Currently throws because the nested MapRef has placeholder: undefined
|
|
@@ -107,7 +107,7 @@ describe("Record with Map entries - placeholder required bug", () => {
|
|
|
107
107
|
// Only set peerId - other fields are missing
|
|
108
108
|
studentMap.set("peerId", "peer-456")
|
|
109
109
|
|
|
110
|
-
const typedDoc = createTypedDoc(AiStateSchema, loroDoc)
|
|
110
|
+
const typedDoc = createTypedDoc(AiStateSchema, { doc: loroDoc })
|
|
111
111
|
|
|
112
112
|
// This should not throw - missing fields should use placeholder defaults
|
|
113
113
|
expect(() => {
|
package/src/fork-at.test.ts
CHANGED
|
@@ -197,7 +197,7 @@ describe("forkAt", () => {
|
|
|
197
197
|
expect(rawForkedDoc.toJSON()).toEqual({ text: "Hello" })
|
|
198
198
|
|
|
199
199
|
// Can wrap it manually if needed
|
|
200
|
-
const typedForkedDoc = createTypedDoc(schema, rawForkedDoc)
|
|
200
|
+
const typedForkedDoc = createTypedDoc(schema, { doc: rawForkedDoc })
|
|
201
201
|
expect(typedForkedDoc.text.toString()).toBe("Hello")
|
|
202
202
|
})
|
|
203
203
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LoroEventBatch } from "loro-crdt"
|
|
1
2
|
import {
|
|
2
3
|
LoroCounter,
|
|
3
4
|
LoroList,
|
|
@@ -7,7 +8,12 @@ import {
|
|
|
7
8
|
LoroTree,
|
|
8
9
|
} from "loro-crdt"
|
|
9
10
|
import { describe, expect, it, vi } from "vitest"
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
change,
|
|
13
|
+
getLoroContainer,
|
|
14
|
+
getLoroDoc,
|
|
15
|
+
getTransition,
|
|
16
|
+
} from "./functional-helpers.js"
|
|
11
17
|
import { loro } from "./loro.js"
|
|
12
18
|
import { Shape } from "./shape.js"
|
|
13
19
|
import { createTypedDoc } from "./typed-doc.js"
|
|
@@ -486,6 +492,49 @@ describe("functional helpers", () => {
|
|
|
486
492
|
})
|
|
487
493
|
})
|
|
488
494
|
|
|
495
|
+
describe("getTransition()", () => {
|
|
496
|
+
it("should return before/after using reverse diff overlay", () => {
|
|
497
|
+
const doc = createTypedDoc(schema)
|
|
498
|
+
|
|
499
|
+
const transitions: Array<{ beforeCount: number; afterCount: number }> = []
|
|
500
|
+
const unsubscribe = loro(doc).subscribe(event => {
|
|
501
|
+
const { before, after } = getTransition(doc, event)
|
|
502
|
+
transitions.push({
|
|
503
|
+
beforeCount: before.count.value,
|
|
504
|
+
afterCount: after.count.value,
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
doc.count.increment(2)
|
|
509
|
+
loro(doc).doc.commit()
|
|
510
|
+
|
|
511
|
+
expect(transitions).toEqual([{ beforeCount: 0, afterCount: 2 }])
|
|
512
|
+
unsubscribe()
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it("should throw on checkout events", () => {
|
|
516
|
+
const doc = createTypedDoc(schema)
|
|
517
|
+
const frontiers = loro(doc).doc.frontiers()
|
|
518
|
+
|
|
519
|
+
doc.count.increment(1)
|
|
520
|
+
loro(doc).doc.commit()
|
|
521
|
+
|
|
522
|
+
let checkoutEvent: LoroEventBatch | undefined
|
|
523
|
+
const unsubscribe = loro(doc).subscribe(event => {
|
|
524
|
+
checkoutEvent = event
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
loro(doc).doc.checkout(frontiers)
|
|
528
|
+
|
|
529
|
+
expect(checkoutEvent).toBeDefined()
|
|
530
|
+
expect(() => getTransition(doc, checkoutEvent as LoroEventBatch)).toThrow(
|
|
531
|
+
"getTransition does not support checkout events",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
unsubscribe()
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
489
538
|
describe("getLoroDoc() on refs", () => {
|
|
490
539
|
it("should return LoroDoc from TextRef", () => {
|
|
491
540
|
const doc = createTypedDoc(fullSchema)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
LoroCounter,
|
|
1
|
+
import {
|
|
2
|
+
type LoroCounter,
|
|
3
3
|
LoroDoc,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
type LoroEventBatch,
|
|
5
|
+
type LoroList,
|
|
6
|
+
type LoroMap,
|
|
7
|
+
type LoroMovableList,
|
|
8
|
+
type LoroText,
|
|
9
|
+
type LoroTree,
|
|
9
10
|
} from "loro-crdt"
|
|
11
|
+
import { createDiffOverlay } from "./diff-overlay.js"
|
|
10
12
|
import { loro } from "./loro.js"
|
|
11
13
|
import type {
|
|
12
14
|
ContainerOrValueShape,
|
|
@@ -274,6 +276,52 @@ export function getLoroContainer(
|
|
|
274
276
|
return loro(ref as any).container
|
|
275
277
|
}
|
|
276
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Creates a new TypedDoc as a fork of the current document.
|
|
281
|
+
* The forked doc contains all history up to the current version.
|
|
282
|
+
* The forked doc has a different PeerID from the original by default.
|
|
283
|
+
*
|
|
284
|
+
* For raw LoroDoc access, use: `loro(doc).doc.fork()`
|
|
285
|
+
*
|
|
286
|
+
* @param doc - The TypedDoc to fork
|
|
287
|
+
* @param options - Optional settings
|
|
288
|
+
* @param options.preservePeerId - If true, copies the original doc's peer ID to the fork
|
|
289
|
+
* @returns A new TypedDoc with the same schema at the current version
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* import { fork, loro } from "@loro-extended/change"
|
|
294
|
+
*
|
|
295
|
+
* const doc = createTypedDoc(schema);
|
|
296
|
+
* doc.title.update("Hello");
|
|
297
|
+
*
|
|
298
|
+
* // Fork the document
|
|
299
|
+
* const forkedDoc = fork(doc);
|
|
300
|
+
* forkedDoc.title.update("World");
|
|
301
|
+
*
|
|
302
|
+
* console.log(doc.title.toString()); // "Hello"
|
|
303
|
+
* console.log(forkedDoc.title.toString()); // "World"
|
|
304
|
+
*
|
|
305
|
+
* // Fork with same peer ID (for World/Worldview pattern)
|
|
306
|
+
* const worldview = fork(world, { preservePeerId: true });
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
export function fork<Shape extends DocShape>(
|
|
310
|
+
doc: TypedDoc<Shape>,
|
|
311
|
+
options?: { preservePeerId?: boolean },
|
|
312
|
+
): TypedDoc<Shape> {
|
|
313
|
+
const loroDoc = loro(doc).doc
|
|
314
|
+
const forkedLoroDoc = loroDoc.fork()
|
|
315
|
+
const shape = loro(doc).docShape as Shape
|
|
316
|
+
|
|
317
|
+
// Optionally preserve the peer ID (useful for World/Worldview pattern)
|
|
318
|
+
if (options?.preservePeerId) {
|
|
319
|
+
forkedLoroDoc.setPeerId(loroDoc.peerId)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return createTypedDoc(shape, { doc: forkedLoroDoc })
|
|
323
|
+
}
|
|
324
|
+
|
|
277
325
|
/**
|
|
278
326
|
* Creates a new TypedDoc at a specified version (frontiers).
|
|
279
327
|
* The forked doc will only contain history before the specified frontiers.
|
|
@@ -307,5 +355,101 @@ export function forkAt<Shape extends DocShape>(
|
|
|
307
355
|
const loroDoc = loro(doc).doc
|
|
308
356
|
const forkedLoroDoc = loroDoc.forkAt(frontiers)
|
|
309
357
|
const shape = loro(doc).docShape as Shape
|
|
310
|
-
return createTypedDoc(shape, forkedLoroDoc)
|
|
358
|
+
return createTypedDoc(shape, { doc: forkedLoroDoc })
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Creates a new TypedDoc at a specified version using a shallow snapshot.
|
|
363
|
+
* Unlike `forkAt`, this creates a "garbage-collected" snapshot that only
|
|
364
|
+
* contains the current state and history since the specified frontiers.
|
|
365
|
+
*
|
|
366
|
+
* This is more memory-efficient than `forkAt` for documents with long history,
|
|
367
|
+
* especially useful for the fork-and-merge pattern in LEA where we only need:
|
|
368
|
+
* 1. Read current state
|
|
369
|
+
* 2. Apply changes
|
|
370
|
+
* 3. Export delta and merge back
|
|
371
|
+
*
|
|
372
|
+
* The shallow fork has a different PeerID from the original by default.
|
|
373
|
+
* Use `preservePeerId: true` to copy the original's peer ID (useful for
|
|
374
|
+
* fork-and-merge patterns where you want consistent frontier progression).
|
|
375
|
+
*
|
|
376
|
+
* @param doc - The TypedDoc to fork
|
|
377
|
+
* @param frontiers - The version to fork at (obtained from `loro(doc).doc.frontiers()`)
|
|
378
|
+
* @param options - Optional settings
|
|
379
|
+
* @param options.preservePeerId - If true, copies the original doc's peer ID to the fork
|
|
380
|
+
* @returns A new TypedDoc with the same schema at the specified version (shallow)
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```typescript
|
|
384
|
+
* import { shallowForkAt, loro } from "@loro-extended/change"
|
|
385
|
+
*
|
|
386
|
+
* const doc = createTypedDoc(schema);
|
|
387
|
+
* doc.title.update("Hello");
|
|
388
|
+
* const frontiers = loro(doc).doc.frontiers();
|
|
389
|
+
*
|
|
390
|
+
* // Create a shallow fork (memory-efficient)
|
|
391
|
+
* const shallowDoc = shallowForkAt(doc, frontiers, { preservePeerId: true });
|
|
392
|
+
*
|
|
393
|
+
* // Modify the shallow doc
|
|
394
|
+
* shallowDoc.title.update("World");
|
|
395
|
+
*
|
|
396
|
+
* // Merge changes back
|
|
397
|
+
* const update = loro(shallowDoc).doc.export({
|
|
398
|
+
* mode: "update",
|
|
399
|
+
* from: loro(doc).doc.version()
|
|
400
|
+
* });
|
|
401
|
+
* loro(doc).doc.import(update);
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
export function shallowForkAt<Shape extends DocShape>(
|
|
405
|
+
doc: TypedDoc<Shape>,
|
|
406
|
+
frontiers: Frontiers,
|
|
407
|
+
options?: { preservePeerId?: boolean },
|
|
408
|
+
): TypedDoc<Shape> {
|
|
409
|
+
const loroDoc = loro(doc).doc
|
|
410
|
+
const shape = loro(doc).docShape as Shape
|
|
411
|
+
|
|
412
|
+
// Export a shallow snapshot at the specified frontiers
|
|
413
|
+
const shallowBytes = loroDoc.export({
|
|
414
|
+
mode: "shallow-snapshot",
|
|
415
|
+
frontiers,
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
// Create a new LoroDoc from the shallow snapshot
|
|
419
|
+
const shallowLoroDoc = LoroDoc.fromSnapshot(shallowBytes)
|
|
420
|
+
|
|
421
|
+
// Optionally preserve the peer ID for consistent frontier progression
|
|
422
|
+
if (options?.preservePeerId) {
|
|
423
|
+
shallowLoroDoc.setPeerId(loroDoc.peerId)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return createTypedDoc(shape, { doc: shallowLoroDoc })
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export type Transition<Shape extends DocShape> = {
|
|
430
|
+
before: TypedDoc<Shape>
|
|
431
|
+
after: TypedDoc<Shape>
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Build a `{ before, after }` transition from a TypedDoc and a Loro event batch.
|
|
436
|
+
* Uses a reverse diff overlay to compute the "before" view without checkout.
|
|
437
|
+
* Throws on checkout events to avoid time-travel transitions.
|
|
438
|
+
*/
|
|
439
|
+
export function getTransition<Shape extends DocShape>(
|
|
440
|
+
doc: TypedDoc<Shape>,
|
|
441
|
+
event: LoroEventBatch,
|
|
442
|
+
): Transition<Shape> {
|
|
443
|
+
if (event.by === "checkout") {
|
|
444
|
+
throw new Error("getTransition does not support checkout events")
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const loroDoc = getLoroDoc(doc)
|
|
448
|
+
const shape = loro(doc).docShape as Shape
|
|
449
|
+
const overlay = createDiffOverlay(loroDoc, event)
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
before: createTypedDoc(shape, { doc: loroDoc, overlay }),
|
|
453
|
+
after: createTypedDoc(shape, { doc: loroDoc }),
|
|
454
|
+
}
|
|
311
455
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,12 +4,18 @@ export {
|
|
|
4
4
|
derivePlaceholder,
|
|
5
5
|
deriveShapePlaceholder,
|
|
6
6
|
} from "./derive-placeholder.js"
|
|
7
|
+
// Diff overlay--make the TypedDoc return values as if a diff is applied
|
|
8
|
+
export { createDiffOverlay } from "./diff-overlay.js"
|
|
9
|
+
export type { Transition } from "./functional-helpers.js"
|
|
7
10
|
// Functional helpers (recommended API)
|
|
8
11
|
export {
|
|
9
12
|
change,
|
|
13
|
+
fork,
|
|
10
14
|
forkAt,
|
|
11
15
|
getLoroContainer,
|
|
12
16
|
getLoroDoc,
|
|
17
|
+
getTransition,
|
|
18
|
+
shallowForkAt,
|
|
13
19
|
} from "./functional-helpers.js"
|
|
14
20
|
// The loro() escape hatch for CRDT internals
|
|
15
21
|
export {
|
|
@@ -23,68 +29,89 @@ export {
|
|
|
23
29
|
type LoroTypedDocRef,
|
|
24
30
|
loro,
|
|
25
31
|
} from "./loro.js"
|
|
32
|
+
// Regular placeholder overlay
|
|
26
33
|
export { mergeValue, overlayPlaceholder } from "./overlay.js"
|
|
34
|
+
|
|
27
35
|
// Path selector DSL exports
|
|
28
36
|
export { createPathBuilder } from "./path-builder.js"
|
|
37
|
+
|
|
29
38
|
export { compileToJsonPath, hasWildcard } from "./path-compiler.js"
|
|
39
|
+
|
|
30
40
|
export { evaluatePath, evaluatePathOnValue } from "./path-evaluator.js"
|
|
41
|
+
|
|
31
42
|
export type {
|
|
32
43
|
PathBuilder,
|
|
33
44
|
PathNode,
|
|
34
45
|
PathSegment,
|
|
35
46
|
PathSelector,
|
|
36
47
|
} from "./path-selector.js"
|
|
48
|
+
|
|
37
49
|
export { createPlaceholderProxy } from "./placeholder-proxy.js"
|
|
50
|
+
|
|
51
|
+
export { replayDiff } from "./replay-diff.js"
|
|
52
|
+
// Doc shapes
|
|
53
|
+
// Container shapes
|
|
54
|
+
// Value shapes
|
|
55
|
+
// Shape utilities
|
|
38
56
|
export type {
|
|
39
|
-
// Escape hatch shapes for untyped integration
|
|
40
57
|
AnyContainerShape,
|
|
41
58
|
AnyValueShape,
|
|
42
59
|
ArrayValueShape,
|
|
60
|
+
BooleanValueShape,
|
|
61
|
+
// A shape type representing any container-type or value-type shape (excludes DocShape)
|
|
43
62
|
ContainerOrValueShape,
|
|
63
|
+
// A shape type representing any container-type shape
|
|
44
64
|
ContainerShape,
|
|
45
65
|
ContainerType as RootContainerType,
|
|
46
|
-
// Container shapes
|
|
47
66
|
CounterContainerShape,
|
|
48
|
-
//
|
|
67
|
+
// Tagged union of two or more plain value types
|
|
49
68
|
DiscriminatedUnionValueShape,
|
|
50
|
-
// Schema node types
|
|
51
69
|
DocShape,
|
|
52
70
|
ListContainerShape,
|
|
53
|
-
/** @deprecated Use StructContainerShape instead */
|
|
54
|
-
MapContainerShape,
|
|
55
71
|
MovableListContainerShape,
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
NullValueShape,
|
|
73
|
+
NumberValueShape,
|
|
58
74
|
RecordContainerShape,
|
|
59
75
|
RecordValueShape,
|
|
76
|
+
StringValueShape,
|
|
60
77
|
StructContainerShape,
|
|
61
78
|
StructValueShape,
|
|
62
79
|
TextContainerShape,
|
|
63
80
|
TreeContainerShape,
|
|
64
|
-
// Tree-related types
|
|
65
81
|
TreeNodeJSON,
|
|
66
82
|
TreeRefInterface,
|
|
83
|
+
Uint8ArrayValueShape,
|
|
84
|
+
UndefinedValueShape,
|
|
85
|
+
// Union of two or more plain value types
|
|
67
86
|
UnionValueShape,
|
|
68
|
-
//
|
|
87
|
+
// A shape type representing any value-type shape
|
|
69
88
|
ValueShape,
|
|
70
89
|
// WithNullable type for shapes that support .nullable()
|
|
71
90
|
WithNullable,
|
|
72
91
|
// WithPlaceholder type for shapes that support .placeholder()
|
|
73
92
|
WithPlaceholder,
|
|
74
93
|
} from "./shape.js"
|
|
94
|
+
|
|
75
95
|
// Schema and type exports
|
|
76
96
|
export { Shape } from "./shape.js"
|
|
97
|
+
|
|
77
98
|
export type { Frontiers, TypedDoc } from "./typed-doc.js"
|
|
99
|
+
|
|
78
100
|
export { createTypedDoc } from "./typed-doc.js"
|
|
101
|
+
|
|
79
102
|
// Typed ref types - for specifying types with the loro() function
|
|
80
|
-
export type {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
export type {
|
|
104
|
+
CounterRef,
|
|
105
|
+
DiffOverlay,
|
|
106
|
+
ListRef,
|
|
107
|
+
MovableListRef,
|
|
108
|
+
RecordRef,
|
|
109
|
+
StructRef,
|
|
110
|
+
TextRef,
|
|
111
|
+
TreeNodeRef,
|
|
112
|
+
TreeRef,
|
|
113
|
+
} from "./typed-refs/index.js"
|
|
114
|
+
|
|
88
115
|
export type {
|
|
89
116
|
// Type inference - Infer<T> is the recommended unified helper
|
|
90
117
|
Infer,
|
|
@@ -94,5 +121,6 @@ export type {
|
|
|
94
121
|
InferRaw,
|
|
95
122
|
Mutable,
|
|
96
123
|
} from "./types.js"
|
|
124
|
+
|
|
97
125
|
// Utility exports
|
|
98
126
|
export { validatePlaceholder } from "./validation.js"
|
package/src/loro.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
Container,
|
|
29
29
|
LoroCounter,
|
|
30
30
|
LoroDoc,
|
|
31
|
+
LoroEventBatch,
|
|
31
32
|
LoroList,
|
|
32
33
|
LoroMap,
|
|
33
34
|
LoroMovableList,
|
|
@@ -85,7 +86,7 @@ export interface LoroRefBase {
|
|
|
85
86
|
* @param callback - Function called when the container changes
|
|
86
87
|
* @returns Subscription that can be used to unsubscribe
|
|
87
88
|
*/
|
|
88
|
-
subscribe(callback: (event:
|
|
89
|
+
subscribe(callback: (event: LoroEventBatch) => void): Subscription
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
/**
|