{"id":27947821,"url":"https://github.com/deeplay-io/abort-controller-x","last_synced_at":"2025-07-09T23:11:50.220Z","repository":{"id":44455712,"uuid":"303051872","full_name":"deeplay-io/abort-controller-x","owner":"deeplay-io","description":"Abortable async function primitives and combinators","archived":false,"fork":false,"pushed_at":"2024-04-23T08:13:58.000Z","size":146,"stargazers_count":23,"open_issues_count":2,"forks_count":5,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-06-09T13:51:47.502Z","etag":null,"topics":["abort","abort-controller","abortable","async","cancel","cancelable","cancellable","coroutine"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/deeplay-io.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-10-11T05:50:30.000Z","updated_at":"2025-04-28T15:28:29.000Z","dependencies_parsed_at":"2024-04-23T09:58:36.271Z","dependency_job_id":"189244ad-ed24-4ec0-91a0-099aad49c270","html_url":"https://github.com/deeplay-io/abort-controller-x","commit_stats":{"total_commits":52,"total_committers":4,"mean_commits":13.0,"dds":0.4423076923076923,"last_synced_commit":"bf9aa2abd0976ed9a61a2aae61297dda2faa8882"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/deeplay-io/abort-controller-x","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeplay-io%2Fabort-controller-x","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeplay-io%2Fabort-controller-x/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeplay-io%2Fabort-controller-x/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeplay-io%2Fabort-controller-x/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deeplay-io","download_url":"https://codeload.github.com/deeplay-io/abort-controller-x/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deeplay-io%2Fabort-controller-x/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264505038,"owners_count":23618882,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["abort","abort-controller","abortable","async","cancel","cancelable","cancellable","coroutine"],"created_at":"2025-05-07T14:37:26.123Z","updated_at":"2025-07-09T23:11:50.200Z","avatar_url":"https://github.com/deeplay-io.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Abort Controller Extras [![npm version][npm-image]][npm-url]\n\nAbortable async function primitives and combinators.\n\n- [Installation](#installation)\n- [Abort Controller](#abort-controller)\n- [Abortable Functions](#abortable-functions)\n- [Composing Abortable Functions](#composing-abortable-functions)\n- [Companion Packages](#companion-packages)\n- [API](#api)\n  - [`all`](#all)\n  - [`race`](#race)\n  - [`delay`](#delay)\n  - [`waitForEvent`](#waitforevent)\n  - [`forever`](#forever)\n  - [`spawn`](#spawn)\n  - [`retry`](#retry)\n  - [`proactiveRetry`](#proactive-retry)\n  - [`execute`](#execute)\n  - [`abortable`](#abortable)\n  - [`run`](#run)\n  - [`AbortError`](#aborterror)\n  - [`isAbortError`](#isaborterror)\n  - [`throwIfAborted`](#throwifaborted)\n  - [`rethrowAbortError`](#rethrowaborterror)\n  - [`catchAbortError`](#catchaborterror)\n\n## Installation\n\n```\nyarn add abort-controller-x\n```\n\n## Abort Controller\n\nSee\n[`AbortController` MDN page](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).\nAbortController is\n[available in NodeJS](https://nodejs.org/api/globals.html#class-abortcontroller)\nsince 15.0.0, NodeJS 14.17+ requires the\n[--experimental-abortcontroller](https://nodejs.org/docs/latest-v14.x/api/cli.html#cli_experimental_abortcontroller)\nflag. A [polyfill](https://www.npmjs.com/package/abort-controller) is available\nfor older NodeJS versions and browsers.\n\n## Abortable Functions\n\nWe define _abortable function_ as a function that obeys following rules:\n\n- It must accept `AbortSignal` in its arguments.\n- It must return a `Promise`.\n- It must add\n  [`abort`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_event)\n  event listener to the `AbortSignal`. Once the `AbortSignal` is aborted, the\n  returned `Promise` must reject with `AbortError` either immediately, or after\n  doing any async cleanup. It's also possible to reject with other errors that\n  happen during cleanup.\n- Once the returned `Promise` is fulfilled or rejected, it must remove\n  [`abort`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_event)\n  event listener.\n\nAn example of _abortable function_ is the standard\n[`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) function.\n\n## Composing Abortable Functions\n\nThis library provides a way to build complex abortable functions using standard\n`async`/`await` syntax, without the burden of manually managing\n[`abort`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/abort_event)\nevent listeners. You can reuse a single `AbortSignal` between many operations\ninside a parent function:\n\n```ts\n/**\n * Make requests repeatedly with a delay between consecutive requests\n */\nasync function makeRequests(signal: AbortSignal): Promise\u003cnever\u003e {\n  while (true) {\n    await fetch('...', {signal});\n    await delay(signal, 1000);\n  }\n}\n\nconst abortController = new AbortController();\n\nmakeRequests(abortController.signal).catch(catchAbortError);\n\nprocess.on('SIGTERM', () =\u003e {\n  abortController.abort();\n});\n```\n\nThe above example can be rewritten in a more ergonomic way using [`run`](#run)\nhelper.\n\nUsually you should only create `AbortController` somewhere on the top level, and\nin regular code use `async`/`await` and pass `AbortSignal` to abortable\nfunctions provided by this library or custom ones composed of other abortable\nfunctions.\n\n## Companion Packages\n\n- [`abort-controller-x-rxjs`](https://github.com/deeplay-io/abort-controller-x-rxjs)\n  — Abortable helpers for RxJS.\n- [`abort-controller-x-reactive-store`](https://github.com/deeplay-io/abort-controller-x-reactive-store)\n  — Reactive store primitive and helpers.\n\n## API\n\n### `all`\n\n```ts\nfunction all\u003cT\u003e(\n  signal: AbortSignal,\n  executor: (innerSignal: AbortSignal) =\u003e readonly PromiseLike\u003cT\u003e[],\n): Promise\u003cT[]\u003e;\n```\n\nAbortable version of `Promise.all`.\n\nCreates new inner `AbortSignal` and passes it to `executor`. That signal is\naborted when `signal` is aborted or any of the promises returned from `executor`\nare rejected.\n\nReturns a promise that fulfills with an array of results when all of the\npromises returned from `executor` fulfill, rejects when any of the promises\nreturned from `executor` are rejected, and rejects with `AbortError` when\n`signal` is aborted.\n\nThe promises returned from `executor` must be abortable, i.e. once `innerSignal`\nis aborted, they must reject with `AbortError` either immediately, or after\ndoing any async cleanup.\n\nExample:\n\n```ts\nconst [result1, result2] = await all(signal, signal =\u003e [\n  makeRequest(signal, params1),\n  makeRequest(signal, params2),\n]);\n```\n\n### `race`\n\n```ts\nfunction race\u003cT\u003e(\n  signal: AbortSignal,\n  executor: (innerSignal: AbortSignal) =\u003e readonly PromiseLike\u003cT\u003e[],\n): Promise\u003cT\u003e;\n```\n\nAbortable version of `Promise.race`.\n\nCreates new inner `AbortSignal` and passes it to `executor`. That signal is\naborted when `signal` is aborted or any of the promises returned from `executor`\nare fulfilled or rejected.\n\nReturns a promise that fulfills or rejects when any of the promises returned\nfrom `executor` are fulfilled or rejected, and rejects with `AbortError` when\n`signal` is aborted.\n\nThe promises returned from `executor` must be abortable, i.e. once `innerSignal`\nis aborted, they must reject with `AbortError` either immediately, or after\ndoing any async cleanup.\n\nExample:\n\n```ts\nconst result = await race(signal, signal =\u003e [\n  delay(signal, 1000).then(() =\u003e ({status: 'timeout'})),\n  makeRequest(signal, params).then(value =\u003e ({status: 'success', value})),\n]);\n\nif (result.status === 'timeout') {\n  // request timed out\n} else {\n  const response = result.value;\n}\n```\n\n### `delay`\n\n```ts\nfunction delay(signal: AbortSignal, dueTime: number | Date): Promise\u003cvoid\u003e;\n```\n\nReturn a promise that resolves after delay and rejects with `AbortError` once\n`signal` is aborted.\n\nThe delay time is specified as a `Date` object or as an integer denoting\nmilliseconds to wait.\n\nExample:\n\n```ts\n// Make a request repeatedly with a delay between consecutive requests\nwhile (true) {\n  await makeRequest(signal, params);\n  await delay(signal, 1000);\n}\n```\n\nExample:\n\n```ts\n// Make a request repeatedly with a fixed interval\nimport {addMilliseconds} from 'date-fns';\n\nlet date = new Date();\n\nwhile (true) {\n  await makeRequest(signal, params);\n\n  date = addMilliseconds(date, 1000);\n  await delay(signal, date);\n}\n```\n\n### `waitForEvent`\n\n```ts\nfunction waitForEvent\u003cT\u003e(\n  signal: AbortSignal,\n  target: EventTargetLike\u003cT\u003e,\n  eventName: string,\n  options?: EventListenerOptions,\n): Promise\u003cT\u003e;\n```\n\nReturns a promise that fulfills when an event of specific type is emitted from\ngiven event target and rejects with `AbortError` once `signal` is aborted.\n\nExample:\n\n```ts\n// Create a WebSocket and wait for connection\nconst webSocket = new WebSocket(url);\n\nconst openEvent = await race(signal, signal =\u003e [\n  waitForEvent\u003cWebSocketEventMap['open']\u003e(signal, webSocket, 'open'),\n  waitForEvent\u003cWebSocketEventMap['close']\u003e(signal, webSocket, 'close').then(\n    event =\u003e {\n      throw new Error(`Failed to connect to ${url}: ${event.reason}`);\n    },\n  ),\n]);\n```\n\n### `forever`\n\n```ts\nfunction forever(signal: AbortSignal): Promise\u003cnever\u003e;\n```\n\nReturn a promise that never fulfills and only rejects with `AbortError` once\n`signal` is aborted.\n\n### `spawn`\n\n```ts\nfunction spawn\u003cT\u003e(\n  signal: AbortSignal,\n  fn: (signal: AbortSignal, effects: SpawnEffects) =\u003e Promise\u003cT\u003e,\n): Promise\u003cT\u003e;\n\ntype SpawnEffects = {\n  defer(fn: () =\u003e void | Promise\u003cvoid\u003e): void;\n  fork\u003cT\u003e(fn: (signal: AbortSignal) =\u003e Promise\u003cT\u003e): ForkTask\u003cT\u003e;\n};\n\ntype ForkTask\u003cT\u003e = {\n  abort(): void;\n  join(): Promise\u003cT\u003e;\n};\n```\n\nRun an abortable function with `fork` and `defer` effects attached to it.\n\n`spawn` allows to write Go-style coroutines.\n\n- `SpawnEffects.defer`\n\n  Schedules a function to run after spawned function finishes.\n\n  Deferred functions run serially in last-in-first-out order.\n\n  Promise returned from `spawn` resolves or rejects only after all deferred\n  functions finish.\n\n- `SpawnEffects.fork`\n\n  Executes an abortable function in background.\n\n  If a forked function throws an exception, spawned function and other forks are\n  aborted and promise returned from `spawn` rejects with that exception.\n\n  When spawned function finishes, all forks are aborted.\n\n- `ForkTask.abort`\n\n  Abort a forked function.\n\n- `ForkTask.join`\n\n  Returns a promise returned from a forked function.\n\nExample:\n\n```ts\n// Connect to a database, then start a server, then block until abort.\n// On abort, gracefully shutdown the server, and once done, disconnect\n// from the database.\nspawn(signal, async (signal, {defer}) =\u003e {\n  const db = await connectToDb();\n\n  defer(async () =\u003e {\n    await db.close();\n  });\n\n  const server = await startServer(db);\n\n  defer(async () =\u003e {\n    await server.close();\n  });\n\n  await forever(signal);\n});\n```\n\nExample:\n\n```ts\n// Connect to a database, then start an infinite polling loop.\n// On abort, disconnect from the database.\nspawn(signal, async (signal, {defer}) =\u003e {\n  const db = await connectToDb();\n\n  defer(async () =\u003e {\n    await db.close();\n  });\n\n  while (true) {\n    await poll(signal, db);\n    await delay(signal, 5000);\n  }\n});\n```\n\nExample:\n\n```ts\n// Acquire a lock and execute a function.\n// Extend the lock while the function is running.\n// Once the function finishes or the signal is aborted, stop extending\n// the lock and release it.\nimport Redlock = require('redlock');\n\nconst lockTtl = 30_000;\n\nfunction withLock\u003cT\u003e(\n  signal: AbortSignal,\n  redlock: Redlock,\n  key: string,\n  fn: (signal: AbortSignal) =\u003e Promise\u003cT\u003e,\n): Promise\u003cT\u003e {\n  return spawn(signal, async (signal, {fork, defer}) =\u003e {\n    const lock = await redlock.lock(key, lockTtl);\n\n    defer(() =\u003e lock.unlock());\n\n    fork(async signal =\u003e {\n      while (true) {\n        await delay(signal, lockTtl / 10);\n        await lock.extend(lockTtl);\n      }\n    });\n\n    return await fn(signal);\n  });\n}\n\nconst redlock = new Redlock([redis], {\n  retryCount: -1,\n});\n\nawait withLock(signal, redlock, 'the-lock-key', async signal =\u003e {\n  // ...\n});\n```\n\n### `retry`\n\n```ts\nfunction retry\u003cT\u003e(\n  signal: AbortSignal,\n  fn: (signal: AbortSignal, attempt: number, reset: () =\u003e void) =\u003e Promise\u003cT\u003e,\n  options?: RetryOptions,\n): Promise\u003cT\u003e;\n\ntype RetryOptions = {\n  baseMs?: number;\n  maxDelayMs?: number;\n  maxAttempts?: number;\n  onError?: (error: unknown, attempt: number, delayMs: number) =\u003e void;\n};\n```\n\nRetry a function with exponential backoff.\n\n- `fn`\n\n  A function that will be called and retried in case of error. It receives:\n\n  - `signal`\n\n    `AbortSignal` that is aborted when the signal passed to `retry` is aborted.\n\n  - `attempt`\n\n    Attempt number starting with 0.\n\n  - `reset`\n\n    Function that sets attempt number to -1 so that the next attempt will be\n    made without delay.\n\n- `RetryOptions.baseMs`\n\n  Starting delay before first retry attempt in milliseconds.\n\n  Defaults to 1000.\n\n  Example: if `baseMs` is 100, then retries will be attempted in 100ms, 200ms,\n  400ms etc (not counting jitter).\n\n- `RetryOptions.maxDelayMs`\n\n  Maximum delay between attempts in milliseconds.\n\n  Defaults to 30 seconds.\n\n  Example: if `baseMs` is 1000 and `maxDelayMs` is 3000, then retries will be\n  attempted in 1000ms, 2000ms, 3000ms, 3000ms etc (not counting jitter).\n\n- `RetryOptions.maxAttempts`\n\n  Maximum for the total number of attempts.\n\n  Defaults to `Infinity`.\n\n- `RetryOptions.onError`\n\n  Called after each failed attempt before setting delay timer.\n\n  Rethrow error from this callback to prevent further retries.\n\n### `proactiveRetry`\n\n```ts\nfunction proactiveRetry\u003cT\u003e(\n  signal: AbortSignal,\n  fn: (signal: AbortSignal, attempt: number) =\u003e Promise\u003cT\u003e,\n  options?: ProactiveRetryOptions,\n): Promise\u003cT\u003e;\n\ntype ProactiveRetryOptions = {\n  baseMs?: number;\n  maxAttempts?: number;\n  onError?: (error: unknown, attempt: number) =\u003e void;\n};\n```\n\nProactively retry a function with exponential backoff.\n\nAlso known as hedging.\n\nThe function will be called multiple times in parallel until it succeeds, in\nwhich case all the other calls will be aborted.\n\n- `fn`\n\n  A function that will be called multiple times in parallel until it succeeds.\n  It receives:\n\n  - `signal`\n\n    `AbortSignal` that is aborted when the signal passed to `retry` is aborted,\n    or when the function succeeds.\n\n  - `attempt`\n\n    Attempt number starting with 0.\n\n- `ProactiveRetryOptions.baseMs`\n\n  Base delay between attempts in milliseconds.\n\n  Defaults to 1000.\n\n  Example: if `baseMs` is 100, then retries will be attempted in 100ms, 200ms,\n  400ms etc (not counting jitter).\n\n- `ProactiveRetryOptions.maxAttempts`\n\n  Maximum for the total number of attempts.\n\n  Defaults to `Infinity`.\n\n- `ProactiveRetryOptions.onError`\n\n  Called after each failed attempt.\n\n  Rethrow error from this callback to prevent further retries.\n\n### `execute`\n\n```ts\nfunction execute\u003cT\u003e(\n  signal: AbortSignal,\n  executor: (\n    resolve: (value: T) =\u003e void,\n    reject: (reason?: any) =\u003e void,\n  ) =\u003e () =\u003e void | PromiseLike\u003cvoid\u003e,\n): Promise\u003cT\u003e;\n```\n\nSimilar to `new Promise(executor)`, but allows executor to return abort callback\nthat is called once `signal` is aborted.\n\nReturned promise rejects with `AbortError` once `signal` is aborted.\n\nCallback can return a promise, e.g. for doing any async cleanup. In this case,\nthe promise returned from `execute` rejects with `AbortError` after that promise\nfulfills.\n\n### `abortable`\n\n```ts\nfunction abortable\u003cT\u003e(signal: AbortSignal, promise: PromiseLike\u003cT\u003e): Promise\u003cT\u003e;\n```\n\nWrap a promise to reject with `AbortError` once `signal` is aborted.\n\nUseful to wrap non-abortable promises. Note that underlying process will NOT be\naborted.\n\n### `run`\n\n```ts\nfunction run(fn: (signal: AbortSignal) =\u003e Promise\u003cvoid\u003e): () =\u003e Promise\u003cvoid\u003e;\n```\n\nInvokes an abortable function with implicitly created `AbortSignal`.\n\nReturns a function that aborts that signal and waits until passed function\nfinishes.\n\nAny error other than `AbortError` thrown from passed function will result in\nunhandled promise rejection.\n\nExample:\n\n```ts\nconst stop = run(async signal =\u003e {\n  try {\n    while (true) {\n      await delay(signal, 1000);\n      console.log('tick');\n    }\n  } finally {\n    await doCleanup();\n  }\n});\n\n// abort and wait until cleanup is done\nawait stop();\n```\n\nThis function is also useful with React `useEffect` hook:\n\n```ts\n// make requests periodically while the component is mounted\nuseEffect(\n  () =\u003e\n    run(async signal =\u003e {\n      while (true) {\n        await makeRequest(signal);\n        await delay(signal, 1000);\n      }\n    }),\n  [],\n);\n```\n\n### `AbortError`\n\n```ts\nclass AbortError extends Error\n```\n\nThrown when an abortable function was aborted.\n\n**Warning**: do not use `instanceof` with this class. Instead, use\n`isAbortError` function.\n\n### `isAbortError`\n\n```ts\nfunction isAbortError(error: unknown): boolean;\n```\n\nChecks whether given `error` is an `AbortError`.\n\n### `throwIfAborted`\n\n```ts\nfunction throwIfAborted(signal: AbortSignal): void;\n```\n\nIf `signal` is aborted, throws `AbortError`. Otherwise does nothing.\n\n### `rethrowAbortError`\n\n```ts\nfunction rethrowAbortError(error: unknown): void;\n```\n\nIf `error` is `AbortError`, throws it. Otherwise does nothing.\n\nUseful for `try/catch` blocks around abortable code:\n\n```ts\ntry {\n  await somethingAbortable(signal);\n} catch (err) {\n  rethrowAbortError(err);\n\n  // do normal error handling\n}\n```\n\n### `catchAbortError`\n\n```ts\nfunction catchAbortError(error: unknown): void;\n```\n\nIf `error` is `AbortError`, does nothing. Otherwise throws it.\n\nUseful for invoking top-level abortable functions:\n\n```ts\nsomethingAbortable(signal).catch(catchAbortError);\n```\n\nWithout `catchAbortError`, aborting would result in unhandled promise rejection.\n\n[npm-image]: https://badge.fury.io/js/abort-controller-x.svg\n[npm-url]: https://badge.fury.io/js/abort-controller-x\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeeplay-io%2Fabort-controller-x","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeeplay-io%2Fabort-controller-x","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeeplay-io%2Fabort-controller-x/lists"}