| 'use strict'; |
| // Flags: --expose-gc |
| |
| const common = require('../common'); |
| const { describe, it } = require('node:test'); |
| const assert = require('node:assert'); |
| const { Worker } = require('node:worker_threads'); |
| const { AsyncLocalStorage } = require('node:async_hooks'); |
| |
| describe('Web Locks with worker threads', () => { |
| it('should handle exclusive locks', async () => { |
| const worker = new Worker(` |
| const { parentPort } = require('worker_threads'); |
| const assert = require('node:assert'); |
| |
| navigator.locks.request('exclusive-test', async (lock) => { |
| assert.strictEqual(lock.mode, 'exclusive'); |
| parentPort.postMessage({ success: true }); |
| }).catch(err => parentPort.postMessage({ error: err.message })); |
| `, { eval: true }); |
| |
| const result = await new Promise((resolve) => { |
| worker.once('message', resolve); |
| }); |
| |
| assert.strictEqual(result.success, true); |
| await worker.terminate(); |
| |
| await navigator.locks.request('exclusive-test', common.mustCall(async (lock) => { |
| assert.strictEqual(lock.mode, 'exclusive'); |
| assert.strictEqual(lock.name, 'exclusive-test'); |
| })); |
| }); |
| |
| it('should handle shared locks', async () => { |
| const worker = new Worker(` |
| const { parentPort } = require('worker_threads'); |
| const assert = require('node:assert'); |
| |
| navigator.locks.request('shared-test', { mode: 'shared' }, async (lock) => { |
| assert.strictEqual(lock.mode, 'shared'); |
| parentPort.postMessage({ success: true }); |
| }).catch(err => parentPort.postMessage({ error: err.message })); |
| `, { eval: true }); |
| |
| const result = await new Promise((resolve) => { |
| worker.once('message', resolve); |
| }); |
| assert.strictEqual(result.success, true); |
| |
| await navigator.locks.request('shared-test', { mode: 'shared' }, common.mustCall(async (lock1) => { |
| await navigator.locks.request('shared-test', { mode: 'shared' }, common.mustCall(async (lock2) => { |
| assert.strictEqual(lock1.mode, 'shared'); |
| assert.strictEqual(lock2.mode, 'shared'); |
| })); |
| })); |
| |
| await worker.terminate(); |
| }); |
| |
| it('should handle steal option - no existing lock', async () => { |
| await navigator.locks.request('steal-simple', { steal: true }, common.mustCall(async (lock) => { |
| assert.strictEqual(lock.name, 'steal-simple'); |
| assert.strictEqual(lock.mode, 'exclusive'); |
| })); |
| }); |
| |
| it('should handle steal option - existing lock', async () => { |
| let originalLockRejected = false; |
| |
| const originalLockPromise = navigator.locks.request('steal-target', common.mustCall(async (lock) => { |
| assert.strictEqual(lock.name, 'steal-target'); |
| return 'original-completed'; |
| })).catch(common.mustCall((err) => { |
| originalLockRejected = true; |
| assert.strictEqual(err.name, 'AbortError'); |
| assert.strictEqual(err.message, 'The operation was aborted'); |
| return 'original-rejected'; |
| })); |
| |
| const stealResult = await navigator.locks.request( |
| 'steal-target', { steal: true }, |
| common.mustCall(async (stolenLock) => { |
| assert.strictEqual(stolenLock.name, 'steal-target'); |
| assert.strictEqual(stolenLock.mode, 'exclusive'); |
| return 'steal-completed'; |
| })); |
| |
| assert.strictEqual(stealResult, 'steal-completed'); |
| |
| const originalResult = await originalLockPromise; |
| assert.strictEqual(originalLockRejected, true); |
| assert.strictEqual(originalResult, 'original-rejected'); |
| }); |
| |
| it('should handle ifAvailable option', async () => { |
| await navigator.locks.request('ifavailable-test', common.mustCall(async () => { |
| const result = await navigator.locks.request('ifavailable-test', { ifAvailable: true }, (lock) => { |
| return lock; // should be null |
| }); |
| |
| assert.strictEqual(result, null); |
| |
| const availableResult = await navigator.locks.request('ifavailable-different-resource', |
| { ifAvailable: true }, (lock) => { |
| return lock !== null; |
| }); |
| |
| assert.strictEqual(availableResult, true); |
| })); |
| }); |
| |
| it('should handle AbortSignal', async () => { |
| const worker = new Worker(` |
| const { parentPort } = require('worker_threads'); |
| const assert = require('node:assert'); |
| |
| const controller = new AbortController(); |
| |
| navigator.locks.request('signal-after-grant', { signal: controller.signal }, async (lock) => { |
| parentPort.postMessage({ acquired: true }); |
| |
| setTimeout(() => controller.abort(), 50); |
| |
| await new Promise(resolve => setTimeout(resolve, 100)); |
| return 'completed successfully'; |
| }).then(result => { |
| parentPort.postMessage({ resolved: result }); |
| }).catch(err => { |
| parentPort.postMessage({ rejected: err.name }); |
| }); |
| `, { eval: true }); |
| |
| const acquired = await new Promise((resolve) => { |
| worker.once('message', resolve); |
| }); |
| assert.strictEqual(acquired.acquired, true); |
| |
| const result = await new Promise((resolve) => { |
| worker.once('message', resolve); |
| }); |
| assert.strictEqual(result.resolved, 'completed successfully'); |
| |
| await worker.terminate(); |
| }); |
| |
| it('should handle many concurrent locks without hanging', async () => { |
| if (global.gc) global.gc(); |
| const before = process.memoryUsage().rss; |
| |
| let callbackCount = 0; |
| let resolveCount = 0; |
| |
| const promises = []; |
| for (let i = 0; i < 100; i++) { |
| const promise = navigator.locks.request(`test-${i}`, async (lock) => { |
| callbackCount++; |
| const innerPromise = navigator.locks.request(`inner-${i}`, async () => { |
| resolveCount++; |
| return 'done'; |
| }); |
| await innerPromise; |
| return `completed-${lock.name}`; |
| }); |
| |
| promises.push(promise); |
| } |
| |
| await Promise.all(promises); |
| |
| if (global.gc) global.gc(); |
| |
| const after = process.memoryUsage().rss; |
| |
| assert.strictEqual(callbackCount, 100); |
| assert.strictEqual(resolveCount, 100); |
| assert(after < before * 3); |
| }); |
| |
| it('should preserve AsyncLocalStorage context across lock callback', async () => { |
| const als = new AsyncLocalStorage(); |
| const store = { id: 'lock' }; |
| |
| als.run(store, common.mustCall(() => { |
| navigator.locks |
| .request('als-context-test', common.mustCall(async () => { |
| assert.strictEqual(als.getStore(), store); |
| })) |
| .then(common.mustCall()); |
| })); |
| }); |
| |
| it('should clean up when worker is terminated with a pending lock', async () => { |
| // Acquire the lock in the main thread so that the worker's request will be pending |
| await navigator.locks.request('cleanup-test', common.mustCall(async () => { |
| // Launch a worker that requests the same lock |
| const worker = new Worker(` |
| const { parentPort } = require('worker_threads'); |
| |
| parentPort.postMessage({ requesting: true }); |
| |
| navigator.locks.request('cleanup-test', async () => { |
| return 'should-not-complete'; |
| }).catch(err => { |
| parentPort.postMessage({ error: err.name }); |
| }); |
| `, { eval: true }); |
| |
| const requestSignal = await new Promise((resolve) => { |
| worker.once('message', resolve); |
| }); |
| |
| assert.strictEqual(requestSignal.requesting, true); |
| |
| await worker.terminate(); |
| |
| })); |
| |
| // Request the lock again to make sure cleanup succeeded |
| await navigator.locks.request('cleanup-test', common.mustCall(async (lock) => { |
| assert.strictEqual(lock.name, 'cleanup-test'); |
| })); |
| }); |
| }); |