├── .gitignore ├── .gitattributes ├── index.d.ts ├── .editorconfig ├── SECURITY.md ├── .github └── workflows │ └── test.yml ├── package.json ├── LICENSE ├── README.md ├── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function runAsync any>( 2 | func: F, 3 | ): (...args: Parameters) => Promise>>; 4 | 5 | export = runAsync; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | 5 | You can also send an email to admin@simonboudrias.com for direct contact with the maintainer. 6 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 20.x, latest] 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: npm ci 22 | - run: npm test 23 | - run: npm run lint 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "run-async", 3 | "version": "4.0.6", 4 | "description": "Utility method to run function either synchronously or asynchronously using the common `this.async()` style.", 5 | "exports": { 6 | "./package.json": "./package.json", 7 | ".": { 8 | "types": "./index.d.ts", 9 | "default": "./index.js" 10 | } 11 | }, 12 | "sideEffects": false, 13 | "files": [ 14 | "index.js", 15 | "index.d.ts" 16 | ], 17 | "scripts": { 18 | "test": "node --test test.js", 19 | "lint": "npx oxlint && npx prettier --check ." 20 | }, 21 | "engines": { 22 | "node": ">=0.12.0" 23 | }, 24 | "repository": "SBoudrias/run-async", 25 | "keywords": [ 26 | "flow", 27 | "flow-control", 28 | "async" 29 | ], 30 | "author": "Simon Boudrias ", 31 | "license": "MIT", 32 | "devDependencies": { 33 | "oxlint": "^1.2.0", 34 | "prettier": "^3.5.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Simon Boudrias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Run Async 2 | 3 | [![npm](https://badge.fury.io/js/run-async.svg)](http://badge.fury.io/js/run-async) 4 | 5 | Utility method to run a function either synchronously or asynchronously using a series of common patterns. This is useful for library author accepting sync or async functions as parameter. `runAsync` will always run them as an async method, and normalize the multiple signature. 6 | 7 | # Installation 8 | 9 | ```bash 10 | npm install --save run-async 11 | ``` 12 | 13 | # Usage 14 | 15 | Here's a simple example print the function results and three options a user can provide a function. 16 | 17 | ```js 18 | var runAsync = require("run-async"); 19 | 20 | var printAfter = function (func) { 21 | var cb = function (err, returnValue) { 22 | console.log(returnValue); 23 | }; 24 | runAsync(func, cb)(/* arguments for func */); 25 | }; 26 | ``` 27 | 28 | #### Using `this.async` 29 | 30 | ```js 31 | printAfter(function () { 32 | var done = this.async(); 33 | 34 | setTimeout(function () { 35 | done(null, "done running with callback"); 36 | }, 10); 37 | }); 38 | ``` 39 | 40 | #### Returning a promise 41 | 42 | ```js 43 | printAfter(function () { 44 | return new Promise(function (resolve, reject) { 45 | resolve("done running with promises"); 46 | }); 47 | }); 48 | ``` 49 | 50 | #### Synchronous function 51 | 52 | ```js 53 | printAfter(function () { 54 | return "done running sync function"; 55 | }); 56 | ``` 57 | 58 | #### Custom done factory 59 | 60 | ```js 61 | var runAsync = require("run-async"); 62 | 63 | runAsync(function () { 64 | var callback = this.customAsync(); 65 | callback(null, a + b); 66 | }, "customAsync")(1, 2); 67 | ``` 68 | 69 | #### Passing context to async method 70 | 71 | ```js 72 | var runAsync = require("run-async"); 73 | 74 | runAsync(function () { 75 | assert(this.isBound); 76 | var callback = this.async(); 77 | callback(null, a + b); 78 | }).call({ isBound: true }, 1, 2); 79 | ``` 80 | 81 | ### runAsync.cb 82 | 83 | `runAsync.cb` supports all the function types that `runAsync` does and additionally a traditional **callback as the last argument** signature: 84 | 85 | ```js 86 | var runAsync = require("run-async"); 87 | 88 | // IMPORTANT: The wrapped function must have a fixed number of parameters. 89 | runAsync.cb( 90 | function (a, b, cb) { 91 | cb(null, a + b); 92 | }, 93 | function (err, result) { 94 | console.log(result); 95 | }, 96 | )(1, 2); 97 | ``` 98 | 99 | If your version of node support Promises natively (node >= 0.12), `runAsync` will return a promise. Example: `runAsync(func)(arg1, arg2).then(cb)` 100 | 101 | # Licence 102 | 103 | Copyright (c) 2014 Simon Boudrias (twitter: @vaxilart) 104 | Licensed under the MIT license. 105 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function isPromise(obj) { 2 | return ( 3 | !!obj && 4 | (typeof obj === "object" || typeof obj === "function") && 5 | typeof obj.then === "function" 6 | ); 7 | } 8 | 9 | /** 10 | * Return a function that will run a function asynchronously or synchronously 11 | * 12 | * example: 13 | * runAsync(wrappedFunction, callback)(...args); 14 | * 15 | * @param {Function} func Function to run 16 | * @param {Function} [cb] Callback function passed the `func` returned value 17 | * @param {string} [proxyProperty] `this` property to be used for the callback factory 18 | * @return {Function(arguments)} Arguments to pass to `func`. This function will in turn 19 | * return a Promise (Node >= 0.12) or call the callbacks. 20 | */ 21 | 22 | var runAsync = (module.exports = function (func, cb, proxyProperty = "async") { 23 | if (typeof cb === "string") { 24 | proxyProperty = cb; 25 | cb = undefined; 26 | } 27 | cb = cb || function () {}; 28 | 29 | return function () { 30 | var args = arguments; 31 | var originalThis = this; 32 | 33 | var promise = new Promise(function (resolve, reject) { 34 | var resolved = false; 35 | const wrappedResolve = function (value) { 36 | if (resolved) { 37 | console.warn("Run-async promise already resolved."); 38 | } 39 | resolved = true; 40 | resolve(value); 41 | }; 42 | 43 | var rejected = false; 44 | const wrappedReject = function (value) { 45 | if (rejected) { 46 | console.warn("Run-async promise already rejected."); 47 | } 48 | rejected = true; 49 | reject(value); 50 | }; 51 | 52 | var usingCallback = false; 53 | var callbackConflict = false; 54 | var contextEnded = false; 55 | 56 | var doneFactory = function () { 57 | if (contextEnded) { 58 | console.warn( 59 | "Run-async async() called outside a valid run-async context, callback will be ignored.", 60 | ); 61 | return function () {}; 62 | } 63 | if (callbackConflict) { 64 | console.warn( 65 | "Run-async wrapped function (async) returned a promise.\nCalls to async() callback can have unexpected results.", 66 | ); 67 | } 68 | usingCallback = true; 69 | return function (err, value) { 70 | if (err) { 71 | wrappedReject(err); 72 | } else { 73 | wrappedResolve(value); 74 | } 75 | }; 76 | }; 77 | 78 | var _this; 79 | if (originalThis && proxyProperty && Proxy) { 80 | _this = new Proxy(originalThis, { 81 | get(_target, prop) { 82 | if (prop === proxyProperty) { 83 | if (prop in _target) { 84 | console.warn( 85 | `${proxyProperty} property is been shadowed by run-sync`, 86 | ); 87 | } 88 | return doneFactory; 89 | } 90 | 91 | return Reflect.get(...arguments); 92 | }, 93 | }); 94 | } else { 95 | _this = { [proxyProperty]: doneFactory }; 96 | } 97 | 98 | var answer = func.apply(_this, Array.prototype.slice.call(args)); 99 | 100 | if (usingCallback) { 101 | if (isPromise(answer)) { 102 | console.warn( 103 | "Run-async wrapped function (sync) returned a promise but async() callback must be executed to resolve.", 104 | ); 105 | } 106 | } else { 107 | if (isPromise(answer)) { 108 | callbackConflict = true; 109 | answer.then(wrappedResolve, wrappedReject); 110 | } else { 111 | wrappedResolve(answer); 112 | } 113 | } 114 | contextEnded = true; 115 | }); 116 | 117 | promise.then(cb.bind(null, null), cb); 118 | 119 | return promise; 120 | }; 121 | }); 122 | 123 | runAsync.cb = function (func, cb) { 124 | return runAsync(function () { 125 | var args = Array.prototype.slice.call(arguments); 126 | if (args.length === func.length - 1) { 127 | args.push(this.async()); 128 | } 129 | return func.apply(this, args); 130 | }, cb); 131 | }; 132 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require("node:test"); 2 | const assert = require("node:assert"); 3 | const runAsync = require("."); 4 | 5 | test("runAsync", async (t) => { 6 | await t.test("run synchronous method", async () => { 7 | let ranAsync = false; 8 | const aFunc = function () { 9 | return "pass1"; 10 | }; 11 | let called = false; 12 | await new Promise((resolve) => { 13 | runAsync(aFunc, function (err, _val) { 14 | assert.ifError(err); 15 | assert(ranAsync); 16 | assert.equal(_val, "pass1"); 17 | called = true; 18 | resolve(); 19 | })(); 20 | ranAsync = true; 21 | }); 22 | assert(called, "callback should have been called"); 23 | }); 24 | 25 | await t.test("run asynchronous method", async () => { 26 | const aFunc = function () { 27 | const returns = this.async(); 28 | setImmediate(returns.bind(null, null, "pass2")); 29 | }; 30 | 31 | await new Promise((resolve) => { 32 | runAsync(aFunc, function (err, _val) { 33 | assert.ifError(err); 34 | assert.equal(_val, "pass2"); 35 | resolve(); 36 | })(); 37 | }); 38 | }); 39 | 40 | await t.test("pass arguments", async () => { 41 | const aFunc = function (a, b) { 42 | assert.equal(a, 1); 43 | assert.equal(b, "bar"); 44 | return "pass1"; 45 | }; 46 | await new Promise((resolve) => { 47 | runAsync(aFunc, function (err, _val) { 48 | assert.ifError(err); 49 | resolve(); 50 | })(1, "bar"); 51 | }); 52 | }); 53 | 54 | await t.test("allow only callback once", async () => { 55 | const aFunc = function () { 56 | const returns = this.async(); 57 | returns(); 58 | returns(); 59 | }; 60 | 61 | await new Promise((resolve) => { 62 | runAsync(aFunc, function (err, _val) { 63 | assert.ifError(err); 64 | resolve(); 65 | })(); 66 | }); 67 | }); 68 | 69 | await t.test("handles promises", async () => { 70 | const fn = function () { 71 | return new Promise(function (resolve, _reject) { 72 | setImmediate(function () { 73 | resolve("as promised!"); 74 | }); 75 | }); 76 | }; 77 | 78 | await new Promise((resolve) => { 79 | runAsync(fn, function (err, _val) { 80 | assert.ifError(err); 81 | assert.equal("as promised!", _val); 82 | resolve(); 83 | })(); 84 | }); 85 | }); 86 | 87 | await t.test("throwing synchronously passes error to callback", async () => { 88 | const throws = function () { 89 | throw new Error("sync error"); 90 | }; 91 | 92 | await new Promise((resolve) => { 93 | runAsync(throws, function (err, _val) { 94 | assert(err); 95 | assert.equal(err.message, "sync error"); 96 | resolve(); 97 | })(); 98 | }); 99 | }); 100 | 101 | await t.test("rejecting a promise passes error to callback", async () => { 102 | const rejects = function () { 103 | return new Promise(function (_resolve, reject) { 104 | setImmediate(function () { 105 | reject(new Error("broken promise")); 106 | }); 107 | }); 108 | }; 109 | 110 | await new Promise((resolve) => { 111 | runAsync(rejects, function (err, _val) { 112 | assert(err); 113 | assert.equal(err.message, "broken promise"); 114 | resolve(); 115 | })(); 116 | }); 117 | }); 118 | 119 | await t.test("returns a promise that is resolved", async () => { 120 | const returns = function () { 121 | return "hello"; 122 | }; 123 | 124 | runAsync(returns)().then((result) => { 125 | assert.equal(result, "hello"); 126 | }); 127 | }); 128 | 129 | await t.test("returns a promise that is rejected", async () => { 130 | const throws = function () { 131 | throw new Error("sync error"); 132 | }; 133 | 134 | try { 135 | await runAsync(throws)(); 136 | assert.fail("Expected an error"); 137 | } catch (reason) { 138 | assert.equal(reason.message, "sync error"); 139 | } 140 | }); 141 | 142 | await t.test("handles async functions", async () => { 143 | const fn = async function () { 144 | return "as promised!"; 145 | }; 146 | 147 | await new Promise((resolve) => { 148 | runAsync(fn, function (err, _val) { 149 | assert.ifError(err); 150 | assert.equal("as promised!", _val); 151 | resolve(); 152 | })(); 153 | }); 154 | }); 155 | 156 | await t.test( 157 | "ignores async callback outside original function context", 158 | async () => { 159 | let outsideContext = false; 160 | const outsideContextCallback = async function () { 161 | outsideContext = true; 162 | this.async()(undefined, "not as promised!"); 163 | }; 164 | 165 | const fn = async function () { 166 | const self = this; 167 | setTimeout(function () { 168 | outsideContextCallback.call(self); 169 | }, 100); 170 | return new Promise(function (resolve, _reject) { 171 | setTimeout(function () { 172 | outsideContext = false; 173 | resolve("as promised!"); 174 | }, 500); 175 | }); 176 | }; 177 | 178 | await new Promise((resolve) => { 179 | runAsync(fn, function (err, _val) { 180 | assert.equal(false, outsideContext); 181 | assert.ifError(err); 182 | assert.equal("as promised!", _val); 183 | resolve(); 184 | })(); 185 | }); 186 | }, 187 | ); 188 | 189 | await t.test( 190 | "handles custom done factory with not bound function", 191 | async () => { 192 | const fn = function () { 193 | const cb = this.customAsync(); 194 | setImmediate(function () { 195 | cb(null, "value"); 196 | }); 197 | }; 198 | 199 | await runAsync(fn, "customAsync")(); 200 | }, 201 | ); 202 | 203 | await t.test("handles bound function", async () => { 204 | const fn = function () { 205 | const cb = this.async(); 206 | if (this.bar === "bar") { 207 | setImmediate(function () { 208 | cb(null, "value"); 209 | }); 210 | } else { 211 | cb(new Error("not bount")); 212 | } 213 | }; 214 | 215 | await runAsync(fn).call({ bar: "bar" }); 216 | }); 217 | }); 218 | 219 | test("runAsync.cb", async (t) => { 220 | await t.test("handles callback parameter", async () => { 221 | const fn = function (cb) { 222 | setImmediate(function () { 223 | cb(null, "value"); 224 | }); 225 | }; 226 | 227 | await new Promise((resolve) => { 228 | runAsync.cb(fn, function (err, _val) { 229 | assert.ifError(err); 230 | assert.equal("value", _val); 231 | resolve(); 232 | })(); 233 | }); 234 | }); 235 | 236 | await t.test("run synchronous method", async () => { 237 | let ranAsync = false; 238 | const aFunc = function () { 239 | return "pass1"; 240 | }; 241 | let called = false; 242 | await new Promise((resolve) => { 243 | runAsync.cb(aFunc, function (err, _val) { 244 | assert.ifError(err); 245 | assert(ranAsync); 246 | assert.equal(_val, "pass1"); 247 | called = true; 248 | resolve(); 249 | })(); 250 | ranAsync = true; 251 | }); 252 | assert(called, "callback should have been called"); 253 | }); 254 | 255 | await t.test("handles a returned promise", async () => { 256 | const aFunc = function (a) { 257 | return Promise.resolve("foo" + a); 258 | }; 259 | 260 | await new Promise((resolve) => { 261 | runAsync.cb(aFunc, function (err, result) { 262 | assert.ifError(err); 263 | assert.equal(result, "foobar"); 264 | resolve(); 265 | })("bar"); 266 | }); 267 | }); 268 | }); 269 | --------------------------------------------------------------------------------