@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 +98 -60
- package/dist/index.d.ts +15 -8
- package/dist/index.js +92 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +251 -0
- package/src/index.ts +2 -0
- package/src/shape.ts +136 -35
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,
|
|
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.
|
|
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
|
-
|
|
61
|
-
draft.title.insert(0, "
|
|
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: "
|
|
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
|
-
//
|
|
92
|
-
metadata: Shape.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 `
|
|
163
|
+
For batched operations (better performance, atomic undo), use `change()`:
|
|
164
164
|
|
|
165
165
|
```typescript
|
|
166
|
-
import {
|
|
166
|
+
import { change } from "@loro-extended/change";
|
|
167
167
|
|
|
168
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
196
|
+
// change() returns the doc for chaining
|
|
197
197
|
console.log(doc.toJSON()); // Updated document state
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
-
### When to Use `
|
|
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: `
|
|
206
|
-
| Atomic undo/redo | Batched: `
|
|
207
|
-
| Performance-critical bulk updates | Batched: `
|
|
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()`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
290
|
+
article: Shape.struct({
|
|
291
291
|
title: Shape.text(),
|
|
292
|
-
metadata: Shape.
|
|
292
|
+
metadata: Shape.struct({
|
|
293
293
|
views: Shape.counter(),
|
|
294
|
-
author: Shape.
|
|
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
|
-
|
|
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
|
-
###
|
|
325
|
+
### Struct Operations
|
|
326
326
|
|
|
327
|
-
For
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
#### `
|
|
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 {
|
|
418
|
+
import { change } from "@loro-extended/change";
|
|
419
419
|
|
|
420
|
-
|
|
420
|
+
change(doc, (draft) => {
|
|
421
421
|
draft.title.insert(0, "Hello");
|
|
422
422
|
draft.count.increment(5);
|
|
423
423
|
});
|
|
424
424
|
|
|
425
|
-
// Chainable -
|
|
426
|
-
|
|
425
|
+
// Chainable - change returns the doc
|
|
426
|
+
change(doc, d => d.count.increment(1)).count.increment(2);
|
|
427
427
|
```
|
|
428
428
|
|
|
429
|
-
#### `toJSON(
|
|
429
|
+
#### `doc.toJSON()`
|
|
430
430
|
|
|
431
431
|
Returns the full plain JavaScript object representation of the document.
|
|
432
432
|
|
|
433
433
|
```typescript
|
|
434
|
-
|
|
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.$.
|
|
453
|
+
#### `doc.$.change(mutator)`
|
|
456
454
|
|
|
457
|
-
Same as `
|
|
455
|
+
Same as `change(doc, mutator)`.
|
|
458
456
|
|
|
459
457
|
```typescript
|
|
460
|
-
doc.$.
|
|
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.
|
|
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.
|
|
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 `$.
|
|
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
|
-
|
|
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 `
|
|
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.
|
|
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 =
|
|
740
|
+
const doc = createTypedDoc(todoSchema);
|
|
703
741
|
|
|
704
742
|
// Mutations are type-safe
|
|
705
|
-
|
|
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.
|
|
758
|
-
cursor: Shape.plain.
|
|
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 `
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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) => {
|