current commit
251115.4 · test: ensure assertions are reached on more tests
'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');
    }));
  });
});