diff --git a/README.md b/README.md index a0fa118..ea86a0b 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,9 @@ Optionally takes a callback to call when async tasks are complete. * `Promise` returned - Completion: [onFulfilled][promise-onfulfilled] method called - Error: [onRejected][promise-onrejected] method called -* `Observable` returned - - Completion: [onCompleted][observable-subscribe] method called - - Error: [onError][observable-subscribe] method called +* `Observable` (e.g. from [RxJS v5][rxjs5-observable] or [RxJS v4][rxjs5-observable]) returned + - Completion: [complete][rxjs5-subscriber-complete] method called + - Error: [error][rxjs5-subscriber-error] method called __Warning:__ Sync tasks are __not supported__ and your function will never complete if the one of the above strategies is not used to signal completion. However, thrown errors will be caught by the domain. @@ -96,7 +96,10 @@ MIT [event-stream]: https://github.com/dominictarr/event-stream [promise-onfulfilled]: http://promisesaplus.com/#point-26 [promise-onrejected]: http://promisesaplus.com/#point-30 -[observable-subscribe]: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/subscribe.md +[rx4-observable]: https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md +[rxjs5-observable]: http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html +[rxjs5-observer-complete]: http://reactivex.io/rxjs/class/es6/MiscJSDoc.js~ObserverDoc.html#instance-method-complete +[rxjs5-observer-error]: http://reactivex.io/rxjs/class/es6/MiscJSDoc.js~ObserverDoc.html#instance-method-error [downloads-image]: http://img.shields.io/npm/dm/async-done.svg [npm-url]: https://www.npmjs.com/package/async-done diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..2c4ab93 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,101 @@ +/** + * Notes about these type definitions: + * + * - Callbacks returning multiple completion values using multiple arguments are not supported by these types. + * Prefer to use Node's style by grouping your values in a single object or array. + * Support for this kind of callback is blocked by Microsoft/TypeScript#5453 + * + * - For ease of use, `asyncDone` lets you pass callback functions with a result type `T` instead of `T | undefined`. + * This matches Node's types but can lead to unsound code being typechecked. + * + * The following code typechecks but fails at runtime: + * ```typescript + * async function getString(): Promise { + * return "Hello, World!"; + * } + * + * async function evilGetString(): Promise { + * throw new Error("Hello, World!"); + * } + * + * function cb(err: Error | null, result: string): void { + * // This is unsound because `result` is `undefined` when `err` is not `null`. + * console.log(result.toLowerCase()); + * } + * + * asyncDone(getString, cb); // Prints `hello, world!` + * asyncDone(evilGetString, cb); // Runtime error: `TypeError: Cannot read property 'toLowerCase' of undefined` + * ``` + * + * Enforcing stricter callbacks would require developers to use `result?: string` and assert the existence + * of the result either by checking it directly or using the `!` assertion operator after testing for errors. + * ```typescript + * function stricterCb1(err: Error | null, result?: string): void { + * if (err !== null) { + * console.error(err); + * return; + * } + * console.log(result!.toLowerCase()); + * } + * + * function stricterCb2(err: Error | null, result?: string): void { + * if (result === undefined) { + * console.error("Undefined result. Error:); + * console.error(err); + * return; + * } + * console.log(result.toLowerCase()); + * } + * ``` + */ +import { ChildProcess } from "child_process"; +import { EventEmitter } from "events"; +import { Stream } from "stream"; + +declare namespace asyncDone { + + /** + * Represents a callback function used to signal the completion of a + * task without any result value. + */ + type VoidCallback = (err: Error | null) => void; + + /** + * Represents a callback function used to signal the completion of a + * task with a single result value. + */ + interface Callback { + (err: null, result: T): void; + + // Use `result?: T` or `result: undefined` to require the consumer to assert the existence of the result + // (even in case of success). See comment at the top of the file. + (err: Error, result?: any): void; + } + + /** + * Minimal `Observable` interface compatible with `async-done`. + * + * @see https://github.com/ReactiveX/rxjs/blob/c3c56867eaf93f302ac7cd588034c7d8712f2834/src/internal/Observable.ts#L77 + */ + interface Observable { + subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): any; + } + + /** + * Represents an async operation. + */ + export type AsyncTask = + ((done: VoidCallback) => void) + | ((done: Callback) => void) + | (() => ChildProcess | EventEmitter | Observable | PromiseLike | Stream); +} + +/** + * Takes a function to execute (`fn`) and a function to call on completion (`callback`). + * + * @param fn Function to execute. + * @param callback Function to call on completion. + */ +declare function asyncDone(fn: asyncDone.AsyncTask, callback: asyncDone.Callback): void; + +export = asyncDone; diff --git a/package.json b/package.json index eebaad2..6e10e16 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "contributors": [ "Blaine Bublitz ", "Pawel Kozlowski ", - "Matthew Podwysocki " + "Matthew Podwysocki ", + "Charles Samborski " ], "repository": "gulpjs/async-done", "license": "MIT", @@ -14,6 +15,7 @@ "node": ">= 0.10" }, "main": "index.js", + "types": "index.d.ts", "files": [ "index.js", "LICENSE" @@ -21,7 +23,8 @@ "scripts": { "lint": "eslint . && jscs index.js test/", "pretest": "npm run lint", - "test": "mocha --async-only", + "test": "mocha --async-only && npm run test-types", + "test-types": "tsc -p test/types", "cover": "istanbul cover _mocha --report lcovonly", "coveralls": "npm run cover && istanbul-coveralls" }, @@ -32,6 +35,7 @@ "stream-exhaust": "^1.0.1" }, "devDependencies": { + "@types/node": "^9.3.0", "eslint": "^1.7.3", "eslint-config-gulp": "^2.0.0", "expect": "^1.19.0", @@ -41,8 +45,9 @@ "jscs-preset-gulp": "^1.0.0", "mocha": "^2.4.5", "pumpify": "^1.3.6", - "rx": "^4.0.6", + "rxjs": "^5.5.6", "through2": "^2.0.0", + "typescript": "^2.6.2", "when": "^3.7.3" }, "keywords": [ diff --git a/test/observables.js b/test/observables.js index 797b994..4956878 100644 --- a/test/observables.js +++ b/test/observables.js @@ -4,14 +4,15 @@ var expect = require('expect'); var asyncDone = require('../'); -var Observable = require('rx').Observable; +var Observable = require('rxjs').Observable; function success() { return Observable.empty(); } function successValue() { - return Observable.return(42); + // This corresponds to `Observable.return(42);` in RxJS 4 + return Observable.of(42); } function failure() { diff --git a/test/types/callback.ts b/test/types/callback.ts new file mode 100644 index 0000000..17a0651 --- /dev/null +++ b/test/types/callback.ts @@ -0,0 +1,44 @@ +import asyncDone, { Callback } from "async-done"; + +function success(cb: Callback): void { + cb(null, 2); +} + +function failure(cb: Callback): void { + cb(new Error("Callback Error")); +} + +function neverDone(): number { + return 2; +} + +// `success` and stricter callback +asyncDone(success, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// The following code fails to compile as expected: +// asyncDone(success, function (err: Error | null, result?: string): void { +// console.log("Done"); +// }); + +// `success` and unsound callback +asyncDone(success, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// `failure` and stricter callback +asyncDone(failure, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `failure` and unsound callback +asyncDone(failure, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// I don't think TS is currently able to prevent the current code from compiling +// (`neverDone` matches with `(done: VoidCallback) => void` for example) +// asyncDone(neverDone, function(err: Error | null, result?: number): void { +// console.log("Done"); +// }); diff --git a/test/types/child_processes.ts b/test/types/child_processes.ts new file mode 100644 index 0000000..d0d431b --- /dev/null +++ b/test/types/child_processes.ts @@ -0,0 +1,19 @@ +import asyncDone from "async-done"; +import cp from "child_process"; + + +function success(): cp.ChildProcess { + return cp.exec("echo hello world"); +} + +function failure(): cp.ChildProcess { + return cp.exec("foo-bar-baz hello world"); +} + +asyncDone(success, function (err: Error | null): void { + console.log("Done"); +}); + +asyncDone(failure, function (err: Error | null): void { + console.log("Done"); +}); diff --git a/test/types/observables.ts b/test/types/observables.ts new file mode 100644 index 0000000..4f0dd53 --- /dev/null +++ b/test/types/observables.ts @@ -0,0 +1,46 @@ +import asyncDone from "async-done"; +import { Observable } from "rxjs/Observable"; +import 'rxjs/add/observable/empty'; +import 'rxjs/add/observable/of'; + +function success(): Observable { + return Observable.empty(); +} + +function successValue(): Observable { + return Observable.of(42); +} + +function failure(): Observable { + return Observable.throw(new Error("Observable error")); +} + +// `success` callback +asyncDone(success, function (err: Error | null): void { + console.log("Done"); +}); + +// The following code fails to compile as expected (`undefined` is not assignable to `number`): +// asyncDone(success, function (err: Error | null, result: number): void { +// console.log("Done"); +// }); + +// `successValue` and stricter callback +asyncDone(successValue, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `successValue` and unsound callback +asyncDone(successValue, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// `failure` and stricter callback +asyncDone(failure, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `failure` and unsound callback +asyncDone(failure, function (err: Error | null, result: number): void { + console.log("Done"); +}); diff --git a/test/types/promises.ts b/test/types/promises.ts new file mode 100644 index 0000000..e7a7498 --- /dev/null +++ b/test/types/promises.ts @@ -0,0 +1,34 @@ +import asyncDone from "async-done"; + +function success(): Promise { + return Promise.resolve(2); +} + +function failure(): Promise { + return Promise.reject(new Error("Promise Error")); +} + +// `successValue` and stricter callback +asyncDone(success, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// The following code fails to compile as expected: +// asyncDone(success, function (err: Error | null, result?: string): void { +// console.log("Done"); +// }); + +// `successValue` and unsound callback +asyncDone(success, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// `failure` and stricter callback +asyncDone(failure, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `failure` and unsound callback +asyncDone(failure, function (err: Error | null, result: number): void { + console.log("Done"); +}); diff --git a/test/types/streams.ts b/test/types/streams.ts new file mode 100644 index 0000000..cda238b --- /dev/null +++ b/test/types/streams.ts @@ -0,0 +1,18 @@ +import asyncDone from "async-done"; +import { Readable, Stream } from "stream"; + +function streamSuccess(): Stream { + return new Stream(); +} + +function streamFail(): Stream { + return new Stream(); +} + +asyncDone(streamSuccess, function (err: Error | null): void { + console.log("Done"); +}); + +asyncDone(streamFail, function (err: Error | null): void { + console.log("Done"); +}); diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 0000000..35ef644 --- /dev/null +++ b/test/types/tsconfig.json @@ -0,0 +1,63 @@ +{ + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "baseUrl": "../..", + "charset": "utf8", + "checkJs": false, + "declaration": true, + "disableSizeLimit": false, + "downlevelIteration": false, + "emitBOM": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": false, + "inlineSourceMap": false, + "inlineSources": false, + "isolatedModules": false, + "lib": [ + "es2017" + ], + "locale": "en-us", + "module": "commonjs", + "moduleResolution": "node", + "newLine": "lf", + "noEmit": true, + "noEmitHelpers": false, + "noEmitOnError": true, + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noStrictGenericChecks": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noLib": false, + "noResolve": false, + "paths": { + "async-done": [ + "index.d.ts" + ] + }, + "preserveConstEnums": true, + "removeComments": false, + "rootDir": "", + "skipLibCheck": false, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "suppressExcessPropertyErrors": false, + "suppressImplicitAnyIndexErrors": false, + "target": "es2017", + "traceResolution": false + }, + "include": [ + "./**/*.ts" + ], + "exclude": [] +} diff --git a/test/types/various.ts b/test/types/various.ts new file mode 100644 index 0000000..73c6956 --- /dev/null +++ b/test/types/various.ts @@ -0,0 +1,5 @@ +import asyncDone, {AsyncTask, VoidCallback} from "async-done"; + +// Do not error if the return value is not `void`. +const fn: AsyncTask = (cb: VoidCallback): NodeJS.Timer => setTimeout(cb, 1000); +asyncDone(fn, () => {});