"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.spawn = void 0;
const AbortError_1 = require("./AbortError");
/**
 * Run an abortable function with `fork` and `defer` effects attached to it.
 *
 * `spawn` allows to write Go-style coroutines.
 *
 * Example:
 *
 *     // Connect to a database, then start a server, then block until abort.
 *     // On abort, gracefully shutdown the server, and once done, disconnect
 *     // from the database.
 *     spawn(signal, async (signal, {defer}) => {
 *       const db = await connectToDb();
 *
 *       defer(async () => {
 *         await db.close();
 *       });
 *
 *       const server = await startServer(db);
 *
 *       defer(async () => {
 *         await server.close();
 *       });
 *
 *       await forever(signal);
 *     });
 *
 * Example:
 *
 *     // Connect to a database, then start an infinite polling loop.
 *     // On abort, disconnect from the database.
 *     spawn(signal, async (signal, {defer}) => {
 *       const db = await connectToDb();
 *
 *       defer(async () => {
 *         await db.close();
 *       });
 *
 *       while (true) {
 *         await poll(signal, db);
 *         await delay(signal, 5000);
 *       }
 *     });
 *
 * Example:
 *
 *     // Acquire a lock and execute a function.
 *     // Extend the lock while the function is running.
 *     // Once the function finishes or the signal is aborted, stop extending
 *     // the lock and release it.
 *     import Redlock = require('redlock');
 *
 *     const lockTtl = 30_000;
 *
 *     function withLock<T>(
 *       signal: AbortSignal,
 *       redlock: Redlock,
 *       key: string,
 *       fn: (signal: AbortSignal) => Promise<T>,
 *     ): Promise<T> {
 *       return spawn(signal, async (signal, {fork, defer}) => {
 *         const lock = await redlock.lock(key, lockTtl);
 *
 *         defer(() => lock.unlock());
 *     ​
 *         fork(async signal => {
 *           while (true) {
 *             await delay(signal, lockTtl / 10);
 *             await lock.extend(lockTtl);
 *           }
 *         });
 *
 *         return await fn(signal);
 *       });
 *     }
 *
 *     const redlock = new Redlock([redis], {
 *       retryCount: -1,
 *     });
 *
 *     await withLock(signal, redlock, 'the-lock-key', async signal => {
 *       // ...
 *     });
 */
function spawn(signal, fn) {
    if (signal.aborted) {
        return Promise.reject(new AbortError_1.AbortError());
    }
    const deferredFunctions = [];
    /**
     * Aborted when spawned function finishes
     * or one of forked functions throws
     * or parent signal aborted.
     */
    const spawnAbortController = new AbortController();
    const spawnSignal = spawnAbortController.signal;
    const abortListener = () => {
        spawnAbortController.abort(signal.reason ?? new AbortError_1.AbortError());
    };
    signal.addEventListener('abort', abortListener);
    const removeAbortListener = () => {
        signal.removeEventListener('abort', abortListener);
    };
    const tasks = new Set();
    const abortTasks = () => {
        for (const task of tasks) {
            task.abort();
        }
    };
    spawnSignal.addEventListener('abort', abortTasks);
    const removeSpawnAbortListener = () => {
        spawnSignal.removeEventListener('abort', abortTasks);
    };
    let promise = new Promise((resolve, reject) => {
        let result;
        let failure;
        fork(signal => fn(signal, {
            defer(fn) {
                deferredFunctions.push(fn);
            },
            fork,
        }))
            .join()
            .then(value => {
            spawnAbortController.abort(new AbortError_1.AbortError('spawn() function finished', false));
            result = { value };
        }, error => {
            spawnAbortController.abort((0, AbortError_1.isAbortError)(error)
                ? error
                : new AbortError_1.AbortError('spawn() function threw', false));
            if (!(0, AbortError_1.isAbortError)(error) || failure == null) {
                failure = { error };
            }
        });
        function fork(forkFn) {
            if (spawnSignal.aborted) {
                // return already aborted task
                return {
                    abort() { },
                    async join() {
                        throw new AbortError_1.AbortError();
                    },
                };
            }
            const taskAbortController = new AbortController();
            const taskSignal = taskAbortController.signal;
            const taskPromise = forkFn(taskSignal);
            const task = {
                abort() {
                    taskAbortController.abort();
                },
                join: () => taskPromise,
            };
            tasks.add(task);
            taskPromise
                .catch(AbortError_1.catchAbortError)
                .catch(error => {
                failure = { error };
                // error in forked function
                spawnAbortController.abort(new AbortError_1.AbortError('A function forked by spawn() threw', false));
            })
                .finally(() => {
                tasks.delete(task);
                if (tasks.size === 0) {
                    if (failure != null) {
                        reject(failure.error);
                    }
                    else {
                        resolve(result.value);
                    }
                }
            });
            return task;
        }
    });
    promise = promise.finally(() => {
        removeAbortListener();
        removeSpawnAbortListener();
        let deferPromise = Promise.resolve();
        for (let i = deferredFunctions.length - 1; i >= 0; i--) {
            deferPromise = deferPromise.finally(deferredFunctions[i]);
        }
        return deferPromise;
    });
    return promise;
}
exports.spawn = spawn;
//# sourceMappingURL=spawn.js.map