import { Optional, Result } from 'ts-data-forge'; import { RootObservableClass } from '../class/index.mjs'; import { type FromPromiseObservable } from '../types/index.mjs'; /** * Creates an observable from a Promise factory that receives an `AbortSignal`. * When the observable is completed (e.g., by `switchMap` switching to a new * inner observable), the `AbortController` is automatically aborted, cancelling * the in-flight request. * * Emits `Result.ok(value)` when the promise resolves, or `Result.err(error)` * when it rejects. Rejections caused by abort (`AbortError`) are silently * ignored and do not emit. * * @template A - The type of the resolved value * @template E - The type of the error (excluding AbortError) * @param factory - A function that receives an `AbortSignal` and returns a Promise * @returns An observable that emits the promise result * * @example * ```ts * const results$ = query.pipe( * switchMap((q) => * fromAbortablePromise((signal) => * fetch(`/api/search?q=${q}`, { signal }).then((r) => r.json()), * ), * ), * ); * ``` */ export const fromAbortablePromise = ( // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types factory: (signal: AbortSignal) => Promise, ): FromPromiseObservable => new FromAbortablePromiseObservableClass(factory); class FromAbortablePromiseObservableClass extends RootObservableClass> implements FromPromiseObservable { readonly #abortController: AbortController; // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types constructor(factory: (signal: AbortSignal) => Promise) { super({ initialValue: Optional.none }); this.#abortController = new AbortController(); const promise = factory(this.#abortController.signal); promise .then((value) => { if (this.isCompleted) return; this.startUpdate(Result.ok(value)); }) .catch((error: unknown) => { if (this.isCompleted) return; // Silently ignore AbortError — it means the observable was // intentionally completed (e.g., by switchMap). if (error instanceof DOMException && error.name === 'AbortError') { return; } this.startUpdate( Result.err( // eslint-disable-next-line total-functions/no-unsafe-type-assertion error as E, ), ); }) .finally(() => { this.complete(); }); } override complete(): void { this.#abortController.abort(); super.complete(); } }