@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/dist/index.d.ts CHANGED
@@ -61,6 +61,11 @@ declare abstract class TypedRef<Shape extends DocShape | ContainerShape> {
61
61
  protected _cachedContainer?: ShapeToContainer<Shape>;
62
62
  constructor(_params: TypedRefParams<Shape>);
63
63
  abstract absorbPlainValues(): void;
64
+ /**
65
+ * Serializes the ref to a plain JSON-compatible value.
66
+ * Returns the plain type inferred from the shape.
67
+ */
68
+ abstract toJSON(): Infer<Shape>;
64
69
  protected get shape(): Shape;
65
70
  protected get placeholder(): Infer<Shape> | undefined;
66
71
  protected get readonly(): boolean;
@@ -121,7 +126,7 @@ declare abstract class ListRefBase<NestedShape extends ContainerOrValueShape, It
121
126
  push(item: Item): void;
122
127
  pushContainer(container: Container): Container;
123
128
  insertContainer(index: number, container: Container): Container;
124
- get(index: number): MutableItem;
129
+ get(index: number): MutableItem | undefined;
125
130
  toArray(): Item[];
126
131
  toJSON(): Item[];
127
132
  [Symbol.iterator](): IterableIterator<MutableItem>;
@@ -131,13 +136,13 @@ declare abstract class ListRefBase<NestedShape extends ContainerOrValueShape, It
131
136
  }
132
137
 
133
138
  declare class ListRef<NestedShape extends ContainerOrValueShape> extends ListRefBase<NestedShape> {
134
- [index: number]: Infer<NestedShape>;
139
+ [index: number]: InferMutableType<NestedShape> | undefined;
135
140
  protected get container(): LoroList;
136
141
  protected absorbValueAtIndex(index: number, value: any): void;
137
142
  }
138
143
 
139
144
  declare class MovableListRef<NestedShape extends ContainerOrValueShape, Item = NestedShape["_plain"]> extends ListRefBase<NestedShape> {
140
- [index: number]: Infer<NestedShape>;
145
+ [index: number]: InferMutableType<NestedShape> | undefined;
141
146
  protected get container(): LoroMovableList;
142
147
  protected absorbValueAtIndex(index: number, value: any): void;
143
148
  move(from: number, to: number): void;
@@ -145,7 +150,7 @@ declare class MovableListRef<NestedShape extends ContainerOrValueShape, Item = N
145
150
  }
146
151
 
147
152
  declare class RecordRef<NestedShape extends ContainerOrValueShape> extends TypedRef<any> {
148
- [key: string]: Infer<NestedShape> | any;
153
+ [key: string]: InferMutableType<NestedShape> | undefined | any;
149
154
  private refCache;
150
155
  protected get shape(): RecordContainerShape<NestedShape>;
151
156
  protected get container(): LoroMap;
@@ -162,7 +167,7 @@ declare class RecordRef<NestedShape extends ContainerOrValueShape> extends Typed
162
167
  * This is the method used for write operations.
163
168
  */
164
169
  getOrCreateRef(key: string): any;
165
- get(key: string): InferMutableType<NestedShape>;
170
+ get(key: string): InferMutableType<NestedShape> | undefined;
166
171
  set(key: string, value: any): void;
167
172
  setContainer<C extends Container>(key: string, container: C): C;
168
173
  delete(key: string): void;
@@ -229,6 +234,13 @@ declare class TextRef extends TypedRef<TextContainerShape> {
229
234
  type WithPlaceholder<S extends Shape<any, any, any>> = S & {
230
235
  placeholder(value: S["_placeholder"]): S;
231
236
  };
237
+ /**
238
+ * Type for value shapes that support the .nullable() method.
239
+ * Returns a union of null and the original shape with null as the default placeholder.
240
+ */
241
+ type WithNullable<S extends ValueShape> = {
242
+ nullable(): WithPlaceholder<UnionValueShape<[NullValueShape, S]>>;
243
+ };
232
244
  interface DocShape<NestedShapes extends Record<string, ContainerShape> = Record<string, ContainerShape>> extends Shape<{
233
245
  [K in keyof NestedShapes]: NestedShapes[K]["_plain"];
234
246
  }, {
@@ -280,7 +292,22 @@ interface RecordContainerShape<NestedShape extends ContainerOrValueShape = Conta
280
292
  readonly _type: "record";
281
293
  readonly shape: NestedShape;
282
294
  }
283
- type ContainerShape = CounterContainerShape | ListContainerShape | MovableListContainerShape | RecordContainerShape | StructContainerShape | TextContainerShape | TreeContainerShape;
295
+ /**
296
+ * Container escape hatch - represents "any LoroContainer".
297
+ * Use this when integrating with external libraries that manage their own document structure.
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * // loro-prosemirror manages its own structure
302
+ * const ProseMirrorDocShape = Shape.doc({
303
+ * doc: Shape.any(), // opt out of typing for this container
304
+ * })
305
+ * ```
306
+ */
307
+ interface AnyContainerShape extends Shape<unknown, unknown, undefined> {
308
+ readonly _type: "any";
309
+ }
310
+ type ContainerShape = AnyContainerShape | CounterContainerShape | ListContainerShape | MovableListContainerShape | RecordContainerShape | StructContainerShape | TextContainerShape | TreeContainerShape;
284
311
  type ContainerType = ContainerShape["_type"];
285
312
  interface StringValueShape<T extends string = string> extends Shape<T, T, T> {
286
313
  readonly _type: "value";
@@ -361,7 +388,22 @@ interface DiscriminatedUnionValueShape<K extends string = string, T extends Reco
361
388
  readonly discriminantKey: K;
362
389
  readonly variants: T;
363
390
  }
364
- type ValueShape = StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | StructValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape;
391
+ /**
392
+ * Value escape hatch - represents "any Loro Value".
393
+ * Use this when you need to accept any valid Loro value type.
394
+ *
395
+ * @example
396
+ * ```typescript
397
+ * const FlexiblePresenceShape = Shape.plain.struct({
398
+ * cursor: Shape.plain.any(), // accept any value type
399
+ * })
400
+ * ```
401
+ */
402
+ interface AnyValueShape extends Shape<Value, Value, undefined> {
403
+ readonly _type: "value";
404
+ readonly valueType: "any";
405
+ }
406
+ type ValueShape = AnyValueShape | StringValueShape | NumberValueShape | BooleanValueShape | NullValueShape | UndefinedValueShape | Uint8ArrayValueShape | StructValueShape | RecordValueShape | ArrayValueShape | UnionValueShape | DiscriminatedUnionValueShape;
365
407
  type ContainerOrValueShape = ContainerShape | ValueShape;
366
408
  interface Shape<Plain, Mutable, Placeholder = Plain> {
367
409
  readonly _type: string;
@@ -378,6 +420,22 @@ interface Shape<Plain, Mutable, Placeholder = Plain> {
378
420
  */
379
421
  declare const Shape: {
380
422
  doc: <T extends Record<string, ContainerShape>>(shape: T) => DocShape<T>;
423
+ /**
424
+ * Creates an "any" container shape - an escape hatch for untyped containers.
425
+ * Use this when integrating with external libraries that manage their own document structure.
426
+ *
427
+ * @example
428
+ * ```typescript
429
+ * // loro-prosemirror manages its own structure
430
+ * const ProseMirrorDocShape = Shape.doc({
431
+ * doc: Shape.any(), // opt out of typing for this container
432
+ * })
433
+ *
434
+ * const handle = repo.get(docId, ProseMirrorDocShape, CursorPresenceShape)
435
+ * // handle.doc.doc is typed as `unknown` - you're on your own
436
+ * ```
437
+ */
438
+ any: () => AnyContainerShape;
381
439
  counter: () => WithPlaceholder<CounterContainerShape>;
382
440
  list: <T extends ContainerOrValueShape>(shape: T) => ListContainerShape<T>;
383
441
  /**
@@ -404,12 +462,37 @@ declare const Shape: {
404
462
  text: () => WithPlaceholder<TextContainerShape>;
405
463
  tree: <T extends MapContainerShape | StructContainerShape>(shape: T) => TreeContainerShape;
406
464
  plain: {
407
- string: <T extends string = string>(...options: T[]) => WithPlaceholder<StringValueShape<T>>;
408
- number: () => WithPlaceholder<NumberValueShape>;
409
- boolean: () => WithPlaceholder<BooleanValueShape>;
465
+ string: <T extends string = string>(...options: T[]) => WithPlaceholder<StringValueShape<T>> & WithNullable<StringValueShape<T>>;
466
+ number: () => WithPlaceholder<NumberValueShape> & WithNullable<NumberValueShape>;
467
+ boolean: () => WithPlaceholder<BooleanValueShape> & WithNullable<BooleanValueShape>;
410
468
  null: () => NullValueShape;
411
469
  undefined: () => UndefinedValueShape;
412
- uint8Array: () => Uint8ArrayValueShape;
470
+ uint8Array: () => Uint8ArrayValueShape & WithNullable<Uint8ArrayValueShape>;
471
+ /**
472
+ * Alias for `uint8Array()` - creates a shape for binary data.
473
+ * Use this for better discoverability when working with binary data like cursor positions.
474
+ *
475
+ * @example
476
+ * ```typescript
477
+ * const CursorPresenceShape = Shape.plain.struct({
478
+ * anchor: Shape.plain.bytes().nullable(),
479
+ * focus: Shape.plain.bytes().nullable(),
480
+ * })
481
+ * ```
482
+ */
483
+ bytes: () => Uint8ArrayValueShape & WithNullable<Uint8ArrayValueShape>;
484
+ /**
485
+ * Creates an "any" value shape - an escape hatch for untyped values.
486
+ * Use this when you need to accept any valid Loro value type.
487
+ *
488
+ * @example
489
+ * ```typescript
490
+ * const FlexiblePresenceShape = Shape.plain.struct({
491
+ * metadata: Shape.plain.any(), // accept any value type
492
+ * })
493
+ * ```
494
+ */
495
+ any: () => AnyValueShape;
413
496
  /**
414
497
  * Creates a struct value shape for plain objects with fixed keys.
415
498
  * This is the preferred way to define fixed-key plain value objects.
@@ -422,13 +505,13 @@ declare const Shape: {
422
505
  * })
423
506
  * ```
424
507
  */
425
- struct: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T>;
508
+ struct: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T> & WithNullable<StructValueShape<T>>;
426
509
  /**
427
510
  * @deprecated Use `Shape.plain.struct` instead. `Shape.plain.struct` will be removed in a future version.
428
511
  */
429
- object: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T>;
430
- record: <T extends ValueShape>(shape: T) => RecordValueShape<T>;
431
- array: <T extends ValueShape>(shape: T) => ArrayValueShape<T>;
512
+ object: <T extends Record<string, ValueShape>>(shape: T) => StructValueShape<T> & WithNullable<StructValueShape<T>>;
513
+ record: <T extends ValueShape>(shape: T) => RecordValueShape<T> & WithNullable<RecordValueShape<T>>;
514
+ array: <T extends ValueShape>(shape: T) => ArrayValueShape<T> & WithNullable<ArrayValueShape<T>>;
432
515
  union: <T extends ValueShape[]>(shapes: T) => WithPlaceholder<UnionValueShape<T>>;
433
516
  /**
434
517
  * Creates a discriminated union shape for type-safe tagged unions.
@@ -712,103 +795,125 @@ declare function overlayPlaceholder<Shape extends DocShape>(shape: Shape, crdtVa
712
795
  */
713
796
  declare function mergeValue<Shape extends ContainerShape | ValueShape>(shape: Shape, crdtValue: Value, placeholderValue: Value): Value;
714
797
 
798
+ type PathSegment = {
799
+ type: "property";
800
+ key: string;
801
+ } | {
802
+ type: "each";
803
+ } | {
804
+ type: "index";
805
+ index: number;
806
+ } | {
807
+ type: "key";
808
+ key: string;
809
+ };
810
+ interface PathSelector<T> {
811
+ readonly __resultType: T;
812
+ readonly __segments: PathSegment[];
813
+ }
814
+ interface ListPathNode<Item extends ContainerOrValueShape, InArray extends boolean> extends PathSelector<WrapType<Infer<Item>[], InArray>> {
815
+ /** Select all items (wildcard) - sets InArray to true for children */
816
+ readonly $each: PathNode<Item, true>;
817
+ /** Select item at specific index (supports negative indices: -1 = last, -2 = second-to-last, etc.) */
818
+ $at(index: number): PathNode<Item, InArray>;
819
+ /** Select first item (alias for $at(0)) */
820
+ readonly $first: PathNode<Item, InArray>;
821
+ /** Select last item (alias for $at(-1)) */
822
+ readonly $last: PathNode<Item, InArray>;
823
+ }
824
+ type StructPathNode<Shapes extends Record<string, ContainerOrValueShape>, InArray extends boolean> = PathSelector<WrapType<{
825
+ [K in keyof Shapes]: Infer<Shapes[K]>;
826
+ }, InArray>> & {
827
+ readonly [K in keyof Shapes]: PathNode<Shapes[K], InArray>;
828
+ };
829
+ interface RecordPathNode<Item extends ContainerOrValueShape, InArray extends boolean> extends PathSelector<WrapType<Record<string, Infer<Item>>, InArray>> {
830
+ /** Select all values (wildcard) - sets InArray to true for children */
831
+ readonly $each: PathNode<Item, true>;
832
+ /** Select value at specific key */
833
+ $key(key: string): PathNode<Item, InArray>;
834
+ }
835
+ type TextPathNode<InArray extends boolean> = PathSelector<WrapType<string, InArray>>;
836
+ type CounterPathNode<InArray extends boolean> = PathSelector<WrapType<number, InArray>>;
837
+ type TerminalPathNode<T, InArray extends boolean> = PathSelector<WrapType<T, InArray>>;
838
+ type WrapType<T, InArray extends boolean> = InArray extends true ? T[] : T;
839
+ type PathNode<S extends ContainerOrValueShape, InArray extends boolean> = S extends ListContainerShape<infer Item> ? ListPathNode<Item, InArray> : S extends MovableListContainerShape<infer Item> ? ListPathNode<Item, InArray> : S extends StructContainerShape<infer Shapes> ? StructPathNode<Shapes, InArray> : S extends RecordContainerShape<infer Item> ? RecordPathNode<Item, InArray> : S extends TextContainerShape ? TextPathNode<InArray> : S extends CounterContainerShape ? CounterPathNode<InArray> : S extends ValueShape ? TerminalPathNode<Infer<S>, InArray> : never;
840
+ type PathBuilder<D extends DocShape> = {
841
+ readonly [K in keyof D["shapes"]]: PathNode<D["shapes"][K], false>;
842
+ };
843
+
715
844
  /**
716
- * Creates a proxy around a placeholder value (plain object/array) that mimics
717
- * the behavior of TypedRef, specifically adding a .toJSON() method.
845
+ * Creates a path builder for a given document shape.
718
846
  *
719
- * This ensures consistent UX where users can call .toJSON() on document state
720
- * regardless of whether it's loading (placeholder) or loaded (live ref).
847
+ * The path builder provides a type-safe DSL for selecting paths within
848
+ * a document. The resulting PathSelector can be compiled to a JSONPath
849
+ * string for use with subscribeJsonpath.
850
+ *
851
+ * @example
852
+ * ```typescript
853
+ * const docShape = Shape.doc({
854
+ * books: Shape.list(Shape.struct({
855
+ * title: Shape.text(),
856
+ * price: Shape.plain.number(),
857
+ * })),
858
+ * })
859
+ *
860
+ * const builder = createPathBuilder(docShape)
861
+ * const selector = builder.books.$each.title
862
+ * // selector.__segments = [
863
+ * // { type: "property", key: "books" },
864
+ * // { type: "each" },
865
+ * // { type: "property", key: "title" }
866
+ * // ]
867
+ * ```
721
868
  */
722
- declare function createPlaceholderProxy<T extends object>(target: T): T;
869
+ declare function createPathBuilder<D extends DocShape>(docShape: D): PathBuilder<D>;
723
870
 
724
871
  /**
725
- * A record of string keys to Loro values, used for presence data.
872
+ * Compiles path segments to a JSONPath string.
873
+ *
874
+ * @example
875
+ * ```typescript
876
+ * const segments = [
877
+ * { type: "property", key: "books" },
878
+ * { type: "each" },
879
+ * { type: "property", key: "title" }
880
+ * ]
881
+ * compileToJsonPath(segments) // => '$.books[*].title'
882
+ * ```
726
883
  */
727
- type ObjectValue = Record<string, Value>;
884
+ declare function compileToJsonPath(segments: PathSegment[]): string;
728
885
  /**
729
- * Interface for presence management that can be implemented by different backends.
730
- * This abstraction allows TypedPresence to work with any presence provider.
886
+ * Check if the path contains any wildcard segments.
887
+ * Paths with wildcards need deep equality checking for change detection.
731
888
  */
732
- interface PresenceInterface {
733
- /**
734
- * Set multiple presence values at once.
735
- */
736
- set: (values: ObjectValue) => void;
737
- /**
738
- * Get a single presence value by key.
739
- */
740
- get: (key: string) => Value;
741
- /**
742
- * The current peer's presence state.
743
- */
744
- readonly self: ObjectValue;
745
- /**
746
- * Other peers' presence states, keyed by peer ID.
747
- * Does NOT include self. Use this for iterating over remote peers.
748
- */
749
- readonly peers: Map<string, ObjectValue>;
750
- /**
751
- * All peers' presence states, keyed by peer ID (includes self).
752
- * @deprecated Use `peers` and `self` separately. This property is synthesized
753
- * from `peers` and `self` for backward compatibility.
754
- */
755
- readonly all: Record<string, ObjectValue>;
756
- /**
757
- * Set a single raw value by key (escape hatch for arbitrary keys).
758
- */
759
- setRaw: (key: string, value: Value) => void;
760
- /**
761
- * Subscribe to presence changes.
762
- * @param cb Callback that receives the aggregated presence values
763
- * @returns Unsubscribe function
764
- */
765
- subscribe: (cb: (values: ObjectValue) => void) => () => void;
766
- }
889
+ declare function hasWildcard(segments: PathSegment[]): boolean;
767
890
 
768
891
  /**
769
- * A strongly-typed wrapper around a PresenceInterface.
770
- * Provides type-safe access to presence data with automatic placeholder merging.
892
+ * Evaluate a path selector against a TypedDoc to get the current value.
893
+ * Returns the value(s) at the path, properly typed.
771
894
  *
772
- * @typeParam S - The shape of the presence data
895
+ * @example
896
+ * ```typescript
897
+ * const selector = builder.books.$each.title
898
+ * const titles = evaluatePath(doc, selector)
899
+ * // titles: string[]
900
+ * ```
773
901
  */
774
- declare class TypedPresence<S extends ContainerShape | ValueShape> {
775
- shape: S;
776
- private presence;
777
- private placeholder;
778
- constructor(shape: S, presence: PresenceInterface);
779
- /**
780
- * Get the current peer's presence state with placeholder values merged in.
781
- */
782
- get self(): Infer<S>;
783
- /**
784
- * Get other peers' presence states with placeholder values merged in.
785
- * Does NOT include self. Use this for iterating over remote peers.
786
- */
787
- get peers(): Map<string, Infer<S>>;
788
- /**
789
- * Get all peers' presence states with placeholder values merged in.
790
- * @deprecated Use `peers` and `self` separately. This property is synthesized
791
- * from `peers` and `self` for backward compatibility.
792
- */
793
- get all(): Record<string, Infer<S>>;
794
- /**
795
- * Set presence values for the current peer.
796
- */
797
- set(value: Partial<Infer<S>>): void;
798
- /**
799
- * Subscribe to presence changes.
800
- * The callback is called immediately with the current state, then on each change.
801
- *
802
- * @param cb Callback that receives the typed presence state
803
- * @returns Unsubscribe function
804
- */
805
- subscribe(cb: (state: {
806
- self: Infer<S>;
807
- peers: Map<string, Infer<S>>;
808
- /** @deprecated Use `peers` and `self` separately */
809
- all: Record<string, Infer<S>>;
810
- }) => void): () => void;
811
- }
902
+ declare function evaluatePath<D extends DocShape, T>(doc: TypedDoc<D>, selector: PathSelector<T>): T;
903
+ /**
904
+ * Evaluate path segments against a plain JavaScript value.
905
+ * This is the core recursive evaluation logic.
906
+ */
907
+ declare function evaluatePathOnValue(value: unknown, segments: PathSegment[]): unknown;
908
+
909
+ /**
910
+ * Creates a proxy around a placeholder value (plain object/array) that mimics
911
+ * the behavior of TypedRef, specifically adding a .toJSON() method.
912
+ *
913
+ * This ensures consistent UX where users can call .toJSON() on document state
914
+ * regardless of whether it's loading (placeholder) or loaded (live ref).
915
+ */
916
+ declare function createPlaceholderProxy<T extends object>(target: T): T;
812
917
 
813
918
  /**
814
919
  * Validates placeholder against schema structure without using Zod
@@ -816,4 +921,4 @@ declare class TypedPresence<S extends ContainerShape | ValueShape> {
816
921
  */
817
922
  declare function validatePlaceholder<T extends DocShape>(placeholder: unknown, schema: T): Infer<T>;
818
923
 
819
- export { type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Infer, type InferMutableType, type InferPlaceholderType, type ListContainerShape, type MapContainerShape, type MovableListContainerShape, type Mutable, type ObjectValue, type ObjectValueShape, type PresenceInterface, type RecordContainerShape, type RecordValueShape, type ContainerType as RootContainerType, Shape, type StructContainerShape, type StructValueShape, type TextContainerShape, type TreeContainerShape, type TypedDoc, TypedPresence, type UnionValueShape, type ValueShape, type WithPlaceholder, change, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, getLoroDoc, mergeValue, overlayPlaceholder, validatePlaceholder };
924
+ export { type AnyContainerShape, type AnyValueShape, type ArrayValueShape, type ContainerOrValueShape, type ContainerShape, type CounterContainerShape, type DiscriminatedUnionValueShape, type DocShape, type Infer, type InferMutableType, type InferPlaceholderType, type ListContainerShape, type MapContainerShape, type MovableListContainerShape, type Mutable, type ObjectValueShape, type PathBuilder, type PathNode, type PathSegment, type PathSelector, type RecordContainerShape, type RecordValueShape, type ContainerType as RootContainerType, Shape, type StructContainerShape, type StructValueShape, type TextContainerShape, type TreeContainerShape, type TypedDoc, type UnionValueShape, type ValueShape, type WithNullable, type WithPlaceholder, change, compileToJsonPath, createPathBuilder, createPlaceholderProxy, createTypedDoc, derivePlaceholder, deriveShapePlaceholder, evaluatePath, evaluatePathOnValue, getLoroDoc, hasWildcard, mergeValue, overlayPlaceholder, validatePlaceholder };