From 10f5a1984047cb50dd80c4100a0322339584807a Mon Sep 17 00:00:00 2001 From: Kuba Orlik Date: Thu, 25 Mar 2021 22:01:55 +0100 Subject: [PATCH] Initial commit --- README.md | 40 ++++++++++++++++++++++ package.json | 23 +++++++++++++ src/index.ts | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 19 +++++++++++ 4 files changed, 175 insertions(+) create mode 100644 README.md create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..fba63ba --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Abortable promises ⚡ + +A module for node that lets you run complex sequence of async functions and: + +1. Use an external signal to stop the sequence of steps + +```typescript +const sleep = promisify((timeout: number, callback: (...args: any[]) => void) => + setTimeout(callback, timeout) +); + +const a = new AbortablePromise(async function* () { + yield await sleep(1000); + console.log("awaited 100"); + yield await sleep(2000); + console.log("awaited 200"); + yield await sleep(3000); + console.log("awaited 300"); +}); + +setTimeout(() => a.abort(), 1500); // doesn't get to sleep(3000) +``` + +2. Use an external signal to kill an already started asynchronous step + +```typescript +const b = new AbortablePromise(async function* (await_or_abort) { + const ping = await deferedSpawn("ping", ["8.8.8.8"]); + while (true) { + console.log( + await await_or_abort(ping.waitForNextData(), () => { + ping.kill(); + }) + ); + yield; + } +}); + +setTimeout(() => b.abort(), 5000); // doesn't get to 5th ping +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..ff4c881 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "abortable-promise", + "version": "0.1.0", + "description": "Abortable promises based on async generators", + "main": "./lib/index.js", + "types": "./@types/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "npm run build -- --watch", + "install": "npm run build", + "prepare": "npm run build" + }, + "repository": "gitea@git.kuba-orlik.name:kuba/async-spawner.git", + "author": "Kuba Orlik", + "license": "ISC", + "dependencies": { + "@types/node": "^14.14.19", + "emittery": "^0.8.0" + }, + "devDependencies": { + "typescript": "^4.1.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ffac842 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,93 @@ +import { deferedSpawn } from "async-spawner"; +import Emittery from "emittery"; +import { promisify } from "util"; + +//needed to pass information on wether or not the promise has been aborted witout 'this' +class AbortableEmittery extends Emittery { + public aborted = false; + + abort() { + this.aborted = true; + this.emit("abort"); + } +} + +class AbortablePromise extends Promise { + private emitter: AbortableEmittery; + + constructor( + make_generator: ( + await_or_abort: ( + promise: Promise, + on_abort: () => void + ) => Promise + ) => AsyncGenerator + ) { + const emitter = new AbortableEmittery(); + const await_or_abort = (promise: Promise, on_abort: () => void) => { + return new Promise((resolve, reject) => { + let resolved = false; + emitter.on("abort", () => { + if (!resolved) { + on_abort(); + resolve(null); + resolved = true; + } + }); + promise + .then((result) => { + if (!resolved) { + resolve(result); + resolved = true; + } + }) + .catch(reject); + }); + }; + super(async (resolve, reject) => { + let step_value; + const generator = make_generator(await_or_abort); + do { + if (emitter.aborted) { + resolve(null); + return; + } + step_value = await generator.next(); + } while (!step_value.done); + }); + this.emitter = emitter; + } + + abort() { + this.emitter.abort(); + } +} + +// const sleep = promisify((timeout: number, callback: (...args: any[]) => void) => +// setTimeout(callback, timeout) +// ); + +// const a = new AbortablePromise(async function* () { +// yield await sleep(1000); +// console.log("awaited 100"); +// yield await sleep(2000); +// console.log("awaited 200"); +// yield await sleep(3000); +// console.log("awaited 300"); +// }); + +// setTimeout(() => a.abort(), 1500); + +const b = new AbortablePromise(async function* (await_or_abort) { + const ping = await deferedSpawn("ping", ["8.8.8.8"]); + while (true) { + console.log( + await await_or_abort(ping.waitForNextData(), () => { + ping.kill(); + }) + ); + yield; + } +}); + +setTimeout(() => b.abort(), 5000); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8fa566d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "target": "ES6", + "declaration": true, + "esModuleInterop": true, + "lib": ["es6", "esnext"], + "outDir": "lib", + "checkJs": true, + "allowJs": true, + "declarationDir": "@types", + "sourceMap": true + }, + "include": ["src/**/*"] +}