11// Copyright 2018-2026 the Deno authors. MIT license.
22// This module is browser compatible.
33
4+ /**
5+ * Options for {@linkcode Lazy.prototype.get}.
6+ *
7+ * @experimental **UNSTABLE**: New API, yet to be vetted.
8+ */
9+ export interface LazyGetOptions {
10+ /**
11+ * Signal used to abort the wait for initialization.
12+ *
13+ * Aborting does not cancel the underlying initializer — it only rejects the
14+ * caller's promise. Other callers and any in-flight initialization are
15+ * unaffected.
16+ */
17+ signal ?: AbortSignal ;
18+ }
19+
420/**
521 * A lazy value that is initialized at most once, with built-in deduplication of
622 * concurrent callers. Prevents the common race where two concurrent `get()` calls
723 * both trigger the initializer; only one initialization runs and all callers share
824 * the same promise.
925 *
10- * @experimental **UNSTABLE**: New API, yet to be vetted.
26+ * If the initializer rejects, the error is propagated to all concurrent callers
27+ * and the internal state is cleared — the next {@linkcode Lazy.prototype.get}
28+ * call will re-run the initializer. Compose with {@linkcode retry} for
29+ * automatic back-off on transient failures.
1130 *
1231 * @example Concurrent deduplication
1332 *
3958 * await db.get();
4059 * ```
4160 *
61+ * @experimental **UNSTABLE**: New API, yet to be vetted.
62+ *
4263 * @typeParam T The type of the lazily initialized value.
4364 */
4465export class Lazy < T > {
@@ -65,8 +86,6 @@ export class Lazy<T> {
6586 *
6687 * Always returns a promise, even when the initializer is synchronous.
6788 *
68- * @experimental **UNSTABLE**: New API, yet to be vetted.
69- *
7089 * @example Usage
7190 * ```ts no-assert
7291 * import { Lazy } from "@std/async/unstable-lazy";
@@ -75,37 +94,72 @@ export class Lazy<T> {
7594 * const value = await config.get();
7695 * ```
7796 *
97+ * @example Abort a slow initialization
98+ * ```ts
99+ * import { Lazy } from "@std/async/unstable-lazy";
100+ * import { assertRejects } from "@std/assert";
101+ *
102+ * const slow = new Lazy(() => new Promise<string>(() => {}));
103+ * const controller = new AbortController();
104+ * controller.abort(new Error("timed out"));
105+ * await assertRejects(
106+ * () => slow.get({ signal: controller.signal }),
107+ * Error,
108+ * "timed out",
109+ * );
110+ * ```
111+ *
112+ * @experimental **UNSTABLE**: New API, yet to be vetted.
113+ *
114+ * @param options Optional settings for this call.
78115 * @returns The cached or newly initialized value.
79116 */
80- get ( ) : Promise < T > {
81- if ( this . #promise !== undefined ) {
82- return this . #promise;
117+ get ( options ?: LazyGetOptions ) : Promise < T > {
118+ if ( this . #settled) return Promise . resolve ( this . #value as T ) ;
119+ const signal = options ?. signal ;
120+ if ( signal ?. aborted ) return Promise . reject ( signal . reason ) ;
121+
122+ if ( this . #promise === undefined ) {
123+ const p = new Promise < T > ( ( resolve , reject ) => {
124+ Promise . resolve ( ) . then ( ( ) => this . #init( ) ) . then (
125+ ( value ) => {
126+ if ( this . #promise === p ) {
127+ this . #value = value ;
128+ this . #settled = true ;
129+ }
130+ resolve ( value ) ;
131+ } ,
132+ ( err ) => {
133+ if ( this . #promise === p ) {
134+ this . #promise = undefined ;
135+ }
136+ reject ( err ) ;
137+ } ,
138+ ) ;
139+ } ) ;
140+ this . #promise = p ;
83141 }
84- const p = Promise . resolve ( ) . then ( ( ) => this . #init ( ) ) ;
85- this . #promise = p ;
86- p . then (
87- ( value ) => {
88- if ( this . #promise === p ) {
89- this . #value = value ;
90- this . #settled = true ;
91- }
92- return value ;
93- } ,
94- ( _err ) => {
95- if ( this . #promise === p ) {
96- this . #promise = undefined ;
97- }
98- } ,
99- ) ;
100- return p ;
142+
143+ if ( ! signal ) return this . #promise;
144+
145+ return new Promise < T > ( ( resolve , reject ) => {
146+ const abort = ( ) => reject ( signal . reason ) ;
147+ signal . addEventListener ( "abort" , abort , { once : true } ) ;
148+ this . #promise ! . then (
149+ ( value ) => {
150+ signal . removeEventListener ( "abort" , abort ) ;
151+ resolve ( value ) ;
152+ } ,
153+ ( err ) => {
154+ signal . removeEventListener ( "abort" , abort ) ;
155+ reject ( err ) ;
156+ } ,
157+ ) ;
158+ } ) ;
101159 }
102160
103161 /**
104- * Whether the value has been successfully initialized. Useful for
105- * distinguishing "not yet initialized" from "initialized with `undefined`"
106- * when `T` can be `undefined`.
107- *
108- * @experimental **UNSTABLE**: New API, yet to be vetted.
162+ * Whether the value has been successfully initialized.
109163 *
110164 * @example Check initialization state
111165 * ```ts
@@ -118,48 +172,56 @@ export class Lazy<T> {
118172 * assertEquals(lazy.initialized, true);
119173 * ```
120174 *
175+ * @experimental **UNSTABLE**: New API, yet to be vetted.
176+ *
121177 * @returns `true` if the value has been initialized, `false` otherwise.
122178 */
123179 get initialized ( ) : boolean {
124180 return this . #settled;
125181 }
126182
127183 /**
128- * Returns the value if already resolved, `undefined` otherwise. Useful for
129- * fast-path checks where you do not want to await. Returns `undefined` while
130- * initialization is in-flight.
131- *
132- * If `T` can be `undefined`, use {@linkcode initialized} to distinguish
133- * "not yet initialized" from "initialized with `undefined`".
134- *
135- * @experimental **UNSTABLE**: New API, yet to be vetted.
184+ * Returns the value if already resolved, or indicates that it is not yet
185+ * available. The discriminated union avoids ambiguity when `T` itself can
186+ * be `undefined`.
136187 *
137188 * @example Fast-path when already initialized
138- * ```ts no-assert
189+ * ```ts
139190 * import { Lazy } from "@std/async/unstable-lazy";
191+ * import { assertEquals } from "@std/assert";
140192 *
141193 * const config = new Lazy(async () => ({ port: 8080 }));
142194 * await config.get();
143195 *
144- * const cached = config.peek();
145- * if (cached !== undefined) {
146- * console.log("using cached", cached.port);
147- * }
196+ * const result = config.peek();
197+ * assertEquals(result, { ok: true, value: { port: 8080 } });
148198 * ```
149199 *
150- * @returns The resolved value, or `undefined` if not yet initialized or still in-flight.
200+ * @example Not yet initialized
201+ * ```ts
202+ * import { Lazy } from "@std/async/unstable-lazy";
203+ * import { assertEquals } from "@std/assert";
204+ *
205+ * const lazy = new Lazy(() => 42);
206+ * assertEquals(lazy.peek(), { ok: false });
207+ * ```
208+ *
209+ * @experimental **UNSTABLE**: New API, yet to be vetted.
210+ *
211+ * @returns `{ ok: true, value }` if the value has been initialized, or
212+ * `{ ok: false }` if not yet initialized or still in-flight.
151213 */
152- peek ( ) : T | undefined {
153- return this . #settled ? this . #value : undefined ;
214+ peek ( ) : { ok : true ; value : T } | { ok : false } {
215+ return this . #settled
216+ ? { ok : true , value : this . #value as T }
217+ : { ok : false } ;
154218 }
155219
156220 /**
157221 * Resets the lazy so the next {@linkcode get} re-runs the initializer. Does
158222 * not cancel an in-flight initialization; callers that already have the
159223 * promise will still receive its result.
160224 *
161- * @experimental **UNSTABLE**: New API, yet to be vetted.
162- *
163225 * @example Force reload
164226 * ```ts ignore
165227 * import { Lazy } from "@std/async/unstable-lazy";
@@ -169,6 +231,8 @@ export class Lazy<T> {
169231 * config.reset();
170232 * const fresh = await config.get();
171233 * ```
234+ *
235+ * @experimental **UNSTABLE**: New API, yet to be vetted.
172236 */
173237 reset ( ) : void {
174238 this . #promise = undefined ;
0 commit comments