diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de46e32..0bc3b42 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,5 +4,8 @@ updates: directory: "/" schedule: interval: daily - time: "11:00" + time: "10:00" open-pull-requests-limit: 10 + commit-message: + prefix: "deps" + prefix-development: "deps(dev)" diff --git a/.gitignore b/.gitignore index 84dc48e..7ad9e67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ node_modules -coverage -.nyc_output -package-lock.json +build dist +.docs +.coverage +node_modules +package-lock.json +yarn.lock +.vscode diff --git a/LICENSE b/LICENSE index f1717ff..20ce483 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,4 @@ -MIT License +This project is dual licensed under MIT and Apache-2.0. -Copyright (c) 2018 Alan Shaw - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..14478a3 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..72dc60d --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index fbc3bc5..12933d6 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,44 @@ -# abortable-iterator +# abortable-iterator -[![Build Status](https://github.com/alanshaw/abortable-iterator/actions/workflows/js-test-and-release.yml/badge.svg?branch=master)](https://github.com/alanshaw/abortable-iterator/actions/workflows/js-test-and-release.yml) -[![Dependencies Status](https://status.david-dm.org/gh/alanshaw/abortable-iterator.svg)](https://david-dm.org/alanshaw/abortable-iterator) -[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) +[![codecov](https://img.shields.io/codecov/c/github/alanshaw/abortable-iterator.svg?style=flat-square)](https://codecov.io/gh/alanshaw/abortable-iterator) +[![CI](https://img.shields.io/github/actions/workflow/status/alanshaw/abortable-iterator/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/alanshaw/abortable-iterator/actions/workflows/js-test-and-release.yml?query=branch%3Amaster) > Make any iterator or iterable abortable via an AbortSignal -The [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) is used in the fetch API to abort in flight requests from, for example, a timeout or user action. The same concept is used here to halt iteration of an async iterator. +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +The [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) is used in the fetch API to abort in flight requests from, for example, a timeout or user action. The same concept is used here to halt iteration of an async iterator. + ## Usage ```js @@ -63,33 +88,34 @@ import { } from 'abortable-iterator' ``` -* [`abortableSource(source, signal, [options])`](#abortablesource-signal-options) -* [`abortableSink(sink, signal, [options])`](#abortablesinksink-signal-options) -* [`abortableTransform(transform, signal, [options])`](#abortabletransformtransform-signal-options) -* [`abortableDuplex(duplex, signal, [options])`](#abortableduplexduplex-signal-options) +- [`abortableSource(source, signal, [options])`](#abortablesource-signal-options) +- [`abortableSink(sink, signal, [options])`](#abortablesinksink-signal-options) +- [`abortableTransform(transform, signal, [options])`](#abortabletransformtransform-signal-options) +- [`abortableDuplex(duplex, signal, [options])`](#abortableduplexduplex-signal-options) ### `abortableSource(source, signal, [options])` + **(alias for `abortable.source(source, signal, [options])`)** Make any iterator or iterable abortable via an `AbortSignal`. #### Parameters -| Name | Type | Description | -|------|------|-------------| -| source | [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol)\|[`Iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol) | The iterator or iterable object to make abortable | -| signal | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | Signal obtained from `AbortController.signal` which is used to abort the iterator. | -| options | `Object` | (optional) options | -| options.onAbort | `Function` | An (async) function called when the iterator is being aborted, before the abort error is thrown. Default `null` | -| options.abortMessage | `String` | The message that the error will have if the iterator is aborted. Default "The operation was aborted" | -| options.abortCode | `String`\|`Number` | The value assigned to the `code` property of the error that is thrown if the iterator is aborted. Default "ABORT_ERR" | -| options.returnOnAbort | `Boolean` | Instead of throwing the abort error, just return from iterating over the source stream. | -| options.onReturnError | `Function` | When a generator is aborted, we call `.return` on it - if this function errors the error value will be passed to the `.onReturnError` callback if passed. Default `null` | +| Name | Type | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| source | [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterable_protocol)\|[`Iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol) | The iterator or iterable object to make abortable | +| signal | [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) | Signal obtained from `AbortController.signal` which is used to abort the iterator. | +| options | `Object` | (optional) options | +| options.onAbort | `Function` | An (async) function called when the iterator is being aborted, before the abort error is thrown. Default `null` | +| options.abortMessage | `String` | The message that the error will have if the iterator is aborted. Default "The operation was aborted" | +| options.abortCode | `String`\|`Number` | The value assigned to the `code` property of the error that is thrown if the iterator is aborted. Default "ABORT\_ERR" | +| options.returnOnAbort | `Boolean` | Instead of throwing the abort error, just return from iterating over the source stream. | +| options.onReturnError | `Function` | When a generator is aborted, we call `.return` on it - if this function errors the error value will be passed to the `.onReturnError` callback if passed. Default `null` | #### Returns -| Type | Description | -|------|-------------| +| Type | Description | +| ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | [`Iterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#The_iterator_protocol) | An iterator that wraps the passed `source` parameter that makes it abortable via the passed `signal` parameter. | The returned iterator will `throw` an `AbortError` when it is aborted that has a `type` with the value `aborted` and `code` property with the value `ABORT_ERR` by default. @@ -106,16 +132,27 @@ The same as [`abortable.source`](#abortablesource-signal-options) except this ma The same as [`abortable.source`](#abortablesource-signal-options) except this makes the passed [`duplex`](https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9#duplex-it) abortable. Returns a new duplex that wraps the passed `duplex` and makes it abortable via the passed `signal` parameter. -Note that this will abort _both_ sides of the duplex. Use `duplex.sink = abortable.sink(duplex.sink)` or `duplex.source = abortable.source(duplex.source)` to abort just the sink or the source. +Note that this will abort *both* sides of the duplex. Use `duplex.sink = abortable.sink(duplex.sink)` or `duplex.source = abortable.source(duplex.source)` to abort just the sink or the source. ## Related -* [`it-pipe`](https://www.npmjs.com/package/it-pipe) Utility to "pipe" async iterables together +- [`it-pipe`](https://www.npmjs.com/package/it-pipe) Utility to "pipe" async iterables together ## Contribute Feel free to dive in! [Open an issue](https://github.com/alanshaw/abortable-iterator/issues/new) or submit PRs. +## API Docs + +- + ## License -[MIT](LICENSE) © Alan Shaw +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/examples/index.ts b/examples/index.ts deleted file mode 100644 index 148415d..0000000 --- a/examples/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { abortableSource } from 'abortable-iterator' - -async function main () { - // An example function that creates an async iterator that yields an increasing - // number every x milliseconds and NEVER ENDS! - const asyncCounter = async function * (start, delay) { - let i = start - while (true) { - yield new Promise(resolve => setTimeout(() => resolve(i++), delay)) - } - } - - // Create a counter that'll yield numbers from 0 upwards every second - const everySecond = asyncCounter(0, 1000) - - // Make everySecond abortable! - const controller = new AbortController() - const abortableEverySecond = abortableSource(everySecond, controller.signal) - - // Abort after 5 seconds - setTimeout(() => controller.abort(), 5000) - - try { - // Start the iteration, which will throw after 5 seconds when it is aborted - for await (const n of abortableEverySecond) { - console.log(n) - } - } catch (err) { - if (err.code === 'ERR_ABORTED') { - // Expected - all ok :D - } else { - throw err - } - } -} - -main() diff --git a/package.json b/package.json index ada40fa..82beddf 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,44 @@ "name": "abortable-iterator", "version": "4.0.2", "description": "Make any iterator or iterable abortable via an AbortSignal", + "author": "Alan Shaw", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/alanshaw/abortable-iterator#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/alanshaw/abortable-iterator.git" + }, + "bugs": { + "url": "https://github.com/alanshaw/abortable-iterator/issues" + }, + "keywords": [ + "AbortController", + "AbortSignal", + "abort", + "abortable", + "async", + "cancel", + "iterator", + "signal", + "stop" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, "type": "module", "types": "./dist/src/index.d.ts", "typesVersions": { "*": { "*": [ "*", - "*/index", "dist/*", - "dist/*/index", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", "dist/src/*", "dist/src/*/index" ] @@ -18,15 +47,17 @@ }, "files": [ "src", - "dist/src", + "dist", "!dist/test", "!**/*.tsbuildinfo" ], "exports": { ".": { + "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" }, "./duplex": { + "types": "./dist/src/duplex.d.ts", "import": "./dist/src/duplex.js" } }, @@ -63,15 +94,15 @@ "release": "patch" }, { - "type": "chore", + "type": "docs", "release": "patch" }, { - "type": "docs", + "type": "test", "release": "patch" }, { - "type": "test", + "type": "deps", "release": "patch" }, { @@ -101,7 +132,11 @@ }, { "type": "docs", - "section": "Trivial Changes" + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" }, { "type": "test", @@ -129,37 +164,17 @@ "test:firefox-webworker": "npm run test -- -t webworker -- --browser firefox", "test:node": "npm run test -- -t node --cov", "test:electron-main": "npm run test -- -t electron-main", - "release": "semantic-release" + "release": "semantic-release", + "docs": "aegir docs" + }, + "dependencies": { + "get-iterator": "^2.0.0", + "it-stream-types": "^1.0.3" }, - "keywords": [ - "async", - "iterator", - "abort", - "abortable", - "cancel", - "stop", - "AbortController", - "AbortSignal", - "signal" - ], - "author": "Alan Shaw", - "license": "MIT", "devDependencies": { "aegir": "^38.1.7", "delay": "^5.0.0", "it-drain": "^2.0.1", "it-pipe": "^2.0.2" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/alanshaw/abortable-iterator.git" - }, - "bugs": { - "url": "https://github.com/alanshaw/abortable-iterator/issues" - }, - "homepage": "https://github.com/alanshaw/abortable-iterator#readme", - "dependencies": { - "get-iterator": "^2.0.0", - "it-stream-types": "^1.0.3" } } diff --git a/src/index.ts b/src/index.ts index a5044d7..404dfa2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,49 @@ +/** + * @packageDocumentation + * + * @example + * + * ```js + * import { abortableSource } from 'abortable-iterator' + * + * async function main () { + * // An example function that creates an async iterator that yields an increasing + * // number every x milliseconds and NEVER ENDS! + * const asyncCounter = async function * (start, delay) { + * let i = start + * while (true) { + * yield new Promise(resolve => setTimeout(() => resolve(i++), delay)) + * } + * } + * + * // Create a counter that'll yield numbers from 0 upwards every second + * const everySecond = asyncCounter(0, 1000) + * + * // Make everySecond abortable! + * const controller = new AbortController() + * const abortableEverySecond = abortableSource(everySecond, controller.signal) + * + * // Abort after 5 seconds + * setTimeout(() => controller.abort(), 5000) + * + * try { + * // Start the iteration, which will throw after 5 seconds when it is aborted + * for await (const n of abortableEverySecond) { + * console.log(n) + * } + * } catch (err) { + * if (err.code === 'ERR_ABORTED') { + * // Expected - all ok :D + * } else { + * throw err + * } + * } + * } + * + * main() + * ``` + */ + import { AbortError } from './abort-error.js' import { getIterator } from 'get-iterator' import type { Duplex, Source, Sink } from 'it-stream-types' @@ -11,13 +57,13 @@ export interface Options { } // Wrap an iterator to make it abortable, allow cleanup when aborted via onAbort -export function abortableSource (source: Source, signal: AbortSignal, options?: Options) { +export function abortableSource (source: Source, signal: AbortSignal, options?: Options): AsyncGenerator, void, unknown> { const opts: Options = options ?? {} const iterator = getIterator(source) - async function * abortable () { + async function * abortable (): AsyncGenerator, void, unknown> { let nextAbortHandler: (() => void) | null - const abortHandler = () => { + const abortHandler = (): void => { if (nextAbortHandler != null) nextAbortHandler() } @@ -49,7 +95,7 @@ export function abortableSource (source: Source, signal: AbortSignal, opt if (isKnownAborter && (opts.onAbort != null)) { // Do any custom abort handling for the iterator - await opts.onAbort(source) + opts.onAbort(source) } // End the iterator if it is a generator @@ -95,7 +141,7 @@ export function abortableSink (sink: Sink, signal: AbortSignal, opt return (source: Source) => sink(abortableSource(source, signal, options)) } -export function abortableDuplex > (duplex: Duplex, signal: AbortSignal, options?: Options) { +export function abortableDuplex > (duplex: Duplex, signal: AbortSignal, options?: Options): Duplex, TSink, RSink> { return { sink: abortableSink(duplex.sink, signal, { ...options, diff --git a/test/index.spec.ts b/test/index.spec.ts index 8c1c7e5..551f62b 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,11 +1,11 @@ -import { expect } from 'aegir/utils/chai.js' +import { expect } from 'aegir/chai' import { abortableDuplex, abortableSink, abortableSource, abortableTransform } from '../src/index.js' import drain from 'it-drain' import delay from 'delay' import { pipe } from 'it-pipe' import type { Sink, Transform, Duplex } from 'it-stream-types' -async function * forever (interval = 1) { +async function * forever (interval = 1): AsyncGenerator { // Never ends! while (true) { if (interval > 0) { @@ -20,7 +20,7 @@ describe('abortable-iterator', () => { const controller = new AbortController() // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(drain(abortableSource(forever(), controller.signal))) .to.eventually.be.rejected.with.property('type', 'aborted') @@ -30,7 +30,7 @@ describe('abortable-iterator', () => { const controller = new AbortController() // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(drain(abortableSource(forever(6000), controller.signal))) .to.eventually.be.rejected.with.property('type', 'aborted') @@ -51,7 +51,7 @@ describe('abortable-iterator', () => { } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) let returnedErr // @ts-expect-error wat @@ -80,7 +80,7 @@ describe('abortable-iterator', () => { } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) // @ts-expect-error wat await expect(drain(abortableSource(iterator, controller.signal))) @@ -94,15 +94,12 @@ describe('abortable-iterator', () => { // Ensure we allow async cleanup let onAbortCalled = false - const onAbort = async () => await new Promise(resolve => { - setTimeout(() => { - onAbortCalled = true - resolve() - }, 1000) - }) + const onAbort = (): void => { + onAbortCalled = true + } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(drain(abortableSource(forever(1000), controller.signal, { onAbort }))) .to.eventually.be.rejected.with.property('type', 'aborted') @@ -114,12 +111,12 @@ describe('abortable-iterator', () => { const controller = new AbortController() const iterator = (async function * () { yield new Promise((resolve, reject) => { - setTimeout(() => resolve(Math.random())) + setTimeout(() => { resolve(Math.random()) }) }) })() // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(drain(abortableSource(iterator, controller.signal))) .to.eventually.be.undefined() @@ -150,7 +147,7 @@ describe('abortable-iterator', () => { } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(pipe( forever(), @@ -166,7 +163,7 @@ describe('abortable-iterator', () => { } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(pipe( forever(), @@ -184,7 +181,7 @@ describe('abortable-iterator', () => { } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(pipe( abortableDuplex(duplex, controller.signal), @@ -201,7 +198,7 @@ describe('abortable-iterator', () => { } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(pipe( forever(), @@ -219,7 +216,7 @@ describe('abortable-iterator', () => { } // Abort after 10ms - setTimeout(() => controller.abort(), 10) + setTimeout(() => { controller.abort() }, 10) await expect(pipe( forever(),