@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/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: "uint8array" as const,
454
- _plain: new Uint8Array(),
455
- _mutable: new Uint8Array(),
456
- _placeholder: new Uint8Array(),
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
- _type: "value" as const,
475
- valueType: "struct" as const,
476
- shape,
477
- _plain: {} as any,
478
- _mutable: {} as any,
479
- _placeholder: {} as any,
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
- _type: "value" as const,
489
- valueType: "struct" as const,
490
- shape,
491
- _plain: {} as any,
492
- _mutable: {} as any,
493
- _placeholder: {} as any,
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>(shape: T): RecordValueShape<T> => ({
497
- _type: "value" as const,
498
- valueType: "record" as const,
499
- shape,
500
- _plain: {} as any,
501
- _mutable: {} as any,
502
- _placeholder: {} as Record<string, never>,
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>(shape: T): ArrayValueShape<T> => ({
506
- _type: "value" as const,
507
- valueType: "array" as const,
508
- shape,
509
- _plain: [] as any,
510
- _mutable: [] as any,
511
- _placeholder: [] as never[],
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?
@@ -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
- doc.$.change(draft => {
61
+ change(doc, draft => {
61
62
  draft.counter.increment(5)
62
63
  })
63
64
 
@@ -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
- const getter = this._doc[containerGetter[shape._type]].bind(this._doc)
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
  })
@@ -336,7 +336,7 @@ export abstract class ListRefBase<
336
336
  return result
337
337
  }
338
338
 
339
- get(index: number): MutableItem {
339
+ get(index: number): MutableItem | undefined {
340
340
  return this.getMutableItem(index)
341
341
  }
342
342
 
@@ -19,7 +19,7 @@ describe("ListRef", () => {
19
19
  draft.users.push({ name: "Alice" })
20
20
 
21
21
  // Update via index
22
- draft.users[0] = { name: "Bob" }
22
+ ;(draft.users as any)[0] = { name: "Bob" }
23
23
  })
24
24
 
25
25
  expect(doc.toJSON().users[0]).toEqual({ name: "Bob" })
@@ -1,13 +1,16 @@
1
1
  import type { LoroList } from "loro-crdt"
2
2
  import type { ContainerOrValueShape } from "../shape.js"
3
- import type { Infer } from "../types.js"
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
- [index: number]: Infer<NestedShape>
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
@@ -19,7 +19,7 @@ describe("MovableListRef", () => {
19
19
  draft.users.push({ name: "Alice" })
20
20
 
21
21
  // Update via index
22
- draft.users[0] = { name: "Bob" }
22
+ ;(draft.users as any)[0] = { name: "Bob" }
23
23
  })
24
24
 
25
25
  expect(doc.toJSON().users[0]).toEqual({ name: "Bob" })
@@ -1,6 +1,6 @@
1
1
  import type { Container, LoroMovableList } from "loro-crdt"
2
2
  import type { ContainerOrValueShape } from "../shape.js"
3
- import type { Infer } from "../types.js"
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]: Infer<NestedShape>
11
+ [index: number]: InferMutableType<NestedShape> | undefined
12
12
 
13
13
  protected get container(): LoroMovableList {
14
14
  return super.container as LoroMovableList
@@ -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]: Infer<NestedShape> | any
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
 
@@ -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 {
@@ -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)
@@ -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.