Gathering detailed insights and metrics for @byojs/locker
Gathering detailed insights and metrics for @byojs/locker
Exclusive, stackable locking to control concurrent access to shared resource(s)
npm install @byojs/locker
Typescript
Module System
Node Version
NPM Version
63.8
Supply Chain
98.9
Quality
81.1
Maintenance
100
Vulnerability
100
License
JavaScript (96.23%)
HTML (3.77%)
Total Downloads
149
Last Day
1
Last Week
4
Last Month
13
Last Year
149
25 Stars
3 Commits
2 Forks
1 Watching
1 Branches
1 Contributors
Latest Version
0.0.2
Package Id
@byojs/locker@0.0.2
Unpacked Size
29.68 kB
Size
9.37 kB
File Count
13
NPM Version
10.8.2
Node Version
21.7.2
Publised On
24 Nov 2024
Cumulative downloads
Total Downloads
Last day
0%
1
Compared to previous day
Last week
0%
4
Compared to previous week
Last month
-90.4%
13
Compared to previous month
Last year
0%
149
Compared to previous year
3
Locker provides exclusive, stackable locking, to control concurrent access to shared resource(s).
1var lock = Locker(); 2 3lock.when(async function myOperation(){ 4 // safely operate against shared resource(s), 5 // such as global state variables, files, etc 6});
Or:
1var lock = Locker(); 2 3await lock.get(); 4 5// safely operate against shared resource(s), 6// such as global state variables, files, etc 7myOperation(); 8 9lock.release();
The main purpose of Locker is to aid in managing asynchrony by gating an operation until an exclusive lock can be obtained. If an operation already holds that lock, subsequent requests to obtain the exclusive lock will stack on top of each other, each waiting in turn.
Essentially, this provides a strictly-sequential asynchronous queue, either explicitly -- with a lock.when().when()...
chain -- or implicitly -- by gating subsequent, separate lock.get()
calls.
1npm install @byojs/locker
The @byojs/locker npm package includes a dist/
directory with all files you need to deploy Locker (and its dependencies) into your application/project.
Note: If you obtain this library via git instead of npm, you'll need to build dist/
manually before deployment.
If you are using a bundler (Astro, Vite, Webpack, etc) for your web application, you should not need to manually copy any files from dist/
.
Just import
like so:
1import Locker from "@byojs/locker";
The bundler tool should pick up and find whatever files (and dependencies) are needed.
If you are not using a bundler (Astro, Vite, Webpack, etc) for your web application, and just deploying the contents of dist/
as-is without changes (e.g., to /path/to/js-assets/locker/
), you'll need an Import Map in your app's HTML:
1<script type="importmap"> 2{ 3 "imports": { 4 "locker": "/path/to/js-assets/locker.mjs" 5 } 6} 7</script>
Now, you'll be able to import
the library in your app in a friendly/readable way:
1import Locker from "locker";
Note: If you omit the above locker import-map entry, you can still import
Locker by specifying the proper full path to the locker.mjs
file.
The API provided by Locker is a single factory function, typically named Locker
. This function takes no arguments, and produces independent exclusive-lock instances.
1import Locker from ".."; 2 3var lockA = Locker(); 4var lockB = Locker();
Exclusive lock instances have 3 methods.
The when(..)
method is the preferred and safest way to use Locker. It executes the passed-in function once an exclusive lock is obtained; the function can be normal/synchronous, or an async
function. And once the function completes (normally, or abnormally with an exception), the lock is automatically released.
1import Locker from ".."; 2 3var lock = Locker(); 4 5lock.when(async function doStuff(){ 6 // now ready to safely access shared resource(s) 7});
Note: when()
normalizes the invocation of the passed-in function to always be asynchronous; even if the lock is not currently held, a promise microtask tick passes before invocation.
To support externally cancelling a pending request for the lock, pass an options object (e.g., { signal: .. }
) as the second argument to when(..)
, with an AbortSignal
instance (from an AbortController
instance).
For example, use a timeout signal to cancel the lock if it takes too long to obtain:
1import Locker from ".."; 2 3var lock = Locker(); 4 5lock.when(doStuff,{ signal: AbortSignal.timeout(5000), });
Note: signal
aborting has no effect on the doStuff()
operation if it has already started; it only cancels obtaining the lock (which prevents doStuff()
from starting).
Set a pass
property on the options object (second argument), with an array of arguments to pass to the function when it's invoked:
1import Locker from ".."; 2 3var lock = Locker(); 4 5lock.when(doStuff,{ pass: [ 1, 2, 3 ] });
when()
returns the lock instance, so you can chain multiple when()
calls together, conceptually expressing an asynchronous queue:
1import Locker from ".."; 2 3var lock = Locker(); 4 5lock.when(doTaskA).when(doTaskB).when(doTaskC);
Note: when()
does not return a promise, so by design there's no way to wait for the completion of the operation (or a chain of operations). If other parts of the application need to coordinate (wait for) a lock-bound task, those parts should be invoked inside the lock-bound task.
As asserted earlier, the preferred and safest usage of Locker is with when()
. However, sometimes it's more convenient to use two lower-level methods: get()
and release()
:
get(..)
: obtains an exclusive lock for this instance.
If the lock is available, returns undefined
(so as to not block an await
unnecessarily). But if lock is already held, returns a promise that will resolve once the exclusive lock is obtained:
1import Locker from ".."; 2 3var lock = Locker(); 4 5await lock.get(); 6 7// now ready to safely access shared resource(s) 8 9lock.release();
Note: lock.release()
should be called as soon as the lock obtained by lock.get()
is no longer needed. See below for important clarifications.
Pass true
as an argument (or the options object { forceResolvedPromise: true }
) to always return a promise; if lock is inactive, that promise will already be resolved. This safely enables promise chaining like lock.get(true).then(..)
, if desired:
1import Locker from ".."; 2 3var lock = Locker(); 4 5lock.get(true) 6 // safe to call .then() 7 .then(/* .. */) 8 .finally(() => lock.release());
To support externally cancelling a pending request for the lock, pass an options object (e.g., { signal: .. }
) as the argument to get(..)
, with an AbortSignal
instance (from an AbortController
instance).
For example, use a timeout signal to cancel the lock if it takes too long to obtain:
1import Locker from ".."; 2 3var lock = Locker(); 4 5try { 6 await lock.get({ signal: AbortSignal.timeout(5000), }); 7 8 // .. 9 10 lock.release(); 11} 12catch (err) { 13 // lock may have timed out 14}
Note: If obtaining the lock is canceled by the signal
, an exception will be thrown -- hence, the try..catch
. In this case, lock.release()
should not be called; doing so might prematurely release a subsequent hold on the lock (elsewhere in the app).
release()
: releases a current hold on the lock (if any); takes no input and has no return value. Has no effect if the lock is not currently active.
1import Locker from ".."; 2 3var lock = Locker(); 4 5await lock.get(); 6 7// now ready to safely access shared resource(s) 8 9lock.release();
Calls to lock.get()
and lock.release()
must always be paired together, and pairs of calls must never be nested. Otherwise, the application will experience deadlocks or other unexpected behavior.
Further, take care not to call lock.release()
unless its paired lock.get()
call succeeded (no cancellation exception). Otherwise, this may prematurely release a subsequent hold on the lock (elsewhere in the app), causing bugs.
dist/*
If you need to rebuild the dist/*
files for any reason, run:
1# only needed one time 2npm install 3 4npm run build:all
Visit https://byojs.dev/locker/
and click the "run tests" button.
To instead run the tests locally in a browser, first make sure you've already run the build, then:
1npm run test:start
This will start a static file webserver (no server logic), serving the interactive test page from http://localhost:8080/
; visit this page in your browser and click the "run tests" button.
By default, the test/browser.js
file imports the code from the src/*
directly. However, to test against the dist/*
files (as included in the npm package), you can modify test/browser.js
, updating the /src
in its import
statements to /dist
(see the import-map in test/index.html
for more details).
To run the tests on the CLI, first make sure you've already run the build, then:
1npm test
By default, the test/cli.js
file imports the code from the src/*
directly. However, to test against the dist/*
files (as included in the npm package), swap out which of the two import
statements at the top of test/cli.js
is commented out, to use the ./dist/locker.mjs
import specifier.
All code and documentation are (c) 2024 locker and released under the MIT License. A copy of the MIT License is also included.
No vulnerabilities found.
No security vulnerabilities found.