@@ -36,6 +36,53 @@ export interface IReadonlyPathTrieNode<TItem extends {}> {
3636 readonly children : ReadonlyMap < string , IReadonlyPathTrieNode < TItem > > | undefined ;
3737}
3838
39+ /**
40+ * JSON-serializable representation of a node in a {@link LookupByPath} trie.
41+ *
42+ * @beta
43+ */
44+ export interface ISerializedPathTrieNode {
45+ /**
46+ * Index into the `values` array of the containing {@link ILookupByPathJson}.
47+ * If `undefined`, this node has no associated value.
48+ */
49+ valueIndex ?: number ;
50+
51+ /**
52+ * Child nodes keyed by path segment.
53+ */
54+ children ?: Record < string , ISerializedPathTrieNode > ;
55+ }
56+
57+ /**
58+ * JSON-serializable representation of a {@link LookupByPath} instance.
59+ *
60+ * @remarks
61+ * The `values` array stores each unique value exactly once (by reference identity).
62+ * Nodes in the tree reference values by their index in this array, which ensures that
63+ * reference equality is preserved across serialization and deserialization.
64+ *
65+ * @beta
66+ */
67+ export interface ILookupByPathJson < TSerialized > {
68+ /**
69+ * The path delimiter used by the serialized trie.
70+ */
71+ delimiter : string ;
72+
73+ /**
74+ * Array of serialized values. Nodes in the tree reference values by their index in this array.
75+ * Using an array with index-based references preserves reference equality: if multiple nodes
76+ * share the same value (by reference), they will reference the same index.
77+ */
78+ values : TSerialized [ ] ;
79+
80+ /**
81+ * The serialized tree structure.
82+ */
83+ tree : ISerializedPathTrieNode ;
84+ }
85+
3986interface IPrefixEntry {
4087 /**
4188 * The prefix that was matched
@@ -195,6 +242,18 @@ export interface IReadonlyLookupByPath<TItem extends {}> extends Iterable<[strin
195242 * @returns The trie node at the specified prefix, or `undefined` if no node was found
196243 */
197244 getNodeAtPrefix ( query : string , delimiter ?: string ) : IReadonlyPathTrieNode < TItem > | undefined ;
245+
246+ /**
247+ * Serializes this `LookupByPath` instance to a JSON-compatible representation.
248+ *
249+ * @param serializeValue - A function that converts a value of type `TItem` to a JSON-serializable form.
250+ * @returns A JSON-serializable representation of this trie.
251+ *
252+ * @remarks
253+ * Values that are reference-equal will be serialized once and referenced by index, ensuring
254+ * that reference equality is preserved when deserialized via {@link LookupByPath.fromJson}.
255+ */
256+ toJson < TSerialized > ( serializeValue : ( value : TItem ) => TSerialized ) : ILookupByPathJson < TSerialized > ;
198257}
199258
200259/**
@@ -545,6 +604,100 @@ export class LookupByPath<TItem extends {}> implements IReadonlyLookupByPath<TIt
545604 return this . _findNodeAtPrefix ( query , delimiter ) ;
546605 }
547606
607+ /**
608+ * {@inheritdoc IReadonlyLookupByPath.toJson }
609+ */
610+ public toJson < TSerialized > (
611+ serializeValue : ( value : TItem ) => TSerialized
612+ ) : ILookupByPathJson < TSerialized > {
613+ const valueToIndex : Map < TItem , number > = new Map ( ) ;
614+ const values : TSerialized [ ] = [ ] ;
615+
616+ const getOrAddValueIndex : ( value : TItem ) => number = ( value : TItem ) => {
617+ let index : number | undefined = valueToIndex . get ( value ) ;
618+ if ( index === undefined ) {
619+ index = values . length ;
620+ valueToIndex . set ( value , index ) ;
621+ values . push ( serializeValue ( value ) ) ;
622+ }
623+ return index ;
624+ } ;
625+
626+ const serializeNode : ( node : IPathTrieNode < TItem > ) => ISerializedPathTrieNode = (
627+ node : IPathTrieNode < TItem >
628+ ) => {
629+ const result : ISerializedPathTrieNode = { } ;
630+
631+ if ( node . value !== undefined ) {
632+ result . valueIndex = getOrAddValueIndex ( node . value ) ;
633+ }
634+
635+ if ( node . children && node . children . size > 0 ) {
636+ const children : Record < string , ISerializedPathTrieNode > = { } ;
637+ for ( const [ segment , child ] of node . children ) {
638+ children [ segment ] = serializeNode ( child ) ;
639+ }
640+ result . children = children ;
641+ }
642+
643+ return result ;
644+ } ;
645+
646+ return {
647+ delimiter : this . delimiter ,
648+ values,
649+ tree : serializeNode ( this . _root )
650+ } ;
651+ }
652+
653+ /**
654+ * Deserializes a `LookupByPath` instance from a JSON representation previously
655+ * created by {@link LookupByPath.toJson}.
656+ *
657+ * @param json - The JSON representation to deserialize.
658+ * @param deserializeValue - A function that converts a serialized value back to its original type.
659+ * @returns A new `LookupByPath` instance.
660+ *
661+ * @remarks
662+ * Reference equality is preserved: if multiple nodes in the serialized trie pointed at the same
663+ * value (i.e., the same index in the `values` array), the deserialized nodes will share the same
664+ * object reference.
665+ */
666+ public static fromJson < TItem extends { } , TSerialized > (
667+ json : ILookupByPathJson < TSerialized > ,
668+ deserializeValue : ( serialized : TSerialized ) => TItem
669+ ) : LookupByPath < TItem > {
670+ const deserializedValues : TItem [ ] = json . values . map ( deserializeValue ) ;
671+
672+ const result : LookupByPath < TItem > = new LookupByPath < TItem > ( undefined , json . delimiter ) ;
673+
674+ const deserializeNode : (
675+ jsonNode : ISerializedPathTrieNode ,
676+ targetNode : IPathTrieNode < TItem >
677+ ) => void = ( jsonNode : ISerializedPathTrieNode , targetNode : IPathTrieNode < TItem > ) => {
678+ if ( jsonNode . valueIndex !== undefined ) {
679+ targetNode . value = deserializedValues [ jsonNode . valueIndex ] ;
680+ result . _size ++ ;
681+ }
682+
683+ if ( jsonNode . children ) {
684+ targetNode . children = new Map ( ) ;
685+ for ( const [ segment , childJson ] of Object . entries ( jsonNode . children ) ) {
686+ const childNode : IPathTrieNode < TItem > = {
687+ value : undefined ,
688+ children : undefined
689+ } ;
690+ targetNode . children . set ( segment , childNode ) ;
691+ deserializeNode ( childJson , childNode ) ;
692+ }
693+ }
694+ } ;
695+
696+ deserializeNode ( json . tree , result . _root ) ;
697+
698+ return result ;
699+ }
700+
548701 /**
549702 * Iterates through progressively longer prefixes of a given string and returns as soon
550703 * as the number of candidate items that match the prefix are 1 or 0.
0 commit comments