@loro-extended/change 0.8.0 → 0.9.0

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 (35) hide show
  1. package/README.md +78 -0
  2. package/dist/index.d.ts +190 -39
  3. package/dist/index.js +480 -295
  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/discriminated-union-assignability.test.ts +45 -0
  8. package/src/discriminated-union-tojson.test.ts +128 -0
  9. package/src/index.ts +7 -0
  10. package/src/placeholder-proxy.test.ts +52 -0
  11. package/src/placeholder-proxy.ts +37 -0
  12. package/src/presence-interface.ts +52 -0
  13. package/src/shape.ts +44 -50
  14. package/src/typed-doc.ts +4 -4
  15. package/src/typed-presence.ts +96 -0
  16. package/src/{draft-nodes → typed-refs}/base.ts +4 -4
  17. package/src/{draft-nodes → typed-refs}/counter.test.ts +1 -1
  18. package/src/{draft-nodes → typed-refs}/counter.ts +9 -3
  19. package/src/{draft-nodes → typed-refs}/doc.ts +27 -13
  20. package/src/typed-refs/json-compatibility.test.ts +255 -0
  21. package/src/{draft-nodes → typed-refs}/list-base.ts +79 -30
  22. package/src/{draft-nodes → typed-refs}/list.test.ts +1 -1
  23. package/src/{draft-nodes → typed-refs}/list.ts +4 -4
  24. package/src/{draft-nodes → typed-refs}/map.ts +33 -22
  25. package/src/{draft-nodes → typed-refs}/movable-list.test.ts +1 -1
  26. package/src/{draft-nodes → typed-refs}/movable-list.ts +6 -6
  27. package/src/{draft-nodes → typed-refs}/proxy-handlers.ts +25 -26
  28. package/src/{draft-nodes → typed-refs}/record.test.ts +69 -0
  29. package/src/{draft-nodes → typed-refs}/record.ts +50 -21
  30. package/src/{draft-nodes → typed-refs}/text.ts +13 -3
  31. package/src/{draft-nodes → typed-refs}/tree.ts +6 -3
  32. package/src/{draft-nodes → typed-refs}/utils.ts +23 -27
  33. package/src/types.test.ts +97 -2
  34. package/src/types.ts +62 -5
  35. package/src/draft-nodes/counter.md +0 -31
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,21 +46,59 @@ 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;
@@ -60,14 +106,15 @@ declare abstract class DraftNode<Shape extends DocShape | ContainerShape> {
60
106
  protected get container(): ShapeToContainer<Shape>;
61
107
  }
62
108
 
63
- declare class CounterDraftNode extends DraftNode<CounterContainerShape> {
109
+ declare class CounterRef extends TypedRef<CounterContainerShape> {
64
110
  absorbPlainValues(): void;
65
111
  increment(value: number): void;
66
112
  decrement(value: number): void;
67
113
  get value(): number;
114
+ toJSON(): number;
68
115
  }
69
116
 
70
- declare abstract class ListDraftNodeBase<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"], DraftItem = NestedShape["_draft"]> extends DraftNode<any> {
117
+ declare abstract class ListRefBase<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"], MutableItem = NestedShape["_mutable"]> extends TypedRef<any> {
71
118
  private itemCache;
72
119
  protected get container(): LoroList | LoroMovableList;
73
120
  protected get shape(): ListContainerShape<NestedShape> | MovableListContainerShape<NestedShape>;
@@ -75,43 +122,47 @@ declare abstract class ListDraftNodeBase<NestedShape extends ContainerOrValueSha
75
122
  protected abstract absorbValueAtIndex(index: number, value: any): void;
76
123
  protected insertWithConversion(index: number, item: Item): void;
77
124
  protected pushWithConversion(item: Item): void;
78
- getDraftNodeParams(index: number, shape: ContainerShape): DraftNodeParams<ContainerShape>;
125
+ getTypedRefParams(index: number, shape: ContainerShape): TypedRefParams<ContainerShape>;
79
126
  protected getPredicateItem(index: number): Item;
80
- protected getDraftItem(index: number): any;
81
- find(predicate: (item: Item, index: number) => boolean): DraftItem | undefined;
127
+ protected getMutableItem(index: number): any;
128
+ find(predicate: (item: Item, index: number) => boolean): MutableItem | undefined;
82
129
  findIndex(predicate: (item: Item, index: number) => boolean): number;
83
130
  map<ReturnType>(callback: (item: Item, index: number) => ReturnType): ReturnType[];
84
- filter(predicate: (item: Item, index: number) => boolean): DraftItem[];
131
+ filter(predicate: (item: Item, index: number) => boolean): MutableItem[];
85
132
  forEach(callback: (item: Item, index: number) => void): void;
86
133
  some(predicate: (item: Item, index: number) => boolean): boolean;
87
134
  every(predicate: (item: Item, index: number) => boolean): boolean;
135
+ slice(start?: number, end?: number): MutableItem[];
88
136
  insert(index: number, item: Item): void;
89
137
  delete(index: number, len: number): void;
90
138
  push(item: Item): void;
91
139
  pushContainer(container: Container): Container;
92
140
  insertContainer(index: number, container: Container): Container;
93
- get(index: number): DraftItem;
141
+ get(index: number): MutableItem;
94
142
  toArray(): Item[];
143
+ toJSON(): Item[];
144
+ [Symbol.iterator](): IterableIterator<MutableItem>;
95
145
  get length(): number;
96
146
  private updateCacheForDelete;
97
147
  private updateCacheForInsert;
98
148
  }
99
149
 
100
- declare class ListDraftNode<NestedShape extends ContainerOrValueShape> extends ListDraftNodeBase<NestedShape> {
150
+ declare class ListRef<NestedShape extends ContainerOrValueShape> extends ListRefBase<NestedShape> {
101
151
  [index: number]: Infer<NestedShape>;
102
152
  protected get container(): LoroList;
103
153
  protected absorbValueAtIndex(index: number, value: any): void;
104
154
  }
105
155
 
106
- declare class MapDraftNode<NestedShapes extends Record<string, ContainerOrValueShape>> extends DraftNode<any> {
156
+ declare class MapRef<NestedShapes extends Record<string, ContainerOrValueShape>> extends TypedRef<any> {
107
157
  private propertyCache;
108
- constructor(params: DraftNodeParams<MapContainerShape<NestedShapes>>);
158
+ constructor(params: TypedRefParams<MapContainerShape<NestedShapes>>);
109
159
  protected get shape(): MapContainerShape<NestedShapes>;
110
160
  protected get container(): LoroMap;
111
161
  absorbPlainValues(): void;
112
- getDraftNodeParams<S extends ContainerShape>(key: string, shape: S): DraftNodeParams<ContainerShape>;
162
+ getTypedRefParams<S extends ContainerShape>(key: string, shape: S): TypedRefParams<ContainerShape>;
113
163
  getOrCreateNode<Shape extends ContainerShape | ValueShape>(key: string, shape: Shape): any;
114
164
  private createLazyProperties;
165
+ toJSON(): any;
115
166
  get(key: string): any;
116
167
  set(key: string, value: Value): void;
117
168
  setContainer<C extends Container>(key: string, container: C): C;
@@ -122,7 +173,7 @@ declare class MapDraftNode<NestedShapes extends Record<string, ContainerOrValueS
122
173
  get size(): number;
123
174
  }
124
175
 
125
- declare class MovableListDraftNode<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"]> extends ListDraftNodeBase<NestedShape> {
176
+ declare class MovableListRef<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"]> extends ListRefBase<NestedShape> {
126
177
  [index: number]: Infer<NestedShape>;
127
178
  protected get container(): LoroMovableList;
128
179
  protected absorbValueAtIndex(index: number, value: any): void;
@@ -130,13 +181,13 @@ declare class MovableListDraftNode<NestedShape extends ContainerOrValueShape, It
130
181
  set(index: number, item: Exclude<Item, Container>): void;
131
182
  }
132
183
 
133
- declare class RecordDraftNode<NestedShape extends ContainerOrValueShape> extends DraftNode<any> {
184
+ declare class RecordRef<NestedShape extends ContainerOrValueShape> extends TypedRef<any> {
134
185
  [key: string]: Infer<NestedShape> | any;
135
186
  private nodeCache;
136
187
  protected get shape(): RecordContainerShape<NestedShape>;
137
188
  protected get container(): LoroMap;
138
189
  absorbPlainValues(): void;
139
- getDraftNodeParams<S extends ContainerShape>(key: string, shape: S): DraftNodeParams<ContainerShape>;
190
+ getTypedRefParams<S extends ContainerShape>(key: string, shape: S): TypedRefParams<ContainerShape>;
140
191
  getOrCreateNode(key: string): any;
141
192
  get(key: string): InferDraftType<NestedShape>;
142
193
  set(key: string, value: any): void;
@@ -146,13 +197,15 @@ declare class RecordDraftNode<NestedShape extends ContainerOrValueShape> extends
146
197
  keys(): string[];
147
198
  values(): any[];
148
199
  get size(): number;
200
+ toJSON(): Record<string, any>;
149
201
  }
150
202
 
151
- declare class TextDraftNode extends DraftNode<TextContainerShape> {
203
+ declare class TextRef extends TypedRef<TextContainerShape> {
152
204
  absorbPlainValues(): void;
153
205
  insert(index: number, content: string): void;
154
206
  delete(index: number, len: number): void;
155
207
  toString(): string;
208
+ toJSON(): string;
156
209
  update(text: string): void;
157
210
  mark(range: {
158
211
  start: number;
@@ -173,42 +226,42 @@ type WithPlaceholder<S extends Shape<any, any, any>> = S & {
173
226
  interface DocShape<NestedShapes extends Record<string, ContainerShape> = Record<string, ContainerShape>> extends Shape<{
174
227
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
175
228
  }, {
176
- [K in keyof NestedShapes]: NestedShapes[K]["_draft"];
229
+ [K in keyof NestedShapes]: NestedShapes[K]["_mutable"];
177
230
  }, {
178
231
  [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"];
179
232
  }> {
180
233
  readonly _type: "doc";
181
234
  readonly shapes: NestedShapes;
182
235
  }
183
- interface TextContainerShape extends Shape<string, TextDraftNode, string> {
236
+ interface TextContainerShape extends Shape<string, TextRef, string> {
184
237
  readonly _type: "text";
185
238
  }
186
- interface CounterContainerShape extends Shape<number, CounterDraftNode, number> {
239
+ interface CounterContainerShape extends Shape<number, CounterRef, number> {
187
240
  readonly _type: "counter";
188
241
  }
189
242
  interface TreeContainerShape<NestedShape = ContainerOrValueShape> extends Shape<any, any, never[]> {
190
243
  readonly _type: "tree";
191
244
  readonly shape: NestedShape;
192
245
  }
193
- interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape>, never[]> {
246
+ interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListRef<NestedShape>, never[]> {
194
247
  readonly _type: "list";
195
248
  readonly shape: NestedShape;
196
249
  }
197
- interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListDraftNode<NestedShape>, never[]> {
250
+ interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListRef<NestedShape>, never[]> {
198
251
  readonly _type: "movableList";
199
252
  readonly shape: NestedShape;
200
253
  }
201
254
  interface MapContainerShape<NestedShapes extends Record<string, ContainerOrValueShape> = Record<string, ContainerOrValueShape>> extends Shape<{
202
255
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
203
- }, MapDraftNode<NestedShapes> & {
204
- [K in keyof NestedShapes]: NestedShapes[K]["_draft"];
256
+ }, MapRef<NestedShapes> & {
257
+ [K in keyof NestedShapes]: NestedShapes[K]["_mutable"];
205
258
  }, {
206
259
  [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"];
207
260
  }> {
208
261
  readonly _type: "map";
209
262
  readonly shapes: NestedShapes;
210
263
  }
211
- interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordDraftNode<NestedShape>, Record<string, never>> {
264
+ interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordRef<NestedShape>, Record<string, never>> {
212
265
  readonly _type: "record";
213
266
  readonly shape: NestedShape;
214
267
  }
@@ -242,7 +295,7 @@ interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array, Uint8Array>
242
295
  interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string, ValueShape>> extends Shape<{
243
296
  [K in keyof T]: T[K]["_plain"];
244
297
  }, {
245
- [K in keyof T]: T[K]["_draft"];
298
+ [K in keyof T]: T[K]["_mutable"];
246
299
  }, {
247
300
  [K in keyof T]: T[K]["_placeholder"];
248
301
  }> {
@@ -250,17 +303,17 @@ interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string,
250
303
  readonly valueType: "object";
251
304
  readonly shape: T;
252
305
  }
253
- interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_draft"]>, Record<string, never>> {
306
+ interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_mutable"]>, Record<string, never>> {
254
307
  readonly _type: "value";
255
308
  readonly valueType: "record";
256
309
  readonly shape: T;
257
310
  }
258
- interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_draft"][], never[]> {
311
+ interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_mutable"][], never[]> {
259
312
  readonly _type: "value";
260
313
  readonly valueType: "array";
261
314
  readonly shape: T;
262
315
  }
263
- interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"], T[number]["_placeholder"]> {
316
+ interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_mutable"], T[number]["_placeholder"]> {
264
317
  readonly _type: "value";
265
318
  readonly valueType: "union";
266
319
  readonly shapes: T;
@@ -278,7 +331,7 @@ interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T
278
331
  * @typeParam K - The discriminant key (e.g., "type")
279
332
  * @typeParam T - A record mapping discriminant values to their object shapes
280
333
  */
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"]> {
334
+ 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
335
  readonly _type: "value";
283
336
  readonly valueType: "discriminatedUnion";
284
337
  readonly discriminantKey: K;
@@ -286,10 +339,10 @@ interface DiscriminatedUnionValueShape<K extends string = string, T extends Reco
286
339
  }
287
340
  type ValueShape = StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | ObjectValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape;
288
341
  type ContainerOrValueShape = ContainerShape | ValueShape;
289
- interface Shape<Plain, Draft, Placeholder = Plain> {
342
+ interface Shape<Plain, Mutable, Placeholder = Plain> {
290
343
  readonly _type: string;
291
344
  readonly _plain: Plain;
292
- readonly _draft: Draft;
345
+ readonly _mutable: Mutable;
293
346
  readonly _placeholder: Placeholder;
294
347
  }
295
348
  /**
@@ -380,6 +433,59 @@ declare function overlayPlaceholder<Shape extends DocShape>(shape: Shape, crdtVa
380
433
  */
381
434
  declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Shape, crdtValue: Value, placeholderValue: Value): Value;
382
435
 
436
+ /**
437
+ * Creates a proxy around a placeholder value (plain object/array) that mimics
438
+ * the behavior of TypedRef, specifically adding a .toJSON() method.
439
+ *
440
+ * This ensures consistent UX where users can call .toJSON() on document state
441
+ * regardless of whether it's loading (placeholder) or loaded (live ref).
442
+ */
443
+ declare function createPlaceholderProxy<T extends object>(target: T): T;
444
+
445
+ /**
446
+ * A record of string keys to Loro values, used for presence data.
447
+ */
448
+ type ObjectValue = Record<string, Value>;
449
+ /**
450
+ * Interface for presence management that can be implemented by different backends.
451
+ * This abstraction allows TypedPresence to work with any presence provider.
452
+ */
453
+ interface PresenceInterface {
454
+ /**
455
+ * Set multiple presence values at once.
456
+ */
457
+ set: (values: ObjectValue) => void;
458
+ /**
459
+ * Get a single presence value by key.
460
+ */
461
+ get: (key: string) => Value;
462
+ /**
463
+ * The current peer's presence state.
464
+ */
465
+ readonly self: ObjectValue;
466
+ /**
467
+ * Other peers' presence states, keyed by peer ID.
468
+ * Does NOT include self. Use this for iterating over remote peers.
469
+ */
470
+ readonly peers: Map<string, ObjectValue>;
471
+ /**
472
+ * All peers' presence states, keyed by peer ID (includes self).
473
+ * @deprecated Use `peers` and `self` separately. This property is synthesized
474
+ * from `peers` and `self` for backward compatibility.
475
+ */
476
+ readonly all: Record<string, ObjectValue>;
477
+ /**
478
+ * Set a single raw value by key (escape hatch for arbitrary keys).
479
+ */
480
+ setRaw: (key: string, value: Value) => void;
481
+ /**
482
+ * Subscribe to presence changes.
483
+ * @param cb Callback that receives the aggregated presence values
484
+ * @returns Unsubscribe function
485
+ */
486
+ subscribe: (cb: (values: ObjectValue) => void) => () => void;
487
+ }
488
+
383
489
  /** biome-ignore-all lint/suspicious/noExplicitAny: JSON Patch values can be any type */
384
490
 
385
491
  type JsonPatchAddOperation = {
@@ -462,10 +568,55 @@ declare class TypedDoc<Shape extends DocShape> {
462
568
  }
463
569
  declare function createTypedDoc<Shape extends DocShape>(shape: Shape, existingDoc?: LoroDoc): TypedDoc<Shape>;
464
570
 
571
+ /**
572
+ * A strongly-typed wrapper around a PresenceInterface.
573
+ * Provides type-safe access to presence data with automatic placeholder merging.
574
+ *
575
+ * @typeParam S - The shape of the presence data
576
+ */
577
+ declare class TypedPresence<S extends ContainerShape | ValueShape> {
578
+ shape: S;
579
+ private presence;
580
+ private placeholder;
581
+ constructor(shape: S, presence: PresenceInterface);
582
+ /**
583
+ * Get the current peer's presence state with placeholder values merged in.
584
+ */
585
+ get self(): Infer<S>;
586
+ /**
587
+ * Get other peers' presence states with placeholder values merged in.
588
+ * Does NOT include self. Use this for iterating over remote peers.
589
+ */
590
+ get peers(): Map<string, Infer<S>>;
591
+ /**
592
+ * Get all peers' presence states with placeholder values merged in.
593
+ * @deprecated Use `peers` and `self` separately. This property is synthesized
594
+ * from `peers` and `self` for backward compatibility.
595
+ */
596
+ get all(): Record<string, Infer<S>>;
597
+ /**
598
+ * Set presence values for the current peer.
599
+ */
600
+ set(value: Partial<Infer<S>>): void;
601
+ /**
602
+ * Subscribe to presence changes.
603
+ * The callback is called immediately with the current state, then on each change.
604
+ *
605
+ * @param cb Callback that receives the typed presence state
606
+ * @returns Unsubscribe function
607
+ */
608
+ subscribe(cb: (state: {
609
+ self: Infer<S>;
610
+ peers: Map<string, Infer<S>>;
611
+ /** @deprecated Use `peers` and `self` separately */
612
+ all: Record<string, Infer<S>>;
613
+ }) => void): () => void;
614
+ }
615
+
465
616
  /**
466
617
  * Validates placeholder against schema structure without using Zod
467
618
  * Combines the functionality of createPlaceholderValidator and createValueValidator
468
619
  */
469
620
  declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
470
621
 
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 };
622
+ 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 };