From 7ecb08ff53c60e823f23cf9693db7b01d784df20 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 20 Nov 2025 14:35:58 +0900 Subject: [PATCH 01/15] =?UTF-8?q?resolve()=20=E3=82=92=20Promise?= =?UTF-8?q?=20=E3=81=8B=E3=82=89=20Promise=20=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=20fix=20#9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lock.test.ts | 29 ++++++++++++++++++++++------- src/request.ts | 25 +++++++++---------------- src/types/Releasable.ts | 2 +- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index f464de5..48c7176 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -128,8 +128,8 @@ describe("simple use", () => { } ], }); - await expect(lock1.release()).resolves.toBe(true); - await expect(lock2.release()).resolves.toBe(true); + await expect(lock1.release()).resolves.toBeUndefined(); + await expect(lock2.release()).resolves.toBeUndefined(); await expect(query()).resolves.toEqual({ held: [ @@ -167,7 +167,7 @@ describe("simple use", () => { await using _ = await request(); const lock2Wait = request({ signal }); controller.abort(); - await expect(lock2Wait).rejects.toThrow(new DOMException("This operation was aborted", "AbortError")); + await expect(lock2Wait).rejects.toThrow(makeAbortError()); } // `signal` only affects the lock acquisition and does not affect the release. { @@ -175,7 +175,7 @@ describe("simple use", () => { const signal = controller.signal; await using lock1 = await request({ signal }); controller.abort(); - await expect(lock1.release()).resolves.toBe(true); + await expect(lock1.release()).resolves.toBeUndefined(); } }); } @@ -205,8 +205,8 @@ describe("simple use", () => { }); expect(lock3).toBeDefined(); expect(lock3.name).toBe(name); - await expect(lock1.release()).resolves.toBe(false); - await expect(lock3.release()).resolves.toBe(true); + await expect(lock1.release()).resolves.toBeUndefined(); + await expect(lock3.release()).resolves.toBeUndefined(); await using lock2 = await lock2Wait; expect(lock2).toBeDefined(); if (!lock2) return; @@ -223,7 +223,7 @@ describe("simple use", () => { ifAvailable: true, steal: true, }); - expect(lockWait).rejects.toThrow(new DOMException("ifAvailable and steal are mutually exclusive", "NotSupportedError")); + await expect(lockWait).rejects.toThrow(makeNotSupportedError("ifAvailable and steal are mutually exclusive")); } }); } @@ -265,3 +265,18 @@ describe("hard error pattern", () => { } }); }); + +/** + * make NotSupportedError + * @param message + */ +function makeNotSupportedError(message: string) { + return new DOMException(message, "NotSupportedError"); +} + +/** + * make AbortError + */ +function makeAbortError() { + return new DOMException("This operation was aborted", "AbortError"); +} \ No newline at end of file diff --git a/src/request.ts b/src/request.ts index f7ecb54..e753f82 100644 --- a/src/request.ts +++ b/src/request.ts @@ -35,7 +35,8 @@ export async function request(this: InnerLock, options?: LockOptions): Promise resolved = true); // Wait for either successful acquisition or rejection const result = await Promise.race([ callbackPromise, @@ -80,9 +81,9 @@ export async function request(this: InnerLock, options?: LockOptions): Promise { - await release(); + if (resolved) return; + releaseResolve(); + return requestPromise; } } -/** - * Helper to return true - */ -export function returnTrue() { - return true; -} - -/** - * Helper to return false - */ -export function returnFalse() { - return false; +export function returnUndefined() { + return undefined; } diff --git a/src/types/Releasable.ts b/src/types/Releasable.ts index 7c99e3a..a1f80de 100644 --- a/src/types/Releasable.ts +++ b/src/types/Releasable.ts @@ -1,4 +1,4 @@ /** * A type representing an object that can release a lock asynchronously. */ -export type Releasable = { release(): Promise; }; +export type Releasable = { release(): Promise; }; From a5822a57f03856ce467b1af408aefd55d0ced312 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 20 Nov 2025 14:42:03 +0900 Subject: [PATCH 02/15] =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=AF?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E5=AE=9A=E7=BE=A9=E3=81=99=E3=82=8B=E6=A7=98?= =?UTF-8?q?=E3=81=AB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lock.test.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index 48c7176..df7b77f 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -167,7 +167,7 @@ describe("simple use", () => { await using _ = await request(); const lock2Wait = request({ signal }); controller.abort(); - await expect(lock2Wait).rejects.toThrow(makeAbortError()); + await expect(lock2Wait).rejects.toThrow(new DOMException("This operation was aborted", "AbortError")); } // `signal` only affects the lock acquisition and does not affect the release. { @@ -223,7 +223,7 @@ describe("simple use", () => { ifAvailable: true, steal: true, }); - await expect(lockWait).rejects.toThrow(makeNotSupportedError("ifAvailable and steal are mutually exclusive")); + await expect(lockWait).rejects.toThrow(new DOMException("ifAvailable and steal are mutually exclusive", "NotSupportedError")); } }); } @@ -265,18 +265,3 @@ describe("hard error pattern", () => { } }); }); - -/** - * make NotSupportedError - * @param message - */ -function makeNotSupportedError(message: string) { - return new DOMException(message, "NotSupportedError"); -} - -/** - * make AbortError - */ -function makeAbortError() { - return new DOMException("This operation was aborted", "AbortError"); -} \ No newline at end of file From 1ce4ebf22618eb9cd7ca8cc0e07fc62287551788 Mon Sep 17 00:00:00 2001 From: juner Date: Thu, 20 Nov 2025 14:49:50 +0900 Subject: [PATCH 03/15] change error compare --- src/lock.test.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index df7b77f..70aba06 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -167,7 +167,10 @@ describe("simple use", () => { await using _ = await request(); const lock2Wait = request({ signal }); controller.abort(); - await expect(lock2Wait).rejects.toThrow(new DOMException("This operation was aborted", "AbortError")); + await expect(lock2Wait).rejects.toThrow(expect.objectContaining({ + message: "This operation was aborted", + name: "AbortError", + })); } // `signal` only affects the lock acquisition and does not affect the release. { @@ -223,7 +226,10 @@ describe("simple use", () => { ifAvailable: true, steal: true, }); - await expect(lockWait).rejects.toThrow(new DOMException("ifAvailable and steal are mutually exclusive", "NotSupportedError")); + await expect(lockWait).rejects.toThrow(expect.objectContaining({ + message: "ifAvailable and steal are mutually exclusive", + name: "NotSupportedError" + })); } }); } @@ -237,7 +243,9 @@ describe("hard error pattern", () => { value: undefined, }); try { - expect(() => lock(name)).toThrow(new Error("navigator.locks is not found. required options.locks argument.")); + expect(() => lock(name)).toThrow(expect.objectContaining({ + message: "navigator.locks is not found. required options.locks argument." + })); const { request, query } = lock(name, { locks }); { From f4b3186843aba5cb5ee099a9a10df12f8fdcb452 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 09:24:58 +0900 Subject: [PATCH 04/15] remove request withResolvers --- src/request.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/request.ts b/src/request.ts index e753f82..19f1c5f 100644 --- a/src/request.ts +++ b/src/request.ts @@ -25,16 +25,12 @@ export async function request(this: InnerLock, options?: LockOptions): Promise(); - // case3: LockManager.request() result - const { resolve: requestResolve, promise: requestPromise, reject: requestReject } = Promise.withResolvers(); - // #endregion // Request the lock using LockManager API - (options + const requestPromise = (options ? this.locks.request(this.name, options, callback) - : this.locks.request(this.name, callback)) - .then(requestResolve, requestReject); + : this.locks.request(this.name, callback)); let resolved = false; requestPromise.finally(() => resolved = true); // Wait for either successful acquisition or rejection @@ -56,7 +52,6 @@ export async function request(this: InnerLock, options?: LockOptions): Promise Date: Fri, 21 Nov 2025 09:50:11 +0900 Subject: [PATCH 05/15] toThrowError check string --- src/lock.test.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index 70aba06..958b8b8 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -167,10 +167,7 @@ describe("simple use", () => { await using _ = await request(); const lock2Wait = request({ signal }); controller.abort(); - await expect(lock2Wait).rejects.toThrow(expect.objectContaining({ - message: "This operation was aborted", - name: "AbortError", - })); + await expect(lock2Wait).rejects.toThrowError("This operation was aborted"); } // `signal` only affects the lock acquisition and does not affect the release. { @@ -226,10 +223,7 @@ describe("simple use", () => { ifAvailable: true, steal: true, }); - await expect(lockWait).rejects.toThrow(expect.objectContaining({ - message: "ifAvailable and steal are mutually exclusive", - name: "NotSupportedError" - })); + await expect(lockWait).rejects.toThrowError("ifAvailable and steal are mutually exclusive"); } }); } @@ -243,9 +237,7 @@ describe("hard error pattern", () => { value: undefined, }); try { - expect(() => lock(name)).toThrow(expect.objectContaining({ - message: "navigator.locks is not found. required options.locks argument." - })); + expect(() => lock(name)).toThrowError("navigator.locks is not found. required options.locks argument."); const { request, query } = lock(name, { locks }); { From 2cc9d0eea3c2cfdfa752e259f08040d2c78f1b70 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 09:59:01 +0900 Subject: [PATCH 06/15] change rejects test --- src/lock.test.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index 958b8b8..82b260e 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -167,7 +167,11 @@ describe("simple use", () => { await using _ = await request(); const lock2Wait = request({ signal }); controller.abort(); - await expect(lock2Wait).rejects.toThrowError("This operation was aborted"); + const reason = await lock2Wait.catch(v => v); + expect(reason).toEqual(expect.objectContaining({ + message: "This operation was aborted", + name: "AbortError", + })); } // `signal` only affects the lock acquisition and does not affect the release. { @@ -223,7 +227,11 @@ describe("simple use", () => { ifAvailable: true, steal: true, }); - await expect(lockWait).rejects.toThrowError("ifAvailable and steal are mutually exclusive"); + const reason = await lockWait.catch(v => v); + expect(reason).toEqual(expect.objectContaining({ + message: "ifAvailable and steal are mutually exclusive", + name: "NotSupportedError" + })); } }); } @@ -237,7 +245,9 @@ describe("hard error pattern", () => { value: undefined, }); try { - expect(() => lock(name)).toThrowError("navigator.locks is not found. required options.locks argument."); + expect(() => lock(name)).toThrowError(expect.objectContaining({ + message: "navigator.locks is not found. required options.locks argument." + })); const { request, query } = lock(name, { locks }); { From 74bdc57df10d9bec43976445f6cf868062dfb46d Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 10:05:21 +0900 Subject: [PATCH 07/15] =?UTF-8?q?=E5=85=83=E3=81=AB=E6=88=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lock.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index 82b260e..8194f1c 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -167,8 +167,7 @@ describe("simple use", () => { await using _ = await request(); const lock2Wait = request({ signal }); controller.abort(); - const reason = await lock2Wait.catch(v => v); - expect(reason).toEqual(expect.objectContaining({ + await expect(lock2Wait).rejects.toThrowError(expect.objectContaining({ message: "This operation was aborted", name: "AbortError", })); @@ -227,8 +226,7 @@ describe("simple use", () => { ifAvailable: true, steal: true, }); - const reason = await lockWait.catch(v => v); - expect(reason).toEqual(expect.objectContaining({ + await expect(lockWait).rejects.toThrowError(expect.objectContaining({ message: "ifAvailable and steal are mutually exclusive", name: "NotSupportedError" })); From bf48cc157480e4bd40417f465e1dee6318df8ba5 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 10:05:57 +0900 Subject: [PATCH 08/15] =?UTF-8?q?vitest=20/=20@vitest/coverage-v8=20?= =?UTF-8?q?=E3=82=92=20=E3=81=9D=E3=82=8C=E3=81=9E=E3=82=8C=204.0.12=20?= =?UTF-8?q?=E3=81=AB=E3=82=A2=E3=83=83=E3=83=97=E3=82=B0=E3=83=AC=E3=83=BC?= =?UTF-8?q?=E3=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 110 ++++++++++++++++++++++++---------------------- package.json | 4 +- 2 files changed, 59 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3872776..af95e72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@eslint/markdown": "^7.5.1", "@stylistic/eslint-plugin": "^5.5.0", "@types/fs-extra": "^11.0.4", - "@vitest/coverage-v8": "^4.0.8", + "@vitest/coverage-v8": "^4.0.12", "eslint": "^9.39.1", "fs-extra": "^11.3.2", "globals": "^16.5.0", @@ -22,7 +22,7 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.2", - "vitest": "^4.0.8" + "vitest": "^4.0.12" }, "engines": { "node": "^24.x" @@ -1547,14 +1547,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.8.tgz", - "integrity": "sha512-wQgmtW6FtPNn4lWUXi8ZSYLpOIb92j3QCujxX3sQ81NTfQ/ORnE0HtK7Kqf2+7J9jeveMGyGyc4NWc5qy3rC4A==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.12.tgz", + "integrity": "sha512-d+w9xAFJJz6jyJRU4BUU7MH409Ush7FWKNkxJU+jASKg6WX33YT0zc+YawMR1JesMWt9QRFQY/uAD3BTn23FaA==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.0.8", + "@vitest/utils": "4.0.12", "ast-v8-to-istanbul": "^0.3.8", "debug": "^4.4.3", "istanbul-lib-coverage": "^3.2.2", @@ -1569,8 +1569,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.0.8", - "vitest": "4.0.8" + "@vitest/browser": "4.0.12", + "vitest": "4.0.12" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1579,17 +1579,17 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.8.tgz", - "integrity": "sha512-Rv0eabdP/xjAHQGr8cjBm+NnLHNoL268lMDK85w2aAGLFoVKLd8QGnVon5lLtkXQCoYaNL0wg04EGnyKkkKhPA==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.12.tgz", + "integrity": "sha512-is+g0w8V3/ZhRNrRizrJNr8PFQKwYmctWlU4qg8zy5r9aIV5w8IxXLlfbbxJCwSpsVl2PXPTm2/zruqTqz3QSg==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", - "chai": "^6.2.0", + "@vitest/spy": "4.0.12", + "@vitest/utils": "4.0.12", + "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, "funding": { @@ -1597,13 +1597,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.8.tgz", - "integrity": "sha512-9FRM3MZCedXH3+pIh+ME5Up2NBBHDq0wqwhOKkN4VnvCiKbVxddqH9mSGPZeawjd12pCOGnl+lo/ZGHt0/dQSg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.12.tgz", + "integrity": "sha512-GsmA/tD5Ht3RUFoz41mZsMU1AXch3lhmgbTnoSPTdH231g7S3ytNN1aU0bZDSyxWs8WA7KDyMPD5L4q6V6vj9w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.8", + "@vitest/spy": "4.0.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1624,9 +1624,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.8.tgz", - "integrity": "sha512-qRrjdRkINi9DaZHAimV+8ia9Gq6LeGz2CgIEmMLz3sBDYV53EsnLZbJMR1q84z1HZCMsf7s0orDgZn7ScXsZKg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.12.tgz", + "integrity": "sha512-R7nMAcnienG17MvRN8TPMJiCG8rrZJblV9mhT7oMFdBXvS0x+QD6S1G4DxFusR2E0QIS73f7DqSR1n87rrmE+g==", "dev": true, "license": "MIT", "dependencies": { @@ -1637,13 +1637,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.8.tgz", - "integrity": "sha512-mdY8Sf1gsM8hKJUQfiPT3pn1n8RF4QBcJYFslgWh41JTfrK1cbqY8whpGCFzBl45LN028g0njLCYm0d7XxSaQQ==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.12.tgz", + "integrity": "sha512-hDlCIJWuwlcLumfukPsNfPDOJokTv79hnOlf11V+n7E14rHNPz0Sp/BO6h8sh9qw4/UjZiKyYpVxK2ZNi+3ceQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.8", + "@vitest/utils": "4.0.12", "pathe": "^2.0.3" }, "funding": { @@ -1651,13 +1651,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.8.tgz", - "integrity": "sha512-Nar9OTU03KGiubrIOFhcfHg8FYaRaNT+bh5VUlNz8stFhCZPNrJvmZkhsr1jtaYvuefYFwK2Hwrq026u4uPWCw==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.12.tgz", + "integrity": "sha512-2jz9zAuBDUSbnfyixnyOd1S2YDBrZO23rt1bicAb6MA/ya5rHdKFRikPIDpBj/Dwvh6cbImDmudegnDAkHvmRQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.8", + "@vitest/pretty-format": "4.0.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1666,9 +1666,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.8.tgz", - "integrity": "sha512-nvGVqUunyCgZH7kmo+Ord4WgZ7lN0sOULYXUOYuHr55dvg9YvMz3izfB189Pgp28w0vWFbEEfNc/c3VTrqrXeA==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.12.tgz", + "integrity": "sha512-GZjI9PPhiOYNX8Nsyqdw7JQB+u0BptL5fSnXiottAUBHlcMzgADV58A7SLTXXQwcN1yZ6gfd1DH+2bqjuUlCzw==", "dev": true, "license": "MIT", "funding": { @@ -1676,13 +1676,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.8.tgz", - "integrity": "sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.12.tgz", + "integrity": "sha512-DVS/TLkLdvGvj1avRy0LSmKfrcI9MNFvNGN6ECjTUHWJdlcgPDOXhjMis5Dh7rBH62nAmSXnkPbE+DZ5YD75Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.8", + "@vitest/pretty-format": "4.0.12", "tinyrainbow": "^3.0.3" }, "funding": { @@ -2580,9 +2580,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -4421,20 +4421,20 @@ } }, "node_modules/vitest": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.8.tgz", - "integrity": "sha512-urzu3NCEV0Qa0Y2PwvBtRgmNtxhj5t5ULw7cuKhIHh3OrkKTLlut0lnBOv9qe5OvbkMH2g38G7KPDCTpIytBVg==", + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.12.tgz", + "integrity": "sha512-pmW4GCKQ8t5Ko1jYjC3SqOr7TUKN7uHOHB/XGsAIb69eYu6d1ionGSsb5H9chmPf+WeXt0VE7jTXsB1IvWoNbw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@vitest/expect": "4.0.8", - "@vitest/mocker": "4.0.8", - "@vitest/pretty-format": "4.0.8", - "@vitest/runner": "4.0.8", - "@vitest/snapshot": "4.0.8", - "@vitest/spy": "4.0.8", - "@vitest/utils": "4.0.8", + "@vitest/expect": "4.0.12", + "@vitest/mocker": "4.0.12", + "@vitest/pretty-format": "4.0.12", + "@vitest/runner": "4.0.12", + "@vitest/snapshot": "4.0.12", + "@vitest/spy": "4.0.12", + "@vitest/utils": "4.0.12", "debug": "^4.4.3", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", @@ -4460,12 +4460,13 @@ }, "peerDependencies": { "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.8", - "@vitest/browser-preview": "4.0.8", - "@vitest/browser-webdriverio": "4.0.8", - "@vitest/ui": "4.0.8", + "@vitest/browser-playwright": "4.0.12", + "@vitest/browser-preview": "4.0.12", + "@vitest/browser-webdriverio": "4.0.12", + "@vitest/ui": "4.0.12", "happy-dom": "*", "jsdom": "*" }, @@ -4473,6 +4474,9 @@ "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/debug": { "optional": true }, diff --git a/package.json b/package.json index 9bed208..754857b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@eslint/markdown": "^7.5.1", "@stylistic/eslint-plugin": "^5.5.0", "@types/fs-extra": "^11.0.4", - "@vitest/coverage-v8": "^4.0.8", + "@vitest/coverage-v8": "^4.0.12", "eslint": "^9.39.1", "fs-extra": "^11.3.2", "globals": "^16.5.0", @@ -56,6 +56,6 @@ "typescript": "^5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.2", - "vitest": "^4.0.8" + "vitest": "^4.0.12" } } From 3e8d153e73a4f09e4ef21628698e38e729dcf3a8 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 11:01:38 +0900 Subject: [PATCH 09/15] =?UTF-8?q?request=20=E3=81=AE=E6=9B=B8=E3=81=8D?= =?UTF-8?q?=E6=96=B9=E3=81=AE=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/request.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/request.ts b/src/request.ts index 19f1c5f..63f45b0 100644 --- a/src/request.ts +++ b/src/request.ts @@ -44,7 +44,10 @@ export async function request(this: InnerLock, options?: LockOptions): Promise; + } // Wait for the callback to resolve with the actual lock const lock = await callbackPromise; @@ -52,6 +55,7 @@ export async function request(this: InnerLock, options?: LockOptions): Promise { if (resolved) return; releaseResolve(); - return requestPromise; + return await requestPromise; } } From b8ba708aea65ab2ec8ed2146a4e2e71330051e58 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 11:44:46 +0900 Subject: [PATCH 10/15] =?UTF-8?q?=E5=AF=BE=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lock.test.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index 8194f1c..e47137d 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -1,6 +1,11 @@ import { describe, test } from "vitest"; import { lock } from "./index.js"; -describe("simple use", () => { +describe("simple use", ({beforeAll, afterAll}) => { + let handle:Disposable|undefined; + beforeAll(() => handle = unhandleRejection()); + afterAll(() => { + using _ = handle; + }); { const name = "simple lock"; test.concurrent("simple lock", async ({ expect }) => { @@ -156,7 +161,7 @@ describe("simple use", () => { }); } }); - } + } { const name = "use signal"; test.concurrent("use signal", async ({ expect }) => { @@ -166,8 +171,8 @@ describe("simple use", () => { const signal = controller.signal; await using _ = await request(); const lock2Wait = request({ signal }); - controller.abort(); - await expect(lock2Wait).rejects.toThrowError(expect.objectContaining({ + const abortWait = timeout().then(() => controller.abort()); + await expect(Promise.all([lock2Wait, abortWait])).rejects.toThrowError(expect.objectContaining({ message: "This operation was aborted", name: "AbortError", })); @@ -273,3 +278,18 @@ describe("hard error pattern", () => { } }); }); + +function timeout(ms?:number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +function unhandleRejection(onUnhandledRejection?: (reason: unknown) => void) { + onUnhandledRejection ??= () => undefined; + process.on("unhandledRejection", onUnhandledRejection); + return { + [Symbol.dispose]: off, + }; + function off() { + process.off("unhandledRejection", onUnhandledRejection!); + } +} \ No newline at end of file From 6c5ac88ff101219e071cb946729c35a38f10f010 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 11:56:16 +0900 Subject: [PATCH 11/15] =?UTF-8?q?=E4=B8=80=E6=97=A6=E7=A2=BA=E8=AA=8D?= =?UTF-8?q?=E3=81=AE=E3=81=9F=E3=82=81=20unhandleRejection=20=E3=82=92?= =?UTF-8?q?=E5=87=BA=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lock.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index e47137d..efb6c2d 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -1,8 +1,8 @@ import { describe, test } from "vitest"; import { lock } from "./index.js"; -describe("simple use", ({beforeAll, afterAll}) => { - let handle:Disposable|undefined; - beforeAll(() => handle = unhandleRejection()); +describe("simple use", ({ beforeAll, afterAll }) => { + let handle: Disposable | undefined; + beforeAll(() => handle = unhandleRejection((reason) => console.error("unhandledRejection:", reason))); afterAll(() => { using _ = handle; }); @@ -161,7 +161,7 @@ describe("simple use", ({beforeAll, afterAll}) => { }); } }); - } + } { const name = "use signal"; test.concurrent("use signal", async ({ expect }) => { @@ -176,6 +176,7 @@ describe("simple use", ({beforeAll, afterAll}) => { message: "This operation was aborted", name: "AbortError", })); + await abortWait; } // `signal` only affects the lock acquisition and does not affect the release. { @@ -279,7 +280,7 @@ describe("hard error pattern", () => { }); }); -function timeout(ms?:number) { +function timeout(ms?: number) { return new Promise(resolve => setTimeout(resolve, ms)); } From f48201da29a930578f8447f71933d611892392c0 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 15:55:12 +0900 Subject: [PATCH 12/15] =?UTF-8?q?beforeAll=20/=20afterAll=20=E3=82=92=20be?= =?UTF-8?q?foreEach=20/=20afterEach=20=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lock.test.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index efb6c2d..7dc9624 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -1,11 +1,17 @@ import { describe, test } from "vitest"; import { lock } from "./index.js"; -describe("simple use", ({ beforeAll, afterAll }) => { - let handle: Disposable | undefined; - beforeAll(() => handle = unhandleRejection((reason) => console.error("unhandledRejection:", reason))); - afterAll(() => { +describe("simple use", ({ beforeEach, afterEach }) => { + const handles = new Map(); + beforeEach(({ task: { id } }) => { + handles.set(id, unhandleRejection(callback)); + }); + afterEach(({ task: { id } }) => { + const handle = handles.get(id); using _ = handle; }); + function callback(reason: unknown) { + console.error("unhandledRejection:", reason); + } { const name = "simple lock"; test.concurrent("simple lock", async ({ expect }) => { @@ -170,13 +176,13 @@ describe("simple use", ({ beforeAll, afterAll }) => { const controller = new AbortController(); const signal = controller.signal; await using _ = await request(); - const lock2Wait = request({ signal }); + const reasonWait = request({ signal }).catch(reason => reason); const abortWait = timeout().then(() => controller.abort()); - await expect(Promise.all([lock2Wait, abortWait])).rejects.toThrowError(expect.objectContaining({ + await Promise.allSettled([reasonWait, abortWait]); + await expect(reasonWait).resolves.toEqual(expect.objectContaining({ message: "This operation was aborted", name: "AbortError", })); - await abortWait; } // `signal` only affects the lock acquisition and does not affect the release. { From 5e92bfc85ac7386bff5a33bf57d2539e95d71397 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 16:14:36 +0900 Subject: [PATCH 13/15] timeout method editing... --- src/lock.test.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index 7dc9624..a24e006 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -286,8 +286,23 @@ describe("hard error pattern", () => { }); }); -function timeout(ms?: number) { - return new Promise(resolve => setTimeout(resolve, ms)); +async function timeout(ms?: number, options?: { signal?: AbortSignal }) { + const { resolve, promise } = Promise.withResolvers(); + const clear = setTimeout(resolve, ms); + if (options?.signal) { + options.signal.addEventListener("abort", abort, { once: true }); + } + try { + return await promise; + } finally { + if (options?.signal) { + options.signal.removeEventListener("abort", abort); + } + } + function abort() { + clearTimeout(clear); + resolve(); + } } function unhandleRejection(onUnhandledRejection?: (reason: unknown) => void) { From 539f128883cee3db3f316beee080df2113e4fc90 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 16:16:10 +0900 Subject: [PATCH 14/15] add utility comments --- src/lock.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lock.test.ts b/src/lock.test.ts index a24e006..ecbe8a2 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -286,6 +286,12 @@ describe("hard error pattern", () => { }); }); +/** + * Promise base setTimeout with abort signal + * @param ms + * @param options + * @returns + */ async function timeout(ms?: number, options?: { signal?: AbortSignal }) { const { resolve, promise } = Promise.withResolvers(); const clear = setTimeout(resolve, ms); @@ -305,6 +311,11 @@ async function timeout(ms?: number, options?: { signal?: AbortSignal }) { } } +/** + * Handle unhandledRejection event and return disposable to off the event. + * @param onUnhandledRejection + * @returns + */ function unhandleRejection(onUnhandledRejection?: (reason: unknown) => void) { onUnhandledRejection ??= () => undefined; process.on("unhandledRejection", onUnhandledRejection); From ea7818d0412cc6888bfb992ce533d64f3971e783 Mon Sep 17 00:00:00 2001 From: juner Date: Fri, 21 Nov 2025 17:17:54 +0900 Subject: [PATCH 15/15] complete test logging --- src/lock.test.ts | 86 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/src/lock.test.ts b/src/lock.test.ts index ecbe8a2..0edd151 100644 --- a/src/lock.test.ts +++ b/src/lock.test.ts @@ -1,17 +1,8 @@ -import { describe, test } from "vitest"; +import { describe, test, vi } from "vitest"; +import type { BeforeEachListener, AfterEachListener } from "@vitest/runner"; import { lock } from "./index.js"; -describe("simple use", ({ beforeEach, afterEach }) => { - const handles = new Map(); - beforeEach(({ task: { id } }) => { - handles.set(id, unhandleRejection(callback)); - }); - afterEach(({ task: { id } }) => { - const handle = handles.get(id); - using _ = handle; - }); - function callback(reason: unknown) { - console.error("unhandledRejection:", reason); - } +describe("simple use", (args) => { + useUnhandleRejectionLogging(args); { const name = "simple lock"; test.concurrent("simple lock", async ({ expect }) => { @@ -176,8 +167,13 @@ describe("simple use", ({ beforeEach, afterEach }) => { const controller = new AbortController(); const signal = controller.signal; await using _ = await request(); - const reasonWait = request({ signal }).catch(reason => reason); - const abortWait = timeout().then(() => controller.abort()); + let reasonWait: Promise; + let abortWait: Promise; + { + await using _ = fakeTimeer(); + reasonWait = request({ signal }).catch(reason => reason); + abortWait = timeout().then(() => controller.abort()); + } await Promise.allSettled([reasonWait, abortWait]); await expect(reasonWait).resolves.toEqual(expect.objectContaining({ message: "This operation was aborted", @@ -246,7 +242,8 @@ describe("simple use", ({ beforeEach, afterEach }) => { }); } }); -describe("hard error pattern", () => { +describe("hard error pattern", (args) => { + useUnhandleRejectionLogging(args); const name = "not found navigator.locks"; test("not found navigator.locks", async ({ expect }) => { const locks = (globalThis.navigator as unknown as { locks: LockManager }).locks; @@ -316,7 +313,7 @@ async function timeout(ms?: number, options?: { signal?: AbortSignal }) { * @param onUnhandledRejection * @returns */ -function unhandleRejection(onUnhandledRejection?: (reason: unknown) => void) { +function unhandleRejection(onUnhandledRejection?: (reason: unknown, promise: Promise) => void) { onUnhandledRejection ??= () => undefined; process.on("unhandledRejection", onUnhandledRejection); return { @@ -325,4 +322,59 @@ function unhandleRejection(onUnhandledRejection?: (reason: unknown) => void) { function off() { process.off("unhandledRejection", onUnhandledRejection!); } +} + +/** + * use fake timer and return async disposable to restore real timer. + * @returns + */ +function fakeTimeer() { + vi.useFakeTimers(); + return { + advanceTimersByTimeAsync, + runAllTimersAsync, + [Symbol.asyncDispose]: runAllTimersAsync, + }; + async function advanceTimersByTimeAsync(time: number) { + await vi.advanceTimersByTimeAsync(time); + } + async function runAllTimersAsync() { + await vi.runAllTimersAsync(); + } +} + +/** + * describe unhandledRejection logging utility + * @param param0 + */ +function useUnhandleRejectionLogging({ beforeEach, afterEach } + : { + beforeEach: (fn: BeforeEachListener, timeout?: number) => void, + afterEach: (fn: AfterEachListener, timeout?: number) => void + }) { + type HandlerInstance = { + [Symbol.dispose]: () => void, + reasones?: unknown[] | undefined, + } + const handles = new Map(); + beforeEach(({ task: { id } }) => { + const instance = {} as HandlerInstance; + const disposable = unhandleRejection(callback.bind(instance)); + (instance as { [Symbol.dispose]?: () => void })[Symbol.dispose] = disposable[Symbol.dispose].bind(disposable); + + handles.set(id, instance); + }); + afterEach(({ task: { id } }) => { + const instance = handles.get(id); + { + using _ = instance; + } + if (!(instance?.reasones)) return; + for (const reason of instance.reasones) { + console.error(reason); + } + }); + function callback(this: HandlerInstance, reason: unknown) { + (this.reasones ??= []).push(reason); + } } \ No newline at end of file