@loro-extended/change 0.6.0 → 0.8.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 +26 -31
- package/dist/index.d.ts +115 -53
- package/dist/index.js +743 -393
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +97 -403
- package/src/derive-placeholder.test.ts +245 -0
- package/src/derive-placeholder.ts +132 -0
- package/src/discriminated-union.test.ts +74 -1
- package/src/draft-nodes/base.ts +9 -4
- package/src/draft-nodes/counter.md +31 -0
- package/src/draft-nodes/counter.test.ts +53 -0
- package/src/draft-nodes/doc.ts +27 -6
- package/src/draft-nodes/list-base.ts +24 -3
- package/src/draft-nodes/list.test.ts +43 -0
- package/src/draft-nodes/list.ts +3 -0
- package/src/draft-nodes/map.ts +57 -16
- package/src/draft-nodes/movable-list.test.ts +27 -0
- package/src/draft-nodes/movable-list.ts +5 -0
- package/src/draft-nodes/proxy-handlers.ts +87 -0
- package/src/{record.test.ts → draft-nodes/record.test.ts} +98 -18
- package/src/draft-nodes/record.ts +42 -80
- package/src/draft-nodes/utils.ts +46 -5
- package/src/equality.test.ts +19 -0
- package/src/index.ts +14 -7
- package/src/json-patch.test.ts +33 -167
- package/src/overlay.ts +46 -39
- package/src/readonly.test.ts +92 -0
- package/src/shape.ts +131 -73
- package/src/{change.ts → typed-doc.ts} +50 -28
- package/src/types.test.ts +188 -0
- package/src/types.ts +37 -5
- package/src/utils/type-guards.ts +1 -0
- package/src/validation.ts +45 -12
package/src/shape.ts
CHANGED
|
@@ -16,11 +16,16 @@ import type { MovableListDraftNode } from "./draft-nodes/movable-list.js"
|
|
|
16
16
|
import type { RecordDraftNode } from "./draft-nodes/record.js"
|
|
17
17
|
import type { TextDraftNode } from "./draft-nodes/text.js"
|
|
18
18
|
|
|
19
|
-
export interface Shape<Plain, Draft,
|
|
19
|
+
export interface Shape<Plain, Draft, Placeholder = Plain> {
|
|
20
20
|
readonly _type: string
|
|
21
21
|
readonly _plain: Plain
|
|
22
22
|
readonly _draft: Draft
|
|
23
|
-
readonly
|
|
23
|
+
readonly _placeholder: Placeholder
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Type for shapes that support placeholder customization
|
|
27
|
+
export type WithPlaceholder<S extends Shape<any, any, any>> = S & {
|
|
28
|
+
placeholder(value: S["_placeholder"]): S
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export interface DocShape<
|
|
@@ -31,7 +36,7 @@ export interface DocShape<
|
|
|
31
36
|
> extends Shape<
|
|
32
37
|
{ [K in keyof NestedShapes]: NestedShapes[K]["_plain"] },
|
|
33
38
|
{ [K in keyof NestedShapes]: NestedShapes[K]["_draft"] },
|
|
34
|
-
{ [K in keyof NestedShapes]: NestedShapes[K]["
|
|
39
|
+
{ [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"] }
|
|
35
40
|
> {
|
|
36
41
|
readonly _type: "doc"
|
|
37
42
|
// A doc's root containers each separately has its own shape, hence 'shapes'
|
|
@@ -54,8 +59,8 @@ export interface TreeContainerShape<NestedShape = ContainerOrValueShape>
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// Container schemas using interfaces for recursive references
|
|
57
|
-
// NOTE: List and Record use never[] and Record<string, never> for
|
|
58
|
-
// to enforce that only empty values ([] and {}) are valid in
|
|
62
|
+
// NOTE: List and Record use never[] and Record<string, never> for Placeholder
|
|
63
|
+
// to enforce that only empty values ([] and {}) are valid in placeholder state.
|
|
59
64
|
// This prevents users from expecting per-entry merging behavior.
|
|
60
65
|
export interface ListContainerShape<
|
|
61
66
|
NestedShape extends ContainerOrValueShape = ContainerOrValueShape,
|
|
@@ -87,7 +92,7 @@ export interface MapContainerShape<
|
|
|
87
92
|
MapDraftNode<NestedShapes> & {
|
|
88
93
|
[K in keyof NestedShapes]: NestedShapes[K]["_draft"]
|
|
89
94
|
},
|
|
90
|
-
{ [K in keyof NestedShapes]: NestedShapes[K]["
|
|
95
|
+
{ [K in keyof NestedShapes]: NestedShapes[K]["_placeholder"] }
|
|
91
96
|
> {
|
|
92
97
|
readonly _type: "map"
|
|
93
98
|
// Each map property has its own shape, hence 'shapes'
|
|
@@ -151,7 +156,7 @@ export interface ObjectValueShape<
|
|
|
151
156
|
> extends Shape<
|
|
152
157
|
{ [K in keyof T]: T[K]["_plain"] },
|
|
153
158
|
{ [K in keyof T]: T[K]["_draft"] },
|
|
154
|
-
{ [K in keyof T]: T[K]["
|
|
159
|
+
{ [K in keyof T]: T[K]["_placeholder"] }
|
|
155
160
|
> {
|
|
156
161
|
readonly _type: "value"
|
|
157
162
|
readonly valueType: "object"
|
|
@@ -159,7 +164,7 @@ export interface ObjectValueShape<
|
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
// NOTE: RecordValueShape and ArrayValueShape use Record<string, never> and never[]
|
|
162
|
-
// for
|
|
167
|
+
// for Placeholder to enforce that only empty values ({} and []) are valid.
|
|
163
168
|
export interface RecordValueShape<T extends ValueShape = ValueShape>
|
|
164
169
|
extends Shape<
|
|
165
170
|
Record<string, T["_plain"]>,
|
|
@@ -182,7 +187,7 @@ export interface UnionValueShape<T extends ValueShape[] = ValueShape[]>
|
|
|
182
187
|
extends Shape<
|
|
183
188
|
T[number]["_plain"],
|
|
184
189
|
T[number]["_draft"],
|
|
185
|
-
T[number]["
|
|
190
|
+
T[number]["_placeholder"]
|
|
186
191
|
> {
|
|
187
192
|
readonly _type: "value"
|
|
188
193
|
readonly valueType: "union"
|
|
@@ -208,7 +213,7 @@ export interface DiscriminatedUnionValueShape<
|
|
|
208
213
|
> extends Shape<
|
|
209
214
|
T[keyof T]["_plain"],
|
|
210
215
|
T[keyof T]["_draft"],
|
|
211
|
-
T[keyof T]["
|
|
216
|
+
T[keyof T]["_placeholder"]
|
|
212
217
|
> {
|
|
213
218
|
readonly _type: "value"
|
|
214
219
|
readonly valueType: "discriminatedUnion"
|
|
@@ -245,24 +250,31 @@ export const Shape = {
|
|
|
245
250
|
shapes: shape,
|
|
246
251
|
_plain: {} as any,
|
|
247
252
|
_draft: {} as any,
|
|
248
|
-
|
|
253
|
+
_placeholder: {} as any,
|
|
249
254
|
}),
|
|
250
255
|
|
|
251
256
|
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
252
257
|
// various CRDT algorithms
|
|
253
|
-
counter: (): CounterContainerShape =>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
counter: (): WithPlaceholder<CounterContainerShape> => {
|
|
259
|
+
const base: CounterContainerShape = {
|
|
260
|
+
_type: "counter" as const,
|
|
261
|
+
_plain: 0,
|
|
262
|
+
_draft: {} as CounterDraftNode,
|
|
263
|
+
_placeholder: 0,
|
|
264
|
+
}
|
|
265
|
+
return Object.assign(base, {
|
|
266
|
+
placeholder(value: number): CounterContainerShape {
|
|
267
|
+
return { ...base, _placeholder: value }
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
},
|
|
259
271
|
|
|
260
272
|
list: <T extends ContainerOrValueShape>(shape: T): ListContainerShape<T> => ({
|
|
261
273
|
_type: "list" as const,
|
|
262
274
|
shape,
|
|
263
275
|
_plain: [] as any,
|
|
264
276
|
_draft: {} as any,
|
|
265
|
-
|
|
277
|
+
_placeholder: [] as never[],
|
|
266
278
|
}),
|
|
267
279
|
|
|
268
280
|
map: <T extends Record<string, ContainerOrValueShape>>(
|
|
@@ -272,7 +284,7 @@ export const Shape = {
|
|
|
272
284
|
shapes: shape,
|
|
273
285
|
_plain: {} as any,
|
|
274
286
|
_draft: {} as any,
|
|
275
|
-
|
|
287
|
+
_placeholder: {} as any,
|
|
276
288
|
}),
|
|
277
289
|
|
|
278
290
|
record: <T extends ContainerOrValueShape>(
|
|
@@ -282,7 +294,7 @@ export const Shape = {
|
|
|
282
294
|
shape,
|
|
283
295
|
_plain: {} as any,
|
|
284
296
|
_draft: {} as any,
|
|
285
|
-
|
|
297
|
+
_placeholder: {} as Record<string, never>,
|
|
286
298
|
}),
|
|
287
299
|
|
|
288
300
|
movableList: <T extends ContainerOrValueShape>(
|
|
@@ -292,22 +304,29 @@ export const Shape = {
|
|
|
292
304
|
shape,
|
|
293
305
|
_plain: [] as any,
|
|
294
306
|
_draft: {} as any,
|
|
295
|
-
|
|
307
|
+
_placeholder: [] as never[],
|
|
296
308
|
}),
|
|
297
309
|
|
|
298
|
-
text: (): TextContainerShape =>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
text: (): WithPlaceholder<TextContainerShape> => {
|
|
311
|
+
const base: TextContainerShape = {
|
|
312
|
+
_type: "text" as const,
|
|
313
|
+
_plain: "",
|
|
314
|
+
_draft: {} as TextDraftNode,
|
|
315
|
+
_placeholder: "",
|
|
316
|
+
}
|
|
317
|
+
return Object.assign(base, {
|
|
318
|
+
placeholder(value: string): TextContainerShape {
|
|
319
|
+
return { ...base, _placeholder: value }
|
|
320
|
+
},
|
|
321
|
+
})
|
|
322
|
+
},
|
|
304
323
|
|
|
305
324
|
tree: <T extends MapContainerShape>(shape: T): TreeContainerShape => ({
|
|
306
325
|
_type: "tree" as const,
|
|
307
326
|
shape,
|
|
308
327
|
_plain: {} as any,
|
|
309
328
|
_draft: {} as any,
|
|
310
|
-
|
|
329
|
+
_placeholder: [] as never[],
|
|
311
330
|
}),
|
|
312
331
|
|
|
313
332
|
// Values are represented as plain JS objects, with the limitation that they MUST be
|
|
@@ -317,37 +336,58 @@ export const Shape = {
|
|
|
317
336
|
plain: {
|
|
318
337
|
string: <T extends string = string>(
|
|
319
338
|
...options: T[]
|
|
320
|
-
): StringValueShape<T
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
339
|
+
): WithPlaceholder<StringValueShape<T>> => {
|
|
340
|
+
const base: StringValueShape<T> = {
|
|
341
|
+
_type: "value" as const,
|
|
342
|
+
valueType: "string" as const,
|
|
343
|
+
_plain: (options[0] ?? "") as T,
|
|
344
|
+
_draft: (options[0] ?? "") as T,
|
|
345
|
+
_placeholder: (options[0] ?? "") as T,
|
|
346
|
+
options: options.length > 0 ? options : undefined,
|
|
347
|
+
}
|
|
348
|
+
return Object.assign(base, {
|
|
349
|
+
placeholder(value: T): StringValueShape<T> {
|
|
350
|
+
return { ...base, _placeholder: value }
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
},
|
|
328
354
|
|
|
329
|
-
number: (): NumberValueShape =>
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
355
|
+
number: (): WithPlaceholder<NumberValueShape> => {
|
|
356
|
+
const base: NumberValueShape = {
|
|
357
|
+
_type: "value" as const,
|
|
358
|
+
valueType: "number" as const,
|
|
359
|
+
_plain: 0,
|
|
360
|
+
_draft: 0,
|
|
361
|
+
_placeholder: 0,
|
|
362
|
+
}
|
|
363
|
+
return Object.assign(base, {
|
|
364
|
+
placeholder(value: number): NumberValueShape {
|
|
365
|
+
return { ...base, _placeholder: value }
|
|
366
|
+
},
|
|
367
|
+
})
|
|
368
|
+
},
|
|
336
369
|
|
|
337
|
-
boolean: (): BooleanValueShape =>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
370
|
+
boolean: (): WithPlaceholder<BooleanValueShape> => {
|
|
371
|
+
const base: BooleanValueShape = {
|
|
372
|
+
_type: "value" as const,
|
|
373
|
+
valueType: "boolean" as const,
|
|
374
|
+
_plain: false,
|
|
375
|
+
_draft: false,
|
|
376
|
+
_placeholder: false,
|
|
377
|
+
}
|
|
378
|
+
return Object.assign(base, {
|
|
379
|
+
placeholder(value: boolean): BooleanValueShape {
|
|
380
|
+
return { ...base, _placeholder: value }
|
|
381
|
+
},
|
|
382
|
+
})
|
|
383
|
+
},
|
|
344
384
|
|
|
345
385
|
null: (): NullValueShape => ({
|
|
346
386
|
_type: "value" as const,
|
|
347
387
|
valueType: "null" as const,
|
|
348
388
|
_plain: null,
|
|
349
389
|
_draft: null,
|
|
350
|
-
|
|
390
|
+
_placeholder: null,
|
|
351
391
|
}),
|
|
352
392
|
|
|
353
393
|
undefined: (): UndefinedValueShape => ({
|
|
@@ -355,7 +395,7 @@ export const Shape = {
|
|
|
355
395
|
valueType: "undefined" as const,
|
|
356
396
|
_plain: undefined,
|
|
357
397
|
_draft: undefined,
|
|
358
|
-
|
|
398
|
+
_placeholder: undefined,
|
|
359
399
|
}),
|
|
360
400
|
|
|
361
401
|
uint8Array: (): Uint8ArrayValueShape => ({
|
|
@@ -363,7 +403,7 @@ export const Shape = {
|
|
|
363
403
|
valueType: "uint8array" as const,
|
|
364
404
|
_plain: new Uint8Array(),
|
|
365
405
|
_draft: new Uint8Array(),
|
|
366
|
-
|
|
406
|
+
_placeholder: new Uint8Array(),
|
|
367
407
|
}),
|
|
368
408
|
|
|
369
409
|
object: <T extends Record<string, ValueShape>>(
|
|
@@ -374,7 +414,7 @@ export const Shape = {
|
|
|
374
414
|
shape,
|
|
375
415
|
_plain: {} as any,
|
|
376
416
|
_draft: {} as any,
|
|
377
|
-
|
|
417
|
+
_placeholder: {} as any,
|
|
378
418
|
}),
|
|
379
419
|
|
|
380
420
|
record: <T extends ValueShape>(shape: T): RecordValueShape<T> => ({
|
|
@@ -383,7 +423,7 @@ export const Shape = {
|
|
|
383
423
|
shape,
|
|
384
424
|
_plain: {} as any,
|
|
385
425
|
_draft: {} as any,
|
|
386
|
-
|
|
426
|
+
_placeholder: {} as Record<string, never>,
|
|
387
427
|
}),
|
|
388
428
|
|
|
389
429
|
array: <T extends ValueShape>(shape: T): ArrayValueShape<T> => ({
|
|
@@ -392,19 +432,28 @@ export const Shape = {
|
|
|
392
432
|
shape,
|
|
393
433
|
_plain: [] as any,
|
|
394
434
|
_draft: [] as any,
|
|
395
|
-
|
|
435
|
+
_placeholder: [] as never[],
|
|
396
436
|
}),
|
|
397
437
|
|
|
398
438
|
// Special value type that helps make things like `string | null` representable
|
|
399
439
|
// TODO(duane): should this be a more general type for containers too?
|
|
400
|
-
union: <T extends ValueShape[]>(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
440
|
+
union: <T extends ValueShape[]>(
|
|
441
|
+
shapes: T,
|
|
442
|
+
): WithPlaceholder<UnionValueShape<T>> => {
|
|
443
|
+
const base: UnionValueShape<T> = {
|
|
444
|
+
_type: "value" as const,
|
|
445
|
+
valueType: "union" as const,
|
|
446
|
+
shapes,
|
|
447
|
+
_plain: {} as any,
|
|
448
|
+
_draft: {} as any,
|
|
449
|
+
_placeholder: {} as any,
|
|
450
|
+
}
|
|
451
|
+
return Object.assign(base, {
|
|
452
|
+
placeholder(value: T[number]["_placeholder"]): UnionValueShape<T> {
|
|
453
|
+
return { ...base, _placeholder: value }
|
|
454
|
+
},
|
|
455
|
+
})
|
|
456
|
+
},
|
|
408
457
|
|
|
409
458
|
/**
|
|
410
459
|
* Creates a discriminated union shape for type-safe tagged unions.
|
|
@@ -438,15 +487,24 @@ export const Shape = {
|
|
|
438
487
|
>(
|
|
439
488
|
discriminantKey: K,
|
|
440
489
|
variants: T,
|
|
441
|
-
): DiscriminatedUnionValueShape<K, T
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
490
|
+
): WithPlaceholder<DiscriminatedUnionValueShape<K, T>> => {
|
|
491
|
+
const base: DiscriminatedUnionValueShape<K, T> = {
|
|
492
|
+
_type: "value" as const,
|
|
493
|
+
valueType: "discriminatedUnion" as const,
|
|
494
|
+
discriminantKey,
|
|
495
|
+
variants,
|
|
496
|
+
_plain: {} as any,
|
|
497
|
+
_draft: {} as any,
|
|
498
|
+
_placeholder: {} as any,
|
|
499
|
+
}
|
|
500
|
+
return Object.assign(base, {
|
|
501
|
+
placeholder(
|
|
502
|
+
value: T[keyof T]["_placeholder"],
|
|
503
|
+
): DiscriminatedUnionValueShape<K, T> {
|
|
504
|
+
return { ...base, _placeholder: value }
|
|
505
|
+
},
|
|
506
|
+
})
|
|
507
|
+
},
|
|
450
508
|
},
|
|
451
509
|
}
|
|
452
510
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** biome-ignore-all lint/suspicious/noExplicitAny: fix later */
|
|
2
2
|
|
|
3
3
|
import { LoroDoc } from "loro-crdt"
|
|
4
|
+
import { derivePlaceholder } from "./derive-placeholder.js"
|
|
4
5
|
import { DraftDoc } from "./draft-nodes/doc.js"
|
|
5
6
|
import {
|
|
6
7
|
type JsonPatch,
|
|
@@ -8,50 +9,75 @@ import {
|
|
|
8
9
|
type JsonPatchOperation,
|
|
9
10
|
normalizePath,
|
|
10
11
|
} from "./json-patch.js"
|
|
11
|
-
import {
|
|
12
|
+
import { overlayPlaceholder } from "./overlay.js"
|
|
12
13
|
import type { DocShape } from "./shape.js"
|
|
13
|
-
import type {
|
|
14
|
-
|
|
14
|
+
import type {
|
|
15
|
+
DeepReadonly,
|
|
16
|
+
Draft,
|
|
17
|
+
Infer,
|
|
18
|
+
InferPlaceholderType,
|
|
19
|
+
} from "./types.js"
|
|
20
|
+
import { validatePlaceholder } from "./validation.js"
|
|
15
21
|
|
|
16
22
|
// Core TypedDoc abstraction around LoroDoc
|
|
17
23
|
export class TypedDoc<Shape extends DocShape> {
|
|
24
|
+
private shape: Shape
|
|
25
|
+
private placeholder: InferPlaceholderType<Shape>
|
|
26
|
+
private doc: LoroDoc
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
|
-
* Creates a new TypedDoc with the given schema
|
|
29
|
+
* Creates a new TypedDoc with the given schema.
|
|
30
|
+
* Placeholder state is automatically derived from the schema's placeholder values.
|
|
20
31
|
*
|
|
21
|
-
* @param shape - The document schema
|
|
22
|
-
* @param emptyState - Default values for the document. For dynamic containers
|
|
23
|
-
* (list, record, etc.), only empty values ([] or {}) are allowed. Use
|
|
24
|
-
* `.change()` to add initial data after construction.
|
|
32
|
+
* @param shape - The document schema (with optional .placeholder() values)
|
|
25
33
|
* @param doc - Optional existing LoroDoc to wrap
|
|
26
34
|
*/
|
|
27
|
-
constructor(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
constructor(shape: Shape, doc: LoroDoc = new LoroDoc()) {
|
|
36
|
+
this.shape = shape
|
|
37
|
+
this.placeholder = derivePlaceholder(shape)
|
|
38
|
+
this.doc = doc
|
|
39
|
+
|
|
40
|
+
validatePlaceholder(this.placeholder, this.shape)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns a read-only, live view of the document.
|
|
45
|
+
* Accessing properties on this object will read directly from the underlying CRDT.
|
|
46
|
+
* This is efficient (O(1) per access) and always up-to-date.
|
|
47
|
+
*/
|
|
48
|
+
get value(): DeepReadonly<Infer<Shape>> {
|
|
49
|
+
return new DraftDoc({
|
|
50
|
+
shape: this.shape,
|
|
51
|
+
placeholder: this.placeholder as any,
|
|
52
|
+
doc: this.doc,
|
|
53
|
+
readonly: true,
|
|
54
|
+
}) as unknown as DeepReadonly<Infer<Shape>>
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Returns the full plain JavaScript object representation of the document.
|
|
59
|
+
* This is an expensive O(N) operation that serializes the entire document.
|
|
60
|
+
*/
|
|
61
|
+
toJSON(): Infer<Shape> {
|
|
36
62
|
const crdtValue = this.doc.toJSON()
|
|
37
|
-
return
|
|
63
|
+
return overlayPlaceholder(
|
|
38
64
|
this.shape,
|
|
39
65
|
crdtValue,
|
|
40
|
-
this.
|
|
41
|
-
) as
|
|
66
|
+
this.placeholder as any,
|
|
67
|
+
) as Infer<Shape>
|
|
42
68
|
}
|
|
43
69
|
|
|
44
|
-
change(fn: (draft: Draft<Shape>) => void):
|
|
45
|
-
// Reuse existing DocumentDraft system with
|
|
70
|
+
change(fn: (draft: Draft<Shape>) => void): Infer<Shape> {
|
|
71
|
+
// Reuse existing DocumentDraft system with placeholder integration
|
|
46
72
|
const draft = new DraftDoc({
|
|
47
73
|
shape: this.shape,
|
|
48
|
-
|
|
74
|
+
placeholder: this.placeholder as any,
|
|
49
75
|
doc: this.doc,
|
|
50
76
|
})
|
|
51
77
|
fn(draft as unknown as Draft<Shape>)
|
|
52
78
|
draft.absorbPlainValues()
|
|
53
79
|
this.doc.commit()
|
|
54
|
-
return this.
|
|
80
|
+
return this.toJSON()
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
/**
|
|
@@ -69,10 +95,7 @@ export class TypedDoc<Shape extends DocShape> {
|
|
|
69
95
|
* ])
|
|
70
96
|
* ```
|
|
71
97
|
*/
|
|
72
|
-
applyPatch(
|
|
73
|
-
patch: JsonPatch,
|
|
74
|
-
pathPrefix?: (string | number)[],
|
|
75
|
-
): InferPlainType<Shape> {
|
|
98
|
+
applyPatch(patch: JsonPatch, pathPrefix?: (string | number)[]): Infer<Shape> {
|
|
76
99
|
return this.change(draft => {
|
|
77
100
|
const applicator = new JsonPatchApplicator(draft)
|
|
78
101
|
|
|
@@ -107,8 +130,7 @@ export class TypedDoc<Shape extends DocShape> {
|
|
|
107
130
|
// Factory function for TypedLoroDoc
|
|
108
131
|
export function createTypedDoc<Shape extends DocShape>(
|
|
109
132
|
shape: Shape,
|
|
110
|
-
emptyState: InferEmptyStateType<Shape>,
|
|
111
133
|
existingDoc?: LoroDoc,
|
|
112
134
|
): TypedDoc<Shape> {
|
|
113
|
-
return new TypedDoc<Shape>(shape,
|
|
135
|
+
return new TypedDoc<Shape>(shape, existingDoc || new LoroDoc())
|
|
114
136
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from "vitest"
|
|
2
|
+
import type { ContainerShape, ValueShape } from "./shape.js"
|
|
3
|
+
import { Shape } from "./shape.js"
|
|
4
|
+
import type { Infer } from "./types.js"
|
|
5
|
+
|
|
6
|
+
describe("Infer type helper", () => {
|
|
7
|
+
it("infers DocShape plain type", () => {
|
|
8
|
+
const schema = Shape.doc({
|
|
9
|
+
title: Shape.text(),
|
|
10
|
+
count: Shape.counter(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
type Result = Infer<typeof schema>
|
|
14
|
+
expectTypeOf<Result>().toEqualTypeOf<{ title: string; count: number }>()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("infers ContainerShape plain type (list)", () => {
|
|
18
|
+
const schema = Shape.list(Shape.plain.string())
|
|
19
|
+
|
|
20
|
+
type Result = Infer<typeof schema>
|
|
21
|
+
expectTypeOf<Result>().toEqualTypeOf<string[]>()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("infers ValueShape plain type (object)", () => {
|
|
25
|
+
const schema = Shape.plain.object({
|
|
26
|
+
name: Shape.plain.string(),
|
|
27
|
+
age: Shape.plain.number(),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
type Result = Infer<typeof schema>
|
|
31
|
+
expectTypeOf<Result>().toEqualTypeOf<{ name: string; age: number }>()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("infers nested shapes", () => {
|
|
35
|
+
const schema = Shape.doc({
|
|
36
|
+
users: Shape.list(
|
|
37
|
+
Shape.map({
|
|
38
|
+
id: Shape.plain.string(),
|
|
39
|
+
profile: Shape.record(
|
|
40
|
+
Shape.plain.object({
|
|
41
|
+
bio: Shape.plain.string(),
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
type Result = Infer<typeof schema>
|
|
49
|
+
expectTypeOf<Result>().toEqualTypeOf<{
|
|
50
|
+
users: {
|
|
51
|
+
id: string
|
|
52
|
+
profile: Record<string, { bio: string }>
|
|
53
|
+
}[]
|
|
54
|
+
}>()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("infers discriminated union plain type", () => {
|
|
58
|
+
const SessionStatusSchema = Shape.plain.discriminatedUnion("status", {
|
|
59
|
+
not_started: Shape.plain.object({
|
|
60
|
+
status: Shape.plain.string("not_started"),
|
|
61
|
+
}),
|
|
62
|
+
lobby: Shape.plain.object({
|
|
63
|
+
status: Shape.plain.string("lobby"),
|
|
64
|
+
lobbyPhase: Shape.plain.string("preparing", "typing"),
|
|
65
|
+
}),
|
|
66
|
+
active: Shape.plain.object({
|
|
67
|
+
status: Shape.plain.string("active"),
|
|
68
|
+
mode: Shape.plain.string("solo", "group"),
|
|
69
|
+
}),
|
|
70
|
+
paused: Shape.plain.object({
|
|
71
|
+
status: Shape.plain.string("paused"),
|
|
72
|
+
previousStatus: Shape.plain.string("lobby", "active"),
|
|
73
|
+
previousMode: Shape.plain.string("solo", "group"),
|
|
74
|
+
reason: Shape.plain.string(
|
|
75
|
+
"no_students",
|
|
76
|
+
"teacher_paused",
|
|
77
|
+
"assignment_empty",
|
|
78
|
+
),
|
|
79
|
+
}),
|
|
80
|
+
ended: Shape.plain.object({
|
|
81
|
+
status: Shape.plain.string<"ended">("ended"),
|
|
82
|
+
}),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
type Result = Infer<typeof SessionStatusSchema>
|
|
86
|
+
|
|
87
|
+
// The result should be a union of all variant types
|
|
88
|
+
type Expected =
|
|
89
|
+
| { status: "not_started" }
|
|
90
|
+
| { status: "lobby"; lobbyPhase: "preparing" | "typing" }
|
|
91
|
+
| { status: "active"; mode: "solo" | "group" }
|
|
92
|
+
| {
|
|
93
|
+
status: "paused"
|
|
94
|
+
previousStatus: "lobby" | "active"
|
|
95
|
+
previousMode: "solo" | "group"
|
|
96
|
+
reason: "no_students" | "teacher_paused" | "assignment_empty"
|
|
97
|
+
}
|
|
98
|
+
| { status: "ended" }
|
|
99
|
+
|
|
100
|
+
expectTypeOf<Result>().toEqualTypeOf<Expected>()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("infers discriminated union inside a map container", () => {
|
|
104
|
+
const SessionStatusSchema = Shape.plain.discriminatedUnion("status", {
|
|
105
|
+
not_started: Shape.plain.object({
|
|
106
|
+
status: Shape.plain.string("not_started"),
|
|
107
|
+
}),
|
|
108
|
+
active: Shape.plain.object({
|
|
109
|
+
status: Shape.plain.string("active"),
|
|
110
|
+
mode: Shape.plain.string("solo", "group"),
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const SessionMetadataSchema = Shape.map({
|
|
115
|
+
sessionStartedAt: Shape.plain.number(),
|
|
116
|
+
sessionStatus: SessionStatusSchema,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
type Result = Infer<typeof SessionMetadataSchema>
|
|
120
|
+
|
|
121
|
+
type Expected = {
|
|
122
|
+
sessionStartedAt: number
|
|
123
|
+
sessionStatus:
|
|
124
|
+
| { status: "not_started" }
|
|
125
|
+
| { status: "active"; mode: "solo" | "group" }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
expectTypeOf<Result>().toEqualTypeOf<Expected>()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("infers discriminated union inside a doc", () => {
|
|
132
|
+
const SessionStatusSchema = Shape.plain.discriminatedUnion("status", {
|
|
133
|
+
not_started: Shape.plain.object({
|
|
134
|
+
status: Shape.plain.string("not_started"),
|
|
135
|
+
}),
|
|
136
|
+
active: Shape.plain.object({
|
|
137
|
+
status: Shape.plain.string("active"),
|
|
138
|
+
}),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const DocSchema = Shape.doc({
|
|
142
|
+
metadata: Shape.map({
|
|
143
|
+
sessionStartedAt: Shape.plain.number(),
|
|
144
|
+
sessionStatus: SessionStatusSchema,
|
|
145
|
+
}),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
type Result = Infer<typeof DocSchema>
|
|
149
|
+
|
|
150
|
+
type Expected = {
|
|
151
|
+
metadata: {
|
|
152
|
+
sessionStartedAt: number
|
|
153
|
+
sessionStatus: { status: "not_started" } | { status: "active" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
expectTypeOf<Result>().toEqualTypeOf<Expected>()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("infers discriminated union type correctly when used with generic constraint", () => {
|
|
161
|
+
// This test verifies the fix for usePresence type inference
|
|
162
|
+
// The issue was that DiscriminatedUnionValueShape<any, any> in the ValueShape union
|
|
163
|
+
// caused type information to be lost when inferring through generic constraints
|
|
164
|
+
const ClientPresenceShape = Shape.plain.object({
|
|
165
|
+
type: Shape.plain.string("client"),
|
|
166
|
+
name: Shape.plain.string(),
|
|
167
|
+
})
|
|
168
|
+
const ServerPresenceShape = Shape.plain.object({
|
|
169
|
+
type: Shape.plain.string("server"),
|
|
170
|
+
tick: Shape.plain.number(),
|
|
171
|
+
})
|
|
172
|
+
const GamePresenceSchema = Shape.plain.discriminatedUnion("type", {
|
|
173
|
+
client: ClientPresenceShape,
|
|
174
|
+
server: ServerPresenceShape,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Simulate the constraint used in usePresence: <S extends ContainerShape | ValueShape>
|
|
178
|
+
type TestInfer<S extends ContainerShape | ValueShape> = Infer<S>
|
|
179
|
+
|
|
180
|
+
type Result = TestInfer<typeof GamePresenceSchema>
|
|
181
|
+
|
|
182
|
+
type Expected =
|
|
183
|
+
| { type: "client"; name: string }
|
|
184
|
+
| { type: "server"; tick: number }
|
|
185
|
+
|
|
186
|
+
expectTypeOf<Result>().toEqualTypeOf<Expected>()
|
|
187
|
+
})
|
|
188
|
+
})
|