{"version":3,"file":"BackgroundProcessManager.mjs","sources":["../../../src/BackgroundProcessManager/BackgroundProcessManager.ts"],"sourcesContent":["// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.\n// SPDX-License-Identifier: Apache-2.0\nimport { BackgroundManagerNotOpenError } from './BackgroundManagerNotOpenError';\nimport { BackgroundProcessManagerState } from './types';\n/**\n * @private For internal Amplify use.\n *\n * Creates a new scope for promises, observables, and other types of work or\n * processes that may be running in the background. This manager provides\n * an singular entrypoint to request termination and await completion.\n *\n * As work completes on its own prior to close, the manager removes them\n * from the registry to avoid holding references to completed jobs.\n */\nexport class BackgroundProcessManager {\n    constructor() {\n        /**\n         * A string indicating whether the manager is accepting new work (\"Open\"),\n         * waiting for work to complete (\"Closing\"), or fully done with all\n         * submitted work and *not* accepting new jobs (\"Closed\").\n         */\n        this._state = BackgroundProcessManagerState.Open;\n        /**\n         * The list of outstanding jobs we'll need to wait for upon `close()`\n         */\n        this.jobs = new Set();\n    }\n    add(jobOrDescription, optionalDescription) {\n        let job;\n        let description;\n        if (typeof jobOrDescription === 'string') {\n            job = undefined;\n            description = jobOrDescription;\n        }\n        else {\n            job = jobOrDescription;\n            description = optionalDescription;\n        }\n        const error = this.closedFailure(description);\n        if (error)\n            return error;\n        if (job === undefined) {\n            return this.addHook(description);\n        }\n        else if (typeof job === 'function') {\n            return this.addFunction(job, description);\n        }\n        else if (job instanceof BackgroundProcessManager) {\n            this.addManager(job, description);\n        }\n        else {\n            throw new Error('If `job` is provided, it must be an Observable, Function, or BackgroundProcessManager.');\n        }\n    }\n    /**\n     * Adds a **cleaner** function that doesn't immediately get executed.\n     * Instead, the caller gets a **terminate** function back. The *cleaner* is\n     * invoked only once the mananger *closes* or the returned **terminate**\n     * function is called.\n     *\n     * @param clean The cleanup function.\n     * @param description Optional description to help identify pending jobs.\n     * @returns A terminate function.\n     */\n    addCleaner(clean, description) {\n        const { resolve, onTerminate } = this.addHook(description);\n        const proxy = async () => {\n            await clean();\n            resolve();\n        };\n        onTerminate.then(proxy);\n        return proxy;\n    }\n    addFunction(job, description) {\n        // the function we call when we want to try to terminate this job.\n        let terminate;\n        // the promise the job can opt into listening to for termination.\n        const onTerminate = new Promise(resolve => {\n            terminate = resolve;\n        });\n        // finally! start the job.\n        const jobResult = job(onTerminate);\n        // depending on what the job gives back, register the result\n        // so we can monitor for completion.\n        if (typeof jobResult?.then === 'function') {\n            this.registerPromise(jobResult, terminate, description);\n        }\n        // At the end of the day, or you know, method call, it doesn't matter\n        // what the return value is at all; we just pass it through to the\n        // caller.\n        return jobResult;\n    }\n    addManager(manager, description) {\n        this.addCleaner(async () => manager.close(), description);\n    }\n    /**\n     * Creates and registers a fabricated job for processes that need to operate\n     * with callbacks/hooks. The returned `resolve` and `reject`\n     * functions can be used to signal the job is done successfully or not.\n     * The returned `onTerminate` is a promise that will resolve when the\n     * manager is requesting the termination of the job.\n     *\n     * @param description Optional description to help identify pending jobs.\n     * @returns `{ resolve, reject, onTerminate }`\n     */\n    addHook(description) {\n        // the resolve/reject functions we'll provide to the caller to signal\n        // the state of the job.\n        let promiseResolve;\n        let promiseReject;\n        // the underlying promise we'll use to manage it, pretty much like\n        // any other promise.\n        const promise = new Promise((resolve, reject) => {\n            promiseResolve = resolve;\n            promiseReject = reject;\n        });\n        // the function we call when we want to try to terminate this job.\n        let terminate;\n        // the promise the job can opt into listening to for termination.\n        const onTerminate = new Promise(resolve => {\n            terminate = resolve;\n        });\n        this.registerPromise(promise, terminate, description);\n        return {\n            resolve: promiseResolve,\n            reject: promiseReject,\n            onTerminate,\n        };\n    }\n    /**\n     * Adds a Promise based job to the list of jobs for monitoring and listens\n     * for either a success or failure, upon which the job is considered \"done\"\n     * and removed from the registry.\n     *\n     * @param promise A promise that is on its way to being returned to a\n     * caller, which needs to be tracked as a background job.\n     * @param terminate The termination function to register, which can be\n     * invoked to request the job stop.\n     * @param description Optional description to help identify pending jobs.\n     */\n    registerPromise(promise, terminate, description) {\n        const jobEntry = { promise, terminate, description };\n        this.jobs.add(jobEntry);\n        // in all of my testing, it is safe to multi-subscribe to a promise.\n        // so, rather than create another layer of promising, we're just going\n        // to hook into the promise we already have, and when it's done\n        // (successfully or not), we no longer need to wait for it upon close.\n        //\n        // sorry this is a bit hand-wavy:\n        //\n        // i believe we use `.then` and `.catch` instead of `.finally` because\n        // `.finally` is invoked in a different order in the sequence, and this\n        // breaks assumptions throughout and causes failures.\n        promise\n            .then(() => {\n            this.jobs.delete(jobEntry);\n        })\n            .catch(() => {\n            this.jobs.delete(jobEntry);\n        });\n    }\n    /**\n     * The number of jobs being waited on.\n     *\n     * We don't use this for anything. It's just informational for the caller,\n     * and can be used in logging and testing.\n     *\n     * @returns the number of jobs.\n     */\n    get length() {\n        return this.jobs.size;\n    }\n    /**\n     * The execution state of the manager. One of:\n     *\n     * 1. \"Open\" -> Accepting new jobs\n     * 1. \"Closing\" -> Not accepting new work. Waiting for jobs to complete.\n     * 1. \"Closed\" -> Not accepting new work. All submitted jobs are complete.\n     */\n    get state() {\n        return this._state;\n    }\n    /**\n     * The registered `description` of all still-pending jobs.\n     *\n     * @returns descriptions as an array.\n     */\n    get pending() {\n        return Array.from(this.jobs).map(job => job.description);\n    }\n    /**\n     * Whether the manager is accepting new jobs.\n     */\n    get isOpen() {\n        return this._state === BackgroundProcessManagerState.Open;\n    }\n    /**\n     * Whether the manager is rejecting new work, but still waiting for\n     * submitted work to complete.\n     */\n    get isClosing() {\n        return this._state === BackgroundProcessManagerState.Closing;\n    }\n    /**\n     * Whether the manager is rejecting work and done waiting for submitted\n     * work to complete.\n     */\n    get isClosed() {\n        return this._state === BackgroundProcessManagerState.Closed;\n    }\n    closedFailure(description) {\n        if (!this.isOpen) {\n            return Promise.reject(new BackgroundManagerNotOpenError([\n                `The manager is ${this.state}.`,\n                `You tried to add \"${description}\".`,\n                `Pending jobs: [\\n${this.pending\n                    .map(t => '    ' + t)\n                    .join(',\\n')}\\n]`,\n            ].join('\\n')));\n        }\n    }\n    /**\n     * Signals jobs to stop (for those that accept interruptions) and waits\n     * for confirmation that jobs have stopped.\n     *\n     * This immediately puts the manager into a closing state and just begins\n     * to reject new work. After all work in the manager is complete, the\n     * manager goes into a `Completed` state and `close()` returns.\n     *\n     * This call is idempotent.\n     *\n     * If the manager is already closing or closed, `finalCleaup` is not executed.\n     *\n     * @param onClosed\n     * @returns The settled results of each still-running job's promise. If the\n     * manager is already closed, this will contain the results as of when the\n     * manager's `close()` was called in an `Open` state.\n     */\n    async close() {\n        if (this.isOpen) {\n            this._state = BackgroundProcessManagerState.Closing;\n            for (const job of Array.from(this.jobs)) {\n                try {\n                    job.terminate();\n                }\n                catch (error) {\n                    // Due to potential races with a job's natural completion, it's\n                    // reasonable to expect the termination call to fail. Hence,\n                    // not logging as an error.\n                    // eslint-disable-next-line no-console\n                    console.warn(`Failed to send termination signal to job. Error: ${error.message}`, job);\n                }\n            }\n            // Use `allSettled()` because we want to wait for all to finish. We do\n            // not want to stop waiting if there is a failure.\n            this._closingPromise = Promise.allSettled(Array.from(this.jobs).map(j => j.promise));\n            await this._closingPromise;\n            this._state = BackgroundProcessManagerState.Closed;\n        }\n        return this._closingPromise;\n    }\n    /**\n     * Signals the manager to start accepting work (again) and returns once\n     * the manager is ready to do so.\n     *\n     * If the state is already `Open`, this call is a no-op.\n     *\n     * If the state is `Closed`, this call simply updates state and returns.\n     *\n     * If the state is `Closing`, this call waits for completion before it\n     * updates the state and returns.\n     */\n    async open() {\n        if (this.isClosing) {\n            await this.close();\n        }\n        this._state = BackgroundProcessManagerState.Open;\n    }\n}\n"],"names":[],"mappings":"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"}