Offload functions to worker threads with shared memory primitives for Node.js.
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Mutex with async lock and disposable MutexGuard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Devin Ivy d65d389c 01304a7a

+110 -1
+2
src/index.ts
··· 12 12 AtomicUint32, 13 13 AtomicInt64, 14 14 AtomicUint64, 15 + Mutex, 16 + MutexGuard, 15 17 } from './sync/index.ts';
+1
src/sync/index.ts
··· 7 7 export { AtomicUint32 } from './atomic-uint32.ts'; 8 8 export { AtomicInt64 } from './atomic-int64.ts'; 9 9 export { AtomicUint64 } from './atomic-uint64.ts'; 10 + export { Mutex, MutexGuard } from './mutex.ts';
+44
src/sync/mutex.ts
··· 1 + const UNLOCKED = 0; 2 + const LOCKED = 1; 3 + 4 + export class MutexGuard { 5 + private readonly mutex: Mutex; 6 + 7 + constructor(mutex: Mutex) { 8 + this.mutex = mutex; 9 + } 10 + 11 + [Symbol.dispose](): void { 12 + this.mutex.unlock(); 13 + } 14 + } 15 + 16 + export class Mutex { 17 + static readonly byteSize = 4; 18 + private readonly view: Int32Array; 19 + 20 + constructor(buffer?: SharedArrayBuffer, byteOffset?: number) { 21 + const buf = buffer ?? new SharedArrayBuffer(4); 22 + const offset = byteOffset ?? 0; 23 + this.view = new Int32Array(buf, offset, 1); 24 + } 25 + 26 + async lock(): Promise<MutexGuard> { 27 + while (true) { 28 + // Try to acquire: CAS from UNLOCKED to LOCKED 29 + if (Atomics.compareExchange(this.view, 0, UNLOCKED, LOCKED) === UNLOCKED) { 30 + return new MutexGuard(this); 31 + } 32 + // Wait until notified that the lock might be free 33 + const result = Atomics.waitAsync(this.view, 0, LOCKED); 34 + if (result.async) { 35 + await result.value; 36 + } 37 + } 38 + } 39 + 40 + unlock(): void { 41 + Atomics.store(this.view, 0, UNLOCKED); 42 + Atomics.notify(this.view, 0, 1); 43 + } 44 + }
+62
test/sync/mutex.test.ts
··· 1 + import { describe, it } from 'node:test'; 2 + import assert from 'node:assert/strict'; 3 + import { Mutex } from 'moroutine'; 4 + 5 + describe('Mutex', () => { 6 + it('lock and unlock', async () => { 7 + const mutex = new Mutex(); 8 + const guard = await mutex.lock(); 9 + mutex.unlock(); 10 + }); 11 + 12 + it('lock returns a MutexGuard', async () => { 13 + const mutex = new Mutex(); 14 + const guard = await mutex.lock(); 15 + assert.equal(typeof guard[Symbol.dispose], 'function'); 16 + mutex.unlock(); 17 + }); 18 + 19 + it('MutexGuard dispose calls unlock', async () => { 20 + const mutex = new Mutex(); 21 + const guard = await mutex.lock(); 22 + guard[Symbol.dispose](); 23 + // Should be able to lock again immediately 24 + const guard2 = await mutex.lock(); 25 + mutex.unlock(); 26 + }); 27 + 28 + it('serializes concurrent access', async () => { 29 + const mutex = new Mutex(); 30 + const order: number[] = []; 31 + 32 + const task = async (id: number) => { 33 + const guard = await mutex.lock(); 34 + order.push(id); 35 + await new Promise((r) => setTimeout(r, 10)); 36 + order.push(id); 37 + mutex.unlock(); 38 + }; 39 + 40 + await Promise.all([task(1), task(2)]); 41 + // Each task should push its id twice in a row (not interleaved) 42 + assert.ok( 43 + (order[0] === order[1] && order[2] === order[3]), 44 + `Expected non-interleaved order, got: ${order}`, 45 + ); 46 + }); 47 + 48 + it('self-allocates', () => { 49 + const mutex = new Mutex(); 50 + assert.ok(mutex); 51 + }); 52 + 53 + it('accepts an external buffer and byteOffset', () => { 54 + const buffer = new SharedArrayBuffer(16); 55 + const mutex = new Mutex(buffer, 4); 56 + assert.ok(mutex); 57 + }); 58 + 59 + it('exposes static byteSize of 4', () => { 60 + assert.equal(Mutex.byteSize, 4); 61 + }); 62 + });
+1 -1
tsconfig.json
··· 1 1 { 2 2 "compilerOptions": { 3 3 "target": "es2022", 4 - "lib": ["es2022", "esnext.disposable"], 4 + "lib": ["es2022", "esnext.disposable", "es2024.sharedmemory"], 5 5 "types": ["node"], 6 6 "module": "nodenext", 7 7 "moduleResolution": "nodenext",