Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ jobs:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: [10.x, 12.x]
# test against official MAINTENANCE + ACTIVE releases:
# https://nodejs.org/en/about/previous-releases#release-schedule
node-version: [20.x, 22.x, 24.x]

steps:
- uses: actions/checkout@v1
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ jobs:
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v3
with:
node-version: '16.x'
# build with latest LTS
# https://nodejs.org/en/about/previous-releases#release-schedule
node-version: '24.x'
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm run build
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v24.0.0
7,899 changes: 4,400 additions & 3,499 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,22 @@
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/jest": "^26.0.23",
"@types/jest": "^30.0.0",
"git-cz": "^4.7.5",
"jest": "^27.0.1",
"jest": "^30.1.3",
"jest-environment-jsdom": "^30.1.2",
"rimraf": "^3.0.2",
"rollup": "^2.34.1",
"rollup-plugin-terser": "^7.0.2",
"ts-jest": "^27.0.0",
"ts-jest": "^29.4.4",
"tslib": "^2.2.0",
"typescript": "^4.2.4"
"typescript": "^5.9.2"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "jsdom"
},
"dependencies": {}
}
"engines": {
"node": ">=20.x <=24.x"
}
}
130 changes: 85 additions & 45 deletions src/polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ enum STORAGE_KEYS {
}
interface LockOptions {
mode: LockMode;
ifAvailable: Boolean;
steal: Boolean;
Comment on lines -26 to -27
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix. We expect primitive booleans here and not boolean objects.

ifAvailable: boolean;
steal: boolean;
signal?: AbortSignal;
}

Expand All @@ -43,6 +43,8 @@ export type LockInfo = Lock & {
type Request = LockInfo & {
resolve: (value?: unknown) => void;
reject: (reason?: any) => void;
closeSignal?: () => void;
signal?: AbortSignal;
Comment on lines +46 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to allow checking signal before lock acquisition as well as remove listener post-acquisition

};

type RequestArgsCase1 = [name: string, callback: LockGrantedCallback];
Expand Down Expand Up @@ -116,7 +118,7 @@ export class LockManager {
public async request(...args: RequestArgsCase2): Promise<any>;
public async request(...args: RequestArgsCase3) {
const self = this;
return new Promise(async function (resolve, reject) {
return new Promise(function (resolve, reject) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Promise constructor callbacks should not be async

const res = self._handleRequestArgs(args, reject);
if (!res) return;
const { cb, _options } = res;
Expand All @@ -129,6 +131,7 @@ export class LockManager {
uuid: `${name}-${generateRandomId()}`,
resolve,
reject,
signal: _options.signal,
};

const resolveWithCB = self._resolveWithCB(cb, resolve, reject);
Expand All @@ -141,7 +144,6 @@ export class LockManager {

// handle request options
if (_options.steal === true) {
if (!self._handleExceptionWhenStealIsTrue(_options, reject)) return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved to _handleRequestArgs to consolidate args errors in one place

// one held lock or multiple shared locks of this source should be remove
heldLockSet = heldLockSet.filter((e) => e.name !== request.name);
heldLock = heldLockSet.find((e) => {
Expand Down Expand Up @@ -253,50 +255,29 @@ export class LockManager {
}
}

private _getAbortError(signal: AbortSignal) {
return signal.reason || new DOMException("The request was aborted.", "AbortError");
}

private _handleSignalExisted(
_options: LockOptions,
{ signal }: LockOptions,
reject: (reason?: any) => void,
request: Request
) {
if (!(_options.signal instanceof AbortSignal)) {
if (!(signal instanceof AbortSignal)) {
reject(
new TypeError(
"Failed to execute 'request' on 'LockManager': member signal is not of type AbortSignal."
)
);
return false;
} else if (_options.signal.aborted) {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': The request was aborted."
)
);
} else if (signal.aborted) {
reject(this._getAbortError(signal));
return false;
} else {
this._signalOnabort(_options.signal, request);
}
return true;
}

private _handleExceptionWhenStealIsTrue(
_options: LockOptions,
reject: (reason?: any) => void
) {
if (_options.mode !== LOCK_MODE.EXCLUSIVE) {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': The 'steal' option may only be used with 'exclusive' locks."
)
);
return false;
}
if (_options.ifAvailable === true) {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': The 'steal' and 'ifAvailable' options cannot be used together."
)
);
return false;
Comment on lines -281 to -299
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to _handleRequestArgs to consolidate args errors in one place

const listener = this._signalOnabort(signal, request);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opted for returning a listener instead of mutating request inside _signalOnAbort

signal.addEventListener("abort", listener);
request.closeSignal = () => signal.removeEventListener("abort", listener);
}
return true;
}
Expand Down Expand Up @@ -354,11 +335,53 @@ export class LockManager {
if (args[0][0] === "-") {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': Names cannot start with '-'."
"Failed to execute 'request' on 'LockManager': Names cannot start with '-'.",
"NotSupportedError"
)
);
return null;
}

if (_options.signal && _options.steal) {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': The 'signal' and 'steal' options cannot be used together.",
"NotSupportedError"
)
);
return null;
}

if (_options.signal && _options.ifAvailable) {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': The 'signal' and 'ifAvailable' options cannot be used together.",
"NotSupportedError"
)
);
return null;
}

if (_options.steal && _options.ifAvailable) {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': The 'steal' and 'ifAvailable' options cannot be used together.",
"NotSupportedError"
)
);
return null;
}

if (_options.steal && _options.mode !== LOCK_MODE.EXCLUSIVE) {
reject(
new DOMException(
"Failed to execute 'request' on 'LockManager': The 'steal' option may only be used with 'exclusive' locks.",
"NotSupportedError"
)
);
return null;
}

return { cb, _options };
}

Expand Down Expand Up @@ -388,17 +411,23 @@ export class LockManager {
}
}

private _signalOnabort(signal: AbortSignal, { name, uuid }: Request) {
signal.onabort = () => {
private _signalOnabort(signal: AbortSignal, { name, uuid, reject }: Request) {
return () => {
// clean the lock request when it is aborted
const _requestLockQueueMap = this._requestLockQueueMap();
const requestLockIndex = _requestLockQueueMap[name].findIndex(
(lock) => lock.uuid === uuid
);
if (requestLockIndex !== -1) {
_requestLockQueueMap[name].splice(requestLockIndex, 1);
this._storeRequestLockQueueMap(_requestLockQueueMap);
const requestLockQueue = _requestLockQueueMap[name];

if (requestLockQueue) {
const requestLockIndex = requestLockQueue.findIndex(
(lock) => lock.uuid === uuid
);
if (requestLockIndex !== -1) {
requestLockQueue.splice(requestLockIndex, 1);
this._storeRequestLockQueueMap(_requestLockQueueMap);
}
}

reject(this._getAbortError(signal));
};
}

Expand Down Expand Up @@ -431,6 +460,17 @@ export class LockManager {
) {
this._pushToHeldLockSet(request, currentHeldLockSet);

// give sync aborts a chance to be processed first
await new Promise((resolve) => setTimeout(resolve, 0));

// ignore any further attempts to abort the request
request.closeSignal?.();

if (request.signal?.aborted) {
this._updateHeldAndRequestLocks(request);
return request.reject(this._getAbortError(request.signal));
}

// check and handle if this held lock has been steal
let callBackResolved = false;
let rejectedForSteal = false;
Expand Down
8 changes: 4 additions & 4 deletions test/acquire.test.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should ideally be refactored to await expect(...).rejects but deferring it to a different PR

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe("Returned Promise rejects if callback throws asynchronously", () => {
try {
// @ts-ignore
await webLocks.request();
} catch ({ name, message }) {
} catch ({ name, message }: any) {
expect(name).toEqual("TypeError");
expect(message).toEqual(
"Failed to execute 'request' on 'LockManager': 2 arguments required, but only 0 present."
Expand All @@ -38,7 +38,7 @@ describe("Returned Promise rejects if callback throws asynchronously", () => {
const sourceName = generateRandomId();
// @ts-ignore
await webLocks.request(sourceName);
} catch ({ name, message }) {
} catch ({ name, message }: any) {
expect(name).toEqual("TypeError");
expect(message).toEqual(
"Failed to execute 'request' on 'LockManager': 2 arguments required, but only 1 present."
Expand All @@ -52,7 +52,7 @@ describe("Returned Promise rejects if callback throws asynchronously", () => {
try {
// @ts-ignore
await webLocks.request(sourceName, { mode: "foo" }, (lock) => {});
} catch ({ name, message }) {
} catch ({ name, message }: any) {
expect(name).toEqual("TypeError");
expect(message).toEqual(
"Failed to execute 'request' on 'LockManager': The provided value 'foo' is not a valid enum value of type LockMode."
Expand All @@ -62,7 +62,7 @@ describe("Returned Promise rejects if callback throws asynchronously", () => {
try {
// @ts-ignore
await webLocks.request(sourceName, { mode: null }, (lock) => {});
} catch ({ name, message }) {
} catch ({ name, message }: any) {
expect(name).toEqual("TypeError");
expect(message).toEqual(
"Failed to execute 'request' on 'LockManager': The provided value 'null' is not a valid enum value of type LockMode."
Expand Down
2 changes: 2 additions & 0 deletions test/mode-exclusive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ describe("test suite of Web Locks API: mode-exclusive", () => {
// But this should be grantable immediately.
webLocks.request("b", log_grant(2)),
]);
// Flush microtask queue
await new Promise((resolve) => setTimeout(resolve, 0));
Comment on lines +42 to +43
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was needed since lock acquisition now yields execution back to the callers scope for a tick to allow sync abortSignal cancellation

});

await inner_promise;
Expand Down
Loading