@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.
Files changed (39) hide show
  1. package/README.md +85 -28
  2. package/dist/index.d.ts +291 -107
  3. package/dist/index.js +587 -36
  4. package/dist/index.js.map +1 -1
  5. package/package.json +3 -2
  6. package/src/change.test.ts +1 -1
  7. package/src/conversion.ts +40 -4
  8. package/src/diff-overlay.test.ts +95 -0
  9. package/src/diff-overlay.ts +10 -0
  10. package/src/discriminated-union-tojson.test.ts +2 -2
  11. package/src/fork-at.test.ts +1 -1
  12. package/src/functional-helpers.test.ts +50 -1
  13. package/src/functional-helpers.ts +152 -8
  14. package/src/index.ts +46 -18
  15. package/src/loro.ts +2 -1
  16. package/src/nested-container-materialization.test.ts +336 -0
  17. package/src/overlay-recursion.test.ts +8 -8
  18. package/src/replay-diff.test.ts +389 -0
  19. package/src/replay-diff.ts +229 -0
  20. package/src/shallow-fork.test.ts +302 -0
  21. package/src/shape.ts +7 -7
  22. package/src/typed-doc-ownkeys.test.ts +116 -0
  23. package/src/typed-doc.ts +33 -10
  24. package/src/typed-refs/base.ts +40 -4
  25. package/src/typed-refs/counter-ref-internals.ts +16 -2
  26. package/src/typed-refs/doc-ref-internals.ts +1 -0
  27. package/src/typed-refs/doc-ref-ownkeys.test.ts +78 -0
  28. package/src/typed-refs/index.ts +17 -0
  29. package/src/typed-refs/json-compatibility.test.ts +1 -1
  30. package/src/typed-refs/list-ref-base-internals.ts +2 -1
  31. package/src/typed-refs/list-ref-base.ts +79 -3
  32. package/src/typed-refs/record-ref-internals.ts +116 -2
  33. package/src/typed-refs/record-ref.test.ts +522 -1
  34. package/src/typed-refs/record-ref.ts +72 -3
  35. package/src/typed-refs/struct-ref-internals.ts +40 -3
  36. package/src/typed-refs/text-ref-internals.ts +70 -4
  37. package/src/typed-refs/tree-node-ref-internals.ts +14 -2
  38. package/src/typed-refs/tree-ref-internals.ts +2 -1
  39. 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.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",
@@ -25,13 +25,14 @@
25
25
  "tsup": "^8.5.0",
26
26
  "tsx": "^4.20.3",
27
27
  "typescript": "^5.9.2",
28
- "vitest": "^3.2.4"
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
  }
@@ -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
- for (const [k, v] of Object.entries(value)) {
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
- if (nestedSchema) {
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
- map.set(k, value)
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(() => {
@@ -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
- import type {
2
- LoroCounter,
1
+ import {
2
+ type LoroCounter,
3
3
  LoroDoc,
4
- LoroList,
5
- LoroMap,
6
- LoroMovableList,
7
- LoroText,
8
- LoroTree,
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
- // Discriminated union for tagged unions
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
- /** @deprecated Use StructValueShape instead */
57
- ObjectValueShape,
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
- // Value shapes
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 { CounterRef } from "./typed-refs/counter-ref.js"
81
- export type { ListRef } from "./typed-refs/list-ref.js"
82
- export type { MovableListRef } from "./typed-refs/movable-list-ref.js"
83
- export type { RecordRef } from "./typed-refs/record-ref.js"
84
- export type { StructRef } from "./typed-refs/struct-ref.js"
85
- export type { TextRef } from "./typed-refs/text-ref.js"
86
- export type { TreeNodeRef } from "./typed-refs/tree-node-ref.js"
87
- 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
+
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: unknown) => void): Subscription
89
+ subscribe(callback: (event: LoroEventBatch) => void): Subscription
89
90
  }
90
91
 
91
92
  /**