@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loro-extended/change",
3
- "version": "5.4.0",
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",
@@ -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(() => {
@@ -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 { change, getLoroContainer, getLoroDoc } from "./functional-helpers.js"
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
- // Shape utilities
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 { CounterRef } from "./typed-refs/counter-ref.js"
85
- export type { ListRef } from "./typed-refs/list-ref.js"
86
- export type { MovableListRef } from "./typed-refs/movable-list-ref.js"
87
- export type { RecordRef } from "./typed-refs/record-ref.js"
88
- export type { StructRef } from "./typed-refs/struct-ref.js"
89
- export type { TextRef } from "./typed-refs/text-ref.js"
90
- export type { TreeNodeRef } from "./typed-refs/tree-node-ref.js"
91
- export type { TreeRef } from "./typed-refs/tree-ref.js"
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()
@@ -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
- | StringValueShape
389
- | NumberValueShape
388
+ | ArrayValueShape
390
389
  | BooleanValueShape
390
+ | DiscriminatedUnionValueShape
391
391
  | NullValueShape
392
- | UndefinedValueShape
393
- | Uint8ArrayValueShape
394
- | StructValueShape
392
+ | NumberValueShape
395
393
  | RecordValueShape
396
- | ArrayValueShape
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(shape: Shape, doc: LoroDoc = new LoroDoc()) {
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 existingDoc - Optional existing LoroDoc to wrap
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
- existingDoc?: LoroDoc,
257
+ options: CreateTypedDocOptions = {},
245
258
  ): TypedDoc<Shape> {
246
- const internal = new TypedDocInternal(shape, existingDoc || new LoroDoc())
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
@@ -1,4 +1,10 @@
1
- import type { LoroDoc, LoroEventBatch, Subscription } from "loro-crdt"
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
  }