@loro-extended/change 0.8.1 → 0.9.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 (41) hide show
  1. package/README.md +78 -0
  2. package/dist/index.d.ts +199 -43
  3. package/dist/index.js +642 -429
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/change.test.ts +277 -1
  7. package/src/conversion.test.ts +72 -72
  8. package/src/conversion.ts +5 -5
  9. package/src/discriminated-union-assignability.test.ts +45 -0
  10. package/src/discriminated-union-tojson.test.ts +128 -0
  11. package/src/index.ts +7 -0
  12. package/src/overlay-recursion.test.ts +325 -0
  13. package/src/overlay.ts +45 -8
  14. package/src/placeholder-proxy.test.ts +52 -0
  15. package/src/placeholder-proxy.ts +37 -0
  16. package/src/presence-interface.ts +52 -0
  17. package/src/shape.ts +44 -50
  18. package/src/typed-doc.ts +4 -4
  19. package/src/typed-presence.ts +96 -0
  20. package/src/{draft-nodes → typed-refs}/base.ts +14 -4
  21. package/src/{draft-nodes → typed-refs}/counter.test.ts +1 -1
  22. package/src/{draft-nodes → typed-refs}/counter.ts +9 -3
  23. package/src/{draft-nodes → typed-refs}/doc.ts +32 -25
  24. package/src/typed-refs/json-compatibility.test.ts +255 -0
  25. package/src/{draft-nodes → typed-refs}/list-base.ts +115 -42
  26. package/src/{draft-nodes → typed-refs}/list.test.ts +1 -1
  27. package/src/{draft-nodes → typed-refs}/list.ts +4 -4
  28. package/src/{draft-nodes → typed-refs}/map.ts +50 -66
  29. package/src/{draft-nodes → typed-refs}/movable-list.test.ts +1 -1
  30. package/src/{draft-nodes → typed-refs}/movable-list.ts +6 -6
  31. package/src/{draft-nodes → typed-refs}/proxy-handlers.ts +25 -26
  32. package/src/{draft-nodes → typed-refs}/record.test.ts +78 -9
  33. package/src/typed-refs/record.ts +193 -0
  34. package/src/{draft-nodes → typed-refs}/text.ts +13 -3
  35. package/src/{draft-nodes → typed-refs}/tree.ts +6 -3
  36. package/src/typed-refs/utils.ts +177 -0
  37. package/src/types.test.ts +97 -2
  38. package/src/types.ts +62 -5
  39. package/src/draft-nodes/counter.md +0 -31
  40. package/src/draft-nodes/record.ts +0 -177
  41. package/src/draft-nodes/utils.ts +0 -96
package/README.md CHANGED
@@ -547,6 +547,26 @@ draft.metadata.values();
547
547
  const value = draft.metadata.get("key");
548
548
  ```
549
549
 
550
+ ### JSON Serialization and Snapshots
551
+
552
+ You can easily get a plain JavaScript object snapshot of any part of the document using `JSON.stringify()` or `.toJSON()`. This works for the entire document, nested containers, and even during loading states (placeholders).
553
+
554
+ ```typescript
555
+ // Get full document snapshot
556
+ const snapshot = doc.toJSON();
557
+
558
+ // Get snapshot of a specific list
559
+ const todos = doc.value.todos.toJSON(); // returns plain array of todos
560
+
561
+ // Works with nested structures
562
+ const metadata = doc.value.metadata.toJSON(); // returns plain object
563
+
564
+ // Serialize as JSON
565
+ const serializedMetadata = JSON.stringify(doc.value.metadata); // returns string
566
+ ```
567
+
568
+ **Note:** `JSON.stringify()` is recommended for serialization as it handles all data types correctly. `.toJSON()` is available on all `TypedRef` objects and proxied placeholders for convenience when you need a direct object snapshot.
569
+
550
570
  ## Type Safety
551
571
 
552
572
  Full TypeScript support with compile-time validation:
@@ -616,6 +636,64 @@ loroDoc.subscribe((event) => {
616
636
  });
617
637
  ```
618
638
 
639
+ ## TypedPresence
640
+
641
+ The `TypedPresence` class provides type-safe access to ephemeral presence data with placeholder defaults:
642
+
643
+ ```typescript
644
+ import { TypedPresence, Shape } from "@loro-extended/change";
645
+
646
+ // Define a presence schema with placeholders
647
+ const PresenceSchema = Shape.plain.object({
648
+ cursor: Shape.plain.object({
649
+ x: Shape.plain.number(),
650
+ y: Shape.plain.number(),
651
+ }),
652
+ name: Shape.plain.string().placeholder("Anonymous"),
653
+ status: Shape.plain.string().placeholder("online"),
654
+ });
655
+
656
+ // Create typed presence from a PresenceInterface
657
+ // (Usually obtained from handle.presence in @loro-extended/repo)
658
+ const typedPresence = new TypedPresence(PresenceSchema, presenceInterface);
659
+
660
+ // Read your presence (with placeholder defaults merged in)
661
+ console.log(typedPresence.self);
662
+ // { cursor: { x: 0, y: 0 }, name: "Anonymous", status: "online" }
663
+
664
+ // Set presence values
665
+ typedPresence.set({ cursor: { x: 100, y: 200 }, name: "Alice" });
666
+
667
+ // Read all peers' presence
668
+ console.log(typedPresence.all);
669
+ // { "peer-1": { cursor: { x: 100, y: 200 }, name: "Alice", status: "online" } }
670
+
671
+ // Subscribe to presence changes
672
+ typedPresence.subscribe(({ self, all }) => {
673
+ console.log("My presence:", self);
674
+ console.log("All peers:", all);
675
+ });
676
+ ```
677
+
678
+ ### PresenceInterface
679
+
680
+ `TypedPresence` works with any object implementing `PresenceInterface`:
681
+
682
+ ```typescript
683
+ import type { PresenceInterface, ObjectValue } from "@loro-extended/change";
684
+
685
+ interface PresenceInterface {
686
+ set: (values: ObjectValue) => void;
687
+ get: (key: string) => Value;
688
+ readonly self: ObjectValue;
689
+ readonly all: Record<string, ObjectValue>;
690
+ setRaw: (key: string, value: Value) => void;
691
+ subscribe: (cb: (values: ObjectValue) => void) => () => void;
692
+ }
693
+ ```
694
+
695
+ This is typically provided by `UntypedDocHandle.presence` in `@loro-extended/repo`.
696
+
619
697
  ## Performance Considerations
620
698
 
621
699
  - All changes within a `change()` block are batched into a single transaction
package/dist/index.d.ts CHANGED
@@ -30,7 +30,15 @@ import { LoroList, LoroMovableList, Container, LoroMap, Value, LoroText, LoroCou
30
30
  * ```
31
31
  */
32
32
  type Infer<T> = T extends Shape<infer P, any, any> ? P : never;
33
- type InferDraftType<T> = T extends Shape<any, infer D, any> ? D : never;
33
+ /**
34
+ * Infers the mutable type from any Shape.
35
+ * This is the type used within change() callbacks for mutation.
36
+ */
37
+ type InferMutableType<T> = T extends Shape<any, infer M, any> ? M : never;
38
+ /**
39
+ * @deprecated Use InferMutableType<T> instead
40
+ */
41
+ type InferDraftType<T> = InferMutableType<T>;
34
42
  /**
35
43
  * Extracts the valid placeholder type from a shape.
36
44
  *
@@ -38,36 +46,80 @@ type InferDraftType<T> = T extends Shape<any, infer D, any> ? D : never;
38
46
  * empty values ([] or {}) to prevent users from expecting per-entry merging.
39
47
  */
40
48
  type InferPlaceholderType<T> = T extends Shape<any, any, infer P> ? P : never;
41
- type Draft<T extends DocShape<Record<string, ContainerShape>>> = InferDraftType<T>;
42
- type DeepReadonly<T> = {
49
+ /**
50
+ * Mutable type for use within change() callbacks.
51
+ * This is the type-safe wrapper around CRDT containers that allows mutation.
52
+ */
53
+ type Mutable<T extends DocShape<Record<string, ContainerShape>>> = InferMutableType<T>;
54
+ /**
55
+ * @deprecated Use Mutable<T> instead
56
+ */
57
+ type Draft<T extends DocShape<Record<string, ContainerShape>>> = Mutable<T>;
58
+ /**
59
+ * Interface for objects that have a toJSON method.
60
+ * This is separate from the data type to avoid polluting Object.values().
61
+ */
62
+ interface HasToJSON<T> {
63
+ toJSON(): T;
64
+ }
65
+ /**
66
+ * Deep readonly wrapper for plain objects (no index signature).
67
+ * Includes toJSON() method.
68
+ */
69
+ type DeepReadonlyObject<T extends object> = {
43
70
  readonly [P in keyof T]: DeepReadonly<T[P]>;
44
- };
71
+ } & HasToJSON<T>;
72
+ /**
73
+ * Deep readonly wrapper for Record types (with string index signature).
74
+ * The toJSON() method is available but NOT part of the index signature,
75
+ * so Object.values() returns clean types.
76
+ */
77
+ type DeepReadonlyRecord<T> = {
78
+ readonly [K in keyof T]: DeepReadonly<T[K]>;
79
+ } & HasToJSON<Record<string, T[keyof T]>>;
80
+ /**
81
+ * Deep readonly wrapper that makes all properties readonly recursively
82
+ * and adds a toJSON() method for JSON serialization.
83
+ *
84
+ * For arrays: Returns ReadonlyArray with toJSON()
85
+ * For objects with string index signature (Records): toJSON() is available
86
+ * but doesn't pollute Object.values() type inference
87
+ * For plain objects: Returns readonly properties with toJSON()
88
+ * For primitives: Returns as-is
89
+ */
90
+ type DeepReadonly<T> = T extends any[] ? ReadonlyArray<DeepReadonly<T[number]>> & HasToJSON<T> : T extends object ? string extends keyof T ? DeepReadonlyRecord<T> : DeepReadonlyObject<T> : T;
45
91
 
46
- type DraftNodeParams<Shape extends DocShape | ContainerShape> = {
92
+ type TypedRefParams<Shape extends DocShape | ContainerShape> = {
47
93
  shape: Shape;
48
94
  placeholder?: Infer<Shape>;
49
95
  getContainer: () => ShapeToContainer<Shape>;
50
96
  readonly?: boolean;
51
97
  };
52
- declare abstract class DraftNode<Shape extends DocShape | ContainerShape> {
53
- protected _params: DraftNodeParams<Shape>;
98
+ declare abstract class TypedRef<Shape extends DocShape | ContainerShape> {
99
+ protected _params: TypedRefParams<Shape>;
54
100
  protected _cachedContainer?: ShapeToContainer<Shape>;
55
- constructor(_params: DraftNodeParams<Shape>);
101
+ constructor(_params: TypedRefParams<Shape>);
56
102
  abstract absorbPlainValues(): void;
57
103
  protected get shape(): Shape;
58
104
  protected get placeholder(): Infer<Shape> | undefined;
59
105
  protected get readonly(): boolean;
106
+ /**
107
+ * Throws an error if this ref is in readonly mode.
108
+ * Call this at the start of any mutating method.
109
+ */
110
+ protected assertMutable(): void;
60
111
  protected get container(): ShapeToContainer<Shape>;
61
112
  }
62
113
 
63
- declare class CounterDraftNode extends DraftNode<CounterContainerShape> {
114
+ declare class CounterRef extends TypedRef<CounterContainerShape> {
64
115
  absorbPlainValues(): void;
65
116
  increment(value: number): void;
66
117
  decrement(value: number): void;
67
118
  get value(): number;
119
+ toJSON(): number;
68
120
  }
69
121
 
70
- declare abstract class ListDraftNodeBase<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"], DraftItem = NestedShape["_draft"]> extends DraftNode<any> {
122
+ declare abstract class ListRefBase<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"], MutableItem = NestedShape["_mutable"]> extends TypedRef<any> {
71
123
  private itemCache;
72
124
  protected get container(): LoroList | LoroMovableList;
73
125
  protected get shape(): ListContainerShape<NestedShape> | MovableListContainerShape<NestedShape>;
@@ -75,43 +127,47 @@ declare abstract class ListDraftNodeBase<NestedShape extends ContainerOrValueSha
75
127
  protected abstract absorbValueAtIndex(index: number, value: any): void;
76
128
  protected insertWithConversion(index: number, item: Item): void;
77
129
  protected pushWithConversion(item: Item): void;
78
- getDraftNodeParams(index: number, shape: ContainerShape): DraftNodeParams<ContainerShape>;
130
+ getTypedRefParams(index: number, shape: ContainerShape): TypedRefParams<ContainerShape>;
79
131
  protected getPredicateItem(index: number): Item;
80
- protected getDraftItem(index: number): any;
81
- find(predicate: (item: Item, index: number) => boolean): DraftItem | undefined;
132
+ protected getMutableItem(index: number): any;
133
+ find(predicate: (item: Item, index: number) => boolean): MutableItem | undefined;
82
134
  findIndex(predicate: (item: Item, index: number) => boolean): number;
83
135
  map<ReturnType>(callback: (item: Item, index: number) => ReturnType): ReturnType[];
84
- filter(predicate: (item: Item, index: number) => boolean): DraftItem[];
136
+ filter(predicate: (item: Item, index: number) => boolean): MutableItem[];
85
137
  forEach(callback: (item: Item, index: number) => void): void;
86
138
  some(predicate: (item: Item, index: number) => boolean): boolean;
87
139
  every(predicate: (item: Item, index: number) => boolean): boolean;
140
+ slice(start?: number, end?: number): MutableItem[];
88
141
  insert(index: number, item: Item): void;
89
142
  delete(index: number, len: number): void;
90
143
  push(item: Item): void;
91
144
  pushContainer(container: Container): Container;
92
145
  insertContainer(index: number, container: Container): Container;
93
- get(index: number): DraftItem;
146
+ get(index: number): MutableItem;
94
147
  toArray(): Item[];
148
+ toJSON(): Item[];
149
+ [Symbol.iterator](): IterableIterator<MutableItem>;
95
150
  get length(): number;
96
151
  private updateCacheForDelete;
97
152
  private updateCacheForInsert;
98
153
  }
99
154
 
100
- declare class ListDraftNode<NestedShape extends ContainerOrValueShape> extends ListDraftNodeBase<NestedShape> {
155
+ declare class ListRef<NestedShape extends ContainerOrValueShape> extends ListRefBase<NestedShape> {
101
156
  [index: number]: Infer<NestedShape>;
102
157
  protected get container(): LoroList;
103
158
  protected absorbValueAtIndex(index: number, value: any): void;
104
159
  }
105
160
 
106
- declare class MapDraftNode<NestedShapes extends Record<string, ContainerOrValueShape>> extends DraftNode<any> {
161
+ declare class MapRef<NestedShapes extends Record<string, ContainerOrValueShape>> extends TypedRef<any> {
107
162
  private propertyCache;
108
- constructor(params: DraftNodeParams<MapContainerShape<NestedShapes>>);
163
+ constructor(params: TypedRefParams<MapContainerShape<NestedShapes>>);
109
164
  protected get shape(): MapContainerShape<NestedShapes>;
110
165
  protected get container(): LoroMap;
111
166
  absorbPlainValues(): void;
112
- getDraftNodeParams<S extends ContainerShape>(key: string, shape: S): DraftNodeParams<ContainerShape>;
113
- getOrCreateNode<Shape extends ContainerShape | ValueShape>(key: string, shape: Shape): any;
167
+ getTypedRefParams<S extends ContainerShape>(key: string, shape: S): TypedRefParams<ContainerShape>;
168
+ getOrCreateRef<Shape extends ContainerShape | ValueShape>(key: string, shape: Shape): any;
114
169
  private createLazyProperties;
170
+ toJSON(): any;
115
171
  get(key: string): any;
116
172
  set(key: string, value: Value): void;
117
173
  setContainer<C extends Container>(key: string, container: C): C;
@@ -122,7 +178,7 @@ declare class MapDraftNode<NestedShapes extends Record<string, ContainerOrValueS
122
178
  get size(): number;
123
179
  }
124
180
 
125
- declare class MovableListDraftNode<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"]> extends ListDraftNodeBase<NestedShape> {
181
+ declare class MovableListRef<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"]> extends ListRefBase<NestedShape> {
126
182
  [index: number]: Infer<NestedShape>;
127
183
  protected get container(): LoroMovableList;
128
184
  protected absorbValueAtIndex(index: number, value: any): void;
@@ -130,15 +186,15 @@ declare class MovableListDraftNode<NestedShape extends ContainerOrValueShape, It
130
186
  set(index: number, item: Exclude<Item, Container>): void;
131
187
  }
132
188
 
133
- declare class RecordDraftNode<NestedShape extends ContainerOrValueShape> extends DraftNode<any> {
189
+ declare class RecordRef<NestedShape extends ContainerOrValueShape> extends TypedRef<any> {
134
190
  [key: string]: Infer<NestedShape> | any;
135
- private nodeCache;
191
+ private refCache;
136
192
  protected get shape(): RecordContainerShape<NestedShape>;
137
193
  protected get container(): LoroMap;
138
194
  absorbPlainValues(): void;
139
- getDraftNodeParams<S extends ContainerShape>(key: string, shape: S): DraftNodeParams<ContainerShape>;
140
- getOrCreateNode(key: string): any;
141
- get(key: string): InferDraftType<NestedShape>;
195
+ getTypedRefParams<S extends ContainerShape>(key: string, shape: S): TypedRefParams<ContainerShape>;
196
+ getOrCreateRef(key: string): any;
197
+ get(key: string): InferMutableType<NestedShape>;
142
198
  set(key: string, value: any): void;
143
199
  setContainer<C extends Container>(key: string, container: C): C;
144
200
  delete(key: string): void;
@@ -146,13 +202,15 @@ declare class RecordDraftNode<NestedShape extends ContainerOrValueShape> extends
146
202
  keys(): string[];
147
203
  values(): any[];
148
204
  get size(): number;
205
+ toJSON(): Record<string, any>;
149
206
  }
150
207
 
151
- declare class TextDraftNode extends DraftNode<TextContainerShape> {
208
+ declare class TextRef extends TypedRef<TextContainerShape> {
152
209
  absorbPlainValues(): void;
153
210
  insert(index: number, content: string): void;
154
211
  delete(index: number, len: number): void;
155
212
  toString(): string;
213
+ toJSON(): string;
156
214
  update(text: string): void;
157
215
  mark(range: {
158
216
  start: number;
@@ -173,42 +231,42 @@ type WithPlaceholder<S extends Shape<any, any, any>> = S & {
173
231
  interface DocShape<NestedShapes extends Record<string, ContainerShape> = Record<string, ContainerShape>> extends Shape<{
174
232
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
175
233
  }, {
176
- [K in keyof NestedShapes]: NestedShapes[K]["_draft"];
234
+ [K in keyof NestedShapes]: NestedShapes[K]["_mutable"];
177
235
  }, {
178
236
  [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"];
179
237
  }> {
180
238
  readonly _type: "doc";
181
239
  readonly shapes: NestedShapes;
182
240
  }
183
- interface TextContainerShape extends Shape<string, TextDraftNode, string> {
241
+ interface TextContainerShape extends Shape<string, TextRef, string> {
184
242
  readonly _type: "text";
185
243
  }
186
- interface CounterContainerShape extends Shape<number, CounterDraftNode, number> {
244
+ interface CounterContainerShape extends Shape<number, CounterRef, number> {
187
245
  readonly _type: "counter";
188
246
  }
189
247
  interface TreeContainerShape<NestedShape = ContainerOrValueShape> extends Shape<any, any, never[]> {
190
248
  readonly _type: "tree";
191
249
  readonly shape: NestedShape;
192
250
  }
193
- interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape>, never[]> {
251
+ interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListRef<NestedShape>, never[]> {
194
252
  readonly _type: "list";
195
253
  readonly shape: NestedShape;
196
254
  }
197
- interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListDraftNode<NestedShape>, never[]> {
255
+ interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListRef<NestedShape>, never[]> {
198
256
  readonly _type: "movableList";
199
257
  readonly shape: NestedShape;
200
258
  }
201
259
  interface MapContainerShape<NestedShapes extends Record<string, ContainerOrValueShape> = Record<string, ContainerOrValueShape>> extends Shape<{
202
260
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
203
- }, MapDraftNode<NestedShapes> & {
204
- [K in keyof NestedShapes]: NestedShapes[K]["_draft"];
261
+ }, MapRef<NestedShapes> & {
262
+ [K in keyof NestedShapes]: NestedShapes[K]["_mutable"];
205
263
  }, {
206
264
  [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"];
207
265
  }> {
208
266
  readonly _type: "map";
209
267
  readonly shapes: NestedShapes;
210
268
  }
211
- interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordDraftNode<NestedShape>, Record<string, never>> {
269
+ interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordRef<NestedShape>, Record<string, never>> {
212
270
  readonly _type: "record";
213
271
  readonly shape: NestedShape;
214
272
  }
@@ -242,7 +300,7 @@ interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array, Uint8Array>
242
300
  interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string, ValueShape>> extends Shape<{
243
301
  [K in keyof T]: T[K]["_plain"];
244
302
  }, {
245
- [K in keyof T]: T[K]["_draft"];
303
+ [K in keyof T]: T[K]["_mutable"];
246
304
  }, {
247
305
  [K in keyof T]: T[K]["_placeholder"];
248
306
  }> {
@@ -250,17 +308,17 @@ interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string,
250
308
  readonly valueType: "object";
251
309
  readonly shape: T;
252
310
  }
253
- interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_draft"]>, Record<string, never>> {
311
+ interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_mutable"]>, Record<string, never>> {
254
312
  readonly _type: "value";
255
313
  readonly valueType: "record";
256
314
  readonly shape: T;
257
315
  }
258
- interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_draft"][], never[]> {
316
+ interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_mutable"][], never[]> {
259
317
  readonly _type: "value";
260
318
  readonly valueType: "array";
261
319
  readonly shape: T;
262
320
  }
263
- interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"], T[number]["_placeholder"]> {
321
+ interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_mutable"], T[number]["_placeholder"]> {
264
322
  readonly _type: "value";
265
323
  readonly valueType: "union";
266
324
  readonly shapes: T;
@@ -278,7 +336,7 @@ interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T
278
336
  * @typeParam K - The discriminant key (e.g., "type")
279
337
  * @typeParam T - A record mapping discriminant values to their object shapes
280
338
  */
281
- interface DiscriminatedUnionValueShape<K extends string = string, T extends Record<string, ObjectValueShape> = Record<string, ObjectValueShape>> extends Shape<T[keyof T]["_plain"], T[keyof T]["_draft"], T[keyof T]["_placeholder"]> {
339
+ interface DiscriminatedUnionValueShape<K extends string = string, T extends Record<string, ObjectValueShape> = Record<string, ObjectValueShape>, Plain = T[keyof T]["_plain"], Mutable = T[keyof T]["_mutable"], Placeholder = T[keyof T]["_placeholder"]> extends Shape<Plain, Mutable, Placeholder> {
282
340
  readonly _type: "value";
283
341
  readonly valueType: "discriminatedUnion";
284
342
  readonly discriminantKey: K;
@@ -286,10 +344,10 @@ interface DiscriminatedUnionValueShape<K extends string = string, T extends Reco
286
344
  }
287
345
  type ValueShape = StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | ObjectValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape;
288
346
  type ContainerOrValueShape = ContainerShape | ValueShape;
289
- interface Shape<Plain, Draft, Placeholder = Plain> {
347
+ interface Shape<Plain, Mutable, Placeholder = Plain> {
290
348
  readonly _type: string;
291
349
  readonly _plain: Plain;
292
- readonly _draft: Draft;
350
+ readonly _mutable: Mutable;
293
351
  readonly _placeholder: Placeholder;
294
352
  }
295
353
  /**
@@ -380,6 +438,59 @@ declare function overlayPlaceholder<Shape extends DocShape>(shape: Shape, crdtVa
380
438
  */
381
439
  declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Shape, crdtValue: Value, placeholderValue: Value): Value;
382
440
 
441
+ /**
442
+ * Creates a proxy around a placeholder value (plain object/array) that mimics
443
+ * the behavior of TypedRef, specifically adding a .toJSON() method.
444
+ *
445
+ * This ensures consistent UX where users can call .toJSON() on document state
446
+ * regardless of whether it's loading (placeholder) or loaded (live ref).
447
+ */
448
+ declare function createPlaceholderProxy<T extends object>(target: T): T;
449
+
450
+ /**
451
+ * A record of string keys to Loro values, used for presence data.
452
+ */
453
+ type ObjectValue = Record<string, Value>;
454
+ /**
455
+ * Interface for presence management that can be implemented by different backends.
456
+ * This abstraction allows TypedPresence to work with any presence provider.
457
+ */
458
+ interface PresenceInterface {
459
+ /**
460
+ * Set multiple presence values at once.
461
+ */
462
+ set: (values: ObjectValue) => void;
463
+ /**
464
+ * Get a single presence value by key.
465
+ */
466
+ get: (key: string) => Value;
467
+ /**
468
+ * The current peer's presence state.
469
+ */
470
+ readonly self: ObjectValue;
471
+ /**
472
+ * Other peers' presence states, keyed by peer ID.
473
+ * Does NOT include self. Use this for iterating over remote peers.
474
+ */
475
+ readonly peers: Map<string, ObjectValue>;
476
+ /**
477
+ * All peers' presence states, keyed by peer ID (includes self).
478
+ * @deprecated Use `peers` and `self` separately. This property is synthesized
479
+ * from `peers` and `self` for backward compatibility.
480
+ */
481
+ readonly all: Record<string, ObjectValue>;
482
+ /**
483
+ * Set a single raw value by key (escape hatch for arbitrary keys).
484
+ */
485
+ setRaw: (key: string, value: Value) => void;
486
+ /**
487
+ * Subscribe to presence changes.
488
+ * @param cb Callback that receives the aggregated presence values
489
+ * @returns Unsubscribe function
490
+ */
491
+ subscribe: (cb: (values: ObjectValue) => void) => () => void;
492
+ }
493
+
383
494
  /** biome-ignore-all lint/suspicious/noExplicitAny: JSON Patch values can be any type */
384
495
 
385
496
  type JsonPatchAddOperation = {
@@ -462,10 +573,55 @@ declare class TypedDoc<Shape extends DocShape> {
462
573
  }
463
574
  declare function createTypedDoc<Shape extends DocShape>(shape: Shape, existingDoc?: LoroDoc): TypedDoc<Shape>;
464
575
 
576
+ /**
577
+ * A strongly-typed wrapper around a PresenceInterface.
578
+ * Provides type-safe access to presence data with automatic placeholder merging.
579
+ *
580
+ * @typeParam S - The shape of the presence data
581
+ */
582
+ declare class TypedPresence<S extends ContainerShape | ValueShape> {
583
+ shape: S;
584
+ private presence;
585
+ private placeholder;
586
+ constructor(shape: S, presence: PresenceInterface);
587
+ /**
588
+ * Get the current peer's presence state with placeholder values merged in.
589
+ */
590
+ get self(): Infer<S>;
591
+ /**
592
+ * Get other peers' presence states with placeholder values merged in.
593
+ * Does NOT include self. Use this for iterating over remote peers.
594
+ */
595
+ get peers(): Map<string, Infer<S>>;
596
+ /**
597
+ * Get all peers' presence states with placeholder values merged in.
598
+ * @deprecated Use `peers` and `self` separately. This property is synthesized
599
+ * from `peers` and `self` for backward compatibility.
600
+ */
601
+ get all(): Record<string, Infer<S>>;
602
+ /**
603
+ * Set presence values for the current peer.
604
+ */
605
+ set(value: Partial<Infer<S>>): void;
606
+ /**
607
+ * Subscribe to presence changes.
608
+ * The callback is called immediately with the current state, then on each change.
609
+ *
610
+ * @param cb Callback that receives the typed presence state
611
+ * @returns Unsubscribe function
612
+ */
613
+ subscribe(cb: (state: {
614
+ self: Infer<S>;
615
+ peers: Map<string, Infer<S>>;
616
+ /** @deprecated Use `peers` and `self` separately */
617
+ all: Record<string, Infer<S>>;
618
+ }) => void): () => void;
619
+ }
620
+
465
621
  /**
466
622
  * Validates placeholder against schema structure without using Zod
467
623
  * Combines the functionality of createPlaceholderValidator and createValueValidator
468
624
  */
469
625
  declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
470
626
 
471
- export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DeepReadonly, type DiscriminatedUnionValueShape, type DocShape, type Draft, type Infer, type InferDraftType, type InferPlaceholderType, type ListContainerShape, type MapContainerShape, type MovableListContainerShape, type ObjectValueShape, type RecordContainerShape, type RecordValueShape, type ContainerType as RootContainerType, Shape, type TextContainerShape, type TreeContainerShape, TypedDoc, type UnionValueShape, type ValueShape, type WithPlaceholder, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, mergeValue, overlayPlaceholder, validatePlaceholder };
627
+ export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DeepReadonly, type DiscriminatedUnionValueShape, type DocShape, type Draft, type Infer, type InferDraftType, type InferMutableType, type InferPlaceholderType, type ListContainerShape, type MapContainerShape, type MovableListContainerShape, type Mutable, type ObjectValue, type ObjectValueShape, type PresenceInterface, type RecordContainerShape, type RecordValueShape, type ContainerType as RootContainerType, Shape, type TextContainerShape, type TreeContainerShape, TypedDoc, TypedPresence, type UnionValueShape, type ValueShape, type WithPlaceholder, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, mergeValue, overlayPlaceholder, validatePlaceholder };