@loro-extended/change 1.0.0 → 1.1.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
@@ -28,13 +28,13 @@ pnpm add @loro-extended/change loro-crdt
28
28
  ## Quick Start
29
29
 
30
30
  ```typescript
31
- import { createTypedDoc, Shape, batch, toJSON } from "@loro-extended/change";
31
+ import { createTypedDoc, Shape, change } from "@loro-extended/change";
32
32
 
33
33
  // Define your document schema
34
34
  const schema = Shape.doc({
35
35
  title: Shape.text().placeholder("My Todo List"),
36
36
  count: Shape.counter(),
37
- users: Shape.record(Shape.plain.object({
37
+ users: Shape.record(Shape.plain.struct({
38
38
  name: Shape.plain.string(),
39
39
  })),
40
40
  });
@@ -57,8 +57,8 @@ if ("alice" in doc.users) {
57
57
 
58
58
  // Batched mutations - commit together (optional, for performance)
59
59
  // Using functional helper (recommended)
60
- batch(doc, draft => {
61
- draft.title.insert(0, "Batch: ");
60
+ change(doc, draft => {
61
+ draft.title.insert(0, "Change: ");
62
62
  draft.count.increment(10);
63
63
  draft.users.set("bob", { name: "Bob" });
64
64
  });
@@ -66,7 +66,7 @@ batch(doc, draft => {
66
66
 
67
67
  // Get JSON snapshot using functional helper
68
68
  console.log(doc.toJSON());
69
- // { title: "Batch: 📝 Todo", count: 15, users: { alice: { name: "Alice" }, bob: { name: "Bob" } } }
69
+ // { title: "Change: 📝 Todo", count: 15, users: { alice: { name: "Alice" }, bob: { name: "Bob" } } }
70
70
  ```
71
71
 
72
72
  Note that this is even more useful in combination with `@loro-extended/react` (if your app uses React) and `@loro-extended/repo` for syncing between client/server or among peers.
@@ -88,8 +88,8 @@ const blogSchema = Shape.doc({
88
88
  // Lists for ordered data
89
89
  tags: Shape.list(Shape.plain.string()), // List of strings
90
90
 
91
- // Maps for structured data
92
- metadata: Shape.map({
91
+ // Structs for structured data with fixed keys
92
+ metadata: Shape.struct({
93
93
  author: Shape.plain.string(), // Plain values (POJOs)
94
94
  publishedAt: Shape.plain.string(), // ISO date string
95
95
  featured: Shape.plain.boolean(),
@@ -97,7 +97,7 @@ const blogSchema = Shape.doc({
97
97
 
98
98
  // Movable lists for reorderable content
99
99
  sections: Shape.movableList(
100
- Shape.map({
100
+ Shape.struct({
101
101
  heading: Shape.text(), // Collaborative headings
102
102
  content: Shape.text(), // Collaborative content
103
103
  order: Shape.plain.number(), // Plain metadata
@@ -106,7 +106,7 @@ const blogSchema = Shape.doc({
106
106
  });
107
107
  ```
108
108
 
109
- **NOTE:** Use `Shape.*` for collaborative containers and `Shape.plain.*` for plain values. Only put plain values inside Loro containers - a Loro container inside a plain JS object or array won't work.
109
+ **NOTE:** Use `Shape.*` for collaborative containers and `Shape.plain.*` for plain values. Only put plain values inside Loro containers - a Loro container inside a plain JS struct or array won't work.
110
110
 
111
111
  ### Empty State Overlay
112
112
 
@@ -118,13 +118,13 @@ const blogSchemaWithDefaults = Shape.doc({
118
118
  title: Shape.text().placeholder("Untitled Document"),
119
119
  viewCount: Shape.counter(), // defaults to 0
120
120
  tags: Shape.list(Shape.plain.string()), // defaults to []
121
- metadata: Shape.map({
121
+ metadata: Shape.struct({
122
122
  author: Shape.plain.string().placeholder("Anonymous"),
123
123
  publishedAt: Shape.plain.string(), // defaults to ""
124
124
  featured: Shape.plain.boolean(), // defaults to false
125
125
  }),
126
126
  sections: Shape.movableList(
127
- Shape.map({
127
+ Shape.struct({
128
128
  heading: Shape.text(),
129
129
  content: Shape.text(),
130
130
  order: Shape.plain.number(),
@@ -139,7 +139,7 @@ console.log(doc.toJSON());
139
139
  // { title: "Untitled Document", viewCount: 0, ... }
140
140
 
141
141
  // After changes, CRDT values take priority over empty state
142
- batch(doc, (draft) => {
142
+ change(doc, (draft) => {
143
143
  draft.title.insert(0, "My Blog Post");
144
144
  draft.viewCount.increment(10);
145
145
  });
@@ -160,12 +160,12 @@ doc.viewCount.increment(1);
160
160
  doc.tags.push("typescript");
161
161
  ```
162
162
 
163
- For batched operations (better performance, atomic undo), use `batch()`:
163
+ For batched operations (better performance, atomic undo), use `change()`:
164
164
 
165
165
  ```typescript
166
- import { batch, toJSON } from "@loro-extended/change";
166
+ import { change } from "@loro-extended/change";
167
167
 
168
- batch(doc, (draft) => {
168
+ change(doc, (draft) => {
169
169
  // Text operations
170
170
  draft.title.insert(0, "📝");
171
171
  draft.title.delete(5, 3);
@@ -179,7 +179,7 @@ batch(doc, (draft) => {
179
179
  draft.tags.insert(0, "loro");
180
180
  draft.tags.delete(1, 1);
181
181
 
182
- // Map operations (POJO values)
182
+ // Struct operations (POJO values)
183
183
  draft.metadata.set("author", "John Doe");
184
184
  draft.metadata.delete("featured");
185
185
 
@@ -193,21 +193,21 @@ batch(doc, (draft) => {
193
193
  });
194
194
 
195
195
  // All changes are committed atomically as one transaction
196
- // batch() returns the doc for chaining
196
+ // change() returns the doc for chaining
197
197
  console.log(doc.toJSON()); // Updated document state
198
198
  ```
199
199
 
200
- ### When to Use `batch()` vs Direct Mutations
200
+ ### When to Use `change()` vs Direct Mutations
201
201
 
202
202
  | Use Case | Approach |
203
203
  |----------|----------|
204
204
  | Single mutation | Direct: `doc.count.increment(1)` |
205
- | Multiple related mutations | Batched: `batch(doc, d => { ... })` |
206
- | Atomic undo/redo | Batched: `batch(doc, d => { ... })` |
207
- | Performance-critical bulk updates | Batched: `batch(doc, d => { ... })` |
205
+ | Multiple related mutations | Batched: `change(doc, d => { ... })` |
206
+ | Atomic undo/redo | Batched: `change(doc, d => { ... })` |
207
+ | Performance-critical bulk updates | Batched: `change(doc, d => { ... })` |
208
208
  | Simple reads + writes | Direct: `doc.users.set(...)` |
209
209
 
210
- > **Note:** The `$.change()` and `$.batch()` methods are available as an escape hatch, but the functional `batch()` helper is recommended for cleaner code.
210
+ > **Note:** The `$.change()` method is available as an escape hatch, but the functional `change()` helper is recommended for cleaner code.
211
211
 
212
212
  ## Advanced Usage
213
213
 
@@ -219,19 +219,19 @@ For type-safe tagged unions (like different message types or presence states), u
219
219
  import { Shape, mergeValue } from "@loro-extended/change";
220
220
 
221
221
  // Define variant shapes - each must have the discriminant key
222
- const ClientPresenceShape = Shape.plain.object({
222
+ const ClientPresenceShape = Shape.plain.struct({
223
223
  type: Shape.plain.string("client"), // Literal type for discrimination
224
224
  name: Shape.plain.string(),
225
- input: Shape.plain.object({
225
+ input: Shape.plain.struct({
226
226
  force: Shape.plain.number(),
227
227
  angle: Shape.plain.number(),
228
228
  }),
229
229
  });
230
230
 
231
- const ServerPresenceShape = Shape.plain.object({
231
+ const ServerPresenceShape = Shape.plain.struct({
232
232
  type: Shape.plain.string("server"), // Literal type for discrimination
233
233
  cars: Shape.plain.record(
234
- Shape.plain.object({
234
+ Shape.plain.struct({
235
235
  x: Shape.plain.number(),
236
236
  y: Shape.plain.number(),
237
237
  })
@@ -287,11 +287,11 @@ Handle complex nested documents with ease:
287
287
 
288
288
  ```typescript
289
289
  const complexSchema = Shape.doc({
290
- article: Shape.map({
290
+ article: Shape.struct({
291
291
  title: Shape.text(),
292
- metadata: Shape.map({
292
+ metadata: Shape.struct({
293
293
  views: Shape.counter(),
294
- author: Shape.map({
294
+ author: Shape.struct({
295
295
  name: Shape.plain.string(),
296
296
  email: Shape.plain.string(),
297
297
  }),
@@ -314,7 +314,7 @@ const emptyState = {
314
314
 
315
315
  const doc = createTypedDoc(complexSchema);
316
316
 
317
- batch(doc, (draft) => {
317
+ change(doc, (draft) => {
318
318
  draft.article.title.insert(0, "Deep Nesting Example");
319
319
  draft.article.metadata.views.increment(5);
320
320
  draft.article.metadata.author.name = "Alice"; // plain string update is captured and applied after closure
@@ -322,20 +322,20 @@ batch(doc, (draft) => {
322
322
  });
323
323
  ```
324
324
 
325
- ### Map Operations
325
+ ### Struct Operations
326
326
 
327
- For map containers, use the standard map methods:
327
+ For struct containers (fixed-key objects), use direct property access:
328
328
 
329
329
  ```typescript
330
330
  const schema = Shape.doc({
331
- settings: Shape.map({
331
+ settings: Shape.struct({
332
332
  theme: Shape.plain.string(),
333
333
  collapsed: Shape.plain.boolean(),
334
334
  width: Shape.plain.number(),
335
335
  }),
336
336
  });
337
337
 
338
- batch(doc, (draft) => {
338
+ change(doc, (draft) => {
339
339
  // Set individual values
340
340
  draft.settings.theme = "dark";
341
341
  draft.settings.collapsed = true;
@@ -350,11 +350,11 @@ Create lists containing CRDT containers for collaborative nested structures:
350
350
  ```typescript
351
351
  const collaborativeSchema = Shape.doc({
352
352
  articles: Shape.list(
353
- Shape.map({
353
+ Shape.struct({
354
354
  title: Shape.text(), // Collaborative title
355
355
  content: Shape.text(), // Collaborative content
356
356
  tags: Shape.list(Shape.plain.string()), // Collaborative tag list
357
- metadata: Shape.plain.object({
357
+ metadata: Shape.plain.struct({
358
358
  // Static metadata
359
359
  authorId: Shape.plain.string(),
360
360
  publishedAt: Shape.plain.string(),
@@ -363,7 +363,7 @@ const collaborativeSchema = Shape.doc({
363
363
  ),
364
364
  });
365
365
 
366
- batch(doc, (draft) => {
366
+ change(doc, (draft) => {
367
367
  // Push creates and configures nested containers automatically
368
368
  draft.articles.push({
369
369
  title: "Collaborative Article",
@@ -410,30 +410,28 @@ const doc = new TypedDoc(schema);
410
410
 
411
411
  These functional helpers provide a cleaner API and are the recommended way to work with TypedDoc:
412
412
 
413
- #### `batch(doc, mutator)`
413
+ #### `change(doc, mutator)`
414
414
 
415
415
  Batches multiple mutations into a single transaction. Returns the doc for chaining.
416
416
 
417
417
  ```typescript
418
- import { batch } from "@loro-extended/change";
418
+ import { change } from "@loro-extended/change";
419
419
 
420
- batch(doc, (draft) => {
420
+ change(doc, (draft) => {
421
421
  draft.title.insert(0, "Hello");
422
422
  draft.count.increment(5);
423
423
  });
424
424
 
425
- // Chainable - batch returns the doc
426
- batch(doc, d => d.count.increment(1)).count.increment(2);
425
+ // Chainable - change returns the doc
426
+ change(doc, d => d.count.increment(1)).count.increment(2);
427
427
  ```
428
428
 
429
- #### `toJSON(doc)`
429
+ #### `doc.toJSON()`
430
430
 
431
431
  Returns the full plain JavaScript object representation of the document.
432
432
 
433
433
  ```typescript
434
- import { toJSON } from "@loro-extended/change";
435
-
436
- const snapshot = toJSON(doc);
434
+ const snapshot = doc.toJSON();
437
435
  // { title: "Hello", count: 5, ... }
438
436
  ```
439
437
 
@@ -452,12 +450,12 @@ loroDoc.subscribe((event) => console.log("Changed:", event));
452
450
 
453
451
  The `$` namespace provides access to meta-operations. While functional helpers are recommended, the `$` namespace is available for advanced use cases:
454
452
 
455
- #### `doc.$.batch(mutator)`
453
+ #### `doc.$.change(mutator)`
456
454
 
457
- Same as `batch(doc, mutator)`.
455
+ Same as `change(doc, mutator)`.
458
456
 
459
457
  ```typescript
460
- doc.$.batch((draft) => {
458
+ doc.$.change((draft) => {
461
459
  // Make changes to draft - all commit together
462
460
  });
463
461
  ```
@@ -481,7 +479,7 @@ const schema = Shape.doc({
481
479
  - `Shape.counter()` - Collaborative increment/decrement counters
482
480
  - `Shape.list(itemSchema)` - Collaborative ordered lists
483
481
  - `Shape.movableList(itemSchema)` - Collaborative reorderable lists
484
- - `Shape.map(shape)` - Collaborative key-value maps with fixed keys
482
+ - `Shape.struct(shape)` - Collaborative structs with fixed keys (uses LoroMap internally)
485
483
  - `Shape.record(valueSchema)` - Collaborative key-value maps with dynamic string keys
486
484
  - `Shape.tree(shape)` - Collaborative hierarchical tree structures (Note: incomplete implementation)
487
485
 
@@ -493,12 +491,52 @@ const schema = Shape.doc({
493
491
  - `Shape.plain.null()` - Null values
494
492
  - `Shape.plain.undefined()` - Undefined values
495
493
  - `Shape.plain.uint8Array()` - Binary data values
496
- - `Shape.plain.object(shape)` - Object values with fixed keys
494
+ - `Shape.plain.struct(shape)` - Struct values with fixed keys
497
495
  - `Shape.plain.record(valueShape)` - Object values with dynamic string keys
498
496
  - `Shape.plain.array(itemShape)` - Array values
499
497
  - `Shape.plain.union(shapes)` - Union of value types (e.g., `string | null`)
500
498
  - `Shape.plain.discriminatedUnion(key, variants)` - Tagged union types with a discriminant key
501
499
 
500
+ #### Nullable Values
501
+
502
+ Use `.nullable()` on value types to create nullable fields with `null` as the default placeholder:
503
+
504
+ ```typescript
505
+ const schema = Shape.doc({
506
+ profile: Shape.struct({
507
+ name: Shape.plain.string().placeholder("Anonymous"),
508
+ email: Shape.plain.string().nullable(), // string | null, defaults to null
509
+ age: Shape.plain.number().nullable(), // number | null, defaults to null
510
+ verified: Shape.plain.boolean().nullable(), // boolean | null, defaults to null
511
+ tags: Shape.plain.array(Shape.plain.string()).nullable(), // string[] | null
512
+ metadata: Shape.plain.record(Shape.plain.string()).nullable(), // Record<string, string> | null
513
+ location: Shape.plain.struct({ // { lat: number, lng: number } | null
514
+ lat: Shape.plain.number(),
515
+ lng: Shape.plain.number(),
516
+ }).nullable(),
517
+ }),
518
+ });
519
+ ```
520
+
521
+ You can chain `.placeholder()` after `.nullable()` to customize the default value:
522
+
523
+ ```typescript
524
+ const schema = Shape.doc({
525
+ settings: Shape.struct({
526
+ // Nullable string with custom default
527
+ nickname: Shape.plain.string().nullable().placeholder("Guest"),
528
+ }),
529
+ });
530
+ ```
531
+
532
+ This is syntactic sugar for the more verbose union pattern:
533
+
534
+ ```typescript
535
+ // These are equivalent:
536
+ email: Shape.plain.string().nullable()
537
+ email: Shape.plain.union([Shape.plain.null(), Shape.plain.string()]).placeholder(null)
538
+ ```
539
+
502
540
  ### TypedDoc API
503
541
 
504
542
  With the proxy-based API, schema properties are accessed directly on the doc object, and meta-operations are accessed via the `$` namespace.
@@ -522,7 +560,7 @@ doc.users.has("alice"); // true
522
560
  "alice" in doc.users; // true
523
561
  ```
524
562
 
525
- For batched mutations, use `$.batch()` instead.
563
+ For batched mutations, use `$.change()` instead.
526
564
 
527
565
  #### `doc.$.toJSON()`
528
566
 
@@ -611,7 +649,7 @@ draft.todos.forEach((todo, index) => {
611
649
  **Important**: Methods like `find()` and `filter()` return **mutable draft objects** that you can modify directly:
612
650
 
613
651
  ```typescript
614
- batch(doc, (draft) => {
652
+ change(doc, (draft) => {
615
653
  // Find and mutate pattern - very common!
616
654
  const todo = draft.todos.find((t) => t.id === "123");
617
655
  if (todo) {
@@ -627,7 +665,7 @@ batch(doc, (draft) => {
627
665
  });
628
666
  ```
629
667
 
630
- This dual interface ensures predicates work with current data (including previous mutations in the same `batch()` block) while returned objects remain mutable.
668
+ This dual interface ensures predicates work with current data (including previous mutations in the same `change()` block) while returned objects remain mutable.
631
669
 
632
670
  ### Movable List Operations
633
671
 
@@ -690,7 +728,7 @@ interface TodoDoc {
690
728
  const todoSchema = Shape.doc({
691
729
  title: Shape.text(),
692
730
  todos: Shape.list(
693
- Shape.plain.object({
731
+ Shape.plain.struct({
694
732
  id: Shape.plain.string(),
695
733
  text: Shape.plain.string(),
696
734
  done: Shape.plain.boolean(),
@@ -699,10 +737,10 @@ const todoSchema = Shape.doc({
699
737
  });
700
738
 
701
739
  // TypeScript will ensure the schema produces the correct type
702
- const doc = new TypedDoc(todoSchema);
740
+ const doc = createTypedDoc(todoSchema);
703
741
 
704
742
  // Mutations are type-safe
705
- batch(doc, (draft) => {
743
+ change(doc, (draft) => {
706
744
  draft.title.insert(0, "Hello"); // ✅ Valid - TypeScript knows this is LoroText
707
745
  draft.todos.push({
708
746
  // ✅ Valid - TypeScript knows the expected shape
@@ -754,8 +792,8 @@ The `TypedPresence` class provides type-safe access to ephemeral presence data w
754
792
  import { TypedPresence, Shape } from "@loro-extended/change";
755
793
 
756
794
  // Define a presence schema with placeholders
757
- const PresenceSchema = Shape.plain.object({
758
- cursor: Shape.plain.object({
795
+ const PresenceSchema = Shape.plain.struct({
796
+ cursor: Shape.plain.struct({
759
797
  x: Shape.plain.number(),
760
798
  y: Shape.plain.number(),
761
799
  }),
@@ -806,7 +844,7 @@ This is typically provided by `UntypedDocHandle.presence` in `@loro-extended/rep
806
844
 
807
845
  ## Performance Considerations
808
846
 
809
- - All changes within a `batch()` call are batched into a single transaction
847
+ - All changes within a `change()` call are batched into a single transaction
810
848
  - Empty state overlay is computed on-demand, not stored
811
849
  - Container creation is lazy - containers are only created when accessed
812
850
  - Type validation occurs at development time, not runtime
package/dist/index.d.ts CHANGED
@@ -229,6 +229,13 @@ declare class TextRef extends TypedRef<TextContainerShape> {
229
229
  type WithPlaceholder<S extends Shape<any, any, any>> = S & {
230
230
  placeholder(value: S["_placeholder"]): S;
231
231
  };
232
+ /**
233
+ * Type for value shapes that support the .nullable() method.
234
+ * Returns a union of null and the original shape with null as the default placeholder.
235
+ */
236
+ type WithNullable<S extends ValueShape> = {
237
+ nullable(): WithPlaceholder<UnionValueShape<[NullValueShape, S]>>;
238
+ };
232
239
  interface DocShape<NestedShapes extends Record<string, ContainerShape> = Record<string, ContainerShape>> extends Shape<{
233
240
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
234
241
  }, {
@@ -404,9 +411,9 @@ declare const Shape: {
404
411
  text: () => WithPlaceholder<TextContainerShape>;
405
412
  tree: <T extends MapContainerShape | StructContainerShape>(shape: T) => TreeContainerShape;
406
413
  plain: {
407
- string: <T extends string = string>(...options: T[]) => WithPlaceholder<StringValueShape<T>>;
408
- number: () => WithPlaceholder<NumberValueShape>;
409
- boolean: () => WithPlaceholder<BooleanValueShape>;
414
+ string: <T extends string = string>(...options: T[]) => WithPlaceholder<StringValueShape<T>> & WithNullable<StringValueShape<T>>;
415
+ number: () => WithPlaceholder<NumberValueShape> & WithNullable<NumberValueShape>;
416
+ boolean: () => WithPlaceholder<BooleanValueShape> & WithNullable<BooleanValueShape>;
410
417
  null: () => NullValueShape;
411
418
  undefined: () => UndefinedValueShape;
412
419
  uint8Array: () => Uint8ArrayValueShape;
@@ -422,13 +429,13 @@ declare const Shape: {
422
429
  * })
423
430
  * ```
424
431
  */
425
- struct: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T>;
432
+ struct: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T> & WithNullable<StructValueShape<T>>;
426
433
  /**
427
434
  * @deprecated Use `Shape.plain.struct` instead. `Shape.plain.struct` will be removed in a future version.
428
435
  */
429
- object: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T>;
430
- record: <T extends ValueShape>(shape: T) => RecordValueShape<T>;
431
- array: <T extends ValueShape>(shape: T) => ArrayValueShape<T>;
436
+ object: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T> & WithNullable<StructValueShape<T>>;
437
+ record: <T extends ValueShape>(shape: T) => RecordValueShape<T> & WithNullable<RecordValueShape<T>>;
438
+ array: <T extends ValueShape>(shape: T) => ArrayValueShape<T> & WithNullable<ArrayValueShape<T>>;
432
439
  union: <T extends ValueShape[]>(shapes: T) => WithPlaceholder<UnionValueShape<T>>;
433
440
  /**
434
441
  * Creates a discriminated union shape for type-safe tagged unions.
@@ -816,4 +823,4 @@ declare class TypedPresence<S extends ContainerShape | ValueShape> {
816
823
  */
817
824
  declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
818
825
 
819
- export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Infer, 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 StructContainerShape, type StructValueShape, type TextContainerShape, type TreeContainerShape, type TypedDoc, TypedPresence, type UnionValueShape, type ValueShape, type WithPlaceholder, change, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, getLoroDoc, mergeValue, overlayPlaceholder, validatePlaceholder };
826
+ export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Infer, 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 StructContainerShape, type StructValueShape, type TextContainerShape, type TreeContainerShape, type TypedDoc, TypedPresence, type UnionValueShape, type ValueShape, type WithNullable, type WithPlaceholder, change, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, getLoroDoc, mergeValue, overlayPlaceholder, validatePlaceholder };
package/dist/index.js CHANGED
@@ -270,6 +270,29 @@ function createPlaceholderProxy(target) {
270
270
  }
271
271
 
272
272
  // src/shape.ts
273
+ function makeNullable(shape) {
274
+ const nullShape = {
275
+ _type: "value",
276
+ valueType: "null",
277
+ _plain: null,
278
+ _mutable: null,
279
+ _placeholder: null
280
+ };
281
+ const base = {
282
+ _type: "value",
283
+ valueType: "union",
284
+ shapes: [nullShape, shape],
285
+ _plain: null,
286
+ _mutable: null,
287
+ _placeholder: null
288
+ // Default placeholder is null
289
+ };
290
+ return Object.assign(base, {
291
+ placeholder(value) {
292
+ return { ...base, _placeholder: value };
293
+ }
294
+ });
295
+ }
273
296
  var Shape = {
274
297
  doc: (shape) => ({
275
298
  _type: "doc",
@@ -382,6 +405,9 @@ var Shape = {
382
405
  return Object.assign(base, {
383
406
  placeholder(value) {
384
407
  return { ...base, _placeholder: value };
408
+ },
409
+ nullable() {
410
+ return makeNullable(base);
385
411
  }
386
412
  });
387
413
  },
@@ -396,6 +422,9 @@ var Shape = {
396
422
  return Object.assign(base, {
397
423
  placeholder(value) {
398
424
  return { ...base, _placeholder: value };
425
+ },
426
+ nullable() {
427
+ return makeNullable(base);
399
428
  }
400
429
  });
401
430
  },
@@ -410,6 +439,9 @@ var Shape = {
410
439
  return Object.assign(base, {
411
440
  placeholder(value) {
412
441
  return { ...base, _placeholder: value };
442
+ },
443
+ nullable() {
444
+ return makeNullable(base);
413
445
  }
414
446
  });
415
447
  },
@@ -446,41 +478,69 @@ var Shape = {
446
478
  * })
447
479
  * ```
448
480
  */
449
- struct: (shape) => ({
450
- _type: "value",
451
- valueType: "struct",
452
- shape,
453
- _plain: {},
454
- _mutable: {},
455
- _placeholder: {}
456
- }),
481
+ struct: (shape) => {
482
+ const base = {
483
+ _type: "value",
484
+ valueType: "struct",
485
+ shape,
486
+ _plain: {},
487
+ _mutable: {},
488
+ _placeholder: {}
489
+ };
490
+ return Object.assign(base, {
491
+ nullable() {
492
+ return makeNullable(base);
493
+ }
494
+ });
495
+ },
457
496
  /**
458
497
  * @deprecated Use `Shape.plain.struct` instead. `Shape.plain.struct` will be removed in a future version.
459
498
  */
460
- object: (shape) => ({
461
- _type: "value",
462
- valueType: "struct",
463
- shape,
464
- _plain: {},
465
- _mutable: {},
466
- _placeholder: {}
467
- }),
468
- record: (shape) => ({
469
- _type: "value",
470
- valueType: "record",
471
- shape,
472
- _plain: {},
473
- _mutable: {},
474
- _placeholder: {}
475
- }),
476
- array: (shape) => ({
477
- _type: "value",
478
- valueType: "array",
479
- shape,
480
- _plain: [],
481
- _mutable: [],
482
- _placeholder: []
483
- }),
499
+ object: (shape) => {
500
+ const base = {
501
+ _type: "value",
502
+ valueType: "struct",
503
+ shape,
504
+ _plain: {},
505
+ _mutable: {},
506
+ _placeholder: {}
507
+ };
508
+ return Object.assign(base, {
509
+ nullable() {
510
+ return makeNullable(base);
511
+ }
512
+ });
513
+ },
514
+ record: (shape) => {
515
+ const base = {
516
+ _type: "value",
517
+ valueType: "record",
518
+ shape,
519
+ _plain: {},
520
+ _mutable: {},
521
+ _placeholder: {}
522
+ };
523
+ return Object.assign(base, {
524
+ nullable() {
525
+ return makeNullable(base);
526
+ }
527
+ });
528
+ },
529
+ array: (shape) => {
530
+ const base = {
531
+ _type: "value",
532
+ valueType: "array",
533
+ shape,
534
+ _plain: [],
535
+ _mutable: [],
536
+ _placeholder: []
537
+ };
538
+ return Object.assign(base, {
539
+ nullable() {
540
+ return makeNullable(base);
541
+ }
542
+ });
543
+ },
484
544
  // Special value type that helps make things like `string | null` representable
485
545
  // TODO(duane): should this be a more general type for containers too?
486
546
  union: (shapes) => {