@loro-extended/change 5.4.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 +56 -36
- package/dist/index.d.ts +132 -109
- package/dist/index.js +187 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +1 -1
- 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 +33 -3
- package/src/index.ts +37 -13
- package/src/overlay-recursion.test.ts +8 -8
- package/src/shallow-fork.test.ts +1 -1
- package/src/shape.ts +7 -7
- package/src/typed-doc.ts +23 -6
- package/src/typed-refs/base.ts +16 -1
- package/src/typed-refs/counter-ref-internals.ts +9 -0
- package/src/typed-refs/doc-ref-internals.ts +1 -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.ts +77 -2
- package/src/typed-refs/record-ref-internals.ts +12 -0
- package/src/typed-refs/struct-ref-internals.ts +12 -0
- package/src/typed-refs/text-ref-internals.ts +69 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loro-extended/change",
|
|
3
|
-
"version": "5.4.
|
|
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",
|
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(() => {
|
|
@@ -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
1
|
import {
|
|
2
2
|
type LoroCounter,
|
|
3
3
|
LoroDoc,
|
|
4
|
+
type LoroEventBatch,
|
|
4
5
|
type LoroList,
|
|
5
6
|
type LoroMap,
|
|
6
7
|
type LoroMovableList,
|
|
7
8
|
type LoroText,
|
|
8
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,
|
|
@@ -317,7 +319,7 @@ export function fork<Shape extends DocShape>(
|
|
|
317
319
|
forkedLoroDoc.setPeerId(loroDoc.peerId)
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
return createTypedDoc(shape, forkedLoroDoc)
|
|
322
|
+
return createTypedDoc(shape, { doc: forkedLoroDoc })
|
|
321
323
|
}
|
|
322
324
|
|
|
323
325
|
/**
|
|
@@ -353,7 +355,7 @@ export function forkAt<Shape extends DocShape>(
|
|
|
353
355
|
const loroDoc = loro(doc).doc
|
|
354
356
|
const forkedLoroDoc = loroDoc.forkAt(frontiers)
|
|
355
357
|
const shape = loro(doc).docShape as Shape
|
|
356
|
-
return createTypedDoc(shape, forkedLoroDoc)
|
|
358
|
+
return createTypedDoc(shape, { doc: forkedLoroDoc })
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
/**
|
|
@@ -421,5 +423,33 @@ export function shallowForkAt<Shape extends DocShape>(
|
|
|
421
423
|
shallowLoroDoc.setPeerId(loroDoc.peerId)
|
|
422
424
|
}
|
|
423
425
|
|
|
424
|
-
return createTypedDoc(shape, shallowLoroDoc)
|
|
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
|
+
}
|
|
425
455
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,9 @@ export {
|
|
|
4
4
|
derivePlaceholder,
|
|
5
5
|
deriveShapePlaceholder,
|
|
6
6
|
} from "./derive-placeholder.js"
|
|
7
|
-
|
|
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"
|
|
8
10
|
// Functional helpers (recommended API)
|
|
9
11
|
export {
|
|
10
12
|
change,
|
|
@@ -12,9 +14,9 @@ export {
|
|
|
12
14
|
forkAt,
|
|
13
15
|
getLoroContainer,
|
|
14
16
|
getLoroDoc,
|
|
17
|
+
getTransition,
|
|
15
18
|
shallowForkAt,
|
|
16
19
|
} from "./functional-helpers.js"
|
|
17
|
-
|
|
18
20
|
// The loro() escape hatch for CRDT internals
|
|
19
21
|
export {
|
|
20
22
|
LORO_SYMBOL,
|
|
@@ -27,36 +29,47 @@ export {
|
|
|
27
29
|
type LoroTypedDocRef,
|
|
28
30
|
loro,
|
|
29
31
|
} from "./loro.js"
|
|
30
|
-
|
|
32
|
+
// Regular placeholder overlay
|
|
31
33
|
export { mergeValue, overlayPlaceholder } from "./overlay.js"
|
|
34
|
+
|
|
32
35
|
// Path selector DSL exports
|
|
33
36
|
export { createPathBuilder } from "./path-builder.js"
|
|
37
|
+
|
|
34
38
|
export { compileToJsonPath, hasWildcard } from "./path-compiler.js"
|
|
39
|
+
|
|
35
40
|
export { evaluatePath, evaluatePathOnValue } from "./path-evaluator.js"
|
|
41
|
+
|
|
36
42
|
export type {
|
|
37
43
|
PathBuilder,
|
|
38
44
|
PathNode,
|
|
39
45
|
PathSegment,
|
|
40
46
|
PathSelector,
|
|
41
47
|
} from "./path-selector.js"
|
|
48
|
+
|
|
42
49
|
export { createPlaceholderProxy } from "./placeholder-proxy.js"
|
|
50
|
+
|
|
43
51
|
export { replayDiff } from "./replay-diff.js"
|
|
44
|
-
//
|
|
52
|
+
// Doc shapes
|
|
45
53
|
// Container shapes
|
|
46
54
|
// Value shapes
|
|
55
|
+
// Shape utilities
|
|
47
56
|
export type {
|
|
48
57
|
AnyContainerShape,
|
|
49
58
|
AnyValueShape,
|
|
50
59
|
ArrayValueShape,
|
|
60
|
+
BooleanValueShape,
|
|
61
|
+
// A shape type representing any container-type or value-type shape (excludes DocShape)
|
|
51
62
|
ContainerOrValueShape,
|
|
63
|
+
// A shape type representing any container-type shape
|
|
52
64
|
ContainerShape,
|
|
53
65
|
ContainerType as RootContainerType,
|
|
54
66
|
CounterContainerShape,
|
|
55
|
-
// Tagged union
|
|
67
|
+
// Tagged union of two or more plain value types
|
|
56
68
|
DiscriminatedUnionValueShape,
|
|
57
69
|
DocShape,
|
|
58
70
|
ListContainerShape,
|
|
59
71
|
MovableListContainerShape,
|
|
72
|
+
NullValueShape,
|
|
60
73
|
NumberValueShape,
|
|
61
74
|
RecordContainerShape,
|
|
62
75
|
RecordValueShape,
|
|
@@ -67,8 +80,11 @@ export type {
|
|
|
67
80
|
TreeContainerShape,
|
|
68
81
|
TreeNodeJSON,
|
|
69
82
|
TreeRefInterface,
|
|
83
|
+
Uint8ArrayValueShape,
|
|
84
|
+
UndefinedValueShape,
|
|
70
85
|
// Union of two or more plain value types
|
|
71
86
|
UnionValueShape,
|
|
87
|
+
// A shape type representing any value-type shape
|
|
72
88
|
ValueShape,
|
|
73
89
|
// WithNullable type for shapes that support .nullable()
|
|
74
90
|
WithNullable,
|
|
@@ -78,17 +94,24 @@ export type {
|
|
|
78
94
|
|
|
79
95
|
// Schema and type exports
|
|
80
96
|
export { Shape } from "./shape.js"
|
|
97
|
+
|
|
81
98
|
export type { Frontiers, TypedDoc } from "./typed-doc.js"
|
|
99
|
+
|
|
82
100
|
export { createTypedDoc } from "./typed-doc.js"
|
|
101
|
+
|
|
83
102
|
// Typed ref types - for specifying types with the loro() function
|
|
84
|
-
export type {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
|
|
92
115
|
export type {
|
|
93
116
|
// Type inference - Infer<T> is the recommended unified helper
|
|
94
117
|
Infer,
|
|
@@ -98,5 +121,6 @@ export type {
|
|
|
98
121
|
InferRaw,
|
|
99
122
|
Mutable,
|
|
100
123
|
} from "./types.js"
|
|
124
|
+
|
|
101
125
|
// Utility exports
|
|
102
126
|
export { validatePlaceholder } from "./validation.js"
|
|
@@ -52,7 +52,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
52
52
|
userMap.set("name", "Alice")
|
|
53
53
|
// Note: 'role' is NOT set - should default to "guest"
|
|
54
54
|
|
|
55
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
55
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
56
56
|
const json = typedDoc.toJSON()
|
|
57
57
|
|
|
58
58
|
expect(json.users[0].name).toBe("Alice")
|
|
@@ -91,7 +91,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
91
91
|
empMap.set("name", "Bob")
|
|
92
92
|
// Note: 'level' and 'status' are NOT set
|
|
93
93
|
|
|
94
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
94
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
95
95
|
const json = typedDoc.toJSON()
|
|
96
96
|
|
|
97
97
|
expect(json.departments[0].name).toBe("Engineering")
|
|
@@ -121,7 +121,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
121
121
|
)
|
|
122
122
|
// Actually we need to create a text container properly
|
|
123
123
|
|
|
124
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
124
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
125
125
|
const json = typedDoc.toJSON()
|
|
126
126
|
|
|
127
127
|
// The counter should default to 100 if not set
|
|
@@ -147,7 +147,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
147
147
|
taskMap.set("title", "Important Task")
|
|
148
148
|
// Note: 'priority' and 'completed' are NOT set
|
|
149
149
|
|
|
150
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
150
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
151
151
|
const json = typedDoc.toJSON()
|
|
152
152
|
|
|
153
153
|
expect(json.tasks[0].title).toBe("Important Task")
|
|
@@ -174,7 +174,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
174
174
|
itemMap.set("name", "Widget")
|
|
175
175
|
// Note: 'count' is NOT set
|
|
176
176
|
|
|
177
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
177
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
178
178
|
|
|
179
179
|
// Access the list ref directly and call toJSON()
|
|
180
180
|
const listJson = typedDoc.items.toJSON()
|
|
@@ -213,7 +213,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
213
213
|
numbersList.insert(1, 2)
|
|
214
214
|
numbersList.insert(2, 3)
|
|
215
215
|
|
|
216
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
216
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
217
217
|
const json = typedDoc.toJSON()
|
|
218
218
|
|
|
219
219
|
expect(json.numbers).toEqual([1, 2, 3])
|
|
@@ -239,7 +239,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
239
239
|
userMap.set("name", "Charlie")
|
|
240
240
|
// Note: 'salary' is NOT set
|
|
241
241
|
|
|
242
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
242
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
243
243
|
const json = typedDoc.toJSON()
|
|
244
244
|
|
|
245
245
|
expect(json.usersByDept.engineering[0].name).toBe("Charlie")
|
|
@@ -294,7 +294,7 @@ describe("Overlay and Placeholder Handling", () => {
|
|
|
294
294
|
const dataMap = loroDoc.getMap("data")
|
|
295
295
|
dataMap.set("value", null)
|
|
296
296
|
|
|
297
|
-
const typedDoc = createTypedDoc(schema, loroDoc)
|
|
297
|
+
const typedDoc = createTypedDoc(schema, { doc: loroDoc })
|
|
298
298
|
const json = typedDoc.toJSON()
|
|
299
299
|
|
|
300
300
|
expect(json.data.value).toBeNull()
|
package/src/shallow-fork.test.ts
CHANGED
|
@@ -205,7 +205,7 @@ describe("shallow fork", () => {
|
|
|
205
205
|
shallowLoroDoc.setPeerId(loro(doc).doc.peerId)
|
|
206
206
|
|
|
207
207
|
// Wrap in TypedDoc
|
|
208
|
-
const shallowDoc = createTypedDoc(TestSchema, shallowLoroDoc)
|
|
208
|
+
const shallowDoc = createTypedDoc(TestSchema, { doc: shallowLoroDoc })
|
|
209
209
|
|
|
210
210
|
// Verify state is correct
|
|
211
211
|
expect(shallowDoc.counter.value).toBe(5)
|
package/src/shape.ts
CHANGED
|
@@ -385,17 +385,17 @@ export interface AnyValueShape extends Shape<Value, Value, undefined> {
|
|
|
385
385
|
// Union of all ValueShapes - these can only contain other ValueShapes, not ContainerShapes
|
|
386
386
|
export type ValueShape =
|
|
387
387
|
| AnyValueShape
|
|
388
|
-
|
|
|
389
|
-
| NumberValueShape
|
|
388
|
+
| ArrayValueShape
|
|
390
389
|
| BooleanValueShape
|
|
390
|
+
| DiscriminatedUnionValueShape
|
|
391
391
|
| NullValueShape
|
|
392
|
-
|
|
|
393
|
-
| Uint8ArrayValueShape
|
|
394
|
-
| StructValueShape
|
|
392
|
+
| NumberValueShape
|
|
395
393
|
| RecordValueShape
|
|
396
|
-
|
|
|
394
|
+
| StringValueShape
|
|
395
|
+
| StructValueShape
|
|
396
|
+
| Uint8ArrayValueShape
|
|
397
|
+
| UndefinedValueShape
|
|
397
398
|
| UnionValueShape
|
|
398
|
-
| DiscriminatedUnionValueShape
|
|
399
399
|
|
|
400
400
|
export type ContainerOrValueShape = ContainerShape | ValueShape
|
|
401
401
|
|
package/src/typed-doc.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { LORO_SYMBOL, type LoroTypedDocRef } from "./loro.js"
|
|
17
17
|
import { overlayPlaceholder } from "./overlay.js"
|
|
18
18
|
import type { DocShape } from "./shape.js"
|
|
19
|
-
import { INTERNAL_SYMBOL } from "./typed-refs/base.js"
|
|
19
|
+
import { type DiffOverlay, INTERNAL_SYMBOL } from "./typed-refs/base.js"
|
|
20
20
|
import { DocRef } from "./typed-refs/doc-ref.js"
|
|
21
21
|
import type { Infer, InferPlaceholderType, Mutable } from "./types.js"
|
|
22
22
|
import { validatePlaceholder } from "./validation.js"
|
|
@@ -29,14 +29,20 @@ class TypedDocInternal<Shape extends DocShape> {
|
|
|
29
29
|
private shape: Shape
|
|
30
30
|
private placeholder: InferPlaceholderType<Shape>
|
|
31
31
|
private doc: LoroDoc
|
|
32
|
+
private overlay?: DiffOverlay
|
|
32
33
|
private valueRef: DocRef<Shape> | null = null
|
|
33
34
|
// Reference to the proxy for returning from change()
|
|
34
35
|
proxy: TypedDoc<Shape> | null = null
|
|
35
36
|
|
|
36
|
-
constructor(
|
|
37
|
+
constructor(
|
|
38
|
+
shape: Shape,
|
|
39
|
+
doc: LoroDoc = new LoroDoc(),
|
|
40
|
+
overlay?: DiffOverlay,
|
|
41
|
+
) {
|
|
37
42
|
this.shape = shape
|
|
38
43
|
this.placeholder = derivePlaceholder(shape)
|
|
39
44
|
this.doc = doc
|
|
45
|
+
this.overlay = overlay
|
|
40
46
|
|
|
41
47
|
validatePlaceholder(this.placeholder, this.shape)
|
|
42
48
|
}
|
|
@@ -48,6 +54,7 @@ class TypedDocInternal<Shape extends DocShape> {
|
|
|
48
54
|
placeholder: this.placeholder as any,
|
|
49
55
|
doc: this.doc,
|
|
50
56
|
autoCommit: true,
|
|
57
|
+
overlay: this.overlay,
|
|
51
58
|
})
|
|
52
59
|
}
|
|
53
60
|
return this.valueRef as unknown as Mutable<Shape>
|
|
@@ -69,6 +76,7 @@ class TypedDocInternal<Shape extends DocShape> {
|
|
|
69
76
|
doc: this.doc,
|
|
70
77
|
autoCommit: false,
|
|
71
78
|
batchedMutation: true, // Enable value shape caching for find-and-mutate patterns
|
|
79
|
+
overlay: this.overlay,
|
|
72
80
|
})
|
|
73
81
|
fn(draft as unknown as Mutable<Shape>)
|
|
74
82
|
draft[INTERNAL_SYMBOL].absorbPlainValues()
|
|
@@ -140,6 +148,11 @@ class TypedDocInternal<Shape extends DocShape> {
|
|
|
140
148
|
*/
|
|
141
149
|
export type Frontiers = { peer: PeerID; counter: number }[]
|
|
142
150
|
|
|
151
|
+
export type CreateTypedDocOptions = {
|
|
152
|
+
doc?: LoroDoc
|
|
153
|
+
overlay?: DiffOverlay
|
|
154
|
+
}
|
|
155
|
+
|
|
143
156
|
export type TypedDoc<Shape extends DocShape> = Mutable<Shape> & {
|
|
144
157
|
/**
|
|
145
158
|
* The primary method of mutating typed documents.
|
|
@@ -208,7 +221,7 @@ export type TypedDoc<Shape extends DocShape> = Mutable<Shape> & {
|
|
|
208
221
|
* Returns a proxied document where schema properties are accessed directly.
|
|
209
222
|
*
|
|
210
223
|
* @param shape - The document schema (with optional .placeholder() values)
|
|
211
|
-
* @param
|
|
224
|
+
* @param options - Optional existing LoroDoc or diff overlay
|
|
212
225
|
* @returns A proxied TypedDoc with direct schema access
|
|
213
226
|
*
|
|
214
227
|
* @example
|
|
@@ -241,9 +254,13 @@ export type TypedDoc<Shape extends DocShape> = Mutable<Shape> & {
|
|
|
241
254
|
*/
|
|
242
255
|
export function createTypedDoc<Shape extends DocShape>(
|
|
243
256
|
shape: Shape,
|
|
244
|
-
|
|
257
|
+
options: CreateTypedDocOptions = {},
|
|
245
258
|
): TypedDoc<Shape> {
|
|
246
|
-
const internal = new TypedDocInternal(
|
|
259
|
+
const internal = new TypedDocInternal(
|
|
260
|
+
shape,
|
|
261
|
+
options.doc || new LoroDoc(),
|
|
262
|
+
options.overlay,
|
|
263
|
+
)
|
|
247
264
|
|
|
248
265
|
// Create the loro() namespace for this doc
|
|
249
266
|
const loroNamespace: LoroTypedDocRef = {
|
|
@@ -278,7 +295,7 @@ export function createTypedDoc<Shape extends DocShape>(
|
|
|
278
295
|
// Create the forkAt() function that returns a new TypedDoc at the specified version
|
|
279
296
|
const forkAtFunction = (frontiers: Frontiers): TypedDoc<Shape> => {
|
|
280
297
|
const forkedLoroDoc = internal.loroDoc.forkAt(frontiers)
|
|
281
|
-
return createTypedDoc(internal.docShape, forkedLoroDoc)
|
|
298
|
+
return createTypedDoc(internal.docShape, { doc: forkedLoroDoc })
|
|
282
299
|
}
|
|
283
300
|
|
|
284
301
|
// Create a proxy that delegates schema properties to the DocRef
|
package/src/typed-refs/base.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ContainerID,
|
|
3
|
+
Diff,
|
|
4
|
+
LoroDoc,
|
|
5
|
+
LoroEventBatch,
|
|
6
|
+
Subscription,
|
|
7
|
+
} from "loro-crdt"
|
|
2
8
|
import { LORO_SYMBOL, type LoroRefBase } from "../loro.js"
|
|
3
9
|
import type { ContainerShape, DocShape, ShapeToContainer } from "../shape.js"
|
|
4
10
|
import type { Infer } from "../types.js"
|
|
@@ -31,6 +37,8 @@ export interface RefInternalsBase {
|
|
|
31
37
|
// TypedRefParams and TypedRef Base Class
|
|
32
38
|
// ============================================================================
|
|
33
39
|
|
|
40
|
+
export type DiffOverlay = ReadonlyMap<ContainerID, Diff>
|
|
41
|
+
|
|
34
42
|
export type TypedRefParams<Shape extends DocShape | ContainerShape> = {
|
|
35
43
|
shape: Shape
|
|
36
44
|
placeholder?: Infer<Shape>
|
|
@@ -38,6 +46,7 @@ export type TypedRefParams<Shape extends DocShape | ContainerShape> = {
|
|
|
38
46
|
autoCommit?: boolean // Auto-commit after mutations
|
|
39
47
|
batchedMutation?: boolean // True when inside change() block - enables value shape caching for find-and-mutate patterns
|
|
40
48
|
getDoc: () => LoroDoc // Needed for auto-commit
|
|
49
|
+
overlay?: DiffOverlay // Optional reverse diff overlay for "before" reads
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
// ============================================================================
|
|
@@ -112,6 +121,11 @@ export abstract class BaseRefInternals<Shape extends DocShape | ContainerShape>
|
|
|
112
121
|
return this.params.getDoc()
|
|
113
122
|
}
|
|
114
123
|
|
|
124
|
+
/** Get the diff overlay map (if provided) */
|
|
125
|
+
getOverlay(): DiffOverlay | undefined {
|
|
126
|
+
return this.params.overlay
|
|
127
|
+
}
|
|
128
|
+
|
|
115
129
|
/**
|
|
116
130
|
* Get the TypedRefParams needed to recreate this ref.
|
|
117
131
|
* Used by change() to create draft refs with modified params.
|
|
@@ -127,6 +141,7 @@ export abstract class BaseRefInternals<Shape extends DocShape | ContainerShape>
|
|
|
127
141
|
autoCommit: this.params.autoCommit,
|
|
128
142
|
batchedMutation: this.params.batchedMutation,
|
|
129
143
|
getDoc: this.params.getDoc,
|
|
144
|
+
overlay: this.params.overlay,
|
|
130
145
|
}
|
|
131
146
|
}
|
|
132
147
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
CounterDiff,
|
|
2
3
|
LoroCounter,
|
|
3
4
|
LoroDoc,
|
|
4
5
|
LoroEventBatch,
|
|
@@ -33,6 +34,14 @@ export class CounterRefInternals extends BaseRefInternals<CounterContainerShape>
|
|
|
33
34
|
getValue(): number {
|
|
34
35
|
const container = this.getContainer() as LoroCounter
|
|
35
36
|
const containerValue = container.value
|
|
37
|
+
const overlay = this.getOverlay()
|
|
38
|
+
if (overlay) {
|
|
39
|
+
const diff = overlay.get((container as any).id)
|
|
40
|
+
if (diff && diff.type === "counter") {
|
|
41
|
+
const counterDiff = diff as CounterDiff
|
|
42
|
+
return containerValue + counterDiff.increment
|
|
43
|
+
}
|
|
44
|
+
}
|
|
36
45
|
if (containerValue !== 0 || this.materialized) {
|
|
37
46
|
return containerValue
|
|
38
47
|
}
|