@inox-tools/utils 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/dist/lazy.d.ts CHANGED
@@ -1,14 +1,43 @@
1
+ /**
2
+ * Callback type for attaching side effects to lazy values with a name identifier.
3
+ *
4
+ * @template T - The type of the lazy value
5
+ * @template O - The return type of the attachment callback (defaults to void)
6
+ */
7
+ type LazyMapping<T, O = void> = (name: string, value: T) => O;
8
+ /**
9
+ * Utility type that extracts the inner types from an array of Lazy instances.
10
+ *
11
+ * @template T - A tuple of Lazy instances
12
+ */
13
+ type UnwrapLazies<T extends Lazy<any>[]> = T extends [
14
+ Lazy<infer First>,
15
+ ...infer Rest extends Lazy<any>[]
16
+ ] ? [First, ...UnwrapLazies<Rest>] : [];
1
17
  /**
2
18
  * A lazily computed memoized value.
3
19
  *
4
20
  * The given factory is only constructed on first use of the value.
5
21
  * Any subsequent use retrieves the same instance of the value.
22
+ *
23
+ * If the value is accessed while it is being created, an error is thrown to prevent circular dependencies.
24
+ * When that happens, the Lazy instance becomes unusable.
25
+ *
26
+ * Lazy implements the Promise interface, allowing it to be awaited directly.
27
+ *
28
+ * @template T - The type of the lazily computed value
6
29
  */
7
- declare class Lazy<T> {
30
+ declare class Lazy<T> implements Promise<T> {
8
31
  private factory;
9
- private initialized;
10
- private value?;
32
+ private value;
33
+ private attachments?;
11
34
  private constructor();
35
+ /**
36
+ * Creates a new Lazy instance from a factory function.
37
+ *
38
+ * @param factory - A function that produces the value when first accessed
39
+ * @returns A new Lazy instance wrapping the factory
40
+ */
12
41
  static of<T>(factory: () => T): Lazy<T>;
13
42
  /**
14
43
  * Wrap the given factory into a lazily computed memoized value.
@@ -16,7 +45,156 @@ declare class Lazy<T> {
16
45
  * The function will at most be called once, on the first use of the value.
17
46
  */
18
47
  static wrap<T>(factory: () => T): () => T;
48
+ /**
49
+ * Gets the lazily computed value, creating it if necessary.
50
+ *
51
+ * @returns The computed value
52
+ * @throws Error if a circular dependency is detected during value creation
53
+ */
19
54
  get(): T;
55
+ ensureInitialized(): void;
56
+ /**
57
+ * Implements Promise.then() for Promise compatibility.
58
+ * Allows the Lazy instance to be awaited.
59
+ */
60
+ then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined): Promise<TResult1 | TResult2>;
61
+ /**
62
+ * Implements Promise.catch() for Promise compatibility.
63
+ */
64
+ catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined): Promise<T | TResult>;
65
+ /**
66
+ * Implements Promise.finally() for Promise compatibility.
67
+ */
68
+ finally(onfinally?: (() => void) | null | undefined): Promise<T>;
69
+ /** String tag for Object.prototype.toString */
70
+ [Symbol.toStringTag]: string;
71
+ /**
72
+ * Registers a callback to be executed when, and if, the lazy value is computed.
73
+ * If the value is already computed, the callback is invoked immediately.
74
+ *
75
+ * @param attach - Callback to execute with the computed value
76
+ * @returns This instance for chaining
77
+ */
78
+ attach(attach: (value: T) => void): this;
79
+ /**
80
+ * Creates a new Lazy that transforms this value using the provided function.
81
+ * The new Lazy is automatically evaluated when this one is evaluated.
82
+ * This way, if either Lazy is evaluated, both values are computed.
83
+ *
84
+ * @template O - The type of the transformed value
85
+ * @param mapper - Transformation function to apply to the value
86
+ * @returns A new Lazy instance containing the transformed value
87
+ */
88
+ chain<O>(mapper: (value: T) => O): Lazy<O>;
89
+ /**
90
+ * Attaches a callback to multiple named lazy values.
91
+ * The callback receives both the name and value for each lazy when evaluated.
92
+ *
93
+ * @template T - The type of the lazy values
94
+ * @param lazies - Record of named Lazy instances
95
+ * @param attach - Callback to execute for each lazy value
96
+ */
97
+ static attachMulti<T>(lazies: Record<string, Lazy<T>>, attach: LazyMapping<T>): void;
98
+ /**
99
+ * Chains a transformation across multiple named lazy values.
100
+ * Creates a new record of Lazy instances with transformed values.
101
+ *
102
+ * @template T - The type of the input lazy values
103
+ * @template R - The record type containing the lazy instances
104
+ * @template O - The type of the transformed values
105
+ * @param lazies - Record of named Lazy instances
106
+ * @param mapper - Transformation function receiving name and value
107
+ * @returns A new record of Lazy instances with the same keys
108
+ */
109
+ static chainMulti<T, const R extends Record<string, Lazy<T>>, O>(lazies: R, mapper: LazyMapping<T, O>): Record<keyof R, Lazy<O>>;
110
+ /**
111
+ * Attaches a callback that is invoked when all lazy values in the array are evaluated.
112
+ * The callback receives all values as arguments in order.
113
+ *
114
+ * @template Ts - Tuple type of Lazy instances
115
+ * @param lazies - Array of Lazy instances to wait for
116
+ * @param attach - Callback receiving all unwrapped values
117
+ */
118
+ static attachAll<const Ts extends Lazy<any>[]>(lazies: Ts, attach: (...args: UnwrapLazies<Ts>) => void): void;
119
+ /**
120
+ * Creates a new Lazy that combines all values from the given lazy instances.
121
+ * The transformation is invoked when all input lazies are evaluated.
122
+ *
123
+ * @template Ts - Tuple type of Lazy instances
124
+ * @template O - The type of the combined result
125
+ * @param lazies - Array of Lazy instances to combine
126
+ * @param mapper - Transformation function receiving all unwrapped values
127
+ * @returns A new Lazy instance containing the combined result
128
+ */
129
+ static chainAll<const Ts extends Lazy<any>[], O>(lazies: Ts, mapper: (...args: UnwrapLazies<Ts>) => O): Lazy<O>;
130
+ }
131
+ /**
132
+ * A keyed lazy value store that memoizes values by string key.
133
+ *
134
+ * Each key maps to a lazily computed value that is only created on first access.
135
+ * Supports attachments that are notified when new values are created.
136
+ * @template T - The type of values stored
137
+ */
138
+ declare class LazyKeyed<T> {
139
+ private factory;
140
+ /** Map of key to either a tuple containing the value or CREATING symbol */
141
+ private readonly instances;
142
+ /** List of callbacks to invoke when any value is created */
143
+ private readonly attachments;
144
+ private constructor();
145
+ /**
146
+ * Creates a new LazyKeyed instance from a factory function.
147
+ * @param factory - A function that produces a value for a given key
148
+ * @returns A new LazyKeyed instance
149
+ */
150
+ static of<T>(factory: (key: string) => T): LazyKeyed<T>;
151
+ /**
152
+ * Gets the value for the given key, creating it if necessary.
153
+ * @param key - The key to look up or create
154
+ * @returns The value associated with the key
155
+ * @throws Error if a circular dependency is detected during value creation
156
+ */
157
+ get(key: string): T;
158
+ /**
159
+ * Pre-initializes values for the given keys.
160
+ * Useful for eagerly creating values that will be needed later.
161
+ * @param key - The first key to reserve
162
+ * @param keys - Additional keys to reserve
163
+ * @returns This instance for chaining
164
+ */
165
+ reserve(key: string, ...keys: string[]): this;
166
+ /**
167
+ * Registers a callback to be executed for each value when created.
168
+ * Immediately invokes the callback for any already-created values.
169
+ * @param attach - Callback receiving the key and value
170
+ * @returns This instance for chaining
171
+ */
172
+ attach(attach: LazyMapping<T>): this;
173
+ /**
174
+ * Registers a callback for a specific key only.
175
+ * The callback is invoked when the value for that key is created.
176
+ * @param key - The key to watch
177
+ * @param attach - Callback receiving the value
178
+ * @returns This instance for chaining
179
+ */
180
+ attachOne(key: string, attach: (value: T) => void): this;
181
+ /**
182
+ * Creates a new LazyKeyed that transforms values using the provided function.
183
+ * Values in the new LazyKeyed are automatically created when source values are created.
184
+ * @template O - The type of the transformed values
185
+ * @param mapper - Transformation function receiving key and value
186
+ * @returns A new LazyKeyed instance with transformed values
187
+ */
188
+ chain<O>(mapper: LazyMapping<T, O>): LazyKeyed<O>;
189
+ /**
190
+ * Creates a Lazy that transforms the value for a specific key.
191
+ * The Lazy is automatically evaluated when the source value is created.
192
+ * @template O - The type of the transformed value
193
+ * @param key - The key to transform
194
+ * @param mapper - Transformation function receiving the value
195
+ * @returns A Lazy instance containing the transformed value
196
+ */
197
+ chainOne<O>(key: string, mapper: (value: T) => O): Lazy<O>;
20
198
  }
21
199
 
22
- export { Lazy };
200
+ export { Lazy, LazyKeyed };
package/dist/lazy.js CHANGED
@@ -1,2 +1,2 @@
1
- class a{constructor(i){this.factory=i;}initialized=false;value;static of(i){return new this(i)}static wrap(i){const t=a.of(i);return t.get.bind(t)}get(){return this.initialized||(this.value=this.factory(),this.initialized=true),this.value}}export{a as Lazy};//# sourceMappingURL=lazy.js.map
1
+ const s=Symbol("creating"),c=Symbol("missing"),l=()=>{throw new Error("This Lazy value has already been consumed.")};class a{constructor(t){this.factory=t;}value=c;attachments=[];static of(t){return new a(t)}static wrap(t){const e=a.of(t);return e.get.bind(e)}get(){if(this.value===c){this.value=s;const t=this.factory();this.factory=l,this.value=t;for(const e of this.attachments)e(t);delete this.attachments;}if(this.value===s)throw new Error("Circular dependency detected during Lazy value creation.");return this.value}ensureInitialized(){this.value!==s&&this.get();}then(t,e){return Promise.resolve().then(()=>this.get()).then(t,e)}catch(t){return this.then(void 0,t)}finally(t){return this.then().finally(t)}[Symbol.toStringTag]="Lazy";attach(t){return this.value!==c&&this.value!==s?t(this.value):this.attachments.push(t),this}chain(t){const e=a.of(()=>t(this.get()));return this.attach(()=>e.ensureInitialized()),e}static attachMulti(t,e){for(const[i,n]of Object.entries(t))n.attach(r=>e(i,r));}static chainMulti(t,e){const i={};for(const[n,r]of Object.entries(t))i[n]=r.chain(o=>e(n,o));return i}static attachAll(t,e){function i(n=0){if(n>=t.length){e(...t.map(r=>r.get()));return}t[n].attach(()=>{i(n+1);});}i();}static chainAll(t,e){const i=a.of(()=>e(...t.map(n=>n.get())));return this.attachAll(t,()=>i.ensureInitialized()),i}}class u{constructor(t){this.factory=t;}instances=new Map;attachments=[];static of(t){return new this(t)}get(t){const e=this.instances.get(t);if(e===s)throw new Error("Circular dependency detected during LazyKeyed value creation with value: "+t);if(e!==void 0)return e[0];this.instances.set(t,s);const i=this.factory(t);this.instances.set(t,[i]);for(const n of this.attachments)n(t,i);return i}reserve(t,...e){e.unshift(t);for(const i of e)this.instances.get(i)===void 0&&this.get(i);return this}attach(t){for(const[e,i]of this.instances.entries())i!==s&&t(e,i[0]);return this.attachments.push(t),this}attachOne(t,e){return this.attach((i,n)=>{i===t&&e(n);})}chain(t){const e=u.of(i=>t(i,this.get(i)));return this.attach(i=>{e.reserve(i);}),e}chainOne(t,e){const i=a.of(()=>e(this.get(t)));return this.attachOne(t,()=>i.ensureInitialized()),i}}export{a as Lazy,u as LazyKeyed};//# sourceMappingURL=lazy.js.map
2
2
  //# sourceMappingURL=lazy.js.map
package/dist/lazy.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lazy.ts"],"names":["Lazy","factory","lazy"],"mappings":"AAMO,MAAMA,CAAQ,CAIZ,WAAA,CAAoBC,CAAAA,CAAkB,CAAlB,aAAAA,EAAmB,CAHvC,WAAA,CAAc,KAAA,CACd,KAAA,CAIR,OAAc,GAAMA,CAAAA,CAA2B,CAC9C,OAAO,IAAI,IAAA,CAAKA,CAAO,CACxB,CAOA,OAAc,IAAA,CAAQA,CAAAA,CAA2B,CAChD,MAAMC,EAAOF,CAAAA,CAAK,EAAA,CAAGC,CAAO,CAAA,CAC5B,OAAOC,CAAAA,CAAK,IAAI,IAAA,CAAKA,CAAI,CAC1B,CAEO,GAAA,EAAS,CACf,OAAK,IAAA,CAAK,WAAA,GACT,IAAA,CAAK,KAAA,CAAQ,IAAA,CAAK,OAAA,EAAQ,CAC1B,IAAA,CAAK,WAAA,CAAc,IAAA,CAAA,CAGb,IAAA,CAAK,KACb,CACD","file":"lazy.js","sourcesContent":["/**\n * A lazily computed memoized value.\n *\n * The given factory is only constructed on first use of the value.\n * Any subsequent use retrieves the same instance of the value.\n */\nexport class Lazy<T> {\n\tprivate initialized = false;\n\tprivate value?: T;\n\n\tprivate constructor(private factory: () => T) {}\n\n\tpublic static of<T>(factory: () => T): Lazy<T> {\n\t\treturn new this(factory);\n\t}\n\n\t/**\n\t * Wrap the given factory into a lazily computed memoized value.\n\t *\n\t * The function will at most be called once, on the first use of the value.\n\t */\n\tpublic static wrap<T>(factory: () => T): () => T {\n\t\tconst lazy = Lazy.of(factory);\n\t\treturn lazy.get.bind(lazy);\n\t}\n\n\tpublic get(): T {\n\t\tif (!this.initialized) {\n\t\t\tthis.value = this.factory();\n\t\t\tthis.initialized = true;\n\t\t}\n\n\t\treturn this.value!;\n\t}\n}\n"]}
1
+ {"version":3,"sources":["../src/lazy.ts"],"names":["CREATING","MISSING","SPENT_FACTORY","Lazy","factory","lazy","value","attach","onfulfilled","onrejected","onfinally","mapper","other","lazies","name","result","attachNext","index","l","LazyKeyed","key","stored","newInstance","keys","k","instance","newKeyed"],"mappings":"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","file":"lazy.js","sourcesContent":["/**\n * Callback type for attaching side effects to lazy values with a name identifier.\n *\n * @template T - The type of the lazy value\n * @template O - The return type of the attachment callback (defaults to void)\n */\ntype LazyMapping<T, O = void> = (name: string, value: T) => O;\n\n/**\n * Utility type that extracts the inner types from an array of Lazy instances.\n *\n * @template T - A tuple of Lazy instances\n */\ntype UnwrapLazies<T extends Lazy<any>[]> = T extends [\n\tLazy<infer First>,\n\t...infer Rest extends Lazy<any>[],\n]\n\t? [First, ...UnwrapLazies<Rest>]\n\t: [];\n\n/** Symbol indicating a lazy value is currently being created (for circular dependency detection) */\nconst CREATING = Symbol('creating');\n/** Symbol indicating a lazy value has not yet been initialized */\nconst MISSING = Symbol('missing');\n\nconst SPENT_FACTORY = (): never => {\n\tthrow new Error('This Lazy value has already been consumed.');\n};\n\n/**\n * A lazily computed memoized value.\n *\n * The given factory is only constructed on first use of the value.\n * Any subsequent use retrieves the same instance of the value.\n *\n * If the value is accessed while it is being created, an error is thrown to prevent circular dependencies.\n * When that happens, the Lazy instance becomes unusable.\n *\n * Lazy implements the Promise interface, allowing it to be awaited directly.\n *\n * @template T - The type of the lazily computed value\n */\nexport class Lazy<T> implements Promise<T> {\n\tprivate value: T | typeof CREATING | typeof MISSING = MISSING;\n\n\tprivate attachments?: ((value: T) => void)[] = [];\n\n\tprivate constructor(private factory: () => T) {}\n\n\t/**\n\t * Creates a new Lazy instance from a factory function.\n\t *\n\t * @param factory - A function that produces the value when first accessed\n\t * @returns A new Lazy instance wrapping the factory\n\t */\n\tpublic static of<T>(factory: () => T): Lazy<T> {\n\t\treturn new Lazy(factory);\n\t}\n\n\t/**\n\t * Wrap the given factory into a lazily computed memoized value.\n\t *\n\t * The function will at most be called once, on the first use of the value.\n\t */\n\tpublic static wrap<T>(factory: () => T): () => T {\n\t\tconst lazy = Lazy.of(factory);\n\t\treturn lazy.get.bind(lazy);\n\t}\n\n\t/**\n\t * Gets the lazily computed value, creating it if necessary.\n\t *\n\t * @returns The computed value\n\t * @throws Error if a circular dependency is detected during value creation\n\t */\n\tpublic get(): T {\n\t\tif (this.value === MISSING) {\n\t\t\tthis.value = CREATING;\n\t\t\tconst value = this.factory();\n\t\t\t// Mark factory as spent to prevent reuse and release references to closure.\n\t\t\tthis.factory = SPENT_FACTORY;\n\t\t\tthis.value = value;\n\t\t\tfor (const attach of this.attachments!) {\n\t\t\t\tattach(value);\n\t\t\t}\n\t\t\tdelete this.attachments;\n\t\t}\n\t\tif (this.value === CREATING) {\n\t\t\tthrow new Error('Circular dependency detected during Lazy value creation.');\n\t\t}\n\n\t\treturn this.value;\n\t}\n\n\tpublic ensureInitialized(): void {\n\t\tif (this.value === CREATING) {\n\t\t\t// this Lazy is already being created; do nothing to avoid circular dependency\n\t\t\treturn;\n\t\t}\n\t\tthis.get();\n\t}\n\n\t/**\n\t * Implements Promise.then() for Promise compatibility.\n\t * Allows the Lazy instance to be awaited.\n\t */\n\tpublic then<TResult1 = T, TResult2 = never>(\n\t\tonfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,\n\t\tonrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined\n\t): Promise<TResult1 | TResult2> {\n\t\treturn Promise.resolve()\n\t\t\t.then(() => this.get())\n\t\t\t.then(onfulfilled, onrejected);\n\t}\n\n\t/**\n\t * Implements Promise.catch() for Promise compatibility.\n\t */\n\tpublic catch<TResult = never>(\n\t\tonrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined\n\t): Promise<T | TResult> {\n\t\treturn this.then(undefined, onrejected);\n\t}\n\n\t/**\n\t * Implements Promise.finally() for Promise compatibility.\n\t */\n\tpublic finally(onfinally?: (() => void) | null | undefined): Promise<T> {\n\t\treturn this.then().finally(onfinally);\n\t}\n\n\t/** String tag for Object.prototype.toString */\n\t[Symbol.toStringTag]: string = 'Lazy';\n\n\t/**\n\t * Registers a callback to be executed when, and if, the lazy value is computed.\n\t * If the value is already computed, the callback is invoked immediately.\n\t *\n\t * @param attach - Callback to execute with the computed value\n\t * @returns This instance for chaining\n\t */\n\tpublic attach(attach: (value: T) => void): this {\n\t\tif (this.value !== MISSING && this.value !== CREATING) {\n\t\t\tattach(this.value as T); // Force evaluation if already present\n\t\t} else {\n\t\t\tthis.attachments!.push(attach);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Creates a new Lazy that transforms this value using the provided function.\n\t * The new Lazy is automatically evaluated when this one is evaluated.\n\t * This way, if either Lazy is evaluated, both values are computed.\n\t *\n\t * @template O - The type of the transformed value\n\t * @param mapper - Transformation function to apply to the value\n\t * @returns A new Lazy instance containing the transformed value\n\t */\n\tpublic chain<O>(mapper: (value: T) => O): Lazy<O> {\n\t\tconst other = Lazy.of(() => mapper(this.get()));\n\n\t\tthis.attach(() => other.ensureInitialized());\n\n\t\treturn other;\n\t}\n\n\t/**\n\t * Attaches a callback to multiple named lazy values.\n\t * The callback receives both the name and value for each lazy when evaluated.\n\t *\n\t * @template T - The type of the lazy values\n\t * @param lazies - Record of named Lazy instances\n\t * @param attach - Callback to execute for each lazy value\n\t */\n\tpublic static attachMulti<T>(lazies: Record<string, Lazy<T>>, attach: LazyMapping<T>): void {\n\t\tfor (const [name, lazy] of Object.entries(lazies)) {\n\t\t\tlazy.attach((value) => attach(name, value));\n\t\t}\n\t}\n\n\t/**\n\t * Chains a transformation across multiple named lazy values.\n\t * Creates a new record of Lazy instances with transformed values.\n\t *\n\t * @template T - The type of the input lazy values\n\t * @template R - The record type containing the lazy instances\n\t * @template O - The type of the transformed values\n\t * @param lazies - Record of named Lazy instances\n\t * @param mapper - Transformation function receiving name and value\n\t * @returns A new record of Lazy instances with the same keys\n\t */\n\tpublic static chainMulti<T, const R extends Record<string, Lazy<T>>, O>(\n\t\tlazies: R,\n\t\tmapper: LazyMapping<T, O>\n\t): Record<keyof R, Lazy<O>> {\n\t\tconst result: Record<string, Lazy<O>> = {};\n\t\tfor (const [name, lazy] of Object.entries(lazies)) {\n\t\t\tresult[name] = lazy.chain((value) => mapper(name, value));\n\t\t}\n\n\t\treturn result as Record<keyof R, Lazy<O>>;\n\t}\n\n\t/**\n\t * Attaches a callback that is invoked when all lazy values in the array are evaluated.\n\t * The callback receives all values as arguments in order.\n\t *\n\t * @template Ts - Tuple type of Lazy instances\n\t * @param lazies - Array of Lazy instances to wait for\n\t * @param attach - Callback receiving all unwrapped values\n\t */\n\tpublic static attachAll<const Ts extends Lazy<any>[]>(\n\t\tlazies: Ts,\n\t\tattach: (...args: UnwrapLazies<Ts>) => void\n\t): void {\n\t\tfunction attachNext(index = 0) {\n\t\t\tif (index >= lazies.length) {\n\t\t\t\tattach(...(lazies.map((l) => l.get()) as UnwrapLazies<Ts>));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlazies[index].attach(() => {\n\t\t\t\tattachNext(index + 1);\n\t\t\t});\n\t\t}\n\n\t\tattachNext();\n\t}\n\n\t/**\n\t * Creates a new Lazy that combines all values from the given lazy instances.\n\t * The transformation is invoked when all input lazies are evaluated.\n\t *\n\t * @template Ts - Tuple type of Lazy instances\n\t * @template O - The type of the combined result\n\t * @param lazies - Array of Lazy instances to combine\n\t * @param mapper - Transformation function receiving all unwrapped values\n\t * @returns A new Lazy instance containing the combined result\n\t */\n\tpublic static chainAll<const Ts extends Lazy<any>[], O>(\n\t\tlazies: Ts,\n\t\tmapper: (...args: UnwrapLazies<Ts>) => O\n\t): Lazy<O> {\n\t\tconst lazy = Lazy.of(() => mapper(...(lazies.map((l) => l.get()) as UnwrapLazies<Ts>)));\n\n\t\tthis.attachAll(lazies, () => lazy.ensureInitialized());\n\n\t\treturn lazy;\n\t}\n}\n\n/**\n * A keyed lazy value store that memoizes values by string key.\n *\n * Each key maps to a lazily computed value that is only created on first access.\n * Supports attachments that are notified when new values are created.\n * @template T - The type of values stored\n */\nexport class LazyKeyed<T> {\n\t/** Map of key to either a tuple containing the value or CREATING symbol */\n\tprivate readonly instances = new Map<string, [T] | typeof CREATING>();\n\n\t/** List of callbacks to invoke when any value is created */\n\tprivate readonly attachments: LazyMapping<T>[] = [];\n\n\tprivate constructor(private factory: (key: string) => T) {}\n\n\t/**\n\t * Creates a new LazyKeyed instance from a factory function.\n\t * @param factory - A function that produces a value for a given key\n\t * @returns A new LazyKeyed instance\n\t */\n\tpublic static of<T>(factory: (key: string) => T): LazyKeyed<T> {\n\t\treturn new this(factory);\n\t}\n\n\t/**\n\t * Gets the value for the given key, creating it if necessary.\n\t * @param key - The key to look up or create\n\t * @returns The value associated with the key\n\t * @throws Error if a circular dependency is detected during value creation\n\t */\n\tpublic get(key: string): T {\n\t\tconst stored = this.instances.get(key);\n\n\t\tif (stored === CREATING) {\n\t\t\tthrow new Error(\n\t\t\t\t'Circular dependency detected during LazyKeyed value creation with value: ' + key\n\t\t\t);\n\t\t}\n\n\t\tif (stored !== undefined) {\n\t\t\treturn stored[0];\n\t\t}\n\n\t\tthis.instances.set(key, CREATING);\n\n\t\tconst newInstance = this.factory(key);\n\n\t\tthis.instances.set(key, [newInstance]);\n\n\t\tfor (const attach of this.attachments) {\n\t\t\tattach(key, newInstance);\n\t\t}\n\n\t\treturn newInstance;\n\t}\n\n\t/**\n\t * Pre-initializes values for the given keys.\n\t * Useful for eagerly creating values that will be needed later.\n\t * @param key - The first key to reserve\n\t * @param keys - Additional keys to reserve\n\t * @returns This instance for chaining\n\t */\n\tpublic reserve(key: string, ...keys: string[]): this {\n\t\tkeys.unshift(key);\n\t\tfor (const k of keys) {\n\t\t\tif (this.instances.get(k) === undefined) {\n\t\t\t\tthis.get(k);\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Registers a callback to be executed for each value when created.\n\t * Immediately invokes the callback for any already-created values.\n\t * @param attach - Callback receiving the key and value\n\t * @returns This instance for chaining\n\t */\n\tpublic attach(attach: LazyMapping<T>): this {\n\t\tfor (const [name, instance] of this.instances.entries()) {\n\t\t\tif (instance !== CREATING) {\n\t\t\t\tattach(name, instance[0]);\n\t\t\t}\n\t\t}\n\t\tthis.attachments.push(attach);\n\t\treturn this;\n\t}\n\n\t/**\n\t * Registers a callback for a specific key only.\n\t * The callback is invoked when the value for that key is created.\n\t * @param key - The key to watch\n\t * @param attach - Callback receiving the value\n\t * @returns This instance for chaining\n\t */\n\tpublic attachOne(key: string, attach: (value: T) => void): this {\n\t\treturn this.attach((name, value) => {\n\t\t\tif (name === key) {\n\t\t\t\tattach(value);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Creates a new LazyKeyed that transforms values using the provided function.\n\t * Values in the new LazyKeyed are automatically created when source values are created.\n\t * @template O - The type of the transformed values\n\t * @param mapper - Transformation function receiving key and value\n\t * @returns A new LazyKeyed instance with transformed values\n\t */\n\tpublic chain<O>(mapper: LazyMapping<T, O>): LazyKeyed<O> {\n\t\tconst newKeyed = LazyKeyed.of((key) => mapper(key, this.get(key)));\n\n\t\tthis.attach((key) => {\n\t\t\tnewKeyed.reserve(key);\n\t\t});\n\n\t\treturn newKeyed;\n\t}\n\n\t/**\n\t * Creates a Lazy that transforms the value for a specific key.\n\t * The Lazy is automatically evaluated when the source value is created.\n\t * @template O - The type of the transformed value\n\t * @param key - The key to transform\n\t * @param mapper - Transformation function receiving the value\n\t * @returns A Lazy instance containing the transformed value\n\t */\n\tpublic chainOne<O>(key: string, mapper: (value: T) => O): Lazy<O> {\n\t\tconst lazy = Lazy.of(() => mapper(this.get(key)));\n\t\tthis.attachOne(key, () => lazy.ensureInitialized());\n\t\treturn lazy;\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inox-tools/utils",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "A collection of utilities used throughout Inox Tools",
5
5
  "keywords": [
6
6
  "utilities"
@@ -22,18 +22,18 @@
22
22
  "devDependencies": {
23
23
  "@types/hast": "^3.0.4",
24
24
  "@types/mdast": "^4.0.4",
25
- "@types/node": "^24.0.15",
25
+ "@types/node": "^24.3.0",
26
26
  "@types/unist": "^3.0.3",
27
27
  "@types/xast": "^2.0.4",
28
28
  "@vitest/coverage-v8": "^3.2.4",
29
29
  "@vitest/ui": "^3.2.4",
30
- "astro": "^5.12.0",
30
+ "astro": "^5.13.3",
31
31
  "jest-extended": "^6.0.0",
32
32
  "mdast-util-from-markdown": "^2.0.2",
33
33
  "tsup": "8.5.0",
34
- "typescript": "^5.8.3",
34
+ "typescript": "^5.9.2",
35
35
  "unist-util-is": "^6.0.0",
36
- "vite": "^7.0.5",
36
+ "vite": "^7.1.3",
37
37
  "vitest": "^3.2.4"
38
38
  },
39
39
  "scripts": {
package/src/lazy.ts CHANGED
@@ -1,17 +1,60 @@
1
+ /**
2
+ * Callback type for attaching side effects to lazy values with a name identifier.
3
+ *
4
+ * @template T - The type of the lazy value
5
+ * @template O - The return type of the attachment callback (defaults to void)
6
+ */
7
+ type LazyMapping<T, O = void> = (name: string, value: T) => O;
8
+
9
+ /**
10
+ * Utility type that extracts the inner types from an array of Lazy instances.
11
+ *
12
+ * @template T - A tuple of Lazy instances
13
+ */
14
+ type UnwrapLazies<T extends Lazy<any>[]> = T extends [
15
+ Lazy<infer First>,
16
+ ...infer Rest extends Lazy<any>[],
17
+ ]
18
+ ? [First, ...UnwrapLazies<Rest>]
19
+ : [];
20
+
21
+ /** Symbol indicating a lazy value is currently being created (for circular dependency detection) */
22
+ const CREATING = Symbol('creating');
23
+ /** Symbol indicating a lazy value has not yet been initialized */
24
+ const MISSING = Symbol('missing');
25
+
26
+ const SPENT_FACTORY = (): never => {
27
+ throw new Error('This Lazy value has already been consumed.');
28
+ };
29
+
1
30
  /**
2
31
  * A lazily computed memoized value.
3
32
  *
4
33
  * The given factory is only constructed on first use of the value.
5
34
  * Any subsequent use retrieves the same instance of the value.
35
+ *
36
+ * If the value is accessed while it is being created, an error is thrown to prevent circular dependencies.
37
+ * When that happens, the Lazy instance becomes unusable.
38
+ *
39
+ * Lazy implements the Promise interface, allowing it to be awaited directly.
40
+ *
41
+ * @template T - The type of the lazily computed value
6
42
  */
7
- export class Lazy<T> {
8
- private initialized = false;
9
- private value?: T;
43
+ export class Lazy<T> implements Promise<T> {
44
+ private value: T | typeof CREATING | typeof MISSING = MISSING;
45
+
46
+ private attachments?: ((value: T) => void)[] = [];
10
47
 
11
48
  private constructor(private factory: () => T) {}
12
49
 
50
+ /**
51
+ * Creates a new Lazy instance from a factory function.
52
+ *
53
+ * @param factory - A function that produces the value when first accessed
54
+ * @returns A new Lazy instance wrapping the factory
55
+ */
13
56
  public static of<T>(factory: () => T): Lazy<T> {
14
- return new this(factory);
57
+ return new Lazy(factory);
15
58
  }
16
59
 
17
60
  /**
@@ -24,12 +67,322 @@ export class Lazy<T> {
24
67
  return lazy.get.bind(lazy);
25
68
  }
26
69
 
70
+ /**
71
+ * Gets the lazily computed value, creating it if necessary.
72
+ *
73
+ * @returns The computed value
74
+ * @throws Error if a circular dependency is detected during value creation
75
+ */
27
76
  public get(): T {
28
- if (!this.initialized) {
29
- this.value = this.factory();
30
- this.initialized = true;
77
+ if (this.value === MISSING) {
78
+ this.value = CREATING;
79
+ const value = this.factory();
80
+ // Mark factory as spent to prevent reuse and release references to closure.
81
+ this.factory = SPENT_FACTORY;
82
+ this.value = value;
83
+ for (const attach of this.attachments!) {
84
+ attach(value);
85
+ }
86
+ delete this.attachments;
87
+ }
88
+ if (this.value === CREATING) {
89
+ throw new Error('Circular dependency detected during Lazy value creation.');
90
+ }
91
+
92
+ return this.value;
93
+ }
94
+
95
+ public ensureInitialized(): void {
96
+ if (this.value === CREATING) {
97
+ // this Lazy is already being created; do nothing to avoid circular dependency
98
+ return;
31
99
  }
100
+ this.get();
101
+ }
102
+
103
+ /**
104
+ * Implements Promise.then() for Promise compatibility.
105
+ * Allows the Lazy instance to be awaited.
106
+ */
107
+ public then<TResult1 = T, TResult2 = never>(
108
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
109
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined
110
+ ): Promise<TResult1 | TResult2> {
111
+ return Promise.resolve()
112
+ .then(() => this.get())
113
+ .then(onfulfilled, onrejected);
114
+ }
32
115
 
33
- return this.value!;
116
+ /**
117
+ * Implements Promise.catch() for Promise compatibility.
118
+ */
119
+ public catch<TResult = never>(
120
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined
121
+ ): Promise<T | TResult> {
122
+ return this.then(undefined, onrejected);
123
+ }
124
+
125
+ /**
126
+ * Implements Promise.finally() for Promise compatibility.
127
+ */
128
+ public finally(onfinally?: (() => void) | null | undefined): Promise<T> {
129
+ return this.then().finally(onfinally);
130
+ }
131
+
132
+ /** String tag for Object.prototype.toString */
133
+ [Symbol.toStringTag]: string = 'Lazy';
134
+
135
+ /**
136
+ * Registers a callback to be executed when, and if, the lazy value is computed.
137
+ * If the value is already computed, the callback is invoked immediately.
138
+ *
139
+ * @param attach - Callback to execute with the computed value
140
+ * @returns This instance for chaining
141
+ */
142
+ public attach(attach: (value: T) => void): this {
143
+ if (this.value !== MISSING && this.value !== CREATING) {
144
+ attach(this.value as T); // Force evaluation if already present
145
+ } else {
146
+ this.attachments!.push(attach);
147
+ }
148
+ return this;
149
+ }
150
+
151
+ /**
152
+ * Creates a new Lazy that transforms this value using the provided function.
153
+ * The new Lazy is automatically evaluated when this one is evaluated.
154
+ * This way, if either Lazy is evaluated, both values are computed.
155
+ *
156
+ * @template O - The type of the transformed value
157
+ * @param mapper - Transformation function to apply to the value
158
+ * @returns A new Lazy instance containing the transformed value
159
+ */
160
+ public chain<O>(mapper: (value: T) => O): Lazy<O> {
161
+ const other = Lazy.of(() => mapper(this.get()));
162
+
163
+ this.attach(() => other.ensureInitialized());
164
+
165
+ return other;
166
+ }
167
+
168
+ /**
169
+ * Attaches a callback to multiple named lazy values.
170
+ * The callback receives both the name and value for each lazy when evaluated.
171
+ *
172
+ * @template T - The type of the lazy values
173
+ * @param lazies - Record of named Lazy instances
174
+ * @param attach - Callback to execute for each lazy value
175
+ */
176
+ public static attachMulti<T>(lazies: Record<string, Lazy<T>>, attach: LazyMapping<T>): void {
177
+ for (const [name, lazy] of Object.entries(lazies)) {
178
+ lazy.attach((value) => attach(name, value));
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Chains a transformation across multiple named lazy values.
184
+ * Creates a new record of Lazy instances with transformed values.
185
+ *
186
+ * @template T - The type of the input lazy values
187
+ * @template R - The record type containing the lazy instances
188
+ * @template O - The type of the transformed values
189
+ * @param lazies - Record of named Lazy instances
190
+ * @param mapper - Transformation function receiving name and value
191
+ * @returns A new record of Lazy instances with the same keys
192
+ */
193
+ public static chainMulti<T, const R extends Record<string, Lazy<T>>, O>(
194
+ lazies: R,
195
+ mapper: LazyMapping<T, O>
196
+ ): Record<keyof R, Lazy<O>> {
197
+ const result: Record<string, Lazy<O>> = {};
198
+ for (const [name, lazy] of Object.entries(lazies)) {
199
+ result[name] = lazy.chain((value) => mapper(name, value));
200
+ }
201
+
202
+ return result as Record<keyof R, Lazy<O>>;
203
+ }
204
+
205
+ /**
206
+ * Attaches a callback that is invoked when all lazy values in the array are evaluated.
207
+ * The callback receives all values as arguments in order.
208
+ *
209
+ * @template Ts - Tuple type of Lazy instances
210
+ * @param lazies - Array of Lazy instances to wait for
211
+ * @param attach - Callback receiving all unwrapped values
212
+ */
213
+ public static attachAll<const Ts extends Lazy<any>[]>(
214
+ lazies: Ts,
215
+ attach: (...args: UnwrapLazies<Ts>) => void
216
+ ): void {
217
+ function attachNext(index = 0) {
218
+ if (index >= lazies.length) {
219
+ attach(...(lazies.map((l) => l.get()) as UnwrapLazies<Ts>));
220
+ return;
221
+ }
222
+
223
+ lazies[index].attach(() => {
224
+ attachNext(index + 1);
225
+ });
226
+ }
227
+
228
+ attachNext();
229
+ }
230
+
231
+ /**
232
+ * Creates a new Lazy that combines all values from the given lazy instances.
233
+ * The transformation is invoked when all input lazies are evaluated.
234
+ *
235
+ * @template Ts - Tuple type of Lazy instances
236
+ * @template O - The type of the combined result
237
+ * @param lazies - Array of Lazy instances to combine
238
+ * @param mapper - Transformation function receiving all unwrapped values
239
+ * @returns A new Lazy instance containing the combined result
240
+ */
241
+ public static chainAll<const Ts extends Lazy<any>[], O>(
242
+ lazies: Ts,
243
+ mapper: (...args: UnwrapLazies<Ts>) => O
244
+ ): Lazy<O> {
245
+ const lazy = Lazy.of(() => mapper(...(lazies.map((l) => l.get()) as UnwrapLazies<Ts>)));
246
+
247
+ this.attachAll(lazies, () => lazy.ensureInitialized());
248
+
249
+ return lazy;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * A keyed lazy value store that memoizes values by string key.
255
+ *
256
+ * Each key maps to a lazily computed value that is only created on first access.
257
+ * Supports attachments that are notified when new values are created.
258
+ * @template T - The type of values stored
259
+ */
260
+ export class LazyKeyed<T> {
261
+ /** Map of key to either a tuple containing the value or CREATING symbol */
262
+ private readonly instances = new Map<string, [T] | typeof CREATING>();
263
+
264
+ /** List of callbacks to invoke when any value is created */
265
+ private readonly attachments: LazyMapping<T>[] = [];
266
+
267
+ private constructor(private factory: (key: string) => T) {}
268
+
269
+ /**
270
+ * Creates a new LazyKeyed instance from a factory function.
271
+ * @param factory - A function that produces a value for a given key
272
+ * @returns A new LazyKeyed instance
273
+ */
274
+ public static of<T>(factory: (key: string) => T): LazyKeyed<T> {
275
+ return new this(factory);
276
+ }
277
+
278
+ /**
279
+ * Gets the value for the given key, creating it if necessary.
280
+ * @param key - The key to look up or create
281
+ * @returns The value associated with the key
282
+ * @throws Error if a circular dependency is detected during value creation
283
+ */
284
+ public get(key: string): T {
285
+ const stored = this.instances.get(key);
286
+
287
+ if (stored === CREATING) {
288
+ throw new Error(
289
+ 'Circular dependency detected during LazyKeyed value creation with value: ' + key
290
+ );
291
+ }
292
+
293
+ if (stored !== undefined) {
294
+ return stored[0];
295
+ }
296
+
297
+ this.instances.set(key, CREATING);
298
+
299
+ const newInstance = this.factory(key);
300
+
301
+ this.instances.set(key, [newInstance]);
302
+
303
+ for (const attach of this.attachments) {
304
+ attach(key, newInstance);
305
+ }
306
+
307
+ return newInstance;
308
+ }
309
+
310
+ /**
311
+ * Pre-initializes values for the given keys.
312
+ * Useful for eagerly creating values that will be needed later.
313
+ * @param key - The first key to reserve
314
+ * @param keys - Additional keys to reserve
315
+ * @returns This instance for chaining
316
+ */
317
+ public reserve(key: string, ...keys: string[]): this {
318
+ keys.unshift(key);
319
+ for (const k of keys) {
320
+ if (this.instances.get(k) === undefined) {
321
+ this.get(k);
322
+ }
323
+ }
324
+ return this;
325
+ }
326
+
327
+ /**
328
+ * Registers a callback to be executed for each value when created.
329
+ * Immediately invokes the callback for any already-created values.
330
+ * @param attach - Callback receiving the key and value
331
+ * @returns This instance for chaining
332
+ */
333
+ public attach(attach: LazyMapping<T>): this {
334
+ for (const [name, instance] of this.instances.entries()) {
335
+ if (instance !== CREATING) {
336
+ attach(name, instance[0]);
337
+ }
338
+ }
339
+ this.attachments.push(attach);
340
+ return this;
341
+ }
342
+
343
+ /**
344
+ * Registers a callback for a specific key only.
345
+ * The callback is invoked when the value for that key is created.
346
+ * @param key - The key to watch
347
+ * @param attach - Callback receiving the value
348
+ * @returns This instance for chaining
349
+ */
350
+ public attachOne(key: string, attach: (value: T) => void): this {
351
+ return this.attach((name, value) => {
352
+ if (name === key) {
353
+ attach(value);
354
+ }
355
+ });
356
+ }
357
+
358
+ /**
359
+ * Creates a new LazyKeyed that transforms values using the provided function.
360
+ * Values in the new LazyKeyed are automatically created when source values are created.
361
+ * @template O - The type of the transformed values
362
+ * @param mapper - Transformation function receiving key and value
363
+ * @returns A new LazyKeyed instance with transformed values
364
+ */
365
+ public chain<O>(mapper: LazyMapping<T, O>): LazyKeyed<O> {
366
+ const newKeyed = LazyKeyed.of((key) => mapper(key, this.get(key)));
367
+
368
+ this.attach((key) => {
369
+ newKeyed.reserve(key);
370
+ });
371
+
372
+ return newKeyed;
373
+ }
374
+
375
+ /**
376
+ * Creates a Lazy that transforms the value for a specific key.
377
+ * The Lazy is automatically evaluated when the source value is created.
378
+ * @template O - The type of the transformed value
379
+ * @param key - The key to transform
380
+ * @param mapper - Transformation function receiving the value
381
+ * @returns A Lazy instance containing the transformed value
382
+ */
383
+ public chainOne<O>(key: string, mapper: (value: T) => O): Lazy<O> {
384
+ const lazy = Lazy.of(() => mapper(this.get(key)));
385
+ this.attachOne(key, () => lazy.ensureInitialized());
386
+ return lazy;
34
387
  }
35
388
  }