@loro-extended/change 0.4.0 → 0.6.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 +91 -15
- package/dist/index.d.ts +93 -23
- package/dist/index.js +128 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +38 -6
- package/src/change.ts +14 -5
- package/src/discriminated-union.test.ts +169 -0
- package/src/draft-nodes/map.ts +3 -2
- package/src/index.ts +5 -0
- package/src/overlay.ts +65 -1
- package/src/record.test.ts +2 -1
- package/src/shape.ts +141 -22
- package/src/string-literal.test.ts +42 -0
- package/src/types.ts +12 -2
- package/src/validation.ts +9 -1
package/README.md
CHANGED
|
@@ -176,6 +176,76 @@ console.log(result); // Updated document state
|
|
|
176
176
|
|
|
177
177
|
## Advanced Usage
|
|
178
178
|
|
|
179
|
+
### Discriminated Unions
|
|
180
|
+
|
|
181
|
+
For type-safe tagged unions (like different message types or presence states), use `Shape.plain.discriminatedUnion()`:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { Shape, mergeValue } from "@loro-extended/change";
|
|
185
|
+
|
|
186
|
+
// Define variant shapes - each must have the discriminant key
|
|
187
|
+
const ClientPresenceShape = Shape.plain.object({
|
|
188
|
+
type: Shape.plain.string("client"), // Literal type for discrimination
|
|
189
|
+
name: Shape.plain.string(),
|
|
190
|
+
input: Shape.plain.object({
|
|
191
|
+
force: Shape.plain.number(),
|
|
192
|
+
angle: Shape.plain.number(),
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const ServerPresenceShape = Shape.plain.object({
|
|
197
|
+
type: Shape.plain.string("server"), // Literal type for discrimination
|
|
198
|
+
cars: Shape.plain.record(
|
|
199
|
+
Shape.plain.object({
|
|
200
|
+
x: Shape.plain.number(),
|
|
201
|
+
y: Shape.plain.number(),
|
|
202
|
+
})
|
|
203
|
+
),
|
|
204
|
+
tick: Shape.plain.number(),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Create the discriminated union
|
|
208
|
+
const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
209
|
+
client: ClientPresenceShape,
|
|
210
|
+
server: ServerPresenceShape,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Empty states for each variant
|
|
214
|
+
const EmptyClientPresence = {
|
|
215
|
+
type: "client" as const,
|
|
216
|
+
name: "",
|
|
217
|
+
input: { force: 0, angle: 0 },
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const EmptyServerPresence = {
|
|
221
|
+
type: "server" as const,
|
|
222
|
+
cars: {},
|
|
223
|
+
tick: 0,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Use with mergeValue for presence data
|
|
227
|
+
const crdtValue = { type: "client", name: "Alice" };
|
|
228
|
+
const result = mergeValue(GamePresenceSchema, crdtValue, EmptyClientPresence);
|
|
229
|
+
// Result: { type: "client", name: "Alice", input: { force: 0, angle: 0 } }
|
|
230
|
+
|
|
231
|
+
// Type-safe filtering
|
|
232
|
+
function handlePresence(presence: typeof result) {
|
|
233
|
+
if (presence.type === "server") {
|
|
234
|
+
// TypeScript knows this is ServerPresence
|
|
235
|
+
console.log(presence.cars, presence.tick);
|
|
236
|
+
} else {
|
|
237
|
+
// TypeScript knows this is ClientPresence
|
|
238
|
+
console.log(presence.name, presence.input);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Key features:**
|
|
244
|
+
- The discriminant key (e.g., `"type"`) determines which variant shape to use
|
|
245
|
+
- Missing fields are filled from the empty state of the matching variant
|
|
246
|
+
- Works seamlessly with `@loro-extended/react`'s `usePresence` hook
|
|
247
|
+
- Full TypeScript support for discriminated union types
|
|
248
|
+
|
|
179
249
|
### Nested Structures
|
|
180
250
|
|
|
181
251
|
Handle complex nested documents with ease:
|
|
@@ -318,18 +388,24 @@ const schema = Shape.doc({
|
|
|
318
388
|
- `Shape.text()` - Collaborative text editing
|
|
319
389
|
- `Shape.counter()` - Collaborative increment/decrement counters
|
|
320
390
|
- `Shape.list(itemSchema)` - Collaborative ordered lists
|
|
321
|
-
- `Shape.movableList(itemSchema)` - Collaborative
|
|
322
|
-
- `Shape.map(shape)` - Collaborative key-value maps
|
|
391
|
+
- `Shape.movableList(itemSchema)` - Collaborative reorderable lists
|
|
392
|
+
- `Shape.map(shape)` - Collaborative key-value maps with fixed keys
|
|
393
|
+
- `Shape.record(valueSchema)` - Collaborative key-value maps with dynamic string keys
|
|
323
394
|
- `Shape.tree(shape)` - Collaborative hierarchical tree structures (Note: incomplete implementation)
|
|
324
395
|
|
|
325
396
|
#### Value Types
|
|
326
397
|
|
|
327
|
-
- `Shape.plain.string()` - String values
|
|
398
|
+
- `Shape.plain.string()` - String values (optionally with literal union types)
|
|
328
399
|
- `Shape.plain.number()` - Number values
|
|
329
400
|
- `Shape.plain.boolean()` - Boolean values
|
|
330
401
|
- `Shape.plain.null()` - Null values
|
|
331
|
-
- `Shape.plain.
|
|
402
|
+
- `Shape.plain.undefined()` - Undefined values
|
|
403
|
+
- `Shape.plain.uint8Array()` - Binary data values
|
|
404
|
+
- `Shape.plain.object(shape)` - Object values with fixed keys
|
|
405
|
+
- `Shape.plain.record(valueShape)` - Object values with dynamic string keys
|
|
332
406
|
- `Shape.plain.array(itemShape)` - Array values
|
|
407
|
+
- `Shape.plain.union(shapes)` - Union of value types (e.g., `string | null`)
|
|
408
|
+
- `Shape.plain.discriminatedUnion(key, variants)` - Tagged union types with a discriminant key
|
|
333
409
|
|
|
334
410
|
### TypedDoc Methods
|
|
335
411
|
|
|
@@ -404,20 +480,20 @@ Lists support familiar JavaScript array methods for filtering and finding items:
|
|
|
404
480
|
|
|
405
481
|
```typescript
|
|
406
482
|
// Find items (returns mutable draft objects)
|
|
407
|
-
const foundItem = draft.todos.find(todo => todo.completed);
|
|
408
|
-
const foundIndex = draft.todos.findIndex(todo => todo.id === "123");
|
|
483
|
+
const foundItem = draft.todos.find((todo) => todo.completed);
|
|
484
|
+
const foundIndex = draft.todos.findIndex((todo) => todo.id === "123");
|
|
409
485
|
|
|
410
486
|
// Filter items (returns array of mutable draft objects)
|
|
411
|
-
const completedTodos = draft.todos.filter(todo => todo.completed);
|
|
412
|
-
const activeTodos = draft.todos.filter(todo => !todo.completed);
|
|
487
|
+
const completedTodos = draft.todos.filter((todo) => todo.completed);
|
|
488
|
+
const activeTodos = draft.todos.filter((todo) => !todo.completed);
|
|
413
489
|
|
|
414
490
|
// Transform items (returns plain array, not mutable)
|
|
415
|
-
const todoTexts = draft.todos.map(todo => todo.text);
|
|
416
|
-
const todoIds = draft.todos.map(todo => todo.id);
|
|
491
|
+
const todoTexts = draft.todos.map((todo) => todo.text);
|
|
492
|
+
const todoIds = draft.todos.map((todo) => todo.id);
|
|
417
493
|
|
|
418
494
|
// Check conditions
|
|
419
|
-
const hasCompleted = draft.todos.some(todo => todo.completed);
|
|
420
|
-
const allCompleted = draft.todos.every(todo => todo.completed);
|
|
495
|
+
const hasCompleted = draft.todos.some((todo) => todo.completed);
|
|
496
|
+
const allCompleted = draft.todos.every((todo) => todo.completed);
|
|
421
497
|
|
|
422
498
|
// Iterate over items
|
|
423
499
|
draft.todos.forEach((todo, index) => {
|
|
@@ -430,15 +506,15 @@ draft.todos.forEach((todo, index) => {
|
|
|
430
506
|
```typescript
|
|
431
507
|
doc.change((draft) => {
|
|
432
508
|
// Find and mutate pattern - very common!
|
|
433
|
-
const todo = draft.todos.find(t => t.id === "123");
|
|
509
|
+
const todo = draft.todos.find((t) => t.id === "123");
|
|
434
510
|
if (todo) {
|
|
435
511
|
todo.completed = true; // ✅ This mutation will persist!
|
|
436
512
|
todo.text = "Updated text"; // ✅ This too!
|
|
437
513
|
}
|
|
438
514
|
|
|
439
515
|
// Filter and modify multiple items
|
|
440
|
-
const activeTodos = draft.todos.filter(t => !t.completed);
|
|
441
|
-
activeTodos.forEach(todo => {
|
|
516
|
+
const activeTodos = draft.todos.filter((t) => !t.completed);
|
|
517
|
+
activeTodos.forEach((todo) => {
|
|
442
518
|
todo.priority = "high"; // ✅ All mutations persist!
|
|
443
519
|
});
|
|
444
520
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { LoroList, LoroMovableList, Container, LoroMap, Value, LoroText, LoroCounter, LoroTree, LoroDoc } from 'loro-crdt';
|
|
2
2
|
|
|
3
|
-
type InferPlainType<T> = T extends Shape<infer P, any> ? P : never;
|
|
4
|
-
type InferDraftType<T> = T extends Shape<any, infer D> ? D : never;
|
|
3
|
+
type InferPlainType<T> = T extends Shape<infer P, any, any> ? P : never;
|
|
4
|
+
type InferDraftType<T> = T extends Shape<any, infer D, any> ? D : never;
|
|
5
|
+
/**
|
|
6
|
+
* Extracts the valid empty state type from a shape.
|
|
7
|
+
*
|
|
8
|
+
* For dynamic containers (list, record, etc.), this will be constrained to
|
|
9
|
+
* empty values ([] or {}) to prevent users from expecting per-entry merging.
|
|
10
|
+
*/
|
|
11
|
+
type InferEmptyStateType<T> = T extends Shape<any, any, infer E> ? E : never;
|
|
5
12
|
type Draft<T extends DocShape<Record<string, ContainerShape>>> = InferDraftType<T>;
|
|
6
13
|
|
|
7
14
|
type DraftNodeParams<Shape extends DocShape | ContainerShape> = {
|
|
@@ -128,25 +135,27 @@ interface DocShape<NestedShapes extends Record<string, ContainerShape> = Record<
|
|
|
128
135
|
[K in keyof NestedShapes]: NestedShapes[K]["_plain"];
|
|
129
136
|
}, {
|
|
130
137
|
[K in keyof NestedShapes]: NestedShapes[K]["_draft"];
|
|
138
|
+
}, {
|
|
139
|
+
[K in keyof NestedShapes]: NestedShapes[K]["_emptyState"];
|
|
131
140
|
}> {
|
|
132
141
|
readonly _type: "doc";
|
|
133
142
|
readonly shapes: NestedShapes;
|
|
134
143
|
}
|
|
135
|
-
interface TextContainerShape extends Shape<string, TextDraftNode> {
|
|
144
|
+
interface TextContainerShape extends Shape<string, TextDraftNode, string> {
|
|
136
145
|
readonly _type: "text";
|
|
137
146
|
}
|
|
138
|
-
interface CounterContainerShape extends Shape<number, CounterDraftNode> {
|
|
147
|
+
interface CounterContainerShape extends Shape<number, CounterDraftNode, number> {
|
|
139
148
|
readonly _type: "counter";
|
|
140
149
|
}
|
|
141
|
-
interface TreeContainerShape<NestedShape = ContainerOrValueShape> extends Shape<any, any> {
|
|
150
|
+
interface TreeContainerShape<NestedShape = ContainerOrValueShape> extends Shape<any, any, never[]> {
|
|
142
151
|
readonly _type: "tree";
|
|
143
152
|
readonly shape: NestedShape;
|
|
144
153
|
}
|
|
145
|
-
interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape
|
|
154
|
+
interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape>, never[]> {
|
|
146
155
|
readonly _type: "list";
|
|
147
156
|
readonly shape: NestedShape;
|
|
148
157
|
}
|
|
149
|
-
interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListDraftNode<NestedShape
|
|
158
|
+
interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListDraftNode<NestedShape>, never[]> {
|
|
150
159
|
readonly _type: "movableList";
|
|
151
160
|
readonly shape: NestedShape;
|
|
152
161
|
}
|
|
@@ -154,37 +163,40 @@ interface MapContainerShape<NestedShapes extends Record<string, ContainerOrValue
|
|
|
154
163
|
[K in keyof NestedShapes]: NestedShapes[K]["_plain"];
|
|
155
164
|
}, MapDraftNode<NestedShapes> & {
|
|
156
165
|
[K in keyof NestedShapes]: NestedShapes[K]["_draft"];
|
|
166
|
+
}, {
|
|
167
|
+
[K in keyof NestedShapes]: NestedShapes[K]["_emptyState"];
|
|
157
168
|
}> {
|
|
158
169
|
readonly _type: "map";
|
|
159
170
|
readonly shapes: NestedShapes;
|
|
160
171
|
}
|
|
161
|
-
interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordDraftNode<NestedShape>> {
|
|
172
|
+
interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordDraftNode<NestedShape>, Record<string, never>> {
|
|
162
173
|
readonly _type: "record";
|
|
163
174
|
readonly shape: NestedShape;
|
|
164
175
|
}
|
|
165
176
|
type ContainerShape = CounterContainerShape | ListContainerShape | MapContainerShape | MovableListContainerShape | RecordContainerShape | TextContainerShape | TreeContainerShape;
|
|
166
177
|
type ContainerType = ContainerShape["_type"];
|
|
167
|
-
interface StringValueShape extends Shape<
|
|
178
|
+
interface StringValueShape<T extends string = string> extends Shape<T, T, T> {
|
|
168
179
|
readonly _type: "value";
|
|
169
180
|
readonly valueType: "string";
|
|
181
|
+
readonly options?: T[];
|
|
170
182
|
}
|
|
171
|
-
interface NumberValueShape extends Shape<number, number> {
|
|
183
|
+
interface NumberValueShape extends Shape<number, number, number> {
|
|
172
184
|
readonly _type: "value";
|
|
173
185
|
readonly valueType: "number";
|
|
174
186
|
}
|
|
175
|
-
interface BooleanValueShape extends Shape<boolean, boolean> {
|
|
187
|
+
interface BooleanValueShape extends Shape<boolean, boolean, boolean> {
|
|
176
188
|
readonly _type: "value";
|
|
177
189
|
readonly valueType: "boolean";
|
|
178
190
|
}
|
|
179
|
-
interface NullValueShape extends Shape<null, null> {
|
|
191
|
+
interface NullValueShape extends Shape<null, null, null> {
|
|
180
192
|
readonly _type: "value";
|
|
181
193
|
readonly valueType: "null";
|
|
182
194
|
}
|
|
183
|
-
interface UndefinedValueShape extends Shape<undefined, undefined> {
|
|
195
|
+
interface UndefinedValueShape extends Shape<undefined, undefined, undefined> {
|
|
184
196
|
readonly _type: "value";
|
|
185
197
|
readonly valueType: "undefined";
|
|
186
198
|
}
|
|
187
|
-
interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array> {
|
|
199
|
+
interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array, Uint8Array> {
|
|
188
200
|
readonly _type: "value";
|
|
189
201
|
readonly valueType: "uint8array";
|
|
190
202
|
}
|
|
@@ -192,32 +204,54 @@ interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string,
|
|
|
192
204
|
[K in keyof T]: T[K]["_plain"];
|
|
193
205
|
}, {
|
|
194
206
|
[K in keyof T]: T[K]["_draft"];
|
|
207
|
+
}, {
|
|
208
|
+
[K in keyof T]: T[K]["_emptyState"];
|
|
195
209
|
}> {
|
|
196
210
|
readonly _type: "value";
|
|
197
211
|
readonly valueType: "object";
|
|
198
212
|
readonly shape: T;
|
|
199
213
|
}
|
|
200
|
-
interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_draft"]>> {
|
|
214
|
+
interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_draft"]>, Record<string, never>> {
|
|
201
215
|
readonly _type: "value";
|
|
202
216
|
readonly valueType: "record";
|
|
203
217
|
readonly shape: T;
|
|
204
218
|
}
|
|
205
|
-
interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_draft"][]> {
|
|
219
|
+
interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_draft"][], never[]> {
|
|
206
220
|
readonly _type: "value";
|
|
207
221
|
readonly valueType: "array";
|
|
208
222
|
readonly shape: T;
|
|
209
223
|
}
|
|
210
|
-
interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"]> {
|
|
224
|
+
interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"], T[number]["_emptyState"]> {
|
|
211
225
|
readonly _type: "value";
|
|
212
226
|
readonly valueType: "union";
|
|
213
227
|
readonly shapes: T;
|
|
214
228
|
}
|
|
215
|
-
|
|
229
|
+
/**
|
|
230
|
+
* A discriminated union shape that uses a discriminant key to determine which variant to use.
|
|
231
|
+
* This enables type-safe handling of tagged unions like:
|
|
232
|
+
*
|
|
233
|
+
* ```typescript
|
|
234
|
+
* type GamePresence =
|
|
235
|
+
* | { type: "client"; name: string; input: { force: number; angle: number } }
|
|
236
|
+
* | { type: "server"; cars: Record<string, CarState>; tick: number }
|
|
237
|
+
* ```
|
|
238
|
+
*
|
|
239
|
+
* @typeParam K - The discriminant key (e.g., "type")
|
|
240
|
+
* @typeParam T - A record mapping discriminant values to their object shapes
|
|
241
|
+
*/
|
|
242
|
+
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"]> {
|
|
243
|
+
readonly _type: "value";
|
|
244
|
+
readonly valueType: "discriminatedUnion";
|
|
245
|
+
readonly discriminantKey: K;
|
|
246
|
+
readonly variants: T;
|
|
247
|
+
}
|
|
248
|
+
type ValueShape = StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | ObjectValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape;
|
|
216
249
|
type ContainerOrValueShape = ContainerShape | ValueShape;
|
|
217
|
-
interface Shape<Plain, Draft> {
|
|
250
|
+
interface Shape<Plain, Draft, EmptyState = Plain> {
|
|
218
251
|
readonly _type: string;
|
|
219
252
|
readonly _plain: Plain;
|
|
220
253
|
readonly _draft: Draft;
|
|
254
|
+
readonly _emptyState: EmptyState;
|
|
221
255
|
}
|
|
222
256
|
/**
|
|
223
257
|
* The LoroShape factory object
|
|
@@ -236,7 +270,7 @@ declare const Shape: {
|
|
|
236
270
|
text: () => TextContainerShape;
|
|
237
271
|
tree: <T extends MapContainerShape>(shape: T) => TreeContainerShape;
|
|
238
272
|
plain: {
|
|
239
|
-
string: () => StringValueShape
|
|
273
|
+
string: <T extends string = string>(...options: T[]) => StringValueShape<T>;
|
|
240
274
|
number: () => NumberValueShape;
|
|
241
275
|
boolean: () => BooleanValueShape;
|
|
242
276
|
null: () => NullValueShape;
|
|
@@ -246,6 +280,33 @@ declare const Shape: {
|
|
|
246
280
|
record: <T extends ValueShape>(shape: T) => RecordValueShape<T>;
|
|
247
281
|
array: <T extends ValueShape>(shape: T) => ArrayValueShape<T>;
|
|
248
282
|
union: <T extends ValueShape[]>(shapes: T) => UnionValueShape<T>;
|
|
283
|
+
/**
|
|
284
|
+
* Creates a discriminated union shape for type-safe tagged unions.
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* const ClientPresenceShape = Shape.plain.object({
|
|
289
|
+
* type: Shape.plain.string("client"),
|
|
290
|
+
* name: Shape.plain.string(),
|
|
291
|
+
* input: Shape.plain.object({ force: Shape.plain.number(), angle: Shape.plain.number() }),
|
|
292
|
+
* })
|
|
293
|
+
*
|
|
294
|
+
* const ServerPresenceShape = Shape.plain.object({
|
|
295
|
+
* type: Shape.plain.string("server"),
|
|
296
|
+
* cars: Shape.plain.record(Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() })),
|
|
297
|
+
* tick: Shape.plain.number(),
|
|
298
|
+
* })
|
|
299
|
+
*
|
|
300
|
+
* const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
301
|
+
* client: ClientPresenceShape,
|
|
302
|
+
* server: ServerPresenceShape,
|
|
303
|
+
* })
|
|
304
|
+
* ```
|
|
305
|
+
*
|
|
306
|
+
* @param discriminantKey - The key used to discriminate between variants (e.g., "type")
|
|
307
|
+
* @param variants - A record mapping discriminant values to their object shapes
|
|
308
|
+
*/
|
|
309
|
+
discriminatedUnion: <K extends string, T extends Record<string, ObjectValueShape>>(discriminantKey: K, variants: T) => DiscriminatedUnionValueShape<K, T>;
|
|
249
310
|
};
|
|
250
311
|
};
|
|
251
312
|
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;
|
|
@@ -290,7 +351,16 @@ declare class TypedDoc<Shape extends DocShape> {
|
|
|
290
351
|
private shape;
|
|
291
352
|
private emptyState;
|
|
292
353
|
private doc;
|
|
293
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Creates a new TypedDoc with the given schema and empty state.
|
|
356
|
+
*
|
|
357
|
+
* @param shape - The document schema
|
|
358
|
+
* @param emptyState - Default values for the document. For dynamic containers
|
|
359
|
+
* (list, record, etc.), only empty values ([] or {}) are allowed. Use
|
|
360
|
+
* `.change()` to add initial data after construction.
|
|
361
|
+
* @param doc - Optional existing LoroDoc to wrap
|
|
362
|
+
*/
|
|
363
|
+
constructor(shape: Shape, emptyState: InferEmptyStateType<Shape>, doc?: LoroDoc);
|
|
294
364
|
get value(): InferPlainType<Shape>;
|
|
295
365
|
change(fn: (draft: Draft<Shape>) => void): InferPlainType<Shape>;
|
|
296
366
|
/**
|
|
@@ -313,7 +383,7 @@ declare class TypedDoc<Shape extends DocShape> {
|
|
|
313
383
|
get docShape(): Shape;
|
|
314
384
|
get rawValue(): any;
|
|
315
385
|
}
|
|
316
|
-
declare function createTypedDoc<Shape extends DocShape>(shape: Shape, emptyState:
|
|
386
|
+
declare function createTypedDoc<Shape extends DocShape>(shape: Shape, emptyState: InferEmptyStateType<Shape>, existingDoc?: LoroDoc): TypedDoc<Shape>;
|
|
317
387
|
|
|
318
388
|
/**
|
|
319
389
|
* Overlays CRDT state with empty state defaults
|
|
@@ -336,4 +406,4 @@ declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Sh
|
|
|
336
406
|
*/
|
|
337
407
|
declare function validateEmptyState<T extends DocShape>(emptyState: unknown, schema: T): InferPlainType<T>;
|
|
338
408
|
|
|
339
|
-
export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DocShape, type Draft, type InferDraftType, type InferPlainType, type ListContainerShape, type MapContainerShape, type MovableListContainerShape, type RecordContainerShape, type RecordValueShape, type ContainerType as RootContainerType, Shape, type TextContainerShape, type TreeContainerShape, TypedDoc, type ValueShape, createTypedDoc, mergeValue, overlayEmptyState, validateEmptyState };
|
|
409
|
+
export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Draft, 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 };
|
package/dist/index.js
CHANGED
|
@@ -487,13 +487,13 @@ var MapDraftNode = class extends DraftNode {
|
|
|
487
487
|
node = containerValue;
|
|
488
488
|
} else {
|
|
489
489
|
const emptyState = this.emptyState?.[key];
|
|
490
|
-
if (
|
|
490
|
+
if (emptyState === void 0) {
|
|
491
491
|
throw new Error("empty state required");
|
|
492
492
|
}
|
|
493
493
|
node = emptyState;
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
|
-
if (
|
|
496
|
+
if (node === void 0) throw new Error("no container made");
|
|
497
497
|
this.propertyCache.set(key, node);
|
|
498
498
|
}
|
|
499
499
|
return node;
|
|
@@ -505,6 +505,7 @@ var MapDraftNode = class extends DraftNode {
|
|
|
505
505
|
get: () => this.getOrCreateNode(key, shape),
|
|
506
506
|
set: isValueShape(shape) ? (value) => {
|
|
507
507
|
this.container.set(key, value);
|
|
508
|
+
this.propertyCache.set(key, value);
|
|
508
509
|
} : void 0
|
|
509
510
|
});
|
|
510
511
|
}
|
|
@@ -1091,9 +1092,43 @@ function mergeValue(shape, crdtValue, emptyValue) {
|
|
|
1091
1092
|
case "tree":
|
|
1092
1093
|
return crdtValue ?? emptyValue ?? [];
|
|
1093
1094
|
default:
|
|
1095
|
+
if (shape._type === "value" && shape.valueType === "object") {
|
|
1096
|
+
const crdtObj = crdtValue ?? {};
|
|
1097
|
+
const emptyObj = emptyValue ?? {};
|
|
1098
|
+
const result = { ...emptyObj };
|
|
1099
|
+
if (typeof crdtObj !== "object" || crdtObj === null) {
|
|
1100
|
+
return crdtValue ?? emptyValue;
|
|
1101
|
+
}
|
|
1102
|
+
for (const [key, propShape] of Object.entries(shape.shape)) {
|
|
1103
|
+
const propCrdt = crdtObj[key];
|
|
1104
|
+
const propEmpty = emptyObj[key];
|
|
1105
|
+
result[key] = mergeValue(propShape, propCrdt, propEmpty);
|
|
1106
|
+
}
|
|
1107
|
+
return result;
|
|
1108
|
+
}
|
|
1109
|
+
if (shape._type === "value" && shape.valueType === "discriminatedUnion") {
|
|
1110
|
+
return mergeDiscriminatedUnion(
|
|
1111
|
+
shape,
|
|
1112
|
+
crdtValue,
|
|
1113
|
+
emptyValue
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1094
1116
|
return crdtValue ?? emptyValue;
|
|
1095
1117
|
}
|
|
1096
1118
|
}
|
|
1119
|
+
function mergeDiscriminatedUnion(shape, crdtValue, emptyValue) {
|
|
1120
|
+
const crdtObj = crdtValue ?? {};
|
|
1121
|
+
const emptyObj = emptyValue ?? {};
|
|
1122
|
+
const discriminantValue = crdtObj[shape.discriminantKey] ?? emptyObj[shape.discriminantKey];
|
|
1123
|
+
if (typeof discriminantValue !== "string") {
|
|
1124
|
+
return emptyValue;
|
|
1125
|
+
}
|
|
1126
|
+
const variantShape = shape.variants[discriminantValue];
|
|
1127
|
+
if (!variantShape) {
|
|
1128
|
+
return crdtValue ?? emptyValue;
|
|
1129
|
+
}
|
|
1130
|
+
return mergeValue(variantShape, crdtValue, emptyValue);
|
|
1131
|
+
}
|
|
1097
1132
|
|
|
1098
1133
|
// src/validation.ts
|
|
1099
1134
|
function validateValue(value, schema, path = "") {
|
|
@@ -1168,13 +1203,20 @@ function validateValue(value, schema, path = "") {
|
|
|
1168
1203
|
if (schema._type === "value") {
|
|
1169
1204
|
const valueSchema = schema;
|
|
1170
1205
|
switch (valueSchema.valueType) {
|
|
1171
|
-
case "string":
|
|
1206
|
+
case "string": {
|
|
1172
1207
|
if (typeof value !== "string") {
|
|
1173
1208
|
throw new Error(
|
|
1174
1209
|
`Expected string at path ${currentPath}, got ${typeof value}`
|
|
1175
1210
|
);
|
|
1176
1211
|
}
|
|
1212
|
+
const stringSchema = valueSchema;
|
|
1213
|
+
if (stringSchema.options && !stringSchema.options.includes(value)) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`Expected one of [${stringSchema.options.join(", ")}] at path ${currentPath}, got "${value}"`
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1177
1218
|
return value;
|
|
1219
|
+
}
|
|
1178
1220
|
case "number":
|
|
1179
1221
|
if (typeof value !== "number") {
|
|
1180
1222
|
throw new Error(
|
|
@@ -1288,6 +1330,15 @@ function validateEmptyState(emptyState, schema) {
|
|
|
1288
1330
|
|
|
1289
1331
|
// src/change.ts
|
|
1290
1332
|
var TypedDoc = class {
|
|
1333
|
+
/**
|
|
1334
|
+
* Creates a new TypedDoc with the given schema and empty state.
|
|
1335
|
+
*
|
|
1336
|
+
* @param shape - The document schema
|
|
1337
|
+
* @param emptyState - Default values for the document. For dynamic containers
|
|
1338
|
+
* (list, record, etc.), only empty values ([] or {}) are allowed. Use
|
|
1339
|
+
* `.change()` to add initial data after construction.
|
|
1340
|
+
* @param doc - Optional existing LoroDoc to wrap
|
|
1341
|
+
*/
|
|
1291
1342
|
constructor(shape, emptyState, doc = new LoroDoc()) {
|
|
1292
1343
|
this.shape = shape;
|
|
1293
1344
|
this.emptyState = emptyState;
|
|
@@ -1361,111 +1412,129 @@ var Shape = {
|
|
|
1361
1412
|
_type: "doc",
|
|
1362
1413
|
shapes: shape,
|
|
1363
1414
|
_plain: {},
|
|
1364
|
-
_draft: {}
|
|
1415
|
+
_draft: {},
|
|
1416
|
+
_emptyState: {}
|
|
1365
1417
|
}),
|
|
1366
1418
|
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
1367
1419
|
// various CRDT algorithms
|
|
1368
1420
|
counter: () => ({
|
|
1369
1421
|
_type: "counter",
|
|
1370
1422
|
_plain: 0,
|
|
1371
|
-
_draft: {}
|
|
1423
|
+
_draft: {},
|
|
1424
|
+
_emptyState: 0
|
|
1372
1425
|
}),
|
|
1373
1426
|
list: (shape) => ({
|
|
1374
1427
|
_type: "list",
|
|
1375
1428
|
shape,
|
|
1376
1429
|
_plain: [],
|
|
1377
|
-
_draft: {}
|
|
1430
|
+
_draft: {},
|
|
1431
|
+
_emptyState: []
|
|
1378
1432
|
}),
|
|
1379
1433
|
map: (shape) => ({
|
|
1380
1434
|
_type: "map",
|
|
1381
1435
|
shapes: shape,
|
|
1382
1436
|
_plain: {},
|
|
1383
|
-
_draft: {}
|
|
1437
|
+
_draft: {},
|
|
1438
|
+
_emptyState: {}
|
|
1384
1439
|
}),
|
|
1385
1440
|
record: (shape) => ({
|
|
1386
1441
|
_type: "record",
|
|
1387
1442
|
shape,
|
|
1388
1443
|
_plain: {},
|
|
1389
|
-
_draft: {}
|
|
1444
|
+
_draft: {},
|
|
1445
|
+
_emptyState: {}
|
|
1390
1446
|
}),
|
|
1391
1447
|
movableList: (shape) => ({
|
|
1392
1448
|
_type: "movableList",
|
|
1393
1449
|
shape,
|
|
1394
1450
|
_plain: [],
|
|
1395
|
-
_draft: {}
|
|
1451
|
+
_draft: {},
|
|
1452
|
+
_emptyState: []
|
|
1396
1453
|
}),
|
|
1397
1454
|
text: () => ({
|
|
1398
1455
|
_type: "text",
|
|
1399
1456
|
_plain: "",
|
|
1400
|
-
_draft: {}
|
|
1457
|
+
_draft: {},
|
|
1458
|
+
_emptyState: ""
|
|
1401
1459
|
}),
|
|
1402
1460
|
tree: (shape) => ({
|
|
1403
1461
|
_type: "tree",
|
|
1404
1462
|
shape,
|
|
1405
1463
|
_plain: {},
|
|
1406
|
-
_draft: {}
|
|
1464
|
+
_draft: {},
|
|
1465
|
+
_emptyState: []
|
|
1407
1466
|
}),
|
|
1408
1467
|
// Values are represented as plain JS objects, with the limitation that they MUST be
|
|
1409
1468
|
// representable as a Loro "Value"--basically JSON. The behavior of a Value is basically
|
|
1410
1469
|
// "Last Write Wins", meaning there is no subtle convergent behavior here, just taking
|
|
1411
1470
|
// the most recent value based on the current available information.
|
|
1412
1471
|
plain: {
|
|
1413
|
-
string: () => ({
|
|
1472
|
+
string: (...options) => ({
|
|
1414
1473
|
_type: "value",
|
|
1415
1474
|
valueType: "string",
|
|
1416
|
-
_plain: "",
|
|
1417
|
-
_draft: ""
|
|
1475
|
+
_plain: options[0] ?? "",
|
|
1476
|
+
_draft: options[0] ?? "",
|
|
1477
|
+
_emptyState: options[0] ?? "",
|
|
1478
|
+
options: options.length > 0 ? options : void 0
|
|
1418
1479
|
}),
|
|
1419
1480
|
number: () => ({
|
|
1420
1481
|
_type: "value",
|
|
1421
1482
|
valueType: "number",
|
|
1422
1483
|
_plain: 0,
|
|
1423
|
-
_draft: 0
|
|
1484
|
+
_draft: 0,
|
|
1485
|
+
_emptyState: 0
|
|
1424
1486
|
}),
|
|
1425
1487
|
boolean: () => ({
|
|
1426
1488
|
_type: "value",
|
|
1427
1489
|
valueType: "boolean",
|
|
1428
1490
|
_plain: false,
|
|
1429
|
-
_draft: false
|
|
1491
|
+
_draft: false,
|
|
1492
|
+
_emptyState: false
|
|
1430
1493
|
}),
|
|
1431
1494
|
null: () => ({
|
|
1432
1495
|
_type: "value",
|
|
1433
1496
|
valueType: "null",
|
|
1434
1497
|
_plain: null,
|
|
1435
|
-
_draft: null
|
|
1498
|
+
_draft: null,
|
|
1499
|
+
_emptyState: null
|
|
1436
1500
|
}),
|
|
1437
1501
|
undefined: () => ({
|
|
1438
1502
|
_type: "value",
|
|
1439
1503
|
valueType: "undefined",
|
|
1440
1504
|
_plain: void 0,
|
|
1441
|
-
_draft: void 0
|
|
1505
|
+
_draft: void 0,
|
|
1506
|
+
_emptyState: void 0
|
|
1442
1507
|
}),
|
|
1443
1508
|
uint8Array: () => ({
|
|
1444
1509
|
_type: "value",
|
|
1445
1510
|
valueType: "uint8array",
|
|
1446
1511
|
_plain: new Uint8Array(),
|
|
1447
|
-
_draft: new Uint8Array()
|
|
1512
|
+
_draft: new Uint8Array(),
|
|
1513
|
+
_emptyState: new Uint8Array()
|
|
1448
1514
|
}),
|
|
1449
1515
|
object: (shape) => ({
|
|
1450
1516
|
_type: "value",
|
|
1451
1517
|
valueType: "object",
|
|
1452
1518
|
shape,
|
|
1453
1519
|
_plain: {},
|
|
1454
|
-
_draft: {}
|
|
1520
|
+
_draft: {},
|
|
1521
|
+
_emptyState: {}
|
|
1455
1522
|
}),
|
|
1456
1523
|
record: (shape) => ({
|
|
1457
1524
|
_type: "value",
|
|
1458
1525
|
valueType: "record",
|
|
1459
1526
|
shape,
|
|
1460
1527
|
_plain: {},
|
|
1461
|
-
_draft: {}
|
|
1528
|
+
_draft: {},
|
|
1529
|
+
_emptyState: {}
|
|
1462
1530
|
}),
|
|
1463
1531
|
array: (shape) => ({
|
|
1464
1532
|
_type: "value",
|
|
1465
1533
|
valueType: "array",
|
|
1466
1534
|
shape,
|
|
1467
1535
|
_plain: [],
|
|
1468
|
-
_draft: []
|
|
1536
|
+
_draft: [],
|
|
1537
|
+
_emptyState: []
|
|
1469
1538
|
}),
|
|
1470
1539
|
// Special value type that helps make things like `string | null` representable
|
|
1471
1540
|
// TODO(duane): should this be a more general type for containers too?
|
|
@@ -1474,7 +1543,43 @@ var Shape = {
|
|
|
1474
1543
|
valueType: "union",
|
|
1475
1544
|
shapes,
|
|
1476
1545
|
_plain: {},
|
|
1477
|
-
_draft: {}
|
|
1546
|
+
_draft: {},
|
|
1547
|
+
_emptyState: {}
|
|
1548
|
+
}),
|
|
1549
|
+
/**
|
|
1550
|
+
* Creates a discriminated union shape for type-safe tagged unions.
|
|
1551
|
+
*
|
|
1552
|
+
* @example
|
|
1553
|
+
* ```typescript
|
|
1554
|
+
* const ClientPresenceShape = Shape.plain.object({
|
|
1555
|
+
* type: Shape.plain.string("client"),
|
|
1556
|
+
* name: Shape.plain.string(),
|
|
1557
|
+
* input: Shape.plain.object({ force: Shape.plain.number(), angle: Shape.plain.number() }),
|
|
1558
|
+
* })
|
|
1559
|
+
*
|
|
1560
|
+
* const ServerPresenceShape = Shape.plain.object({
|
|
1561
|
+
* type: Shape.plain.string("server"),
|
|
1562
|
+
* cars: Shape.plain.record(Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() })),
|
|
1563
|
+
* tick: Shape.plain.number(),
|
|
1564
|
+
* })
|
|
1565
|
+
*
|
|
1566
|
+
* const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
1567
|
+
* client: ClientPresenceShape,
|
|
1568
|
+
* server: ServerPresenceShape,
|
|
1569
|
+
* })
|
|
1570
|
+
* ```
|
|
1571
|
+
*
|
|
1572
|
+
* @param discriminantKey - The key used to discriminate between variants (e.g., "type")
|
|
1573
|
+
* @param variants - A record mapping discriminant values to their object shapes
|
|
1574
|
+
*/
|
|
1575
|
+
discriminatedUnion: (discriminantKey, variants) => ({
|
|
1576
|
+
_type: "value",
|
|
1577
|
+
valueType: "discriminatedUnion",
|
|
1578
|
+
discriminantKey,
|
|
1579
|
+
variants,
|
|
1580
|
+
_plain: {},
|
|
1581
|
+
_draft: {},
|
|
1582
|
+
_emptyState: {}
|
|
1478
1583
|
})
|
|
1479
1584
|
}
|
|
1480
1585
|
};
|