@twin.org/core 0.0.3 → 0.0.4-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,12 +33,12 @@ export class Mutex {
33
33
  */
34
34
  static _LOCKS_KEY = "mutexLocks";
35
35
  /**
36
- * Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.
37
- * The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.
38
- *
39
- * WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the
40
- * duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a
41
- * buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.
36
+ * Acquires a lock for the given key without blocking the event loop. If the lock is already
37
+ * held, it suspends the current async task until the lock is released or the timeout is reached.
38
+ * Use this in async single-threaded contexts (e.g. the main thread or a Fastify route handler)
39
+ * where calling the synchronous lock() would freeze the event loop and deadlock.
40
+ * The lock is not re-entrant: if the same context holds the key and calls lockAsync() again on
41
+ * the same key, it will suspend until the timeout elapses.
42
42
  * @param key The key to lock on.
43
43
  * @param options Lock options.
44
44
  * @param options.timeoutMs The maximum time to wait for the lock in milliseconds, default is 5000.
@@ -46,11 +46,13 @@ export class Mutex {
46
46
  * @returns True if the lock was acquired, false if it timed out and throwOnTimeout is false.
47
47
  * @throws GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.
48
48
  */
49
- static lock(key, options) {
49
+ static async lock(key, options) {
50
50
  Guards.stringValue(Mutex.CLASS_NAME, "key", key);
51
51
  const timeoutMs = options?.timeoutMs ?? 5000;
52
52
  const throwOnTimeout = options?.throwOnTimeout ?? false;
53
53
  const deadline = Date.now() + timeoutMs;
54
+ // getOrFetchLock may block once per key on worker threads to negotiate the
55
+ // shared buffer with the main thread; that one-time fetch is acceptable here.
54
56
  const lock = Mutex.getOrFetchLock(key, deadline);
55
57
  for (;;) {
56
58
  // Atomically swap 0 → 1; if the previous value was 0 we acquired the lock.
@@ -58,7 +60,7 @@ export class Mutex {
58
60
  if (previous === 0) {
59
61
  return true;
60
62
  }
61
- // Otherwise, the lock is held by someone else. Check if we've already timed out before blocking.
63
+ // Otherwise, the lock is held by someone else. Check if we've already timed out.
62
64
  const remaining = deadline - Date.now();
63
65
  if (remaining <= 0) {
64
66
  if (throwOnTimeout) {
@@ -66,10 +68,11 @@ export class Mutex {
66
68
  }
67
69
  return false;
68
70
  }
69
- // Block until the value changes away from 1 (i.e. the holder calls unlock)
70
- // or until the remaining timeout elapses.
71
- const waitResult = Atomics.wait(lock, 0, 1, remaining);
72
- if (waitResult === "timed-out") {
71
+ // Suspend without blocking the event loop so the lock holder's async
72
+ // continuations can run and eventually call unlock().
73
+ const waitResult = Atomics.waitAsync(lock, 0, 1, remaining);
74
+ const outcome = waitResult.async ? await waitResult.value : waitResult.value;
75
+ if (outcome === "timed-out") {
73
76
  if (throwOnTimeout) {
74
77
  throw new GeneralError(Mutex.CLASS_NAME, "lockTimeout", { key, timeoutMs });
75
78
  }
@@ -1 +1 @@
1
- {"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../../src/utils/mutex.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tMessageChannel,\n\ttype MessagePort,\n\tisMainThread,\n\tparentPort,\n\treceiveMessageOnPort\n} from \"node:worker_threads\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { Guards } from \"./guards.js\";\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\nimport { GeneralError } from \"../errors/generalError.js\";\nimport type { IMutexWorkerMessage } from \"../models/IMutexWorkerMessage.js\";\nimport { MutexMessageTypes } from \"../models/mutexMessageTypes.js\";\n\n/**\n * A cross-thread mutex built on Atomics and SharedArrayBuffer.\n *\n * When isMainThread is true (main thread or fork-mode child process) the class acts as\n * the authoritative registry: it creates a SharedArrayBuffer-backed Int32Array for each\n * key on first use and never discards it, because worker threads may hold references to\n * the same underlying memory.\n *\n * When isMainThread is false (a true worker thread) the class synchronously negotiates\n * the shared buffer with the main thread on first use of each key, then caches it locally.\n * The main thread must call Mutex.handleWorkerMessage(msg) from its worker message handler\n * before that worker first calls Mutex.lock().\n *\n * The lock is not re-entrant: a thread that already holds a key and calls lock() again on\n * the same key will block until the timeout elapses.\n */\nexport class Mutex {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<Mutex>();\n\n\t/**\n\t * SharedStore key for the per-thread sparse map from lock key strings to Int32Arrays.\n\t * @internal\n\t */\n\tprivate static readonly _LOCKS_KEY = \"mutexLocks\";\n\n\t/**\n\t * Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.\n\t * The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.\n\t *\n\t * WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the\n\t * duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a\n\t * buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.\n\t * @param key The key to lock on.\n\t * @param options Lock options.\n\t * @param options.timeoutMs The maximum time to wait for the lock in milliseconds, default is 5000.\n\t * @param options.throwOnTimeout Whether to throw an error if the lock could not be acquired within the timeout, default is false.\n\t * @returns True if the lock was acquired, false if it timed out and throwOnTimeout is false.\n\t * @throws GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.\n\t */\n\tpublic static lock(\n\t\tkey: string,\n\t\toptions?: { timeoutMs?: number; throwOnTimeout?: boolean }\n\t): boolean {\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(key), key);\n\n\t\tconst timeoutMs = options?.timeoutMs ?? 5000;\n\t\tconst throwOnTimeout = options?.throwOnTimeout ?? false;\n\t\tconst deadline = Date.now() + timeoutMs;\n\n\t\tconst lock = Mutex.getOrFetchLock(key, deadline);\n\n\t\tfor (;;) {\n\t\t\t// Atomically swap 0 → 1; if the previous value was 0 we acquired the lock.\n\t\t\tconst previous = Atomics.compareExchange(lock, 0, 0, 1);\n\t\t\tif (previous === 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Otherwise, the lock is held by someone else. Check if we've already timed out before blocking.\n\t\t\tconst remaining = deadline - Date.now();\n\t\t\tif (remaining <= 0) {\n\t\t\t\tif (throwOnTimeout) {\n\t\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Block until the value changes away from 1 (i.e. the holder calls unlock)\n\t\t\t// or until the remaining timeout elapses.\n\t\t\tconst waitResult = Atomics.wait(lock, 0, 1, remaining);\n\t\t\tif (waitResult === \"timed-out\") {\n\t\t\t\tif (throwOnTimeout) {\n\t\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Releases the lock for the given key.\n\t * @param key The key to unlock.\n\t * @throws GeneralError if the key is invalid or the lock is not currently held.\n\t */\n\tpublic static unlock(key: string): void {\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(key), key);\n\n\t\tconst locks = Mutex.getLocks();\n\t\tconst lock = locks[key];\n\t\tif (Is.empty(lock)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockNotFound\", { key });\n\t\t}\n\n\t\tconst previous = Atomics.compareExchange(lock, 0, 1, 0);\n\t\tif (previous !== 1) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockAlreadyReleased\", { key });\n\t\t}\n\n\t\tAtomics.notify(lock, 0, 1);\n\t}\n\n\t/**\n\t * Inspect a message received from a worker and, if it is a Mutex buffer-fetch request,\n\t * respond to it synchronously. Call from the main thread's worker message handler.\n\t * @param msg The raw message received from the worker.\n\t * @returns True if the message was a Mutex protocol message and was handled, false otherwise.\n\t */\n\tpublic static handleWorkerMessage(msg: unknown): boolean {\n\t\tif (!Is.object<IMutexWorkerMessage>(msg) || msg.type !== MutexMessageTypes.GetBuffer) {\n\t\t\treturn false;\n\t\t}\n\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(msg.key), msg.key);\n\t\tGuards.object<SharedArrayBuffer>(Mutex.CLASS_NAME, nameof(msg.signal), msg.signal);\n\t\tGuards.object<MessagePort>(Mutex.CLASS_NAME, nameof(msg.port), msg.port);\n\n\t\tconst locks = Mutex.getLocks();\n\t\tlocks[msg.key] ??= new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\t\t// Send the buffer to the worker before notifying so that it is guaranteed\n\t\t// to be in port1's receive queue when Atomics.wait returns on the worker side.\n\t\tmsg.port.postMessage({ buffer: locks[msg.key].buffer });\n\t\tAtomics.notify(new Int32Array(msg.signal), 0, 1);\n\t\tmsg.port.close();\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Returns the Int32Array for the given key, fetching it from the main thread if this\n\t * is a worker thread and the key is not yet in the local cache.\n\t * @param key The lock key.\n\t * @returns The Int32Array backed by a SharedArrayBuffer for this key.\n\t * @internal\n\t */\n\tprivate static getOrFetchLock(key: string, deadline: number): Int32Array {\n\t\tconst locks = Mutex.getLocks();\n\t\tif (!Is.empty(locks[key])) {\n\t\t\treturn locks[key];\n\t\t}\n\n\t\tif (isMainThread) {\n\t\t\t// Main thread or fork-mode process: own the registry entry.\n\t\t\tlocks[key] = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\t\t\treturn locks[key];\n\t\t}\n\n\t\t// Worker thread: synchronously request the SharedArrayBuffer from the main thread.\n\t\t// Mutex.handleWorkerMessage(msg) must be called on the main thread's worker message handler.\n\t\tif (Is.empty(parentPort)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t}\n\n\t\tconst { port1, port2 } = new MessageChannel();\n\t\tconst signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\n\t\tconst msg: IMutexWorkerMessage = {\n\t\t\ttype: MutexMessageTypes.GetBuffer,\n\t\t\tkey,\n\t\t\tsignal: signal.buffer,\n\t\t\tport: port2\n\t\t};\n\n\t\tparentPort.postMessage(msg, [port2]);\n\n\t\ttry {\n\t\t\t// Block until the main thread posts the response and fires Atomics.notify.\n\t\t\t// The response is guaranteed to be in port1's queue at this point because\n\t\t\t// port.postMessage executes before Atomics.notify on the main thread.\n\t\t\t// Use the lock deadline so the buffer fetch is bounded by the same timeout.\n\t\t\tconst waitResult = Atomics.wait(signal, 0, 0, Math.max(0, deadline - Date.now()));\n\t\t\tif (waitResult === \"timed-out\") {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t\t}\n\n\t\t\tconst response = receiveMessageOnPort(port1) as {\n\t\t\t\tmessage: { buffer: SharedArrayBuffer };\n\t\t\t} | null;\n\n\t\t\tif (Is.empty(response)) {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t\t}\n\n\t\t\tlocks[key] = new Int32Array(response.message.buffer);\n\t\t\treturn locks[key];\n\t\t} finally {\n\t\t\tport1.close();\n\t\t}\n\t}\n\n\t/**\n\t * Get the shared locks map, creating it if it does not exist.\n\t * @returns The shared locks map.\n\t * @internal\n\t */\n\tprivate static getLocks(): { [key: string]: Int32Array } {\n\t\tlet locks = SharedStore.get<{ [key: string]: Int32Array }>(Mutex._LOCKS_KEY);\n\t\tif (Is.undefined(locks)) {\n\t\t\tlocks = {};\n\t\t\tSharedStore.set(Mutex._LOCKS_KEY, locks);\n\t\t}\n\t\treturn locks;\n\t}\n}\n"]}
1
+ {"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../../src/utils/mutex.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tMessageChannel,\n\ttype MessagePort,\n\tisMainThread,\n\tparentPort,\n\treceiveMessageOnPort\n} from \"node:worker_threads\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { Guards } from \"./guards.js\";\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\nimport { GeneralError } from \"../errors/generalError.js\";\nimport type { IMutexWorkerMessage } from \"../models/IMutexWorkerMessage.js\";\nimport { MutexMessageTypes } from \"../models/mutexMessageTypes.js\";\n\n/**\n * A cross-thread mutex built on Atomics and SharedArrayBuffer.\n *\n * When isMainThread is true (main thread or fork-mode child process) the class acts as\n * the authoritative registry: it creates a SharedArrayBuffer-backed Int32Array for each\n * key on first use and never discards it, because worker threads may hold references to\n * the same underlying memory.\n *\n * When isMainThread is false (a true worker thread) the class synchronously negotiates\n * the shared buffer with the main thread on first use of each key, then caches it locally.\n * The main thread must call Mutex.handleWorkerMessage(msg) from its worker message handler\n * before that worker first calls Mutex.lock().\n *\n * The lock is not re-entrant: a thread that already holds a key and calls lock() again on\n * the same key will block until the timeout elapses.\n */\nexport class Mutex {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<Mutex>();\n\n\t/**\n\t * SharedStore key for the per-thread sparse map from lock key strings to Int32Arrays.\n\t * @internal\n\t */\n\tprivate static readonly _LOCKS_KEY = \"mutexLocks\";\n\n\t/**\n\t * Acquires a lock for the given key without blocking the event loop. If the lock is already\n\t * held, it suspends the current async task until the lock is released or the timeout is reached.\n\t * Use this in async single-threaded contexts (e.g. the main thread or a Fastify route handler)\n\t * where calling the synchronous lock() would freeze the event loop and deadlock.\n\t * The lock is not re-entrant: if the same context holds the key and calls lockAsync() again on\n\t * the same key, it will suspend until the timeout elapses.\n\t * @param key The key to lock on.\n\t * @param options Lock options.\n\t * @param options.timeoutMs The maximum time to wait for the lock in milliseconds, default is 5000.\n\t * @param options.throwOnTimeout Whether to throw an error if the lock could not be acquired within the timeout, default is false.\n\t * @returns True if the lock was acquired, false if it timed out and throwOnTimeout is false.\n\t * @throws GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.\n\t */\n\tpublic static async lock(\n\t\tkey: string,\n\t\toptions?: { timeoutMs?: number; throwOnTimeout?: boolean }\n\t): Promise<boolean> {\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(key), key);\n\n\t\tconst timeoutMs = options?.timeoutMs ?? 5000;\n\t\tconst throwOnTimeout = options?.throwOnTimeout ?? false;\n\t\tconst deadline = Date.now() + timeoutMs;\n\n\t\t// getOrFetchLock may block once per key on worker threads to negotiate the\n\t\t// shared buffer with the main thread; that one-time fetch is acceptable here.\n\t\tconst lock = Mutex.getOrFetchLock(key, deadline);\n\n\t\tfor (;;) {\n\t\t\t// Atomically swap 0 → 1; if the previous value was 0 we acquired the lock.\n\t\t\tconst previous = Atomics.compareExchange(lock, 0, 0, 1);\n\t\t\tif (previous === 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Otherwise, the lock is held by someone else. Check if we've already timed out.\n\t\t\tconst remaining = deadline - Date.now();\n\t\t\tif (remaining <= 0) {\n\t\t\t\tif (throwOnTimeout) {\n\t\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Suspend without blocking the event loop so the lock holder's async\n\t\t\t// continuations can run and eventually call unlock().\n\t\t\tconst waitResult = Atomics.waitAsync(lock, 0, 1, remaining);\n\t\t\tconst outcome = waitResult.async ? await waitResult.value : waitResult.value;\n\t\t\tif (outcome === \"timed-out\") {\n\t\t\t\tif (throwOnTimeout) {\n\t\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Releases the lock for the given key.\n\t * @param key The key to unlock.\n\t * @throws GeneralError if the key is invalid or the lock is not currently held.\n\t */\n\tpublic static unlock(key: string): void {\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(key), key);\n\n\t\tconst locks = Mutex.getLocks();\n\t\tconst lock = locks[key];\n\t\tif (Is.empty(lock)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockNotFound\", { key });\n\t\t}\n\n\t\tconst previous = Atomics.compareExchange(lock, 0, 1, 0);\n\t\tif (previous !== 1) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockAlreadyReleased\", { key });\n\t\t}\n\n\t\tAtomics.notify(lock, 0, 1);\n\t}\n\n\t/**\n\t * Inspect a message received from a worker and, if it is a Mutex buffer-fetch request,\n\t * respond to it synchronously. Call from the main thread's worker message handler.\n\t * @param msg The raw message received from the worker.\n\t * @returns True if the message was a Mutex protocol message and was handled, false otherwise.\n\t */\n\tpublic static handleWorkerMessage(msg: unknown): boolean {\n\t\tif (!Is.object<IMutexWorkerMessage>(msg) || msg.type !== MutexMessageTypes.GetBuffer) {\n\t\t\treturn false;\n\t\t}\n\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(msg.key), msg.key);\n\t\tGuards.object<SharedArrayBuffer>(Mutex.CLASS_NAME, nameof(msg.signal), msg.signal);\n\t\tGuards.object<MessagePort>(Mutex.CLASS_NAME, nameof(msg.port), msg.port);\n\n\t\tconst locks = Mutex.getLocks();\n\t\tlocks[msg.key] ??= new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\t\t// Send the buffer to the worker before notifying so that it is guaranteed\n\t\t// to be in port1's receive queue when Atomics.wait returns on the worker side.\n\t\tmsg.port.postMessage({ buffer: locks[msg.key].buffer });\n\t\tAtomics.notify(new Int32Array(msg.signal), 0, 1);\n\t\tmsg.port.close();\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Returns the Int32Array for the given key, fetching it from the main thread if this\n\t * is a worker thread and the key is not yet in the local cache.\n\t * @param key The lock key.\n\t * @returns The Int32Array backed by a SharedArrayBuffer for this key.\n\t * @internal\n\t */\n\tprivate static getOrFetchLock(key: string, deadline: number): Int32Array {\n\t\tconst locks = Mutex.getLocks();\n\t\tif (!Is.empty(locks[key])) {\n\t\t\treturn locks[key];\n\t\t}\n\n\t\tif (isMainThread) {\n\t\t\t// Main thread or fork-mode process: own the registry entry.\n\t\t\tlocks[key] = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\t\t\treturn locks[key];\n\t\t}\n\n\t\t// Worker thread: synchronously request the SharedArrayBuffer from the main thread.\n\t\t// Mutex.handleWorkerMessage(msg) must be called on the main thread's worker message handler.\n\t\tif (Is.empty(parentPort)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t}\n\n\t\tconst { port1, port2 } = new MessageChannel();\n\t\tconst signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\n\t\tconst msg: IMutexWorkerMessage = {\n\t\t\ttype: MutexMessageTypes.GetBuffer,\n\t\t\tkey,\n\t\t\tsignal: signal.buffer,\n\t\t\tport: port2\n\t\t};\n\n\t\tparentPort.postMessage(msg, [port2]);\n\n\t\ttry {\n\t\t\t// Block until the main thread posts the response and fires Atomics.notify.\n\t\t\t// The response is guaranteed to be in port1's queue at this point because\n\t\t\t// port.postMessage executes before Atomics.notify on the main thread.\n\t\t\t// Use the lock deadline so the buffer fetch is bounded by the same timeout.\n\t\t\tconst waitResult = Atomics.wait(signal, 0, 0, Math.max(0, deadline - Date.now()));\n\t\t\tif (waitResult === \"timed-out\") {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t\t}\n\n\t\t\tconst response = receiveMessageOnPort(port1) as {\n\t\t\t\tmessage: { buffer: SharedArrayBuffer };\n\t\t\t} | null;\n\n\t\t\tif (Is.empty(response)) {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t\t}\n\n\t\t\tlocks[key] = new Int32Array(response.message.buffer);\n\t\t\treturn locks[key];\n\t\t} finally {\n\t\t\tport1.close();\n\t\t}\n\t}\n\n\t/**\n\t * Get the shared locks map, creating it if it does not exist.\n\t * @returns The shared locks map.\n\t * @internal\n\t */\n\tprivate static getLocks(): { [key: string]: Int32Array } {\n\t\tlet locks = SharedStore.get<{ [key: string]: Int32Array }>(Mutex._LOCKS_KEY);\n\t\tif (Is.undefined(locks)) {\n\t\t\tlocks = {};\n\t\t\tSharedStore.set(Mutex._LOCKS_KEY, locks);\n\t\t}\n\t\treturn locks;\n\t}\n}\n"]}
@@ -20,12 +20,12 @@ export declare class Mutex {
20
20
  */
21
21
  static readonly CLASS_NAME: string;
22
22
  /**
23
- * Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.
24
- * The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.
25
- *
26
- * WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the
27
- * duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a
28
- * buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.
23
+ * Acquires a lock for the given key without blocking the event loop. If the lock is already
24
+ * held, it suspends the current async task until the lock is released or the timeout is reached.
25
+ * Use this in async single-threaded contexts (e.g. the main thread or a Fastify route handler)
26
+ * where calling the synchronous lock() would freeze the event loop and deadlock.
27
+ * The lock is not re-entrant: if the same context holds the key and calls lockAsync() again on
28
+ * the same key, it will suspend until the timeout elapses.
29
29
  * @param key The key to lock on.
30
30
  * @param options Lock options.
31
31
  * @param options.timeoutMs The maximum time to wait for the lock in milliseconds, default is 5000.
@@ -36,7 +36,7 @@ export declare class Mutex {
36
36
  static lock(key: string, options?: {
37
37
  timeoutMs?: number;
38
38
  throwOnTimeout?: boolean;
39
- }): boolean;
39
+ }): Promise<boolean>;
40
40
  /**
41
41
  * Releases the lock for the given key.
42
42
  * @param key The key to unlock.
package/docs/changelog.md CHANGED
@@ -1,5 +1,86 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.4-next.1](https://github.com/iotaledger/twin-framework/compare/core-v0.0.4-next.0...core-v0.0.4-next.1) (2026-05-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * add context id features ([#206](https://github.com/iotaledger/twin-framework/issues/206)) ([ef0d4ee](https://github.com/iotaledger/twin-framework/commit/ef0d4ee11a4f5fc6cc6f52a4958ce905c04ee13b))
9
+ * add factory.createIfExists ([aad5a53](https://github.com/iotaledger/twin-framework/commit/aad5a53cef1b1c2e04344ea46244d41e371dff9b))
10
+ * add guards arrayEndsWith and arrayStartsWith ([95d875e](https://github.com/iotaledger/twin-framework/commit/95d875ec8ccb4713c145fdde941d4cfedcec2ed3))
11
+ * add health method to components ([a88016d](https://github.com/iotaledger/twin-framework/commit/a88016d90d172413e5bc5238dc1b3e35f82fcc2c))
12
+ * add Is.class method ([4988205](https://github.com/iotaledger/twin-framework/commit/498820543e256a130b4888c958fe1d87ca865d7f))
13
+ * add isDefault option to factory registration ([a8a700b](https://github.com/iotaledger/twin-framework/commit/a8a700bb8ddaf7dd5097869a358b8fc5f7c40ce7))
14
+ * add mutex ([#316](https://github.com/iotaledger/twin-framework/issues/316)) ([1027e5a](https://github.com/iotaledger/twin-framework/commit/1027e5ac77aa9804178b4253abdeefd6f1c3d521))
15
+ * add objectHelper.split ([386830a](https://github.com/iotaledger/twin-framework/commit/386830a77f8e842a5b119be0983708e042c3b14b))
16
+ * add ObjectOrArray and ArrayHelper methods ([0ac9077](https://github.com/iotaledger/twin-framework/commit/0ac907764d64b38ad1b04b0e9c3027055b527559))
17
+ * add rsa cipher support ([7af6cc6](https://github.com/iotaledger/twin-framework/commit/7af6cc67512d3363bd4a2f2e87bd7733c2800147))
18
+ * add single occurrence array type ([e890e43](https://github.com/iotaledger/twin-framework/commit/e890e4399e75ae5097f3ad8b1007321cbb1ed4ac))
19
+ * add single occurrence array type ([#245](https://github.com/iotaledger/twin-framework/issues/245)) ([771dc78](https://github.com/iotaledger/twin-framework/commit/771dc78025b5546c9c9681be9494a76ab71171c6))
20
+ * add support for health i18n validation ([7a286dd](https://github.com/iotaledger/twin-framework/commit/7a286ddb0c1bfa498bf3a77126cd589042bad6de))
21
+ * add teardown to IComponent interface ([32c173c](https://github.com/iotaledger/twin-framework/commit/32c173cda115c20d3b4cee14b8a29cdc08e91555))
22
+ * add teardown to IComponent interface ([98ce864](https://github.com/iotaledger/twin-framework/commit/98ce8648e709310c4435de334825b381fb4e32cb))
23
+ * add typeName method to factory ([699fcbd](https://github.com/iotaledger/twin-framework/commit/699fcbd1168228401ddb81fdacb959b8cdc4206a))
24
+ * add uuidv7 support ([#219](https://github.com/iotaledger/twin-framework/issues/219)) ([916c657](https://github.com/iotaledger/twin-framework/commit/916c657d270ce99fafe554233739fdd9ca28635b))
25
+ * add zlib/deflate mime types detection ([72c472b](https://github.com/iotaledger/twin-framework/commit/72c472b5a35a973e7109336f5b6cdd84dbb8bbcb))
26
+ * adding link header helper ([#225](https://github.com/iotaledger/twin-framework/issues/225)) ([703c072](https://github.com/iotaledger/twin-framework/commit/703c0725aceac6b6ec0c4fa729ef832d12fb3fd7))
27
+ * additional nameof operators ([a5aab60](https://github.com/iotaledger/twin-framework/commit/a5aab60bf66a86f1b7ff8af7c4f044cb03706d50))
28
+ * additional RSA methods and async ([1fceee2](https://github.com/iotaledger/twin-framework/commit/1fceee2d1248a24a7620846025fcf906495c07f4))
29
+ * async cache don't cache failures unless requested ([658ec4b](https://github.com/iotaledger/twin-framework/commit/658ec4b67a58a075de4702a3886d151e25ad3ddc))
30
+ * bump version ([c5354fa](https://github.com/iotaledger/twin-framework/commit/c5354fa906f4493c70f2642a9175a66780a10636))
31
+ * eslint migration to flat config ([74427d7](https://github.com/iotaledger/twin-framework/commit/74427d78d342167f7850e49ab87269326355befe))
32
+ * expand error params to accept properties ([032e9fd](https://github.com/iotaledger/twin-framework/commit/032e9fd1388e457cde32ca1005dfe014a5a9c077))
33
+ * factory create and integrity ([#235](https://github.com/iotaledger/twin-framework/issues/235)) ([9f98b99](https://github.com/iotaledger/twin-framework/commit/9f98b99daf46eb365346fae49cc4ffba63e74cb3))
34
+ * health status grouping ([5007c29](https://github.com/iotaledger/twin-framework/commit/5007c29fe4123fe56f754450b993dbe5dc32a057))
35
+ * improve async cache edge cases ([4e57a6e](https://github.com/iotaledger/twin-framework/commit/4e57a6ec4113533b0ea903eae7d469200fa1348c))
36
+ * improve base error data extraction ([dccc933](https://github.com/iotaledger/twin-framework/commit/dccc93361a1544b41db0e7c126ff90e858d87960))
37
+ * improve error display in CLI ([94b6ca8](https://github.com/iotaledger/twin-framework/commit/94b6ca8bdcfe3ca7671c4095b436ea7bddaae98e))
38
+ * improve error formatting ([#313](https://github.com/iotaledger/twin-framework/issues/313)) ([5a19623](https://github.com/iotaledger/twin-framework/commit/5a196231bcbf088bf9ba92a93b7478d3b8c5593f))
39
+ * improve Is.function and ModuleHelper.getModuleMethod signatures ([ecf968b](https://github.com/iotaledger/twin-framework/commit/ecf968b02934b3676be4bf7cd2d1e7f8e7af6ce2))
40
+ * improve Is.function definition to retain types ([f20b6b0](https://github.com/iotaledger/twin-framework/commit/f20b6b0dd16e74b75dc359be72b05989305c6410))
41
+ * improve signatures ([45a7d58](https://github.com/iotaledger/twin-framework/commit/45a7d58baa5b6abc8c0735656f393d402fabb4ad))
42
+ * improve signatures ([cdd24be](https://github.com/iotaledger/twin-framework/commit/cdd24be6fb898d33955b6f2f93c3ddbd73582269))
43
+ * improve signatures ([2041ae2](https://github.com/iotaledger/twin-framework/commit/2041ae214cbd8d472d5e8be8d3403cd06a114040))
44
+ * improve signatures ([d8a0ee1](https://github.com/iotaledger/twin-framework/commit/d8a0ee16542134a3f8b0653065239f42045125b4))
45
+ * improve signatures ([1d084c5](https://github.com/iotaledger/twin-framework/commit/1d084c58c7eb41ae50b0ce80ce57e48a22dd3102))
46
+ * improve signatures ([#287](https://github.com/iotaledger/twin-framework/issues/287)) ([c9f38f0](https://github.com/iotaledger/twin-framework/commit/c9f38f00e1cea8759e1105b41872a650c66c00f4))
47
+ * locales validation ([#197](https://github.com/iotaledger/twin-framework/issues/197)) ([55fdadb](https://github.com/iotaledger/twin-framework/commit/55fdadb13595ce0047f787bd1d4135d429a99f12))
48
+ * nodeIdentity optional in IComponent methods ([c78dc17](https://github.com/iotaledger/twin-framework/commit/c78dc17f4357d3e1ae40e415f468d3eae13e81f4))
49
+ * propagate includeStackTrace on error conversion ([8471cbb](https://github.com/iotaledger/twin-framework/commit/8471cbb71f8fc98247a0e92126c438c1a8b04d9b))
50
+ * propagate includeStackTrace on error conversion ([818337d](https://github.com/iotaledger/twin-framework/commit/818337d50d14bf5a7e8b3204649aa7527115cca9))
51
+ * relocate core packages from tools ([bcab8f3](https://github.com/iotaledger/twin-framework/commit/bcab8f3160442ea4fcaf442947462504f3d6a17d))
52
+ * simplify async set ([#121](https://github.com/iotaledger/twin-framework/issues/121)) ([2693c32](https://github.com/iotaledger/twin-framework/commit/2693c325266fd1a0aede6f1336c8b254c981a9ca))
53
+ * simplify factory options ([7f85a85](https://github.com/iotaledger/twin-framework/commit/7f85a8553dd3008364e8e1104f2e6f6b6595d77e))
54
+ * simplify StringHelper signature ([0390403](https://github.com/iotaledger/twin-framework/commit/039040344952f91ee3c671249bc0e1c8f3b38e17))
55
+ * support indexed properties set in objects ([b9c001d](https://github.com/iotaledger/twin-framework/commit/b9c001dc4614f6ff7486f4370735a553613d823a))
56
+ * typescript 6 update ([1d10f31](https://github.com/iotaledger/twin-framework/commit/1d10f31e6516ec622773f45e88af82fe749b384a))
57
+ * update dependencies ([4da77ab](https://github.com/iotaledger/twin-framework/commit/4da77ab30f499e52825ac5a76f51436ceb59c26e))
58
+ * update dependencies ([f3bd015](https://github.com/iotaledger/twin-framework/commit/f3bd015efd169196b7e0335f5cab876ba6ca1d75))
59
+ * urn random switched to using uuidv7 ([606c9a2](https://github.com/iotaledger/twin-framework/commit/606c9a2ed68a10fc7fc2bc5f93388c1b71033154))
60
+ * urn random switched to using uuidv7 ([6a29f8b](https://github.com/iotaledger/twin-framework/commit/6a29f8bd573d06992b7eaa027b1daf4c2a2e1e85))
61
+ * use cause instead of inner for errors ([1f4acc4](https://github.com/iotaledger/twin-framework/commit/1f4acc4d7a6b71a134d9547da9bf40de1e1e49da))
62
+ * use new shared store mechanism ([#131](https://github.com/iotaledger/twin-framework/issues/131)) ([934385b](https://github.com/iotaledger/twin-framework/commit/934385b2fbaf9f5c00a505ebf9d093bd5a425f55))
63
+
64
+
65
+ ### Bug Fixes
66
+
67
+ * async thread lock ([97a96a5](https://github.com/iotaledger/twin-framework/commit/97a96a57f60c7b45a0ccbbfa1c43199b6e7512f9))
68
+ * ensure __decorate is defined for decorators ([103a563](https://github.com/iotaledger/twin-framework/commit/103a563ce01ebdef6240d2e590e7b026e8692684))
69
+ * improve exception handling in asyncCache ([c81b29b](https://github.com/iotaledger/twin-framework/commit/c81b29b660b152d2f0757d323430287e6491bf59))
70
+ * improve jsdoc comments ([f7c8e43](https://github.com/iotaledger/twin-framework/commit/f7c8e43fab9803dffa6461950d5b9979e25bcd24))
71
+ * pick non iterable keys ([399399a](https://github.com/iotaledger/twin-framework/commit/399399abfb8947c4eb235df527532ffe186986b3))
72
+ * remove misleading undefined from AsyncCache return type ([05b3291](https://github.com/iotaledger/twin-framework/commit/05b32914e03a63c7daf7b77952a2ebedb7ca7f6b))
73
+
74
+
75
+ ### Dependencies
76
+
77
+ * The following workspace dependencies were updated
78
+ * dependencies
79
+ * @twin.org/nameof bumped from 0.0.4-next.0 to 0.0.4-next.1
80
+ * devDependencies
81
+ * @twin.org/nameof-transformer bumped from 0.0.4-next.0 to 0.0.4-next.1
82
+ * @twin.org/nameof-vitest-plugin bumped from 0.0.4-next.0 to 0.0.4-next.1
83
+
3
84
  ## [0.0.3](https://github.com/iotaledger/twin-framework/compare/core-v0.0.3...core-v0.0.3) (2026-05-27)
4
85
 
5
86
 
@@ -37,14 +37,14 @@ Runtime name for the class.
37
37
 
38
38
  ### lock() {#lock}
39
39
 
40
- > `static` **lock**(`key`, `options?`): `boolean`
40
+ > `static` **lock**(`key`, `options?`): `Promise`\<`boolean`\>
41
41
 
42
- Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.
43
- The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.
44
-
45
- WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the
46
- duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a
47
- buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.
42
+ Acquires a lock for the given key without blocking the event loop. If the lock is already
43
+ held, it suspends the current async task until the lock is released or the timeout is reached.
44
+ Use this in async single-threaded contexts (e.g. the main thread or a Fastify route handler)
45
+ where calling the synchronous lock() would freeze the event loop and deadlock.
46
+ The lock is not re-entrant: if the same context holds the key and calls lockAsync() again on
47
+ the same key, it will suspend until the timeout elapses.
48
48
 
49
49
  #### Parameters
50
50
 
@@ -72,7 +72,7 @@ Whether to throw an error if the lock could not be acquired within the timeout,
72
72
 
73
73
  #### Returns
74
74
 
75
- `boolean`
75
+ `Promise`\<`boolean`\>
76
76
 
77
77
  True if the lock was acquired, false if it timed out and throwOnTimeout is false.
78
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/core",
3
- "version": "0.0.3",
3
+ "version": "0.0.4-next.1",
4
4
  "description": "Helper methods/classes for data type checking/validation/guarding/error handling",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,7 +14,7 @@
14
14
  "node": ">=20.0.0"
15
15
  },
16
16
  "dependencies": {
17
- "@twin.org/nameof": "^0.0.3",
17
+ "@twin.org/nameof": "0.0.4-next.1",
18
18
  "intl-messageformat": "11.2.6",
19
19
  "rfc6902": "5.2.0"
20
20
  },