pax_global_header00006660000000000000000000000064141243054670014520gustar00rootroot0000000000000052 comment=2bc2e134b7728a6b37c6838a3b5d5c290e60a539 once-3.0.0/000077500000000000000000000000001412430546700124445ustar00rootroot00000000000000once-3.0.0/.editorconfig000066400000000000000000000013131412430546700151170ustar00rootroot00000000000000root = true [*] indent_style = tab indent_size = 4 tab_width = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [{*.json,*.json.example,*.gyp,*.yml,*.yaml,*.workflow}] indent_style = space indent_size = 2 [{*.py,*.asm}] indent_style = space [*.py] indent_size = 4 [*.asm] indent_size = 8 [*.md] trim_trailing_whitespace = false # Ideal settings - some plugins might support these. [*.js] quote_type = single [{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}] curly_bracket_next_line = false spaces_around_operators = true spaces_around_brackets = outside # close enough to 1TB indent_brace_style = K&R once-3.0.0/.github/000077500000000000000000000000001412430546700140045ustar00rootroot00000000000000once-3.0.0/.github/workflows/000077500000000000000000000000001412430546700160415ustar00rootroot00000000000000once-3.0.0/.github/workflows/test.yml000066400000000000000000000014151412430546700175440ustar00rootroot00000000000000name: Node CI on: [push] jobs: build: name: Test Node.js ${{ matrix.node-version }} on ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] node-version: [10.x, 12.x, 14.x, 16.x] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v1 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Print Node.js Version run: node --version - name: Install Dependencies run: npm install env: CI: true - name: Run "build" step run: npm run build --if-present env: CI: true - name: Run tests run: npm test env: CI: true once-3.0.0/.gitignore000066400000000000000000000000711412430546700144320ustar00rootroot00000000000000/yarn.lock /node_modules /src/?.?s /dist /.vscode /*.tgz once-3.0.0/LICENSE000066400000000000000000000020571412430546700134550ustar00rootroot00000000000000MIT License Copyright (c) 2020 Nathan Rajlich 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. once-3.0.0/README.md000066400000000000000000000052051412430546700137250ustar00rootroot00000000000000# @tootallnate/once ### Creates a Promise that waits for a single event ## Installation Install with `npm`: ```bash $ npm install @tootallnate/once ``` ## API ### once(emitter: EventEmitter, name: string, opts?: OnceOptions): Promise<[...Args]> Creates a Promise that waits for event `name` to occur on `emitter`, and resolves the promise with an array of the values provided to the event handler. If an `error` event occurs before the event specified by `name`, then the Promise is rejected with the error argument. ```typescript import once from '@tootallnate/once'; import { EventEmitter } from 'events'; const emitter = new EventEmitter(); setTimeout(() => { emitter.emit('foo', 'bar'); }, 100); const [result] = await once(emitter, 'foo'); console.log({ result }); // { result: 'bar' } ``` #### Promise Strong Typing The main feature that this module provides over other "once" implementations is that the Promise that is returned is _**strongly typed**_ based on the type of `emitter` and the `name` of the event. Some examples are shown below. _The process "exit" event contains a single number for exit code:_ ```typescript const [code] = await once(process, 'exit'); // ^ number ``` _A child process "exit" event contains either an exit code or a signal:_ ```typescript const child = spawn('echo', []); const [code, signal] = await once(child, 'exit'); // ^ number | null // ^ string | null ``` _A forked child process "message" event is type `any`, so you can cast the Promise directly:_ ```typescript const child = fork('file.js'); // With `await` const [message, _]: [WorkerPayload, unknown] = await once(child, 'message'); // With Promise const messagePromise: Promise<[WorkerPayload, unknown]> = once(child, 'message'); // Better yet would be to leave it as `any`, and validate the payload // at runtime with i.e. `ajv` + `json-schema-to-typescript` ``` _If the TypeScript definition does not contain an overload for the specified event name, then the Promise will have type `unknown[]` and your code will need to narrow the result manually:_ ```typescript interface CustomEmitter extends EventEmitter { on(name: 'foo', listener: (a: string, b: number) => void): this; } const emitter: CustomEmitter = new EventEmitter(); // "foo" event is a defined overload, so it's properly typed const fooPromise = once(emitter, 'foo'); // ^ Promise<[a: string, b: number]> // "bar" event in not a defined overload, so it gets `unknown[]` const barPromise = once(emitter, 'bar'); // ^ Promise ``` ### OnceOptions - `signal` - `AbortSignal` instance to unbind event handlers before the Promise has been fulfilled. once-3.0.0/package.json000066400000000000000000000022361412430546700147350ustar00rootroot00000000000000{ "name": "@tootallnate/once", "version": "3.0.0", "description": "Creates a Promise that waits for a single event", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ "dist", "src" ], "scripts": { "prebuild": "rimraf dist", "build": "tsc", "test": "jest", "prepublishOnly": "npm run build" }, "repository": { "type": "git", "url": "git://github.com/TooTallNate/once.git" }, "keywords": [], "author": "Nathan Rajlich (http://n8.io/)", "license": "MIT", "bugs": { "url": "https://github.com/TooTallNate/once/issues" }, "devDependencies": { "@types/jest": "^27.0.2", "@types/node": "^12.12.11", "abort-controller": "^3.0.0", "jest": "^27.2.1", "rimraf": "^3.0.0", "ts-jest": "^27.0.5", "typescript": "^4.4.3" }, "engines": { "node": ">= 10" }, "jest": { "preset": "ts-jest", "globals": { "ts-jest": { "diagnostics": false, "isolatedModules": true } }, "verbose": false, "testEnvironment": "node", "testMatch": [ "/test/**/*.test.ts" ] } } once-3.0.0/scripts/000077500000000000000000000000001412430546700141335ustar00rootroot00000000000000once-3.0.0/scripts/gen-overloaded-parameters.ts000066400000000000000000000012431412430546700215370ustar00rootroot00000000000000/** * This script generated a TypeScript file that defines * a type based on this StackOverflow answer: * https://stackoverflow.com/a/52761156/376773 */ process.stdout.write('export type OverloadedParameters = \n'); const overloads = parseInt(process.argv[2], 10) || 5; for (let i = overloads; i > 0; i--) { process.stdout.write(`\tT extends { `); for (let j = 1; j <= i; j++) { process.stdout.write( `(...args: infer A${j}): any; ` ); } process.stdout.write(`} ? `); for (let j = 1; j <= i; j++) { process.stdout.write(`A${j} `); if (j < i) { process.stdout.write(`| `); } } process.stdout.write(`:\n`); } process.stdout.write(`\tany;\n`); once-3.0.0/src/000077500000000000000000000000001412430546700132335ustar00rootroot00000000000000once-3.0.0/src/index.ts000066400000000000000000000015671412430546700147230ustar00rootroot00000000000000import { EventEmitter } from 'events'; import { EventNames, EventListenerParameters, AbortSignal } from './types.js'; export interface OnceOptions { signal?: AbortSignal; } export default function once< Emitter extends EventEmitter, Event extends EventNames >( emitter: Emitter, name: Event, { signal }: OnceOptions = {} ): Promise> { return new Promise((resolve, reject) => { function cleanup() { signal?.removeEventListener('abort', cleanup); emitter.removeListener(name, onEvent); emitter.removeListener('error', onError); } function onEvent(...args: EventListenerParameters) { cleanup(); resolve(args); } function onError(err: Error) { cleanup(); reject(err); } signal?.addEventListener('abort', cleanup); emitter.on(name, onEvent); emitter.on('error', onError); }); } once-3.0.0/src/overloaded-parameters.ts000066400000000000000000000155631412430546700201020ustar00rootroot00000000000000export type OverloadedParameters = T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; (...args: infer A18): any; (...args: infer A19): any; (...args: infer A20): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 | A18 | A19 | A20 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; (...args: infer A18): any; (...args: infer A19): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 | A18 | A19 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; (...args: infer A18): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 | A18 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; (...args: infer A17): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 | A17 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; (...args: infer A16): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 | A16 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; (...args: infer A15): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 | A15 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; (...args: infer A14): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 | A14 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; (...args: infer A13): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 | A13 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; (...args: infer A12): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 | A12 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; (...args: infer A11): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 | A11 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; (...args: infer A10): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 | A10 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; (...args: infer A9): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 | A9 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; (...args: infer A8): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 | A8 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; (...args: infer A7): any; } ? A1 | A2 | A3 | A4 | A5 | A6 | A7 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; (...args: infer A6): any; } ? A1 | A2 | A3 | A4 | A5 | A6 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; (...args: infer A5): any; } ? A1 | A2 | A3 | A4 | A5 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; (...args: infer A4): any; } ? A1 | A2 | A3 | A4 : T extends { (...args: infer A1): any; (...args: infer A2): any; (...args: infer A3): any; } ? A1 | A2 | A3 : T extends { (...args: infer A1): any; (...args: infer A2): any; } ? A1 | A2 : T extends { (...args: infer A1): any; } ? A1 : any; once-3.0.0/src/types.ts000066400000000000000000000016671412430546700147610ustar00rootroot00000000000000import { EventEmitter } from 'events'; import { OverloadedParameters } from './overloaded-parameters'; export type FirstParameter = T extends [infer R, ...any[]] ? R : never; export type EventListener = F extends [ T, infer R, ...any[] ] ? R : never; export type EventParameters< Emitter extends EventEmitter > = OverloadedParameters; export type EventNames = FirstParameter< EventParameters >; export type EventListenerParameters< Emitter extends EventEmitter, Event extends EventNames > = WithDefault< Parameters, Event>>, unknown[] >; export type WithDefault = [T] extends [never] ? D : T; export interface AbortSignal { addEventListener: (name: string, listener: (...args: any[]) => any) => void; removeEventListener: ( name: string, listener: (...args: any[]) => any ) => void; } once-3.0.0/test/000077500000000000000000000000001412430546700134235ustar00rootroot00000000000000once-3.0.0/test/once.test.ts000066400000000000000000000063461412430546700157060ustar00rootroot00000000000000import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import { AbortController } from 'abort-controller'; import once from '../src'; describe('once()', () => { it('should work with vanilla EventEmitter', async () => { const emitter = new EventEmitter(); const promise = once(emitter, 'foo'); emitter.emit('foo', 'bar'); const [foo] = await promise; expect(foo).toEqual('bar'); }); it('should work with vanilla EventEmitter - nextTick', async () => { const ee = new EventEmitter(); process.nextTick(() => { ee.emit('myevent', 42); }); const [value] = await once(ee, 'myevent'); expect(value).toEqual(42); }); it('should reject Promise upon "error" event', async () => { let err: Error | null = null; const emitter = new EventEmitter(); const promise = once(emitter, 'foo').catch((_err) => { err = _err; }); emitter.emit('error', new Error('test')); await promise; expect(err!.message).toEqual('test'); }); it('should reject Promise upon "error" event - nextTick', async () => { const ee = new EventEmitter(); const err = new Error('kaboom'); process.nextTick(() => { ee.emit('error', err); }); try { await once(ee, 'myevent'); throw new Error('Should not happen'); } catch (err: any) { expect(err.message).toEqual('kaboom'); } }); it('should work with interface extending EventEmitter with overload', async () => { interface TestEmitter extends EventEmitter { on(name: 'foo', listener: (a: string, b: number) => void): this; } const emitter: TestEmitter = new EventEmitter(); const promise = once(emitter, 'foo'); emitter.emit('foo', 'bar', 4); const [a, b] = await promise; expect(a).toEqual('bar'); expect(b).toEqual(4); }); it('should allow casting from an `any` param', async () => { interface TestEmitter extends EventEmitter { on(name: 'foo', listener: (a: any, b: number) => void): this; } const emitter: TestEmitter = new EventEmitter(); const promise: Promise<[string, unknown]> = once(emitter, 'foo'); emitter.emit('foo', 'bar', 4); const [a] = await promise; // TypeScript will fail if `a` is not `string` type expect(a.toUpperCase()).toEqual('BAR'); }); it('should work with ChildProcess "exit" event', async () => { const child = spawn('echo', ['hi']); const [code, signal] = await once(child, 'exit'); expect(code).toEqual(0); expect(signal).toBeNull(); }); it('should be abortable with `AbortController`', async () => { let wasResolved = false; const emitter = new EventEmitter(); const controller = new AbortController(); const { signal } = controller; const onResolve = () => { wasResolved = true; }; once(emitter, 'foo', { signal }).then(onResolve, onResolve); // First time without `abort()`, so it will be fulfilled emitter.emit('foo'); // Promise is fulfilled on next tick, so wait a bit await new Promise((r) => process.nextTick(r)); expect(wasResolved).toEqual(true); // Reset wasResolved = false; once(emitter, 'foo', { signal }).then(onResolve, onResolve); // This time abort controller.abort(); emitter.emit('foo'); // Promise is fulfilled on next tick, so wait a bit await new Promise((r) => process.nextTick(r)); expect(wasResolved).toEqual(false); }); }); once-3.0.0/test/tsconfig.json000066400000000000000000000001031412430546700161240ustar00rootroot00000000000000{ "extends": "../tsconfig.json", "include": ["**/*.test.ts"] } once-3.0.0/tsconfig.json000066400000000000000000000010171412430546700151520ustar00rootroot00000000000000{ "compilerOptions": { "strict": true, "strictNullChecks": true, "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, "module": "ES2020", "target": "ES2019", "esModuleInterop": true, "lib": ["ES2019"], "outDir": "dist", "sourceMap": true, "declaration": true, "skipLibCheck": true, "moduleResolution": "node", "typeRoots": [ "./@types", "./node_modules/@types" ] }, "include": ["src/**/*"], "exclude": ["node_modules"] }