@loro-extended/change 0.5.0 → 0.7.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 +71 -0
- package/dist/index.d.ts +121 -22
- package/dist/index.js +130 -20
- 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 +246 -0
- package/src/draft-nodes/map.ts +3 -2
- package/src/index.ts +7 -1
- package/src/overlay.ts +55 -1
- package/src/record.test.ts +2 -1
- package/src/shape.ts +136 -19
- package/src/types.test.ts +169 -0
- package/src/types.ts +44 -2
- package/src/utils/type-guards.ts +1 -0
- package/src/validation.ts +33 -0
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:
|
|
@@ -335,6 +405,7 @@ const schema = Shape.doc({
|
|
|
335
405
|
- `Shape.plain.record(valueShape)` - Object values with dynamic string keys
|
|
336
406
|
- `Shape.plain.array(itemShape)` - Array values
|
|
337
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
|
|
338
409
|
|
|
339
410
|
### TypedDoc Methods
|
|
340
411
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
1
|
import { LoroList, LoroMovableList, Container, LoroMap, Value, LoroText, LoroCounter, LoroTree, LoroDoc } from 'loro-crdt';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Infers the plain (JSON-serializable) type from any Shape.
|
|
5
|
+
*
|
|
6
|
+
* This is the recommended way to extract types from shapes.
|
|
7
|
+
* Works with DocShape, ContainerShape, and ValueShape.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const ChatSchema = Shape.doc({
|
|
12
|
+
* messages: Shape.list(Shape.map({
|
|
13
|
+
* id: Shape.plain.string(),
|
|
14
|
+
* content: Shape.text(),
|
|
15
|
+
* })),
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* // Extract the document type
|
|
19
|
+
* type ChatDoc = Infer<typeof ChatSchema>
|
|
20
|
+
* // Result: { messages: { id: string; content: string }[] }
|
|
21
|
+
*
|
|
22
|
+
* const PresenceSchema = Shape.plain.object({
|
|
23
|
+
* name: Shape.plain.string(),
|
|
24
|
+
* cursor: Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() }),
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* // Extract the presence type
|
|
28
|
+
* type Presence = Infer<typeof PresenceSchema>
|
|
29
|
+
* // Result: { name: string; cursor: { x: number; y: number } }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
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
|
+
type InferDraftType<T> = T extends Shape<any, infer D, any> ? D : never;
|
|
35
|
+
/**
|
|
36
|
+
* Extracts the valid empty state type from a shape.
|
|
37
|
+
*
|
|
38
|
+
* For dynamic containers (list, record, etc.), this will be constrained to
|
|
39
|
+
* empty values ([] or {}) to prevent users from expecting per-entry merging.
|
|
40
|
+
*/
|
|
41
|
+
type InferEmptyStateType<T> = T extends Shape<any, any, infer E> ? E : never;
|
|
5
42
|
type Draft<T extends DocShape<Record<string, ContainerShape>>> = InferDraftType<T>;
|
|
6
43
|
|
|
7
44
|
type DraftNodeParams<Shape extends DocShape | ContainerShape> = {
|
|
@@ -128,25 +165,27 @@ interface DocShape<NestedShapes extends Record<string, ContainerShape> = Record<
|
|
|
128
165
|
[K in keyof NestedShapes]: NestedShapes[K]["_plain"];
|
|
129
166
|
}, {
|
|
130
167
|
[K in keyof NestedShapes]: NestedShapes[K]["_draft"];
|
|
168
|
+
}, {
|
|
169
|
+
[K in keyof NestedShapes]: NestedShapes[K]["_emptyState"];
|
|
131
170
|
}> {
|
|
132
171
|
readonly _type: "doc";
|
|
133
172
|
readonly shapes: NestedShapes;
|
|
134
173
|
}
|
|
135
|
-
interface TextContainerShape extends Shape<string, TextDraftNode> {
|
|
174
|
+
interface TextContainerShape extends Shape<string, TextDraftNode, string> {
|
|
136
175
|
readonly _type: "text";
|
|
137
176
|
}
|
|
138
|
-
interface CounterContainerShape extends Shape<number, CounterDraftNode> {
|
|
177
|
+
interface CounterContainerShape extends Shape<number, CounterDraftNode, number> {
|
|
139
178
|
readonly _type: "counter";
|
|
140
179
|
}
|
|
141
|
-
interface TreeContainerShape<NestedShape = ContainerOrValueShape> extends Shape<any, any> {
|
|
180
|
+
interface TreeContainerShape<NestedShape = ContainerOrValueShape> extends Shape<any, any, never[]> {
|
|
142
181
|
readonly _type: "tree";
|
|
143
182
|
readonly shape: NestedShape;
|
|
144
183
|
}
|
|
145
|
-
interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape
|
|
184
|
+
interface ListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], ListDraftNode<NestedShape>, never[]> {
|
|
146
185
|
readonly _type: "list";
|
|
147
186
|
readonly shape: NestedShape;
|
|
148
187
|
}
|
|
149
|
-
interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListDraftNode<NestedShape
|
|
188
|
+
interface MovableListContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<NestedShape["_plain"][], MovableListDraftNode<NestedShape>, never[]> {
|
|
150
189
|
readonly _type: "movableList";
|
|
151
190
|
readonly shape: NestedShape;
|
|
152
191
|
}
|
|
@@ -154,38 +193,40 @@ interface MapContainerShape<NestedShapes extends Record<string, ContainerOrValue
|
|
|
154
193
|
[K in keyof NestedShapes]: NestedShapes[K]["_plain"];
|
|
155
194
|
}, MapDraftNode<NestedShapes> & {
|
|
156
195
|
[K in keyof NestedShapes]: NestedShapes[K]["_draft"];
|
|
196
|
+
}, {
|
|
197
|
+
[K in keyof NestedShapes]: NestedShapes[K]["_emptyState"];
|
|
157
198
|
}> {
|
|
158
199
|
readonly _type: "map";
|
|
159
200
|
readonly shapes: NestedShapes;
|
|
160
201
|
}
|
|
161
|
-
interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordDraftNode<NestedShape>> {
|
|
202
|
+
interface RecordContainerShape<NestedShape extends ContainerOrValueShape = ContainerOrValueShape> extends Shape<Record<string, NestedShape["_plain"]>, RecordDraftNode<NestedShape>, Record<string, never>> {
|
|
162
203
|
readonly _type: "record";
|
|
163
204
|
readonly shape: NestedShape;
|
|
164
205
|
}
|
|
165
206
|
type ContainerShape = CounterContainerShape | ListContainerShape | MapContainerShape | MovableListContainerShape | RecordContainerShape | TextContainerShape | TreeContainerShape;
|
|
166
207
|
type ContainerType = ContainerShape["_type"];
|
|
167
|
-
interface StringValueShape<T extends string = string> extends Shape<T, T> {
|
|
208
|
+
interface StringValueShape<T extends string = string> extends Shape<T, T, T> {
|
|
168
209
|
readonly _type: "value";
|
|
169
210
|
readonly valueType: "string";
|
|
170
211
|
readonly options?: T[];
|
|
171
212
|
}
|
|
172
|
-
interface NumberValueShape extends Shape<number, number> {
|
|
213
|
+
interface NumberValueShape extends Shape<number, number, number> {
|
|
173
214
|
readonly _type: "value";
|
|
174
215
|
readonly valueType: "number";
|
|
175
216
|
}
|
|
176
|
-
interface BooleanValueShape extends Shape<boolean, boolean> {
|
|
217
|
+
interface BooleanValueShape extends Shape<boolean, boolean, boolean> {
|
|
177
218
|
readonly _type: "value";
|
|
178
219
|
readonly valueType: "boolean";
|
|
179
220
|
}
|
|
180
|
-
interface NullValueShape extends Shape<null, null> {
|
|
221
|
+
interface NullValueShape extends Shape<null, null, null> {
|
|
181
222
|
readonly _type: "value";
|
|
182
223
|
readonly valueType: "null";
|
|
183
224
|
}
|
|
184
|
-
interface UndefinedValueShape extends Shape<undefined, undefined> {
|
|
225
|
+
interface UndefinedValueShape extends Shape<undefined, undefined, undefined> {
|
|
185
226
|
readonly _type: "value";
|
|
186
227
|
readonly valueType: "undefined";
|
|
187
228
|
}
|
|
188
|
-
interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array> {
|
|
229
|
+
interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array, Uint8Array> {
|
|
189
230
|
readonly _type: "value";
|
|
190
231
|
readonly valueType: "uint8array";
|
|
191
232
|
}
|
|
@@ -193,32 +234,54 @@ interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string,
|
|
|
193
234
|
[K in keyof T]: T[K]["_plain"];
|
|
194
235
|
}, {
|
|
195
236
|
[K in keyof T]: T[K]["_draft"];
|
|
237
|
+
}, {
|
|
238
|
+
[K in keyof T]: T[K]["_emptyState"];
|
|
196
239
|
}> {
|
|
197
240
|
readonly _type: "value";
|
|
198
241
|
readonly valueType: "object";
|
|
199
242
|
readonly shape: T;
|
|
200
243
|
}
|
|
201
|
-
interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_draft"]>> {
|
|
244
|
+
interface RecordValueShape<T extends ValueShape = ValueShape> extends Shape<Record<string, T["_plain"]>, Record<string, T["_draft"]>, Record<string, never>> {
|
|
202
245
|
readonly _type: "value";
|
|
203
246
|
readonly valueType: "record";
|
|
204
247
|
readonly shape: T;
|
|
205
248
|
}
|
|
206
|
-
interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_draft"][]> {
|
|
249
|
+
interface ArrayValueShape<T extends ValueShape = ValueShape> extends Shape<T["_plain"][], T["_draft"][], never[]> {
|
|
207
250
|
readonly _type: "value";
|
|
208
251
|
readonly valueType: "array";
|
|
209
252
|
readonly shape: T;
|
|
210
253
|
}
|
|
211
|
-
interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"]> {
|
|
254
|
+
interface UnionValueShape<T extends ValueShape[] = ValueShape[]> extends Shape<T[number]["_plain"], T[number]["_draft"], T[number]["_emptyState"]> {
|
|
212
255
|
readonly _type: "value";
|
|
213
256
|
readonly valueType: "union";
|
|
214
257
|
readonly shapes: T;
|
|
215
258
|
}
|
|
216
|
-
|
|
259
|
+
/**
|
|
260
|
+
* A discriminated union shape that uses a discriminant key to determine which variant to use.
|
|
261
|
+
* This enables type-safe handling of tagged unions like:
|
|
262
|
+
*
|
|
263
|
+
* ```typescript
|
|
264
|
+
* type GamePresence =
|
|
265
|
+
* | { type: "client"; name: string; input: { force: number; angle: number } }
|
|
266
|
+
* | { type: "server"; cars: Record<string, CarState>; tick: number }
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* @typeParam K - The discriminant key (e.g., "type")
|
|
270
|
+
* @typeParam T - A record mapping discriminant values to their object shapes
|
|
271
|
+
*/
|
|
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"]> {
|
|
273
|
+
readonly _type: "value";
|
|
274
|
+
readonly valueType: "discriminatedUnion";
|
|
275
|
+
readonly discriminantKey: K;
|
|
276
|
+
readonly variants: T;
|
|
277
|
+
}
|
|
278
|
+
type ValueShape = StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | ObjectValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape<any, any>;
|
|
217
279
|
type ContainerOrValueShape = ContainerShape | ValueShape;
|
|
218
|
-
interface Shape<Plain, Draft> {
|
|
280
|
+
interface Shape<Plain, Draft, EmptyState = Plain> {
|
|
219
281
|
readonly _type: string;
|
|
220
282
|
readonly _plain: Plain;
|
|
221
283
|
readonly _draft: Draft;
|
|
284
|
+
readonly _emptyState: EmptyState;
|
|
222
285
|
}
|
|
223
286
|
/**
|
|
224
287
|
* The LoroShape factory object
|
|
@@ -247,6 +310,33 @@ declare const Shape: {
|
|
|
247
310
|
record: <T extends ValueShape>(shape: T) => RecordValueShape<T>;
|
|
248
311
|
array: <T extends ValueShape>(shape: T) => ArrayValueShape<T>;
|
|
249
312
|
union: <T extends ValueShape[]>(shapes: T) => UnionValueShape<T>;
|
|
313
|
+
/**
|
|
314
|
+
* Creates a discriminated union shape for type-safe tagged unions.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```typescript
|
|
318
|
+
* const ClientPresenceShape = Shape.plain.object({
|
|
319
|
+
* type: Shape.plain.string("client"),
|
|
320
|
+
* name: Shape.plain.string(),
|
|
321
|
+
* input: Shape.plain.object({ force: Shape.plain.number(), angle: Shape.plain.number() }),
|
|
322
|
+
* })
|
|
323
|
+
*
|
|
324
|
+
* const ServerPresenceShape = Shape.plain.object({
|
|
325
|
+
* type: Shape.plain.string("server"),
|
|
326
|
+
* cars: Shape.plain.record(Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() })),
|
|
327
|
+
* tick: Shape.plain.number(),
|
|
328
|
+
* })
|
|
329
|
+
*
|
|
330
|
+
* const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
331
|
+
* client: ClientPresenceShape,
|
|
332
|
+
* server: ServerPresenceShape,
|
|
333
|
+
* })
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* @param discriminantKey - The key used to discriminate between variants (e.g., "type")
|
|
337
|
+
* @param variants - A record mapping discriminant values to their object shapes
|
|
338
|
+
*/
|
|
339
|
+
discriminatedUnion: <K extends string, T extends Record<string, ObjectValueShape>>(discriminantKey: K, variants: T) => DiscriminatedUnionValueShape<K, T>;
|
|
250
340
|
};
|
|
251
341
|
};
|
|
252
342
|
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;
|
|
@@ -291,7 +381,16 @@ declare class TypedDoc<Shape extends DocShape> {
|
|
|
291
381
|
private shape;
|
|
292
382
|
private emptyState;
|
|
293
383
|
private doc;
|
|
294
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Creates a new TypedDoc with the given schema and empty state.
|
|
386
|
+
*
|
|
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.
|
|
391
|
+
* @param doc - Optional existing LoroDoc to wrap
|
|
392
|
+
*/
|
|
393
|
+
constructor(shape: Shape, emptyState: InferEmptyStateType<Shape>, doc?: LoroDoc);
|
|
295
394
|
get value(): InferPlainType<Shape>;
|
|
296
395
|
change(fn: (draft: Draft<Shape>) => void): InferPlainType<Shape>;
|
|
297
396
|
/**
|
|
@@ -314,7 +413,7 @@ declare class TypedDoc<Shape extends DocShape> {
|
|
|
314
413
|
get docShape(): Shape;
|
|
315
414
|
get rawValue(): any;
|
|
316
415
|
}
|
|
317
|
-
declare function createTypedDoc<Shape extends DocShape>(shape: Shape, emptyState:
|
|
416
|
+
declare function createTypedDoc<Shape extends DocShape>(shape: Shape, emptyState: InferEmptyStateType<Shape>, existingDoc?: LoroDoc): TypedDoc<Shape>;
|
|
318
417
|
|
|
319
418
|
/**
|
|
320
419
|
* Overlays CRDT state with empty state defaults
|
|
@@ -337,4 +436,4 @@ declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Sh
|
|
|
337
436
|
*/
|
|
338
437
|
declare function validateEmptyState<T extends DocShape>(emptyState: unknown, schema: T): InferPlainType<T>;
|
|
339
438
|
|
|
340
|
-
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -63,7 +63,8 @@ function isValueShape(schema) {
|
|
|
63
63
|
"object",
|
|
64
64
|
"record",
|
|
65
65
|
"array",
|
|
66
|
-
"union"
|
|
66
|
+
"union",
|
|
67
|
+
"discriminatedUnion"
|
|
67
68
|
].includes(schema.valueType);
|
|
68
69
|
}
|
|
69
70
|
function isObjectValue(value) {
|
|
@@ -487,13 +488,13 @@ var MapDraftNode = class extends DraftNode {
|
|
|
487
488
|
node = containerValue;
|
|
488
489
|
} else {
|
|
489
490
|
const emptyState = this.emptyState?.[key];
|
|
490
|
-
if (
|
|
491
|
+
if (emptyState === void 0) {
|
|
491
492
|
throw new Error("empty state required");
|
|
492
493
|
}
|
|
493
494
|
node = emptyState;
|
|
494
495
|
}
|
|
495
496
|
}
|
|
496
|
-
if (
|
|
497
|
+
if (node === void 0) throw new Error("no container made");
|
|
497
498
|
this.propertyCache.set(key, node);
|
|
498
499
|
}
|
|
499
500
|
return node;
|
|
@@ -505,6 +506,7 @@ var MapDraftNode = class extends DraftNode {
|
|
|
505
506
|
get: () => this.getOrCreateNode(key, shape),
|
|
506
507
|
set: isValueShape(shape) ? (value) => {
|
|
507
508
|
this.container.set(key, value);
|
|
509
|
+
this.propertyCache.set(key, value);
|
|
508
510
|
} : void 0
|
|
509
511
|
});
|
|
510
512
|
}
|
|
@@ -1105,9 +1107,31 @@ function mergeValue(shape, crdtValue, emptyValue) {
|
|
|
1105
1107
|
}
|
|
1106
1108
|
return result;
|
|
1107
1109
|
}
|
|
1110
|
+
if (shape._type === "value" && shape.valueType === "discriminatedUnion") {
|
|
1111
|
+
return mergeDiscriminatedUnion(
|
|
1112
|
+
shape,
|
|
1113
|
+
crdtValue,
|
|
1114
|
+
emptyValue
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1108
1117
|
return crdtValue ?? emptyValue;
|
|
1109
1118
|
}
|
|
1110
1119
|
}
|
|
1120
|
+
function mergeDiscriminatedUnion(shape, crdtValue, emptyValue) {
|
|
1121
|
+
const crdtObj = crdtValue ?? {};
|
|
1122
|
+
const emptyObj = emptyValue ?? {};
|
|
1123
|
+
const discriminantValue = crdtObj[shape.discriminantKey] ?? emptyObj[shape.discriminantKey];
|
|
1124
|
+
if (typeof discriminantValue !== "string") {
|
|
1125
|
+
return emptyValue;
|
|
1126
|
+
}
|
|
1127
|
+
const variantShape = shape.variants[discriminantValue];
|
|
1128
|
+
if (!variantShape) {
|
|
1129
|
+
return crdtValue ?? emptyValue;
|
|
1130
|
+
}
|
|
1131
|
+
const emptyDiscriminant = emptyObj[shape.discriminantKey];
|
|
1132
|
+
const effectiveEmptyValue = emptyDiscriminant === discriminantValue ? emptyValue : void 0;
|
|
1133
|
+
return mergeValue(variantShape, crdtValue, effectiveEmptyValue);
|
|
1134
|
+
}
|
|
1111
1135
|
|
|
1112
1136
|
// src/validation.ts
|
|
1113
1137
|
function validateValue(value, schema, path = "") {
|
|
@@ -1289,6 +1313,30 @@ function validateValue(value, schema, path = "") {
|
|
|
1289
1313
|
`Value at path ${currentPath} does not match any union type: ${lastError?.message}`
|
|
1290
1314
|
);
|
|
1291
1315
|
}
|
|
1316
|
+
case "discriminatedUnion": {
|
|
1317
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`Expected object at path ${currentPath}, got ${typeof value}`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
const unionSchema = valueSchema;
|
|
1323
|
+
const discriminantKey = unionSchema.discriminantKey;
|
|
1324
|
+
const discriminantValue = value[discriminantKey];
|
|
1325
|
+
if (typeof discriminantValue !== "string") {
|
|
1326
|
+
throw new Error(
|
|
1327
|
+
`Expected string for discriminant key "${discriminantKey}" at path ${currentPath}, got ${typeof discriminantValue}`
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
const variantSchema = unionSchema.variants[discriminantValue];
|
|
1331
|
+
if (!variantSchema) {
|
|
1332
|
+
throw new Error(
|
|
1333
|
+
`Invalid discriminant value "${discriminantValue}" at path ${currentPath}. Expected one of: ${Object.keys(
|
|
1334
|
+
unionSchema.variants
|
|
1335
|
+
).join(", ")}`
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
return validateValue(value, variantSchema, currentPath);
|
|
1339
|
+
}
|
|
1292
1340
|
default:
|
|
1293
1341
|
throw new Error(`Unknown value type: ${valueSchema.valueType}`);
|
|
1294
1342
|
}
|
|
@@ -1309,6 +1357,15 @@ function validateEmptyState(emptyState, schema) {
|
|
|
1309
1357
|
|
|
1310
1358
|
// src/change.ts
|
|
1311
1359
|
var TypedDoc = class {
|
|
1360
|
+
/**
|
|
1361
|
+
* Creates a new TypedDoc with the given schema and empty state.
|
|
1362
|
+
*
|
|
1363
|
+
* @param shape - The document schema
|
|
1364
|
+
* @param emptyState - Default values for the document. For dynamic containers
|
|
1365
|
+
* (list, record, etc.), only empty values ([] or {}) are allowed. Use
|
|
1366
|
+
* `.change()` to add initial data after construction.
|
|
1367
|
+
* @param doc - Optional existing LoroDoc to wrap
|
|
1368
|
+
*/
|
|
1312
1369
|
constructor(shape, emptyState, doc = new LoroDoc()) {
|
|
1313
1370
|
this.shape = shape;
|
|
1314
1371
|
this.emptyState = emptyState;
|
|
@@ -1382,49 +1439,57 @@ var Shape = {
|
|
|
1382
1439
|
_type: "doc",
|
|
1383
1440
|
shapes: shape,
|
|
1384
1441
|
_plain: {},
|
|
1385
|
-
_draft: {}
|
|
1442
|
+
_draft: {},
|
|
1443
|
+
_emptyState: {}
|
|
1386
1444
|
}),
|
|
1387
1445
|
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
1388
1446
|
// various CRDT algorithms
|
|
1389
1447
|
counter: () => ({
|
|
1390
1448
|
_type: "counter",
|
|
1391
1449
|
_plain: 0,
|
|
1392
|
-
_draft: {}
|
|
1450
|
+
_draft: {},
|
|
1451
|
+
_emptyState: 0
|
|
1393
1452
|
}),
|
|
1394
1453
|
list: (shape) => ({
|
|
1395
1454
|
_type: "list",
|
|
1396
1455
|
shape,
|
|
1397
1456
|
_plain: [],
|
|
1398
|
-
_draft: {}
|
|
1457
|
+
_draft: {},
|
|
1458
|
+
_emptyState: []
|
|
1399
1459
|
}),
|
|
1400
1460
|
map: (shape) => ({
|
|
1401
1461
|
_type: "map",
|
|
1402
1462
|
shapes: shape,
|
|
1403
1463
|
_plain: {},
|
|
1404
|
-
_draft: {}
|
|
1464
|
+
_draft: {},
|
|
1465
|
+
_emptyState: {}
|
|
1405
1466
|
}),
|
|
1406
1467
|
record: (shape) => ({
|
|
1407
1468
|
_type: "record",
|
|
1408
1469
|
shape,
|
|
1409
1470
|
_plain: {},
|
|
1410
|
-
_draft: {}
|
|
1471
|
+
_draft: {},
|
|
1472
|
+
_emptyState: {}
|
|
1411
1473
|
}),
|
|
1412
1474
|
movableList: (shape) => ({
|
|
1413
1475
|
_type: "movableList",
|
|
1414
1476
|
shape,
|
|
1415
1477
|
_plain: [],
|
|
1416
|
-
_draft: {}
|
|
1478
|
+
_draft: {},
|
|
1479
|
+
_emptyState: []
|
|
1417
1480
|
}),
|
|
1418
1481
|
text: () => ({
|
|
1419
1482
|
_type: "text",
|
|
1420
1483
|
_plain: "",
|
|
1421
|
-
_draft: {}
|
|
1484
|
+
_draft: {},
|
|
1485
|
+
_emptyState: ""
|
|
1422
1486
|
}),
|
|
1423
1487
|
tree: (shape) => ({
|
|
1424
1488
|
_type: "tree",
|
|
1425
1489
|
shape,
|
|
1426
1490
|
_plain: {},
|
|
1427
|
-
_draft: {}
|
|
1491
|
+
_draft: {},
|
|
1492
|
+
_emptyState: []
|
|
1428
1493
|
}),
|
|
1429
1494
|
// Values are represented as plain JS objects, with the limitation that they MUST be
|
|
1430
1495
|
// representable as a Loro "Value"--basically JSON. The behavior of a Value is basically
|
|
@@ -1436,58 +1501,67 @@ var Shape = {
|
|
|
1436
1501
|
valueType: "string",
|
|
1437
1502
|
_plain: options[0] ?? "",
|
|
1438
1503
|
_draft: options[0] ?? "",
|
|
1504
|
+
_emptyState: options[0] ?? "",
|
|
1439
1505
|
options: options.length > 0 ? options : void 0
|
|
1440
1506
|
}),
|
|
1441
1507
|
number: () => ({
|
|
1442
1508
|
_type: "value",
|
|
1443
1509
|
valueType: "number",
|
|
1444
1510
|
_plain: 0,
|
|
1445
|
-
_draft: 0
|
|
1511
|
+
_draft: 0,
|
|
1512
|
+
_emptyState: 0
|
|
1446
1513
|
}),
|
|
1447
1514
|
boolean: () => ({
|
|
1448
1515
|
_type: "value",
|
|
1449
1516
|
valueType: "boolean",
|
|
1450
1517
|
_plain: false,
|
|
1451
|
-
_draft: false
|
|
1518
|
+
_draft: false,
|
|
1519
|
+
_emptyState: false
|
|
1452
1520
|
}),
|
|
1453
1521
|
null: () => ({
|
|
1454
1522
|
_type: "value",
|
|
1455
1523
|
valueType: "null",
|
|
1456
1524
|
_plain: null,
|
|
1457
|
-
_draft: null
|
|
1525
|
+
_draft: null,
|
|
1526
|
+
_emptyState: null
|
|
1458
1527
|
}),
|
|
1459
1528
|
undefined: () => ({
|
|
1460
1529
|
_type: "value",
|
|
1461
1530
|
valueType: "undefined",
|
|
1462
1531
|
_plain: void 0,
|
|
1463
|
-
_draft: void 0
|
|
1532
|
+
_draft: void 0,
|
|
1533
|
+
_emptyState: void 0
|
|
1464
1534
|
}),
|
|
1465
1535
|
uint8Array: () => ({
|
|
1466
1536
|
_type: "value",
|
|
1467
1537
|
valueType: "uint8array",
|
|
1468
1538
|
_plain: new Uint8Array(),
|
|
1469
|
-
_draft: new Uint8Array()
|
|
1539
|
+
_draft: new Uint8Array(),
|
|
1540
|
+
_emptyState: new Uint8Array()
|
|
1470
1541
|
}),
|
|
1471
1542
|
object: (shape) => ({
|
|
1472
1543
|
_type: "value",
|
|
1473
1544
|
valueType: "object",
|
|
1474
1545
|
shape,
|
|
1475
1546
|
_plain: {},
|
|
1476
|
-
_draft: {}
|
|
1547
|
+
_draft: {},
|
|
1548
|
+
_emptyState: {}
|
|
1477
1549
|
}),
|
|
1478
1550
|
record: (shape) => ({
|
|
1479
1551
|
_type: "value",
|
|
1480
1552
|
valueType: "record",
|
|
1481
1553
|
shape,
|
|
1482
1554
|
_plain: {},
|
|
1483
|
-
_draft: {}
|
|
1555
|
+
_draft: {},
|
|
1556
|
+
_emptyState: {}
|
|
1484
1557
|
}),
|
|
1485
1558
|
array: (shape) => ({
|
|
1486
1559
|
_type: "value",
|
|
1487
1560
|
valueType: "array",
|
|
1488
1561
|
shape,
|
|
1489
1562
|
_plain: [],
|
|
1490
|
-
_draft: []
|
|
1563
|
+
_draft: [],
|
|
1564
|
+
_emptyState: []
|
|
1491
1565
|
}),
|
|
1492
1566
|
// Special value type that helps make things like `string | null` representable
|
|
1493
1567
|
// TODO(duane): should this be a more general type for containers too?
|
|
@@ -1496,7 +1570,43 @@ var Shape = {
|
|
|
1496
1570
|
valueType: "union",
|
|
1497
1571
|
shapes,
|
|
1498
1572
|
_plain: {},
|
|
1499
|
-
_draft: {}
|
|
1573
|
+
_draft: {},
|
|
1574
|
+
_emptyState: {}
|
|
1575
|
+
}),
|
|
1576
|
+
/**
|
|
1577
|
+
* Creates a discriminated union shape for type-safe tagged unions.
|
|
1578
|
+
*
|
|
1579
|
+
* @example
|
|
1580
|
+
* ```typescript
|
|
1581
|
+
* const ClientPresenceShape = Shape.plain.object({
|
|
1582
|
+
* type: Shape.plain.string("client"),
|
|
1583
|
+
* name: Shape.plain.string(),
|
|
1584
|
+
* input: Shape.plain.object({ force: Shape.plain.number(), angle: Shape.plain.number() }),
|
|
1585
|
+
* })
|
|
1586
|
+
*
|
|
1587
|
+
* const ServerPresenceShape = Shape.plain.object({
|
|
1588
|
+
* type: Shape.plain.string("server"),
|
|
1589
|
+
* cars: Shape.plain.record(Shape.plain.object({ x: Shape.plain.number(), y: Shape.plain.number() })),
|
|
1590
|
+
* tick: Shape.plain.number(),
|
|
1591
|
+
* })
|
|
1592
|
+
*
|
|
1593
|
+
* const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
1594
|
+
* client: ClientPresenceShape,
|
|
1595
|
+
* server: ServerPresenceShape,
|
|
1596
|
+
* })
|
|
1597
|
+
* ```
|
|
1598
|
+
*
|
|
1599
|
+
* @param discriminantKey - The key used to discriminate between variants (e.g., "type")
|
|
1600
|
+
* @param variants - A record mapping discriminant values to their object shapes
|
|
1601
|
+
*/
|
|
1602
|
+
discriminatedUnion: (discriminantKey, variants) => ({
|
|
1603
|
+
_type: "value",
|
|
1604
|
+
valueType: "discriminatedUnion",
|
|
1605
|
+
discriminantKey,
|
|
1606
|
+
variants,
|
|
1607
|
+
_plain: {},
|
|
1608
|
+
_draft: {},
|
|
1609
|
+
_emptyState: {}
|
|
1500
1610
|
})
|
|
1501
1611
|
}
|
|
1502
1612
|
};
|