Initial version

main
Sven Slootweg 3 months ago
commit 5ad97cd53e

1
.gitignore vendored

@ -0,0 +1 @@
node_modules

@ -0,0 +1,45 @@
# single-concurrent
Small utility function to ensure that you're not running two copies of an asynchronous procedure at once.
Particularly useful for cases where you have some sort of processing loop that gets kicked off by a specific event, but that should only do so if it isn't already running.
Works with any function that returns a Promise (so this includes `async` functions).
## Example
```js
"use strict";
const singleConcurrent = require("single-concurrent");
(async function () {
let trySomeAsyncProcess = singleConcurrent(async function someAsyncProcess() {
// Let's pretend that this is some long-running task that internally recurses
console.log("started some async process");
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * 2000);
});
console.log("finished some async process");
});
trySomeAsyncProcess();
trySomeAsyncProcess(); // oops! it was already running and we tried to run it again
})();
/* Output (note how it's only output once):
started some async process
finished some async process
*/
```
## API
### singleConcurrent(callback)
- __callback:__ The function that does the actual work, and that should only be run one at a time. This must return a Promise! A function marked `async` also meets that requirement by default.
__Returns:__ A wrapper function that will only execute the `callback` if one is not already in progress; it passes through the arguments and return value (assuming the underlying callback is actually run, of course).
If the returned wrapper function is called while the procedure is still running, the call is simply ignored and synchronously returns with `undefined`. Once the underlying async procedure finishes, a call to the wrapper function will activate the underlying callback again, so it *can* be reused - it just prevents multiple runs from occurring *simultaneously*.

@ -0,0 +1,23 @@
"use strict";
const singleConcurrent = require("./");
(async function () {
let trySomeAsyncProcess = singleConcurrent(async function someAsyncProcess() {
// Let's pretend that this is some long-running task that internally recurses
console.log("started some async process");
await new Promise((resolve) => {
setTimeout(resolve, Math.random() * 2000);
});
console.log("finished some async process");
});
trySomeAsyncProcess();
trySomeAsyncProcess(); // oops! it was already running and we tried to run it again
})();
/* Output (note how it's only output once):
started some async process
finished some async process
*/

@ -0,0 +1,33 @@
"use strict";
const capturePromise = require("capture-promise");
const debug = require("debug")("single-concurrent");
const { validateArguments } = require("@validatem/core");
const isFunction = require("@validatem/is-function");
const required = require("@validatem/required");
module.exports = function singleConcurrent(_func) {
let [ func ] = validateArguments(arguments, {
func: [ required, isFunction ]
});
let running = false;
return async function (... args) {
if (running === false) {
debug(`starting`);
running = true;
try {
return await capturePromise(() => func(... args));
} finally {
// Yes, this gets called *even if* the above successfully returns, before the resulting Promise is resolved.
debug(`completed`);
running = false;
}
} else {
debug(`already running; skipping`);
}
};
};

@ -0,0 +1,34 @@
{
"name": "single-concurrent",
"version": "1.0.0",
"description": "Ensures that only one 'instance' of an asynchronous process is running at any given time",
"main": "index.js",
"files": [
"index.js",
"example.js",
"README.md"
],
"repository": {
"url": "https://git.cryto.net/joepie91/single-concurrent.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"async",
"concurrency",
"serial",
"promises",
"bluebird",
"loop"
],
"author": "Sven Slootweg <admin@cryto.net>",
"license": "WTFPL OR CC0-1.0",
"dependencies": {
"@validatem/core": "^0.5.0",
"@validatem/is-function": "^0.1.0",
"@validatem/required": "^0.1.1",
"capture-promise": "^1.0.0",
"debug": "^4.3.5"
}
}

@ -0,0 +1,352 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@validatem/core':
specifier: ^0.5.0
version: 0.5.0
'@validatem/is-function':
specifier: ^0.1.0
version: 0.1.0
'@validatem/required':
specifier: ^0.1.1
version: 0.1.1
capture-promise:
specifier: ^1.0.0
version: 1.0.0
debug:
specifier: ^4.3.5
version: 4.3.5
packages:
/@validatem/annotate-errors@0.1.2:
resolution: {integrity: sha512-EuX7pzdYI/YpTmZcgdPG481Oi3elAg8JWh/LYXuE1h6MaZk3A8eP5DD33/l7EoKzrysn6y8nCsqNa1ngei562w==}
dependencies:
'@validatem/match-validation-error': 0.1.0
dev: false
/@validatem/any-property@0.1.3:
resolution: {integrity: sha512-jYWxif5ff9pccu7566LIQ/4+snlApXEJUimBywzAriBgS3r4eDBbz3oZFHuiPmhxNK/NNof5YUS+L6Sk3zaMfg==}
dependencies:
'@validatem/annotate-errors': 0.1.2
'@validatem/combinator': 0.1.2
'@validatem/error': 1.1.0
'@validatem/validation-result': 0.1.2
'@validatem/virtual-property': 0.1.0
default-value: 1.0.0
dev: false
/@validatem/combinator@0.1.2:
resolution: {integrity: sha512-vE8t1tNXknmN62FlN6LxQmA2c6TwVKZ+fl/Wit3H2unFdOhu7SZj2kRPGjAXdK/ARh/3svYfUBeD75pea0j1Sw==}
dev: false
/@validatem/core@0.5.0:
resolution: {integrity: sha512-hLEdoRFRvFGUqHFFK0eR8r7sTJaqQjzB81FVMp86esZJiBrblnWhpZtzVouguoaAaKFX9oiWI3nAQc73xYrTJg==}
dependencies:
'@validatem/annotate-errors': 0.1.2
'@validatem/any-property': 0.1.3
'@validatem/error': 1.1.0
'@validatem/match-validation-error': 0.1.0
'@validatem/match-versioned-special': 0.1.1
'@validatem/match-virtual-property': 0.1.0
'@validatem/normalize-rules': 0.1.3
'@validatem/required': 0.1.1
'@validatem/validation-result': 0.1.2
'@validatem/virtual-property': 0.1.0
as-expression: 1.0.0
assure-array: 1.0.0
create-error: 0.3.1
default-value: 1.0.0
execall: 2.0.0
indent-string: 4.0.0
is-arguments: 1.1.1
supports-color: 7.2.0
syncpipe: 1.0.0
dev: false
/@validatem/error@1.1.0:
resolution: {integrity: sha512-gZJEoZq1COi/8/5v0fVKQ9uX54x5lb5HbV7mzIOhY6dqjmLNfxdQmpECZPQrCAOpcRkRMJ7zaFhq4UTslpY9yA==}
dev: false
/@validatem/has-shape@0.1.8:
resolution: {integrity: sha512-x2i8toW1uraFF2Vl6WBl4CScbBeg5alrtoCKMyXbJkHf2B5QxL/ftUh2RQRcBzx6U0i7KUb8vdShcWAa+fehRQ==}
dependencies:
'@validatem/annotate-errors': 0.1.2
'@validatem/combinator': 0.1.2
'@validatem/error': 1.1.0
'@validatem/validation-result': 0.1.2
array-union: 2.1.0
as-expression: 1.0.0
assure-array: 1.0.0
default-value: 1.0.0
flatten: 1.0.3
dev: false
/@validatem/is-function@0.1.0:
resolution: {integrity: sha512-UtVrwTGhaIdIJ0mPG5XkAmYZUeWgRoMP1G9ZEHbKvAZJ4+SXf/prC0jPgE0pw+sPjdQG4hblsXSfo/9Bf3PGdQ==}
dependencies:
'@validatem/error': 1.1.0
is-callable: 1.2.7
dev: false
/@validatem/is-plain-object@0.1.1:
resolution: {integrity: sha512-aNGbNIbKRpYI0lRBczlTBbiA+nqN52ADAASdySKg2/QeSCVtYS4uOIeCNIJRAgXe/5sUnLTuL4pgq628uAl7Kw==}
dependencies:
'@validatem/error': 1.1.0
is-plain-obj: 2.1.0
dev: false
/@validatem/match-special@0.1.0:
resolution: {integrity: sha512-TFiq9Wk/1Hoja4PK85WwNYnwBXk3+Lgoj59ZIMxm2an1qmNYp8j+BnSvkKBflba451yIn6V1laU9NJf+/NYZgw==}
dev: false
/@validatem/match-validation-error@0.1.0:
resolution: {integrity: sha512-6akGTk7DdulOreyqDiGdikwRSixQz/AlvARSX18dcWaTFc79KxCLouL2hyoFcor9IIUhu5RTY4/i756y4T1yxA==}
dependencies:
'@validatem/match-versioned-special': 0.1.1
dev: false
/@validatem/match-versioned-special@0.1.1:
resolution: {integrity: sha512-RRNeFSgzqSo0sKck/92a+yC9zKdt+DD6y4TK70+VDKVppdWsb8YzC/FBTucseN1OYrr1KcBPKNVZePg1NTROYw==}
dev: false
/@validatem/match-virtual-property@0.1.0:
resolution: {integrity: sha512-ssd3coFgwbLuqvZftLZTy3eHN0TFST8oTS2XTViQdXJPXVoJmwEKBpFhXgwnb5Ly1CE037R/KWpjhd1TP/56kQ==}
dev: false
/@validatem/normalize-rules@0.1.3:
resolution: {integrity: sha512-HHPceAP2ce9NWymIZrgLCTzpdwXNRBCCB5H6ZPc5ggOrbmh4STpT83fLazleHtvYNlqgXZ4GjQOvCwrjaM+qEA==}
dependencies:
'@validatem/has-shape': 0.1.8
'@validatem/is-plain-object': 0.1.1
'@validatem/match-special': 0.1.0
assure-array: 1.0.0
default-value: 1.0.0
flatten: 1.0.3
is-plain-obj: 2.1.0
dev: false
/@validatem/required@0.1.1:
resolution: {integrity: sha512-vI4NzLfay4RFAzp7xyU34PHb8sAo6w/3frrNh1EY9Xjnw2zxjY5oaxwmbFP1jVevBE6QQEnKogtzUHz/Zuvh6g==}
dev: false
/@validatem/validation-result@0.1.2:
resolution: {integrity: sha512-okmP8JarIwIgfpaVcvZGuQ1yOsLKT3Egt49Ynz6h1MAeGsP/bGHXkkXtbiWOVsk5Tzku5vDVFSrFnF+5IEHKxw==}
dependencies:
default-value: 1.0.0
dev: false
/@validatem/virtual-property@0.1.0:
resolution: {integrity: sha512-JUUvWtdqoSkOwlsl20oB3qFHYIL05a/TAfdY4AJcs55QeVTiX5iI1b8IoQW644sIWWooBuLv+XwoxjRsQFczlQ==}
dev: false
/array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
dev: false
/as-expression@1.0.0:
resolution: {integrity: sha512-Iqh4GxNUfxbJdGn6b7/XMzc8m1Dz2ZHouBQ9DDTzyMRO3VPPIAXeoY/sucRxxxXKbUtzwzWZSN6jPR3zfpYHHA==}
dev: false
/assure-array@1.0.0:
resolution: {integrity: sha512-igvOvGYidAcJKr6YQIHzLivUpAdqUfi7MN0QfrEnFtifQvuw6D0W4oInrIVgTaefJ+QBVWAj8ZYuUGNnwq6Ydw==}
dev: false
/call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
dependencies:
es-define-property: 1.0.0
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.4
set-function-length: 1.2.2
dev: false
/capture-promise@1.0.0:
resolution: {integrity: sha512-40FXZr0YaUWw2q6T8DcUOIF885saJni87ZbAOXkwDB14nMl6M1vIYAKUs2BjoeMyqnv3PGqEcDskGUXvgNIbZw==}
dev: false
/clone-regexp@2.2.0:
resolution: {integrity: sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==}
engines: {node: '>=6'}
dependencies:
is-regexp: 2.1.0
dev: false
/create-error@0.3.1:
resolution: {integrity: sha512-n/Q4aSCtYuuDneEW5Q+nd0IIZwbwmX/oF6wKcDUhXGJNwhmp2WHEoWKz7X+/H7rBtjimInW7f0ceouxU0SmuzQ==}
dev: false
/debug@4.3.5:
resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: false
/default-value@1.0.0:
resolution: {integrity: sha512-y6j7G55tgWG7nfjXUNy/WkTLGExiPEUlhGv0zqgqKdlOwJnDDy/dbk7yCozn4biAGIRnMI+9fyZ1V2fZ7tjp6Q==}
dependencies:
es6-promise-try: 0.0.1
dev: false
/define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'}
dependencies:
es-define-property: 1.0.0
es-errors: 1.3.0
gopd: 1.0.1
dev: false
/es-define-property@1.0.0:
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
engines: {node: '>= 0.4'}
dependencies:
get-intrinsic: 1.2.4
dev: false
/es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
dev: false
/es6-promise-try@0.0.1:
resolution: {integrity: sha512-T6f3cNyF8y+3uua2IDGpGmeoDe2w7PXGfPGS94TyLfQLPzYVvZUfM8dQuN4DuVXpelK4tg9F7zKzZHzNS2f2IQ==}
dev: false
/execall@2.0.0:
resolution: {integrity: sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==}
engines: {node: '>=8'}
dependencies:
clone-regexp: 2.2.0
dev: false
/flatten@1.0.3:
resolution: {integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==}
deprecated: flatten is deprecated in favor of utility frameworks such as lodash.
dev: false
/function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
dev: false
/get-intrinsic@1.2.4:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
has-proto: 1.0.3
has-symbols: 1.0.3
hasown: 2.0.2
dev: false
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
get-intrinsic: 1.2.4
dev: false
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
dev: false
/has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
dependencies:
es-define-property: 1.0.0
dev: false
/has-proto@1.0.3:
resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
engines: {node: '>= 0.4'}
dev: false
/has-symbols@1.0.3:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
dev: false
/has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
dependencies:
has-symbols: 1.0.3
dev: false
/hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
dependencies:
function-bind: 1.1.2
dev: false
/indent-string@4.0.0:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'}
dev: false
/is-arguments@1.1.1:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.7
has-tostringtag: 1.0.2
dev: false
/is-callable@1.2.7:
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
engines: {node: '>= 0.4'}
dev: false
/is-plain-obj@2.1.0:
resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
engines: {node: '>=8'}
dev: false
/is-regexp@2.1.0:
resolution: {integrity: sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==}
engines: {node: '>=6'}
dev: false
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: false
/set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
dependencies:
define-data-property: 1.1.4
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.4
gopd: 1.0.1
has-property-descriptors: 1.0.2
dev: false
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
dependencies:
has-flag: 4.0.0
dev: false
/syncpipe@1.0.0:
resolution: {integrity: sha512-cdiAFTnFJRvUaNPDc2n9CqoFvtIL3+JUMJZrC3kA3FzpugHOqu0TvkgNwmnxPZ5/WjAzMcfMS3xm+AO7rg/j/w==}
dependencies:
assure-array: 1.0.0
dev: false
Loading…
Cancel
Save