@loro-extended/change 1.0.1 → 2.0.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 +165 -46
- package/dist/index.d.ts +206 -101
- package/dist/index.js +363 -109
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/any-shape.test.ts +164 -0
- package/src/change.test.ts +255 -2
- package/src/derive-placeholder.ts +8 -0
- package/src/index.ts +15 -2
- package/src/overlay.ts +10 -0
- package/src/path-builder.ts +131 -0
- package/src/path-compiler.ts +64 -0
- package/src/path-evaluator.ts +76 -0
- package/src/path-selector.test.ts +322 -0
- package/src/path-selector.ts +131 -0
- package/src/readonly.test.ts +5 -4
- package/src/shape.ts +256 -40
- package/src/typed-refs/base.ts +6 -0
- package/src/typed-refs/counter.test.ts +2 -1
- package/src/typed-refs/doc.ts +13 -2
- package/src/typed-refs/json-compatibility.test.ts +27 -0
- package/src/typed-refs/list-base.ts +1 -1
- package/src/typed-refs/list.test.ts +1 -1
- package/src/typed-refs/list.ts +5 -2
- package/src/typed-refs/movable-list.test.ts +1 -1
- package/src/typed-refs/movable-list.ts +2 -2
- package/src/typed-refs/record.ts +11 -2
- package/src/typed-refs/struct.ts +9 -0
- package/src/typed-refs/tree.ts +6 -0
- package/src/typed-refs/utils.ts +13 -0
- package/src/validation.ts +9 -0
- package/src/presence-interface.ts +0 -52
- package/src/typed-presence.ts +0 -96
package/src/shape.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
LoroMovableList,
|
|
8
8
|
LoroText,
|
|
9
9
|
LoroTree,
|
|
10
|
+
Value,
|
|
10
11
|
} from "loro-crdt"
|
|
11
12
|
|
|
12
13
|
import type { CounterRef } from "./typed-refs/counter.js"
|
|
@@ -28,6 +29,14 @@ export type WithPlaceholder<S extends Shape<any, any, any>> = S & {
|
|
|
28
29
|
placeholder(value: S["_placeholder"]): S
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Type for value shapes that support the .nullable() method.
|
|
34
|
+
* Returns a union of null and the original shape with null as the default placeholder.
|
|
35
|
+
*/
|
|
36
|
+
export type WithNullable<S extends ValueShape> = {
|
|
37
|
+
nullable(): WithPlaceholder<UnionValueShape<[NullValueShape, S]>>
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
export interface DocShape<
|
|
32
41
|
NestedShapes extends Record<string, ContainerShape> = Record<
|
|
33
42
|
string,
|
|
@@ -120,7 +129,24 @@ export interface RecordContainerShape<
|
|
|
120
129
|
readonly shape: NestedShape
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Container escape hatch - represents "any LoroContainer".
|
|
134
|
+
* Use this when integrating with external libraries that manage their own document structure.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* // loro-prosemirror manages its own structure
|
|
139
|
+
* const ProseMirrorDocShape = Shape.doc({
|
|
140
|
+
* doc: Shape.any(), // opt out of typing for this container
|
|
141
|
+
* })
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
export interface AnyContainerShape extends Shape<unknown, unknown, undefined> {
|
|
145
|
+
readonly _type: "any"
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
export type ContainerShape =
|
|
149
|
+
| AnyContainerShape
|
|
124
150
|
| CounterContainerShape
|
|
125
151
|
| ListContainerShape
|
|
126
152
|
| MovableListContainerShape
|
|
@@ -242,8 +268,25 @@ export interface DiscriminatedUnionValueShape<
|
|
|
242
268
|
readonly variants: T
|
|
243
269
|
}
|
|
244
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Value escape hatch - represents "any Loro Value".
|
|
273
|
+
* Use this when you need to accept any valid Loro value type.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* const FlexiblePresenceShape = Shape.plain.struct({
|
|
278
|
+
* cursor: Shape.plain.any(), // accept any value type
|
|
279
|
+
* })
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
export interface AnyValueShape extends Shape<Value, Value, undefined> {
|
|
283
|
+
readonly _type: "value"
|
|
284
|
+
readonly valueType: "any"
|
|
285
|
+
}
|
|
286
|
+
|
|
245
287
|
// Union of all ValueShapes - these can only contain other ValueShapes, not ContainerShapes
|
|
246
288
|
export type ValueShape =
|
|
289
|
+
| AnyValueShape
|
|
247
290
|
| StringValueShape
|
|
248
291
|
| NumberValueShape
|
|
249
292
|
| BooleanValueShape
|
|
@@ -258,6 +301,41 @@ export type ValueShape =
|
|
|
258
301
|
|
|
259
302
|
export type ContainerOrValueShape = ContainerShape | ValueShape
|
|
260
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Creates a nullable version of a value shape.
|
|
306
|
+
* @internal
|
|
307
|
+
*/
|
|
308
|
+
function makeNullable<S extends ValueShape>(
|
|
309
|
+
shape: S,
|
|
310
|
+
): WithPlaceholder<UnionValueShape<[NullValueShape, S]>> {
|
|
311
|
+
const nullShape: NullValueShape = {
|
|
312
|
+
_type: "value" as const,
|
|
313
|
+
valueType: "null" as const,
|
|
314
|
+
_plain: null,
|
|
315
|
+
_mutable: null,
|
|
316
|
+
_placeholder: null,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const base: UnionValueShape<[NullValueShape, S]> = {
|
|
320
|
+
_type: "value" as const,
|
|
321
|
+
valueType: "union" as const,
|
|
322
|
+
shapes: [nullShape, shape] as [NullValueShape, S],
|
|
323
|
+
_plain: null as any,
|
|
324
|
+
_mutable: null as any,
|
|
325
|
+
_placeholder: null as any, // Default placeholder is null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return Object.assign(base, {
|
|
329
|
+
placeholder(
|
|
330
|
+
value: S["_placeholder"] | null,
|
|
331
|
+
): UnionValueShape<[NullValueShape, S]> {
|
|
332
|
+
return { ...base, _placeholder: value } as UnionValueShape<
|
|
333
|
+
[NullValueShape, S]
|
|
334
|
+
>
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
261
339
|
/**
|
|
262
340
|
* The LoroShape factory object
|
|
263
341
|
*
|
|
@@ -274,6 +352,28 @@ export const Shape = {
|
|
|
274
352
|
_placeholder: {} as any,
|
|
275
353
|
}),
|
|
276
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Creates an "any" container shape - an escape hatch for untyped containers.
|
|
357
|
+
* Use this when integrating with external libraries that manage their own document structure.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* // loro-prosemirror manages its own structure
|
|
362
|
+
* const ProseMirrorDocShape = Shape.doc({
|
|
363
|
+
* doc: Shape.any(), // opt out of typing for this container
|
|
364
|
+
* })
|
|
365
|
+
*
|
|
366
|
+
* const handle = repo.get(docId, ProseMirrorDocShape, CursorPresenceShape)
|
|
367
|
+
* // handle.doc.doc is typed as `unknown` - you're on your own
|
|
368
|
+
* ```
|
|
369
|
+
*/
|
|
370
|
+
any: (): AnyContainerShape => ({
|
|
371
|
+
_type: "any" as const,
|
|
372
|
+
_plain: undefined as unknown,
|
|
373
|
+
_mutable: undefined as unknown,
|
|
374
|
+
_placeholder: undefined,
|
|
375
|
+
}),
|
|
376
|
+
|
|
277
377
|
// CRDTs are represented by Loro Containers--they converge on state using Loro's
|
|
278
378
|
// various CRDT algorithms
|
|
279
379
|
counter: (): WithPlaceholder<CounterContainerShape> => {
|
|
@@ -386,7 +486,8 @@ export const Shape = {
|
|
|
386
486
|
plain: {
|
|
387
487
|
string: <T extends string = string>(
|
|
388
488
|
...options: T[]
|
|
389
|
-
): WithPlaceholder<StringValueShape<T>>
|
|
489
|
+
): WithPlaceholder<StringValueShape<T>> &
|
|
490
|
+
WithNullable<StringValueShape<T>> => {
|
|
390
491
|
const base: StringValueShape<T> = {
|
|
391
492
|
_type: "value" as const,
|
|
392
493
|
valueType: "string" as const,
|
|
@@ -399,10 +500,16 @@ export const Shape = {
|
|
|
399
500
|
placeholder(value: T): StringValueShape<T> {
|
|
400
501
|
return { ...base, _placeholder: value }
|
|
401
502
|
},
|
|
503
|
+
nullable(): WithPlaceholder<
|
|
504
|
+
UnionValueShape<[NullValueShape, StringValueShape<T>]>
|
|
505
|
+
> {
|
|
506
|
+
return makeNullable(base)
|
|
507
|
+
},
|
|
402
508
|
})
|
|
403
509
|
},
|
|
404
510
|
|
|
405
|
-
number: (): WithPlaceholder<NumberValueShape>
|
|
511
|
+
number: (): WithPlaceholder<NumberValueShape> &
|
|
512
|
+
WithNullable<NumberValueShape> => {
|
|
406
513
|
const base: NumberValueShape = {
|
|
407
514
|
_type: "value" as const,
|
|
408
515
|
valueType: "number" as const,
|
|
@@ -414,10 +521,16 @@ export const Shape = {
|
|
|
414
521
|
placeholder(value: number): NumberValueShape {
|
|
415
522
|
return { ...base, _placeholder: value }
|
|
416
523
|
},
|
|
524
|
+
nullable(): WithPlaceholder<
|
|
525
|
+
UnionValueShape<[NullValueShape, NumberValueShape]>
|
|
526
|
+
> {
|
|
527
|
+
return makeNullable(base)
|
|
528
|
+
},
|
|
417
529
|
})
|
|
418
530
|
},
|
|
419
531
|
|
|
420
|
-
boolean: (): WithPlaceholder<BooleanValueShape>
|
|
532
|
+
boolean: (): WithPlaceholder<BooleanValueShape> &
|
|
533
|
+
WithNullable<BooleanValueShape> => {
|
|
421
534
|
const base: BooleanValueShape = {
|
|
422
535
|
_type: "value" as const,
|
|
423
536
|
valueType: "boolean" as const,
|
|
@@ -429,6 +542,11 @@ export const Shape = {
|
|
|
429
542
|
placeholder(value: boolean): BooleanValueShape {
|
|
430
543
|
return { ...base, _placeholder: value }
|
|
431
544
|
},
|
|
545
|
+
nullable(): WithPlaceholder<
|
|
546
|
+
UnionValueShape<[NullValueShape, BooleanValueShape]>
|
|
547
|
+
> {
|
|
548
|
+
return makeNullable(base)
|
|
549
|
+
},
|
|
432
550
|
})
|
|
433
551
|
},
|
|
434
552
|
|
|
@@ -448,12 +566,70 @@ export const Shape = {
|
|
|
448
566
|
_placeholder: undefined,
|
|
449
567
|
}),
|
|
450
568
|
|
|
451
|
-
uint8Array: (): Uint8ArrayValueShape
|
|
569
|
+
uint8Array: (): Uint8ArrayValueShape &
|
|
570
|
+
WithNullable<Uint8ArrayValueShape> => {
|
|
571
|
+
const base: Uint8ArrayValueShape = {
|
|
572
|
+
_type: "value" as const,
|
|
573
|
+
valueType: "uint8array" as const,
|
|
574
|
+
_plain: new Uint8Array(),
|
|
575
|
+
_mutable: new Uint8Array(),
|
|
576
|
+
_placeholder: new Uint8Array(),
|
|
577
|
+
}
|
|
578
|
+
return Object.assign(base, {
|
|
579
|
+
nullable(): WithPlaceholder<
|
|
580
|
+
UnionValueShape<[NullValueShape, Uint8ArrayValueShape]>
|
|
581
|
+
> {
|
|
582
|
+
return makeNullable(base)
|
|
583
|
+
},
|
|
584
|
+
})
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Alias for `uint8Array()` - creates a shape for binary data.
|
|
589
|
+
* Use this for better discoverability when working with binary data like cursor positions.
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* ```typescript
|
|
593
|
+
* const CursorPresenceShape = Shape.plain.struct({
|
|
594
|
+
* anchor: Shape.plain.bytes().nullable(),
|
|
595
|
+
* focus: Shape.plain.bytes().nullable(),
|
|
596
|
+
* })
|
|
597
|
+
* ```
|
|
598
|
+
*/
|
|
599
|
+
bytes: (): Uint8ArrayValueShape & WithNullable<Uint8ArrayValueShape> => {
|
|
600
|
+
const base: Uint8ArrayValueShape = {
|
|
601
|
+
_type: "value" as const,
|
|
602
|
+
valueType: "uint8array" as const,
|
|
603
|
+
_plain: new Uint8Array(),
|
|
604
|
+
_mutable: new Uint8Array(),
|
|
605
|
+
_placeholder: new Uint8Array(),
|
|
606
|
+
}
|
|
607
|
+
return Object.assign(base, {
|
|
608
|
+
nullable(): WithPlaceholder<
|
|
609
|
+
UnionValueShape<[NullValueShape, Uint8ArrayValueShape]>
|
|
610
|
+
> {
|
|
611
|
+
return makeNullable(base)
|
|
612
|
+
},
|
|
613
|
+
})
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Creates an "any" value shape - an escape hatch for untyped values.
|
|
618
|
+
* Use this when you need to accept any valid Loro value type.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```typescript
|
|
622
|
+
* const FlexiblePresenceShape = Shape.plain.struct({
|
|
623
|
+
* metadata: Shape.plain.any(), // accept any value type
|
|
624
|
+
* })
|
|
625
|
+
* ```
|
|
626
|
+
*/
|
|
627
|
+
any: (): AnyValueShape => ({
|
|
452
628
|
_type: "value" as const,
|
|
453
|
-
valueType: "
|
|
454
|
-
_plain:
|
|
455
|
-
_mutable:
|
|
456
|
-
_placeholder:
|
|
629
|
+
valueType: "any" as const,
|
|
630
|
+
_plain: undefined as unknown as Value,
|
|
631
|
+
_mutable: undefined as unknown as Value,
|
|
632
|
+
_placeholder: undefined,
|
|
457
633
|
}),
|
|
458
634
|
|
|
459
635
|
/**
|
|
@@ -470,46 +646,86 @@ export const Shape = {
|
|
|
470
646
|
*/
|
|
471
647
|
struct: <T extends Record<string, ValueShape>>(
|
|
472
648
|
shape: T,
|
|
473
|
-
): StructValueShape<T> =>
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
649
|
+
): StructValueShape<T> & WithNullable<StructValueShape<T>> => {
|
|
650
|
+
const base: StructValueShape<T> = {
|
|
651
|
+
_type: "value" as const,
|
|
652
|
+
valueType: "struct" as const,
|
|
653
|
+
shape,
|
|
654
|
+
_plain: {} as any,
|
|
655
|
+
_mutable: {} as any,
|
|
656
|
+
_placeholder: {} as any,
|
|
657
|
+
}
|
|
658
|
+
return Object.assign(base, {
|
|
659
|
+
nullable(): WithPlaceholder<
|
|
660
|
+
UnionValueShape<[NullValueShape, StructValueShape<T>]>
|
|
661
|
+
> {
|
|
662
|
+
return makeNullable(base)
|
|
663
|
+
},
|
|
664
|
+
})
|
|
665
|
+
},
|
|
481
666
|
|
|
482
667
|
/**
|
|
483
668
|
* @deprecated Use `Shape.plain.struct` instead. `Shape.plain.struct` will be removed in a future version.
|
|
484
669
|
*/
|
|
485
670
|
object: <T extends Record<string, ValueShape>>(
|
|
486
671
|
shape: T,
|
|
487
|
-
): StructValueShape<T> =>
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
672
|
+
): StructValueShape<T> & WithNullable<StructValueShape<T>> => {
|
|
673
|
+
const base: StructValueShape<T> = {
|
|
674
|
+
_type: "value" as const,
|
|
675
|
+
valueType: "struct" as const,
|
|
676
|
+
shape,
|
|
677
|
+
_plain: {} as any,
|
|
678
|
+
_mutable: {} as any,
|
|
679
|
+
_placeholder: {} as any,
|
|
680
|
+
}
|
|
681
|
+
return Object.assign(base, {
|
|
682
|
+
nullable(): WithPlaceholder<
|
|
683
|
+
UnionValueShape<[NullValueShape, StructValueShape<T>]>
|
|
684
|
+
> {
|
|
685
|
+
return makeNullable(base)
|
|
686
|
+
},
|
|
687
|
+
})
|
|
688
|
+
},
|
|
495
689
|
|
|
496
|
-
record: <T extends ValueShape>(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
690
|
+
record: <T extends ValueShape>(
|
|
691
|
+
shape: T,
|
|
692
|
+
): RecordValueShape<T> & WithNullable<RecordValueShape<T>> => {
|
|
693
|
+
const base: RecordValueShape<T> = {
|
|
694
|
+
_type: "value" as const,
|
|
695
|
+
valueType: "record" as const,
|
|
696
|
+
shape,
|
|
697
|
+
_plain: {} as any,
|
|
698
|
+
_mutable: {} as any,
|
|
699
|
+
_placeholder: {} as Record<string, never>,
|
|
700
|
+
}
|
|
701
|
+
return Object.assign(base, {
|
|
702
|
+
nullable(): WithPlaceholder<
|
|
703
|
+
UnionValueShape<[NullValueShape, RecordValueShape<T>]>
|
|
704
|
+
> {
|
|
705
|
+
return makeNullable(base)
|
|
706
|
+
},
|
|
707
|
+
})
|
|
708
|
+
},
|
|
504
709
|
|
|
505
|
-
array: <T extends ValueShape>(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
710
|
+
array: <T extends ValueShape>(
|
|
711
|
+
shape: T,
|
|
712
|
+
): ArrayValueShape<T> & WithNullable<ArrayValueShape<T>> => {
|
|
713
|
+
const base: ArrayValueShape<T> = {
|
|
714
|
+
_type: "value" as const,
|
|
715
|
+
valueType: "array" as const,
|
|
716
|
+
shape,
|
|
717
|
+
_plain: [] as any,
|
|
718
|
+
_mutable: [] as any,
|
|
719
|
+
_placeholder: [] as never[],
|
|
720
|
+
}
|
|
721
|
+
return Object.assign(base, {
|
|
722
|
+
nullable(): WithPlaceholder<
|
|
723
|
+
UnionValueShape<[NullValueShape, ArrayValueShape<T>]>
|
|
724
|
+
> {
|
|
725
|
+
return makeNullable(base)
|
|
726
|
+
},
|
|
727
|
+
})
|
|
728
|
+
},
|
|
513
729
|
|
|
514
730
|
// Special value type that helps make things like `string | null` representable
|
|
515
731
|
// TODO(duane): should this be a more general type for containers too?
|
package/src/typed-refs/base.ts
CHANGED
|
@@ -19,6 +19,12 @@ export abstract class TypedRef<Shape extends DocShape | ContainerShape> {
|
|
|
19
19
|
|
|
20
20
|
abstract absorbPlainValues(): void
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Serializes the ref to a plain JSON-compatible value.
|
|
24
|
+
* Returns the plain type inferred from the shape.
|
|
25
|
+
*/
|
|
26
|
+
abstract toJSON(): Infer<Shape>
|
|
27
|
+
|
|
22
28
|
protected get shape(): Shape {
|
|
23
29
|
return this._params.shape
|
|
24
30
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { change } from "../functional-helpers.js"
|
|
2
3
|
import { Shape } from "../shape.js"
|
|
3
4
|
import { createTypedDoc } from "../typed-doc.js"
|
|
4
5
|
|
|
@@ -57,7 +58,7 @@ describe("Counter Ref", () => {
|
|
|
57
58
|
})
|
|
58
59
|
const doc = createTypedDoc(schema)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
change(doc, draft => {
|
|
61
62
|
draft.counter.increment(5)
|
|
62
63
|
})
|
|
63
64
|
|
package/src/typed-refs/doc.ts
CHANGED
|
@@ -16,7 +16,9 @@ const containerGetter = {
|
|
|
16
16
|
struct: "getMap", // Structs use LoroMap as their underlying container
|
|
17
17
|
text: "getText",
|
|
18
18
|
tree: "getTree",
|
|
19
|
-
} as const
|
|
19
|
+
} as const satisfies Record<string, keyof LoroDoc>
|
|
20
|
+
|
|
21
|
+
type ContainerGetterKey = keyof typeof containerGetter
|
|
20
22
|
|
|
21
23
|
// Doc Ref class -- the actual object passed to the change `mutation` function
|
|
22
24
|
export class DocRef<Shape extends DocShape> extends TypedRef<Shape> {
|
|
@@ -47,7 +49,16 @@ export class DocRef<Shape extends DocShape> extends TypedRef<Shape> {
|
|
|
47
49
|
key: string,
|
|
48
50
|
shape: S,
|
|
49
51
|
): TypedRefParams<ContainerShape> {
|
|
50
|
-
|
|
52
|
+
// Handle "any" shape type - it's an escape hatch that doesn't have a specific getter
|
|
53
|
+
if (shape._type === "any") {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Cannot get typed ref params for "any" shape type. ` +
|
|
56
|
+
`The "any" shape is an escape hatch for untyped containers and should be accessed directly via loroDoc.`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const getterName = containerGetter[shape._type as ContainerGetterKey]
|
|
61
|
+
const getter = this._doc[getterName].bind(this._doc)
|
|
51
62
|
|
|
52
63
|
return {
|
|
53
64
|
shape,
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"
|
|
|
3
3
|
import { change } from "../functional-helpers.js"
|
|
4
4
|
import { Shape } from "../shape.js"
|
|
5
5
|
import { createTypedDoc } from "../typed-doc.js"
|
|
6
|
+
import type { Mutable } from "../types.js"
|
|
6
7
|
|
|
7
8
|
const MessageSchema = Shape.struct({
|
|
8
9
|
id: Shape.plain.string(),
|
|
@@ -257,4 +258,30 @@ describe("JSON Compatibility", () => {
|
|
|
257
258
|
expect(root.messages[0].content.toJSON()).toBe("A")
|
|
258
259
|
})
|
|
259
260
|
})
|
|
261
|
+
|
|
262
|
+
it("should expose toJSON() in Mutable type signature", () => {
|
|
263
|
+
const doc = createTypedDoc(ChatSchema)
|
|
264
|
+
|
|
265
|
+
// This test verifies that TypeScript sees toJSON() on Mutable types
|
|
266
|
+
// If this compiles, the type fix is working correctly
|
|
267
|
+
change(doc, (root: Mutable<typeof ChatSchema>) => {
|
|
268
|
+
root.meta.title = "Type Test"
|
|
269
|
+
root.messages.push({ id: "1", content: "Hello", timestamp: 123 })
|
|
270
|
+
|
|
271
|
+
// These should all compile without errors - toJSON() is visible on the type
|
|
272
|
+
const metaJson: { title: string; count: number } = root.meta.toJSON()
|
|
273
|
+
const messagesJson: Array<{
|
|
274
|
+
id: string
|
|
275
|
+
content: string
|
|
276
|
+
timestamp: number
|
|
277
|
+
}> = root.messages.toJSON()
|
|
278
|
+
const countJson: number = root.meta.count.toJSON()
|
|
279
|
+
|
|
280
|
+
expect(metaJson).toEqual({ title: "Type Test", count: 0 })
|
|
281
|
+
expect(messagesJson).toEqual([
|
|
282
|
+
{ id: "1", content: "Hello", timestamp: 123 },
|
|
283
|
+
])
|
|
284
|
+
expect(countJson).toBe(0)
|
|
285
|
+
})
|
|
286
|
+
})
|
|
260
287
|
})
|
package/src/typed-refs/list.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { LoroList } from "loro-crdt"
|
|
2
2
|
import type { ContainerOrValueShape } from "../shape.js"
|
|
3
|
-
import type {
|
|
3
|
+
import type { InferMutableType } from "../types.js"
|
|
4
4
|
import { ListRefBase } from "./list-base.js"
|
|
5
5
|
|
|
6
6
|
// List typed ref
|
|
7
7
|
export class ListRef<
|
|
8
8
|
NestedShape extends ContainerOrValueShape,
|
|
9
9
|
> extends ListRefBase<NestedShape> {
|
|
10
|
-
|
|
10
|
+
// Returns the mutable type which has toJSON() and other ref methods.
|
|
11
|
+
// For assignment, the proxy handler accepts plain values and converts them.
|
|
12
|
+
// TypeScript may require type assertions for plain value assignments.
|
|
13
|
+
[index: number]: InferMutableType<NestedShape> | undefined
|
|
11
14
|
|
|
12
15
|
protected get container(): LoroList {
|
|
13
16
|
return super.container as LoroList
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Container, LoroMovableList } from "loro-crdt"
|
|
2
2
|
import type { ContainerOrValueShape } from "../shape.js"
|
|
3
|
-
import type {
|
|
3
|
+
import type { InferMutableType } from "../types.js"
|
|
4
4
|
import { ListRefBase } from "./list-base.js"
|
|
5
5
|
|
|
6
6
|
// Movable list typed ref
|
|
@@ -8,7 +8,7 @@ export class MovableListRef<
|
|
|
8
8
|
NestedShape extends ContainerOrValueShape,
|
|
9
9
|
Item = NestedShape["_plain"],
|
|
10
10
|
> extends ListRefBase<NestedShape> {
|
|
11
|
-
[index: number]:
|
|
11
|
+
[index: number]: InferMutableType<NestedShape> | undefined
|
|
12
12
|
|
|
13
13
|
protected get container(): LoroMovableList {
|
|
14
14
|
return super.container as LoroMovableList
|
package/src/typed-refs/record.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
assignPlainValueToTypedRef,
|
|
15
15
|
containerConstructor,
|
|
16
16
|
createContainerTypedRef,
|
|
17
|
+
hasContainerConstructor,
|
|
17
18
|
serializeRefToJSON,
|
|
18
19
|
unwrapReadonlyPrimitive,
|
|
19
20
|
} from "./utils.js"
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
export class RecordRef<
|
|
23
24
|
NestedShape extends ContainerOrValueShape,
|
|
24
25
|
> extends TypedRef<any> {
|
|
25
|
-
[key: string]:
|
|
26
|
+
[key: string]: InferMutableType<NestedShape> | undefined | any
|
|
26
27
|
private refCache = new Map<string, TypedRef<ContainerShape> | Value>()
|
|
27
28
|
|
|
28
29
|
protected get shape(): RecordContainerShape<NestedShape> {
|
|
@@ -51,6 +52,14 @@ export class RecordRef<
|
|
|
51
52
|
placeholder = deriveShapePlaceholder(shape)
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
// AnyContainerShape is an escape hatch - it doesn't have a constructor
|
|
56
|
+
if (!hasContainerConstructor(shape._type)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Cannot create typed ref for shape type "${shape._type}". ` +
|
|
59
|
+
`Use Shape.any() only at the document root level.`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
const LoroContainer = containerConstructor[shape._type]
|
|
55
64
|
|
|
56
65
|
return {
|
|
@@ -129,7 +138,7 @@ export class RecordRef<
|
|
|
129
138
|
return ref as any
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
get(key: string): InferMutableType<NestedShape> {
|
|
141
|
+
get(key: string): InferMutableType<NestedShape> | undefined {
|
|
133
142
|
return this.getRef(key)
|
|
134
143
|
}
|
|
135
144
|
|
package/src/typed-refs/struct.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
assignPlainValueToTypedRef,
|
|
15
15
|
containerConstructor,
|
|
16
16
|
createContainerTypedRef,
|
|
17
|
+
hasContainerConstructor,
|
|
17
18
|
serializeRefToJSON,
|
|
18
19
|
unwrapReadonlyPrimitive,
|
|
19
20
|
} from "./utils.js"
|
|
@@ -50,6 +51,14 @@ export class StructRef<
|
|
|
50
51
|
): TypedRefParams<ContainerShape> {
|
|
51
52
|
const placeholder = (this.placeholder as any)?.[key]
|
|
52
53
|
|
|
54
|
+
// AnyContainerShape is an escape hatch - it doesn't have a constructor
|
|
55
|
+
if (!hasContainerConstructor(shape._type)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Cannot create typed ref for shape type "${shape._type}". ` +
|
|
58
|
+
`Use Shape.any() only at the document root level.`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
const LoroContainer = containerConstructor[shape._type]
|
|
54
63
|
|
|
55
64
|
return {
|
package/src/typed-refs/tree.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TreeContainerShape } from "../shape.js"
|
|
2
|
+
import type { Infer } from "../types.js"
|
|
2
3
|
import { TypedRef } from "./base.js"
|
|
3
4
|
|
|
4
5
|
// Tree typed ref
|
|
@@ -7,6 +8,11 @@ export class TreeRef<T extends TreeContainerShape> extends TypedRef<T> {
|
|
|
7
8
|
// TODO(duane): implement for trees
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
toJSON(): Infer<T> {
|
|
12
|
+
// TODO(duane): implement proper tree serialization
|
|
13
|
+
return this.container.toJSON() as Infer<T>
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
createNode(parent?: any, index?: number): any {
|
|
11
17
|
this.assertMutable()
|
|
12
18
|
return this.container.createNode(parent, index)
|
package/src/typed-refs/utils.ts
CHANGED
|
@@ -34,6 +34,9 @@ import { TreeRef } from "./tree.js"
|
|
|
34
34
|
/**
|
|
35
35
|
* Mapping from container shape types to their Loro constructor classes.
|
|
36
36
|
* Used when creating new containers via getOrCreateContainer().
|
|
37
|
+
*
|
|
38
|
+
* Note: "any" is not included because AnyContainerShape is an escape hatch
|
|
39
|
+
* that doesn't create typed refs - it returns raw Loro containers.
|
|
37
40
|
*/
|
|
38
41
|
export const containerConstructor = {
|
|
39
42
|
counter: LoroCounter,
|
|
@@ -45,6 +48,16 @@ export const containerConstructor = {
|
|
|
45
48
|
tree: LoroTree,
|
|
46
49
|
} as const
|
|
47
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Type guard to check if a container shape type has a constructor.
|
|
53
|
+
* Returns false for "any" which is an escape hatch.
|
|
54
|
+
*/
|
|
55
|
+
export function hasContainerConstructor(
|
|
56
|
+
type: string,
|
|
57
|
+
): type is keyof typeof containerConstructor {
|
|
58
|
+
return type in containerConstructor
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
/**
|
|
49
62
|
* Unwraps a TypedRef to its primitive value for readonly access.
|
|
50
63
|
* Counter refs return their numeric value, Text refs return their string.
|