@loro-extended/change 0.7.0 → 0.8.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.
package/README.md CHANGED
@@ -32,7 +32,7 @@ import { TypedDoc, Shape } from "@loro-extended/change";
32
32
 
33
33
  // Define your document schema
34
34
  const schema = Shape.doc({
35
- title: Shape.text(),
35
+ title: Shape.text().placeholder("My Todo List"),
36
36
  todos: Shape.list(
37
37
  Shape.plain.object({
38
38
  id: Shape.plain.string(),
@@ -42,14 +42,8 @@ const schema = Shape.doc({
42
42
  ),
43
43
  });
44
44
 
45
- // Define empty state (default values)
46
- const emptyState = {
47
- title: "My Todo List",
48
- todos: [],
49
- };
50
-
51
45
  // Create a typed document
52
- const doc = new TypedDoc(schema, emptyState);
46
+ const doc = new TypedDoc(schema);
53
47
 
54
48
  // Make changes with natural syntax
55
49
  const result = doc.change((draft) => {
@@ -109,19 +103,26 @@ const blogSchema = Shape.doc({
109
103
  Empty state provides default values that are merged when CRDT containers are empty, keeping the whole document typesafe:
110
104
 
111
105
  ```typescript
112
- const emptyState = {
113
- title: "Untitled Document", // unusual empty state, but technically ok
114
- viewCount: 0,
115
- tags: [],
116
- metadata: {
117
- author: "Anonymous",
118
- publishedAt: "",
119
- featured: false,
120
- },
121
- sections: [],
122
- };
106
+ // Use .placeholder() to set default values
107
+ const blogSchemaWithDefaults = Shape.doc({
108
+ title: Shape.text().placeholder("Untitled Document"),
109
+ viewCount: Shape.counter(), // defaults to 0
110
+ tags: Shape.list(Shape.plain.string()), // defaults to []
111
+ metadata: Shape.map({
112
+ author: Shape.plain.string().placeholder("Anonymous"),
113
+ publishedAt: Shape.plain.string(), // defaults to ""
114
+ featured: Shape.plain.boolean(), // defaults to false
115
+ }),
116
+ sections: Shape.movableList(
117
+ Shape.map({
118
+ heading: Shape.text(),
119
+ content: Shape.text(),
120
+ order: Shape.plain.number(),
121
+ })
122
+ ),
123
+ });
123
124
 
124
- const doc = new TypedDoc(blogSchema, emptyState);
125
+ const doc = new TypedDoc(blogSchemaWithDefaults);
125
126
 
126
127
  // Initially returns empty state
127
128
  console.log(doc.value);
@@ -351,13 +352,13 @@ doc.change((draft) => {
351
352
 
352
353
  ### Core Functions
353
354
 
354
- #### `new TypedDoc<T>(schema, emptyState, existingDoc?)`
355
+ #### `new TypedDoc<T>(schema, existingDoc?)`
355
356
 
356
357
  Creates a new typed Loro document.
357
358
 
358
359
  ```typescript
359
- const doc = new TypedDoc(schema, emptyState);
360
- const docFromExisting = new TypedDoc(schema, emptyState, existingLoroDoc);
360
+ const doc = new TypedDoc(schema);
361
+ const docFromExisting = new TypedDoc(schema, existingLoroDoc);
361
362
  ```
362
363
 
363
364
  #### `doc.change(mutator)`
@@ -571,14 +572,8 @@ const todoSchema = Shape.doc({
571
572
  ),
572
573
  });
573
574
 
574
- // Define empty state that matches your interface
575
- const emptyState: TodoDoc = {
576
- title: "My Todos",
577
- todos: [],
578
- };
579
-
580
575
  // TypeScript will ensure the schema produces the correct type
581
- const doc = new TypedDoc(todoSchema, emptyState);
576
+ const doc = new TypedDoc(todoSchema);
582
577
 
583
578
  // The result will be properly typed as TodoDoc
584
579
  const result: TodoDoc = doc.change((draft) => {
@@ -610,7 +605,7 @@ import { LoroDoc } from "loro-crdt";
610
605
 
611
606
  // Wrap existing LoroDoc
612
607
  const existingDoc = new LoroDoc();
613
- const typedDoc = new TypedDoc(schema, emptyState, existingDoc);
608
+ const typedDoc = new TypedDoc(schema, existingDoc);
614
609
 
615
610
  // Access underlying LoroDoc
616
611
  const loroDoc = typedDoc.loroDoc;
package/dist/index.d.ts CHANGED
@@ -30,21 +30,24 @@ 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 InferPlainType<T> = T extends Shape<infer P, any, any> ? P : never;
34
33
  type InferDraftType<T> = T extends Shape<any, infer D, any> ? D : never;
35
34
  /**
36
- * Extracts the valid empty state type from a shape.
35
+ * Extracts the valid placeholder type from a shape.
37
36
  *
38
37
  * For dynamic containers (list, record, etc.), this will be constrained to
39
38
  * empty values ([] or {}) to prevent users from expecting per-entry merging.
40
39
  */
41
- type InferEmptyStateType<T> = T extends Shape<any, any, infer E> ? E : never;
40
+ type InferPlaceholderType<T> = T extends Shape<any, any, infer P> ? P : never;
42
41
  type Draft<T extends DocShape<Record<string, ContainerShape>>> = InferDraftType<T>;
42
+ type DeepReadonly<T> = {
43
+ readonly [P in keyof T]: DeepReadonly<T[P]>;
44
+ };
43
45
 
44
46
  type DraftNodeParams<Shape extends DocShape | ContainerShape> = {
45
47
  shape: Shape;
46
- emptyState?: InferPlainType<Shape>;
48
+ placeholder?: Infer<Shape>;
47
49
  getContainer: () => ShapeToContainer<Shape>;
50
+ readonly?: boolean;
48
51
  };
49
52
  declare abstract class DraftNode<Shape extends DocShape | ContainerShape> {
50
53
  protected _params: DraftNodeParams<Shape>;
@@ -52,7 +55,8 @@ declare abstract class DraftNode<Shape extends DocShape | ContainerShape> {
52
55
  constructor(_params: DraftNodeParams<Shape>);
53
56
  abstract absorbPlainValues(): void;
54
57
  protected get shape(): Shape;
55
- protected get emptyState(): InferPlainType<Shape> | undefined;
58
+ protected get placeholder(): Infer<Shape> | undefined;
59
+ protected get readonly(): boolean;
56
60
  protected get container(): ShapeToContainer<Shape>;
57
61
  }
58
62
 
@@ -73,7 +77,7 @@ declare abstract class ListDraftNodeBase<NestedShape extends ContainerOrValueSha
73
77
  protected pushWithConversion(item: Item): void;
74
78
  getDraftNodeParams(index: number, shape: ContainerShape): DraftNodeParams<ContainerShape>;
75
79
  protected getPredicateItem(index: number): Item;
76
- protected getDraftItem(index: number): DraftItem;
80
+ protected getDraftItem(index: number): any;
77
81
  find(predicate: (item: Item, index: number) => boolean): DraftItem | undefined;
78
82
  findIndex(predicate: (item: Item, index: number) => boolean): number;
79
83
  map<ReturnType>(callback: (item: Item, index: number) => ReturnType): ReturnType[];
@@ -94,6 +98,7 @@ declare abstract class ListDraftNodeBase<NestedShape extends ContainerOrValueSha
94
98
  }
95
99
 
96
100
  declare class ListDraftNode<NestedShape extends ContainerOrValueShape> extends ListDraftNodeBase<NestedShape> {
101
+ [index: number]: Infer<NestedShape>;
97
102
  protected get container(): LoroList;
98
103
  protected absorbValueAtIndex(index: number, value: any): void;
99
104
  }
@@ -105,7 +110,7 @@ declare class MapDraftNode<NestedShapes extends Record<string, ContainerOrValueS
105
110
  protected get container(): LoroMap;
106
111
  absorbPlainValues(): void;
107
112
  getDraftNodeParams<S extends ContainerShape>(key: string, shape: S): DraftNodeParams<ContainerShape>;
108
- getOrCreateNode<Shape extends ContainerShape | ValueShape>(key: string, shape: Shape): Shape extends ContainerShape ? DraftNode<Shape> : Value;
113
+ getOrCreateNode<Shape extends ContainerShape | ValueShape>(key: string, shape: Shape): any;
109
114
  private createLazyProperties;
110
115
  get(key: string): any;
111
116
  set(key: string, value: Value): void;
@@ -118,6 +123,7 @@ declare class MapDraftNode<NestedShapes extends Record<string, ContainerOrValueS
118
123
  }
119
124
 
120
125
  declare class MovableListDraftNode<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"]> extends ListDraftNodeBase<NestedShape> {
126
+ [index: number]: Infer<NestedShape>;
121
127
  protected get container(): LoroMovableList;
122
128
  protected absorbValueAtIndex(index: number, value: any): void;
123
129
  move(from: number, to: number): void;
@@ -125,13 +131,13 @@ declare class MovableListDraftNode<NestedShape extends ContainerOrValueShape, It
125
131
  }
126
132
 
127
133
  declare class RecordDraftNode<NestedShape extends ContainerOrValueShape> extends DraftNode<any> {
134
+ [key: string]: Infer<NestedShape> | any;
128
135
  private nodeCache;
129
- constructor(params: DraftNodeParams<RecordContainerShape<NestedShape>>);
130
136
  protected get shape(): RecordContainerShape<NestedShape>;
131
137
  protected get container(): LoroMap;
132
138
  absorbPlainValues(): void;
133
139
  getDraftNodeParams<S extends ContainerShape>(key: string, shape: S): DraftNodeParams<ContainerShape>;
134
- getOrCreateNode(key: string): InferDraftType<NestedShape>;
140
+ getOrCreateNode(key: string): any;
135
141
  get(key: string): InferDraftType<NestedShape>;
136
142
  set(key: string, value: any): void;
137
143
  setContainer<C extends Container>(key: string, container: C): C;
@@ -161,12 +167,15 @@ declare class TextDraftNode extends DraftNode<TextContainerShape> {
161
167
  get length(): number;
162
168
  }
163
169
 
170
+ type WithPlaceholder<S extends Shape<any, any, any>> = S & {
171
+ placeholder(value: S["_placeholder"]): S;
172
+ };
164
173
  interface DocShape<NestedShapes extends Record<string, ContainerShape> = Record<string, ContainerShape>> extends Shape<{
165
174
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
166
175
  }, {
167
176
  [K in keyof NestedShapes]: NestedShapes[K]["_draft"];
168
177
  }, {
169
- [K in keyof NestedShapes]: NestedShapes[K]["_emptyState"];
178
+ [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"];
170
179
  }> {
171
180
  readonly _type: "doc";
172
181
  readonly shapes: NestedShapes;
@@ -194,7 +203,7 @@ interface MapContainerShape<NestedShapes extends Record<string, ContainerOrValue
194
203
  }, MapDraftNode<NestedShapes> & {
195
204
  [K in keyof NestedShapes]: NestedShapes[K]["_draft"];
196
205
  }, {
197
- [K in keyof NestedShapes]: NestedShapes[K]["_emptyState"];
206
+ [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"];
198
207
  }> {
199
208
  readonly _type: "map";
200
209
  readonly shapes: NestedShapes;
@@ -235,7 +244,7 @@ interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string,
235
244
  }, {
236
245
  [K in keyof T]: T[K]["_draft"];
237
246
  }, {
238
- [K in keyof T]: T[K]["_emptyState"];
247
+ [K in keyof T]: T[K]["_placeholder"];
239
248
  }> {
240
249
  readonly _type: "value";
241
250
  readonly valueType: "object";
@@ -251,7 +260,7 @@ interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_p
251
260
  readonly valueType: "array";
252
261
  readonly shape: T;
253
262
  }
254
- interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"], T[number]["_emptyState"]> {
263
+ interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"], T[number]["_placeholder"]> {
255
264
  readonly _type: "value";
256
265
  readonly valueType: "union";
257
266
  readonly shapes: T;
@@ -269,19 +278,19 @@ interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T
269
278
  * @typeParam K - The discriminant key (e.g., "type")
270
279
  * @typeParam T - A record mapping discriminant values to their object shapes
271
280
  */
272
- 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]["_emptyState"]> {
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"]> {
273
282
  readonly _type: "value";
274
283
  readonly valueType: "discriminatedUnion";
275
284
  readonly discriminantKey: K;
276
285
  readonly variants: T;
277
286
  }
278
- type ValueShape = StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | ObjectValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape<any, any>;
287
+ type ValueShape = StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | ObjectValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape;
279
288
  type ContainerOrValueShape = ContainerShape | ValueShape;
280
- interface Shape<Plain, Draft, EmptyState = Plain> {
289
+ interface Shape<Plain, Draft, Placeholder = Plain> {
281
290
  readonly _type: string;
282
291
  readonly _plain: Plain;
283
292
  readonly _draft: Draft;
284
- readonly _emptyState: EmptyState;
293
+ readonly _placeholder: Placeholder;
285
294
  }
286
295
  /**
287
296
  * The LoroShape factory object
@@ -292,24 +301,24 @@ interface Shape<Plain, Draft, EmptyState = Plain> {
292
301
  */
293
302
  declare const Shape: {
294
303
  doc: <T extends Record<string, ContainerShape>>(shape: T) => DocShape<T>;
295
- counter: () => CounterContainerShape;
304
+ counter: () => WithPlaceholder<CounterContainerShape>;
296
305
  list: <T extends ContainerOrValueShape>(shape: T) => ListContainerShape<T>;
297
306
  map: <T extends Record<string, ContainerOrValueShape>>(shape: T) => MapContainerShape<T>;
298
307
  record: <T extends ContainerOrValueShape>(shape: T) => RecordContainerShape<T>;
299
308
  movableList: <T extends ContainerOrValueShape>(shape: T) => MovableListContainerShape<T>;
300
- text: () => TextContainerShape;
309
+ text: () => WithPlaceholder<TextContainerShape>;
301
310
  tree: <T extends MapContainerShape>(shape: T) => TreeContainerShape;
302
311
  plain: {
303
- string: <T extends string = string>(...options: T[]) => StringValueShape<T>;
304
- number: () => NumberValueShape;
305
- boolean: () => BooleanValueShape;
312
+ string: <T extends string = string>(...options: T[]) => WithPlaceholder<StringValueShape<T>>;
313
+ number: () => WithPlaceholder<NumberValueShape>;
314
+ boolean: () => WithPlaceholder<BooleanValueShape>;
306
315
  null: () => NullValueShape;
307
316
  undefined: () => UndefinedValueShape;
308
317
  uint8Array: () => Uint8ArrayValueShape;
309
318
  object: <T extends Record<string, ValueShape>>(shape: T) => ObjectValueShape<T>;
310
319
  record: <T extends ValueShape>(shape: T) => RecordValueShape<T>;
311
320
  array: <T extends ValueShape>(shape: T) => ArrayValueShape<T>;
312
- union: <T extends ValueShape[]>(shapes: T) => UnionValueShape<T>;
321
+ union: <T extends ValueShape[]>(shapes: T) => WithPlaceholder<UnionValueShape<T>>;
313
322
  /**
314
323
  * Creates a discriminated union shape for type-safe tagged unions.
315
324
  *
@@ -336,11 +345,41 @@ declare const Shape: {
336
345
  * @param discriminantKey - The key used to discriminate between variants (e.g., "type")
337
346
  * @param variants - A record mapping discriminant values to their object shapes
338
347
  */
339
- discriminatedUnion: <K extends string, T extends Record<string, ObjectValueShape>>(discriminantKey: K, variants: T) => DiscriminatedUnionValueShape<K, T>;
348
+ discriminatedUnion: <K extends string, T extends Record<string, ObjectValueShape>>(discriminantKey: K, variants: T) => WithPlaceholder<DiscriminatedUnionValueShape<K, T>>;
340
349
  };
341
350
  };
342
351
  type ShapeToContainer<T extends DocShape | ContainerShape> = T extends TextContainerShape ? LoroText : T extends CounterContainerShape ? LoroCounter : T extends ListContainerShape ? LoroList : T extends MovableListContainerShape ? LoroMovableList : T extends MapContainerShape | RecordContainerShape ? LoroMap : T extends TreeContainerShape ? LoroTree : never;
343
352
 
353
+ /**
354
+ * Derives the placeholder state from a schema by composing placeholder values.
355
+ *
356
+ * For leaf nodes (text, counter, values): uses _placeholder directly
357
+ * For containers (map, list, record): recurses into nested shapes
358
+ */
359
+ declare function derivePlaceholder<T extends DocShape>(schema: T): InferPlaceholderType<T>;
360
+ /**
361
+ * Derives placeholder for a single shape.
362
+ *
363
+ * Leaf nodes: return _placeholder directly
364
+ * Containers: recurse into nested shapes (ignore _placeholder on containers)
365
+ */
366
+ declare function deriveShapePlaceholder(shape: ContainerOrValueShape): unknown;
367
+
368
+ /**
369
+ * Overlays CRDT state with placeholder defaults
370
+ */
371
+ declare function overlayPlaceholder<Shape extends DocShape>(shape: Shape, crdtValue: {
372
+ [key: string]: Value;
373
+ }, placeholderValue: {
374
+ [key: string]: Value;
375
+ }): {
376
+ [key: string]: Value;
377
+ };
378
+ /**
379
+ * Merges individual CRDT values with placeholder defaults
380
+ */
381
+ declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Shape, crdtValue: Value, placeholderValue: Value): Value;
382
+
344
383
  /** biome-ignore-all lint/suspicious/noExplicitAny: JSON Patch values can be any type */
345
384
 
346
385
  type JsonPatchAddOperation = {
@@ -379,20 +418,28 @@ type JsonPatch = JsonPatchOperation[];
379
418
 
380
419
  declare class TypedDoc<Shape extends DocShape> {
381
420
  private shape;
382
- private emptyState;
421
+ private placeholder;
383
422
  private doc;
384
423
  /**
385
- * Creates a new TypedDoc with the given schema and empty state.
424
+ * Creates a new TypedDoc with the given schema.
425
+ * Placeholder state is automatically derived from the schema's placeholder values.
386
426
  *
387
- * @param shape - The document schema
388
- * @param emptyState - Default values for the document. For dynamic containers
389
- * (list, record, etc.), only empty values ([] or {}) are allowed. Use
390
- * `.change()` to add initial data after construction.
427
+ * @param shape - The document schema (with optional .placeholder() values)
391
428
  * @param doc - Optional existing LoroDoc to wrap
392
429
  */
393
- constructor(shape: Shape, emptyState: InferEmptyStateType<Shape>, doc?: LoroDoc);
394
- get value(): InferPlainType<Shape>;
395
- change(fn: (draft: Draft<Shape>) => void): InferPlainType<Shape>;
430
+ constructor(shape: Shape, doc?: LoroDoc);
431
+ /**
432
+ * Returns a read-only, live view of the document.
433
+ * Accessing properties on this object will read directly from the underlying CRDT.
434
+ * This is efficient (O(1) per access) and always up-to-date.
435
+ */
436
+ get value(): DeepReadonly<Infer<Shape>>;
437
+ /**
438
+ * Returns the full plain JavaScript object representation of the document.
439
+ * This is an expensive O(N) operation that serializes the entire document.
440
+ */
441
+ toJSON(): Infer<Shape>;
442
+ change(fn: (draft: Draft<Shape>) => void): Infer<Shape>;
396
443
  /**
397
444
  * Apply JSON Patch operations to the document
398
445
  *
@@ -408,32 +455,17 @@ declare class TypedDoc<Shape extends DocShape> {
408
455
  * ])
409
456
  * ```
410
457
  */
411
- applyPatch(patch: JsonPatch, pathPrefix?: (string | number)[]): InferPlainType<Shape>;
458
+ applyPatch(patch: JsonPatch, pathPrefix?: (string | number)[]): Infer<Shape>;
412
459
  get loroDoc(): LoroDoc;
413
460
  get docShape(): Shape;
414
461
  get rawValue(): any;
415
462
  }
416
- declare function createTypedDoc<Shape extends DocShape>(shape: Shape, emptyState: InferEmptyStateType<Shape>, existingDoc?: LoroDoc): TypedDoc<Shape>;
417
-
418
- /**
419
- * Overlays CRDT state with empty state defaults
420
- */
421
- declare function overlayEmptyState<Shape extends DocShape>(shape: Shape, crdtValue: {
422
- [key: string]: Value;
423
- }, emptyValue: {
424
- [key: string]: Value;
425
- }): {
426
- [key: string]: Value;
427
- };
428
- /**
429
- * Merges individual CRDT values with empty state defaults
430
- */
431
- declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Shape, crdtValue: Value, emptyValue: Value): Value;
463
+ declare function createTypedDoc<Shape extends DocShape>(shape: Shape, existingDoc?: LoroDoc): TypedDoc<Shape>;
432
464
 
433
465
  /**
434
- * Validates empty state against schema structure without using Zod
435
- * Combines the functionality of createEmptyStateValidator and createValueValidator
466
+ * Validates placeholder against schema structure without using Zod
467
+ * Combines the functionality of createPlaceholderValidator and createValueValidator
436
468
  */
437
- declare function validateEmptyState<T extends DocShape>(emptyState: unknown, schema: T): InferPlainType<T>;
469
+ declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
438
470
 
439
- export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Draft, type Infer, type InferDraftType, type InferEmptyStateType, type InferPlainType, 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, createTypedDoc, mergeValue, overlayEmptyState, validateEmptyState };
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 };