@loro-extended/change 0.5.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 +71 -0
- package/dist/index.d.ts +91 -22
- package/dist/index.js +102 -19
- 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 +49 -1
- package/src/record.test.ts +2 -1
- package/src/shape.ts +133 -19
- package/src/types.ts +12 -2
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,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,38 +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<T extends string = string> extends Shape<T, T> {
|
|
178
|
+
interface StringValueShape<T extends string = string> extends Shape<T, T, T> {
|
|
168
179
|
readonly _type: "value";
|
|
169
180
|
readonly valueType: "string";
|
|
170
181
|
readonly options?: T[];
|
|
171
182
|
}
|
|
172
|
-
interface NumberValueShape extends Shape<number, number> {
|
|
183
|
+
interface NumberValueShape extends Shape<number, number, number> {
|
|
173
184
|
readonly _type: "value";
|
|
174
185
|
readonly valueType: "number";
|
|
175
186
|
}
|
|
176
|
-
interface BooleanValueShape extends Shape<boolean, boolean> {
|
|
187
|
+
interface BooleanValueShape extends Shape<boolean, boolean, boolean> {
|
|
177
188
|
readonly _type: "value";
|
|
178
189
|
readonly valueType: "boolean";
|
|
179
190
|
}
|
|
180
|
-
interface NullValueShape extends Shape<null, null> {
|
|
191
|
+
interface NullValueShape extends Shape<null, null, null> {
|
|
181
192
|
readonly _type: "value";
|
|
182
193
|
readonly valueType: "null";
|
|
183
194
|
}
|
|
184
|
-
interface UndefinedValueShape extends Shape<undefined, undefined> {
|
|
195
|
+
interface UndefinedValueShape extends Shape<undefined, undefined, undefined> {
|
|
185
196
|
readonly _type: "value";
|
|
186
197
|
readonly valueType: "undefined";
|
|
187
198
|
}
|
|
188
|
-
interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array> {
|
|
199
|
+
interface Uint8ArrayValueShape extends Shape<Uint8Array, Uint8Array, Uint8Array> {
|
|
189
200
|
readonly _type: "value";
|
|
190
201
|
readonly valueType: "uint8array";
|
|
191
202
|
}
|
|
@@ -193,32 +204,54 @@ interface ObjectValueShape<T extends Record<string, ValueShape> = Record<string,
|
|
|
193
204
|
[K in keyof T]: T[K]["_plain"];
|
|
194
205
|
}, {
|
|
195
206
|
[K in keyof T]: T[K]["_draft"];
|
|
207
|
+
}, {
|
|
208
|
+
[K in keyof T]: T[K]["_emptyState"];
|
|
196
209
|
}> {
|
|
197
210
|
readonly _type: "value";
|
|
198
211
|
readonly valueType: "object";
|
|
199
212
|
readonly shape: T;
|
|
200
213
|
}
|
|
201
|
-
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>> {
|
|
202
215
|
readonly _type: "value";
|
|
203
216
|
readonly valueType: "record";
|
|
204
217
|
readonly shape: T;
|
|
205
218
|
}
|
|
206
|
-
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[]> {
|
|
207
220
|
readonly _type: "value";
|
|
208
221
|
readonly valueType: "array";
|
|
209
222
|
readonly shape: T;
|
|
210
223
|
}
|
|
211
|
-
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"]> {
|
|
212
225
|
readonly _type: "value";
|
|
213
226
|
readonly valueType: "union";
|
|
214
227
|
readonly shapes: T;
|
|
215
228
|
}
|
|
216
|
-
|
|
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;
|
|
217
249
|
type ContainerOrValueShape = ContainerShape | ValueShape;
|
|
218
|
-
interface Shape<Plain, Draft> {
|
|
250
|
+
interface Shape<Plain, Draft, EmptyState = Plain> {
|
|
219
251
|
readonly _type: string;
|
|
220
252
|
readonly _plain: Plain;
|
|
221
253
|
readonly _draft: Draft;
|
|
254
|
+
readonly _emptyState: EmptyState;
|
|
222
255
|
}
|
|
223
256
|
/**
|
|
224
257
|
* The LoroShape factory object
|
|
@@ -247,6 +280,33 @@ declare const Shape: {
|
|
|
247
280
|
record: <T extends ValueShape>(shape: T) => RecordValueShape<T>;
|
|
248
281
|
array: <T extends ValueShape>(shape: T) => ArrayValueShape<T>;
|
|
249
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>;
|
|
250
310
|
};
|
|
251
311
|
};
|
|
252
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;
|
|
@@ -291,7 +351,16 @@ declare class TypedDoc<Shape extends DocShape> {
|
|
|
291
351
|
private shape;
|
|
292
352
|
private emptyState;
|
|
293
353
|
private doc;
|
|
294
|
-
|
|
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);
|
|
295
364
|
get value(): InferPlainType<Shape>;
|
|
296
365
|
change(fn: (draft: Draft<Shape>) => void): InferPlainType<Shape>;
|
|
297
366
|
/**
|
|
@@ -314,7 +383,7 @@ declare class TypedDoc<Shape extends DocShape> {
|
|
|
314
383
|
get docShape(): Shape;
|
|
315
384
|
get rawValue(): any;
|
|
316
385
|
}
|
|
317
|
-
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>;
|
|
318
387
|
|
|
319
388
|
/**
|
|
320
389
|
* Overlays CRDT state with empty state defaults
|
|
@@ -337,4 +406,4 @@ declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Sh
|
|
|
337
406
|
*/
|
|
338
407
|
declare function validateEmptyState<T extends DocShape>(emptyState: unknown, schema: T): InferPlainType<T>;
|
|
339
408
|
|
|
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 };
|
|
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
|
}
|
|
@@ -1105,9 +1106,29 @@ function mergeValue(shape, crdtValue, emptyValue) {
|
|
|
1105
1106
|
}
|
|
1106
1107
|
return result;
|
|
1107
1108
|
}
|
|
1109
|
+
if (shape._type === "value" && shape.valueType === "discriminatedUnion") {
|
|
1110
|
+
return mergeDiscriminatedUnion(
|
|
1111
|
+
shape,
|
|
1112
|
+
crdtValue,
|
|
1113
|
+
emptyValue
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1108
1116
|
return crdtValue ?? emptyValue;
|
|
1109
1117
|
}
|
|
1110
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
|
+
}
|
|
1111
1132
|
|
|
1112
1133
|
// src/validation.ts
|
|
1113
1134
|
function validateValue(value, schema, path = "") {
|
|
@@ -1309,6 +1330,15 @@ function validateEmptyState(emptyState, schema) {
|
|
|
1309
1330
|
|
|
1310
1331
|
// src/change.ts
|
|
1311
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
|
+
*/
|
|
1312
1342
|
constructor(shape, emptyState, doc = new LoroDoc()) {
|
|
1313
1343
|
this.shape = shape;
|
|
1314
1344
|
this.emptyState = emptyState;
|
|
@@ -1382,49 +1412,57 @@ var Shape = {
|
|
|
1382
1412
|
_type: "doc",
|
|
1383
1413
|
shapes: shape,
|
|
1384
1414
|
_plain: {},
|
|
1385
|
-
_draft: {}
|
|
1415
|
+
_draft: {},
|
|
1416
|
+
_emptyState: {}
|
|
1386
1417
|
}),
|
|
1387
1418
|
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
1388
1419
|
// various CRDT algorithms
|
|
1389
1420
|
counter: () => ({
|
|
1390
1421
|
_type: "counter",
|
|
1391
1422
|
_plain: 0,
|
|
1392
|
-
_draft: {}
|
|
1423
|
+
_draft: {},
|
|
1424
|
+
_emptyState: 0
|
|
1393
1425
|
}),
|
|
1394
1426
|
list: (shape) => ({
|
|
1395
1427
|
_type: "list",
|
|
1396
1428
|
shape,
|
|
1397
1429
|
_plain: [],
|
|
1398
|
-
_draft: {}
|
|
1430
|
+
_draft: {},
|
|
1431
|
+
_emptyState: []
|
|
1399
1432
|
}),
|
|
1400
1433
|
map: (shape) => ({
|
|
1401
1434
|
_type: "map",
|
|
1402
1435
|
shapes: shape,
|
|
1403
1436
|
_plain: {},
|
|
1404
|
-
_draft: {}
|
|
1437
|
+
_draft: {},
|
|
1438
|
+
_emptyState: {}
|
|
1405
1439
|
}),
|
|
1406
1440
|
record: (shape) => ({
|
|
1407
1441
|
_type: "record",
|
|
1408
1442
|
shape,
|
|
1409
1443
|
_plain: {},
|
|
1410
|
-
_draft: {}
|
|
1444
|
+
_draft: {},
|
|
1445
|
+
_emptyState: {}
|
|
1411
1446
|
}),
|
|
1412
1447
|
movableList: (shape) => ({
|
|
1413
1448
|
_type: "movableList",
|
|
1414
1449
|
shape,
|
|
1415
1450
|
_plain: [],
|
|
1416
|
-
_draft: {}
|
|
1451
|
+
_draft: {},
|
|
1452
|
+
_emptyState: []
|
|
1417
1453
|
}),
|
|
1418
1454
|
text: () => ({
|
|
1419
1455
|
_type: "text",
|
|
1420
1456
|
_plain: "",
|
|
1421
|
-
_draft: {}
|
|
1457
|
+
_draft: {},
|
|
1458
|
+
_emptyState: ""
|
|
1422
1459
|
}),
|
|
1423
1460
|
tree: (shape) => ({
|
|
1424
1461
|
_type: "tree",
|
|
1425
1462
|
shape,
|
|
1426
1463
|
_plain: {},
|
|
1427
|
-
_draft: {}
|
|
1464
|
+
_draft: {},
|
|
1465
|
+
_emptyState: []
|
|
1428
1466
|
}),
|
|
1429
1467
|
// Values are represented as plain JS objects, with the limitation that they MUST be
|
|
1430
1468
|
// representable as a Loro "Value"--basically JSON. The behavior of a Value is basically
|
|
@@ -1436,58 +1474,67 @@ var Shape = {
|
|
|
1436
1474
|
valueType: "string",
|
|
1437
1475
|
_plain: options[0] ?? "",
|
|
1438
1476
|
_draft: options[0] ?? "",
|
|
1477
|
+
_emptyState: options[0] ?? "",
|
|
1439
1478
|
options: options.length > 0 ? options : void 0
|
|
1440
1479
|
}),
|
|
1441
1480
|
number: () => ({
|
|
1442
1481
|
_type: "value",
|
|
1443
1482
|
valueType: "number",
|
|
1444
1483
|
_plain: 0,
|
|
1445
|
-
_draft: 0
|
|
1484
|
+
_draft: 0,
|
|
1485
|
+
_emptyState: 0
|
|
1446
1486
|
}),
|
|
1447
1487
|
boolean: () => ({
|
|
1448
1488
|
_type: "value",
|
|
1449
1489
|
valueType: "boolean",
|
|
1450
1490
|
_plain: false,
|
|
1451
|
-
_draft: false
|
|
1491
|
+
_draft: false,
|
|
1492
|
+
_emptyState: false
|
|
1452
1493
|
}),
|
|
1453
1494
|
null: () => ({
|
|
1454
1495
|
_type: "value",
|
|
1455
1496
|
valueType: "null",
|
|
1456
1497
|
_plain: null,
|
|
1457
|
-
_draft: null
|
|
1498
|
+
_draft: null,
|
|
1499
|
+
_emptyState: null
|
|
1458
1500
|
}),
|
|
1459
1501
|
undefined: () => ({
|
|
1460
1502
|
_type: "value",
|
|
1461
1503
|
valueType: "undefined",
|
|
1462
1504
|
_plain: void 0,
|
|
1463
|
-
_draft: void 0
|
|
1505
|
+
_draft: void 0,
|
|
1506
|
+
_emptyState: void 0
|
|
1464
1507
|
}),
|
|
1465
1508
|
uint8Array: () => ({
|
|
1466
1509
|
_type: "value",
|
|
1467
1510
|
valueType: "uint8array",
|
|
1468
1511
|
_plain: new Uint8Array(),
|
|
1469
|
-
_draft: new Uint8Array()
|
|
1512
|
+
_draft: new Uint8Array(),
|
|
1513
|
+
_emptyState: new Uint8Array()
|
|
1470
1514
|
}),
|
|
1471
1515
|
object: (shape) => ({
|
|
1472
1516
|
_type: "value",
|
|
1473
1517
|
valueType: "object",
|
|
1474
1518
|
shape,
|
|
1475
1519
|
_plain: {},
|
|
1476
|
-
_draft: {}
|
|
1520
|
+
_draft: {},
|
|
1521
|
+
_emptyState: {}
|
|
1477
1522
|
}),
|
|
1478
1523
|
record: (shape) => ({
|
|
1479
1524
|
_type: "value",
|
|
1480
1525
|
valueType: "record",
|
|
1481
1526
|
shape,
|
|
1482
1527
|
_plain: {},
|
|
1483
|
-
_draft: {}
|
|
1528
|
+
_draft: {},
|
|
1529
|
+
_emptyState: {}
|
|
1484
1530
|
}),
|
|
1485
1531
|
array: (shape) => ({
|
|
1486
1532
|
_type: "value",
|
|
1487
1533
|
valueType: "array",
|
|
1488
1534
|
shape,
|
|
1489
1535
|
_plain: [],
|
|
1490
|
-
_draft: []
|
|
1536
|
+
_draft: [],
|
|
1537
|
+
_emptyState: []
|
|
1491
1538
|
}),
|
|
1492
1539
|
// Special value type that helps make things like `string | null` representable
|
|
1493
1540
|
// TODO(duane): should this be a more general type for containers too?
|
|
@@ -1496,7 +1543,43 @@ var Shape = {
|
|
|
1496
1543
|
valueType: "union",
|
|
1497
1544
|
shapes,
|
|
1498
1545
|
_plain: {},
|
|
1499
|
-
_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: {}
|
|
1500
1583
|
})
|
|
1501
1584
|
}
|
|
1502
1585
|
};
|