@twin.org/core 0.0.4-next.8 → 0.9.0-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.
- package/dist/es/encoding/base64.js +1 -1
- package/dist/es/encoding/base64.js.map +1 -1
- package/dist/es/errors/conflictError.js +1 -1
- package/dist/es/errors/conflictError.js.map +1 -1
- package/dist/es/errors/generalError.js +1 -1
- package/dist/es/errors/generalError.js.map +1 -1
- package/dist/es/errors/notImplementedError.js +1 -1
- package/dist/es/errors/notImplementedError.js.map +1 -1
- package/dist/es/errors/validationError.js +1 -1
- package/dist/es/errors/validationError.js.map +1 -1
- package/dist/es/helpers/arrayHelper.js +1 -1
- package/dist/es/helpers/arrayHelper.js.map +1 -1
- package/dist/es/helpers/errorHelper.js +1 -1
- package/dist/es/helpers/errorHelper.js.map +1 -1
- package/dist/es/helpers/jsonHelper.js +1 -1
- package/dist/es/helpers/jsonHelper.js.map +1 -1
- package/dist/es/helpers/objectHelper.js +2 -2
- package/dist/es/helpers/objectHelper.js.map +1 -1
- package/dist/es/helpers/stringHelper.js +2 -2
- package/dist/es/helpers/stringHelper.js.map +1 -1
- package/dist/es/index.js +7 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IComponent.js.map +1 -1
- package/dist/es/models/IDuration.js +4 -0
- package/dist/es/models/IDuration.js.map +1 -0
- package/dist/es/models/ILocale.js.map +1 -1
- package/dist/es/models/ISharedObjectBufferOptions.js +4 -0
- package/dist/es/models/ISharedObjectBufferOptions.js.map +1 -0
- package/dist/es/models/ISharedObjectBufferWorkerMessage.js +2 -0
- package/dist/es/models/ISharedObjectBufferWorkerMessage.js.map +1 -0
- package/dist/es/models/IValidationFailure.js.map +1 -1
- package/dist/es/models/coerceType.js +5 -1
- package/dist/es/models/coerceType.js.map +1 -1
- package/dist/es/models/sharedObjectBufferMessageTypes.js +13 -0
- package/dist/es/models/sharedObjectBufferMessageTypes.js.map +1 -0
- package/dist/es/types/duration.js +184 -0
- package/dist/es/types/duration.js.map +1 -0
- package/dist/es/types/durationRegExp.js +4 -0
- package/dist/es/types/durationRegExp.js.map +1 -0
- package/dist/es/types/url.js +1 -0
- package/dist/es/types/url.js.map +1 -1
- package/dist/es/utils/asyncCache.js +1 -1
- package/dist/es/utils/asyncCache.js.map +1 -1
- package/dist/es/utils/coerce.js +34 -20
- package/dist/es/utils/coerce.js.map +1 -1
- package/dist/es/utils/compression.js +1 -1
- package/dist/es/utils/compression.js.map +1 -1
- package/dist/es/utils/guards.js +12 -0
- package/dist/es/utils/guards.js.map +1 -1
- package/dist/es/utils/is.js +65 -16
- package/dist/es/utils/is.js.map +1 -1
- package/dist/es/utils/mutex.js +49 -8
- package/dist/es/utils/mutex.js.map +1 -1
- package/dist/es/utils/sharedObjectBuffer.js +308 -0
- package/dist/es/utils/sharedObjectBuffer.js.map +1 -0
- package/dist/es/utils/sharedStore.js +2 -2
- package/dist/es/utils/sharedStore.js.map +1 -1
- package/dist/types/errors/conflictError.d.ts +1 -1
- package/dist/types/errors/generalError.d.ts +1 -1
- package/dist/types/errors/notImplementedError.d.ts +1 -1
- package/dist/types/errors/validationError.d.ts +1 -1
- package/dist/types/helpers/arrayHelper.d.ts +1 -1
- package/dist/types/helpers/errorHelper.d.ts +1 -1
- package/dist/types/helpers/jsonHelper.d.ts +1 -1
- package/dist/types/helpers/objectHelper.d.ts +2 -2
- package/dist/types/helpers/stringHelper.d.ts +2 -2
- package/dist/types/index.d.ts +7 -0
- package/dist/types/models/IComponent.d.ts +2 -2
- package/dist/types/models/IDuration.d.ts +45 -0
- package/dist/types/models/ILocale.d.ts +1 -1
- package/dist/types/models/ISharedObjectBufferOptions.d.ts +17 -0
- package/dist/types/models/ISharedObjectBufferWorkerMessage.d.ts +29 -0
- package/dist/types/models/IValidationFailure.d.ts +1 -1
- package/dist/types/models/coerceType.d.ts +4 -0
- package/dist/types/models/sharedObjectBufferMessageTypes.d.ts +13 -0
- package/dist/types/types/duration.d.ts +29 -0
- package/dist/types/types/durationRegExp.d.ts +1 -0
- package/dist/types/types/url.d.ts +1 -0
- package/dist/types/utils/asyncCache.d.ts +1 -1
- package/dist/types/utils/coerce.d.ts +19 -20
- package/dist/types/utils/compression.d.ts +1 -1
- package/dist/types/utils/guards.d.ts +9 -0
- package/dist/types/utils/is.d.ts +16 -9
- package/dist/types/utils/mutex.d.ts +12 -1
- package/dist/types/utils/sharedObjectBuffer.d.ts +74 -0
- package/dist/types/utils/sharedStore.d.ts +2 -2
- package/docs/changelog.md +273 -0
- package/docs/reference/classes/ArrayHelper.md +1 -1
- package/docs/reference/classes/AsyncCache.md +1 -1
- package/docs/reference/classes/Coerce.md +32 -48
- package/docs/reference/classes/Compression.md +1 -1
- package/docs/reference/classes/ConflictError.md +1 -1
- package/docs/reference/classes/Duration.md +88 -0
- package/docs/reference/classes/ErrorHelper.md +1 -1
- package/docs/reference/classes/GeneralError.md +1 -1
- package/docs/reference/classes/Guards.md +36 -0
- package/docs/reference/classes/Is.md +31 -9
- package/docs/reference/classes/JsonHelper.md +1 -1
- package/docs/reference/classes/Mutex.md +39 -1
- package/docs/reference/classes/NotImplementedError.md +1 -1
- package/docs/reference/classes/ObjectHelper.md +10 -2
- package/docs/reference/classes/SharedObjectBuffer.md +192 -0
- package/docs/reference/classes/SharedStore.md +2 -2
- package/docs/reference/classes/StringHelper.md +16 -13
- package/docs/reference/classes/Url.md +4 -0
- package/docs/reference/classes/ValidationError.md +1 -1
- package/docs/reference/index.md +8 -0
- package/docs/reference/interfaces/IComponent.md +2 -2
- package/docs/reference/interfaces/IDuration.md +83 -0
- package/docs/reference/interfaces/ILocale.md +1 -1
- package/docs/reference/interfaces/ISharedObjectBufferOptions.md +33 -0
- package/docs/reference/interfaces/ISharedObjectBufferWorkerMessage.md +44 -0
- package/docs/reference/interfaces/IValidationFailure.md +1 -1
- package/docs/reference/type-aliases/SharedObjectBufferMessageTypes.md +5 -0
- package/docs/reference/variables/CoerceType.md +6 -0
- package/docs/reference/variables/DURATION_REG_EXP.md +3 -0
- package/docs/reference/variables/SharedObjectBufferMessageTypes.md +13 -0
- package/locales/en.json +9 -2
- package/package.json +3 -3
package/dist/es/utils/mutex.js
CHANGED
|
@@ -29,6 +29,11 @@ export class Mutex {
|
|
|
29
29
|
* @internal
|
|
30
30
|
*/
|
|
31
31
|
static _LOCKS_KEY = "mutexLocks";
|
|
32
|
+
/**
|
|
33
|
+
* SharedStore key for the default timeout in milliseconds.
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
static _DEFAULT_TIMEOUT_KEY = "mutexDefaultTimeoutMs";
|
|
32
37
|
/**
|
|
33
38
|
* Cached reference to the node:worker_threads module, null if unavailable (browser).
|
|
34
39
|
* @internal
|
|
@@ -36,6 +41,25 @@ export class Mutex {
|
|
|
36
41
|
// false positive: this is a type not an actual import
|
|
37
42
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
38
43
|
static _workerThreadsModule;
|
|
44
|
+
/**
|
|
45
|
+
* Gets the default timeout in milliseconds for lock acquisition.
|
|
46
|
+
* @returns The default timeout in milliseconds.
|
|
47
|
+
*/
|
|
48
|
+
static getDefaultTimeoutMs() {
|
|
49
|
+
return SharedStore.get(Mutex._DEFAULT_TIMEOUT_KEY) ?? 5000;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sets the default timeout in milliseconds for lock acquisition.
|
|
53
|
+
* @param timeoutMs The default timeout in milliseconds.
|
|
54
|
+
* @throws GeneralError if timeoutMs is not a non-negative integer.
|
|
55
|
+
*/
|
|
56
|
+
static setDefaultTimeoutMs(timeoutMs) {
|
|
57
|
+
Guards.integer(Mutex.CLASS_NAME, "timeoutMs", timeoutMs);
|
|
58
|
+
if (timeoutMs < 0) {
|
|
59
|
+
throw new GeneralError(Mutex.CLASS_NAME, "invalidTimeout", { timeoutMs });
|
|
60
|
+
}
|
|
61
|
+
SharedStore.set(Mutex._DEFAULT_TIMEOUT_KEY, timeoutMs);
|
|
62
|
+
}
|
|
39
63
|
/**
|
|
40
64
|
* Acquires a lock for the given key without blocking the event loop. If the lock is already
|
|
41
65
|
* held, it suspends the current async task until the lock is released or the timeout is reached.
|
|
@@ -45,14 +69,22 @@ export class Mutex {
|
|
|
45
69
|
* the same key, it will suspend until the timeout elapses.
|
|
46
70
|
* @param key The key to lock on.
|
|
47
71
|
* @param options Lock options.
|
|
48
|
-
* @param options.timeoutMs The maximum time to wait for the lock in milliseconds,
|
|
72
|
+
* @param options.timeoutMs The maximum time to wait for the lock in milliseconds, defaults to getDefaultTimeoutMs().
|
|
49
73
|
* @param options.throwOnTimeout Whether to throw an error if the lock could not be acquired within the timeout, default is false.
|
|
50
74
|
* @returns True if the lock was acquired, false if it timed out and throwOnTimeout is false.
|
|
51
75
|
* @throws GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.
|
|
52
76
|
*/
|
|
53
77
|
static async lock(key, options) {
|
|
54
78
|
Guards.stringValue(Mutex.CLASS_NAME, "key", key);
|
|
55
|
-
|
|
79
|
+
if (!Is.empty(options?.timeoutMs)) {
|
|
80
|
+
Guards.integer(Mutex.CLASS_NAME, "options.timeoutMs", options.timeoutMs);
|
|
81
|
+
if (options.timeoutMs < 0) {
|
|
82
|
+
throw new GeneralError(Mutex.CLASS_NAME, "invalidTimeout", {
|
|
83
|
+
timeoutMs: options.timeoutMs
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const timeoutMs = options?.timeoutMs ?? Mutex.getDefaultTimeoutMs();
|
|
56
88
|
const throwOnTimeout = options?.throwOnTimeout ?? false;
|
|
57
89
|
const deadline = Date.now() + timeoutMs;
|
|
58
90
|
// getOrFetchLock may block once per key on worker threads to negotiate the
|
|
@@ -117,10 +149,17 @@ export class Mutex {
|
|
|
117
149
|
Guards.object(Mutex.CLASS_NAME, "msg.port", msg.port);
|
|
118
150
|
const locks = Mutex.getLocks();
|
|
119
151
|
locks[msg.key] ??= new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
120
|
-
// Send the buffer
|
|
121
|
-
//
|
|
152
|
+
// Send the buffer before updating the signal so it is guaranteed to be in
|
|
153
|
+
// port1's receive queue when Atomics.wait returns on the worker side.
|
|
122
154
|
msg.port.postMessage({ buffer: locks[msg.key].buffer });
|
|
123
|
-
|
|
155
|
+
// Set signal[0] = 1 before notifying. If the OS scheduled the main thread
|
|
156
|
+
// to process this request before the worker reached Atomics.wait, the notify
|
|
157
|
+
// would fire with no waiters (lost wakeup). Setting the value first means
|
|
158
|
+
// Atomics.wait(signal, 0, 0) sees a non-zero value and returns "not-equal"
|
|
159
|
+
// immediately instead of blocking indefinitely.
|
|
160
|
+
const signalArr = new Int32Array(msg.signal);
|
|
161
|
+
Atomics.store(signalArr, 0, 1);
|
|
162
|
+
Atomics.notify(signalArr, 0, 1);
|
|
124
163
|
msg.port.close();
|
|
125
164
|
return true;
|
|
126
165
|
}
|
|
@@ -163,9 +202,11 @@ export class Mutex {
|
|
|
163
202
|
};
|
|
164
203
|
wt.parentPort.postMessage(msg, [port2]);
|
|
165
204
|
try {
|
|
166
|
-
// Block until the main thread
|
|
167
|
-
//
|
|
168
|
-
//
|
|
205
|
+
// Block until the main thread signals readiness. The main thread sets
|
|
206
|
+
// signal[0] = 1 before calling notify, so if the notify fired before this
|
|
207
|
+
// wait call (lost-wakeup scenario with concurrent workers), Atomics.wait
|
|
208
|
+
// sees a non-zero value and returns "not-equal" immediately.
|
|
209
|
+
// Either way the port message is already in port1's receive queue.
|
|
169
210
|
// Use the lock deadline so the buffer fetch is bounded by the same timeout.
|
|
170
211
|
const waitResult = Atomics.wait(signal, 0, 0, Math.max(0, deadline - Date.now()));
|
|
171
212
|
if (waitResult === "timed-out") {
|
|
@@ -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 type { MessagePort } 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 * Cached reference to the node:worker_threads module, null if unavailable (browser).\n\t * @internal\n\t */\n\t// false positive: this is a type not an actual import\n\t// eslint-disable-next-line @typescript-eslint/consistent-type-imports\n\tprivate static _workerThreadsModule: typeof import(\"node:worker_threads\") | null | undefined;\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 = await 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 * @param deadline The deadline to use while waiting for the main thread to provide the lock.\n\t * @returns The Int32Array backed by a SharedArrayBuffer for this key.\n\t * @internal\n\t */\n\tprivate static async getOrFetchLock(key: string, deadline: number): Promise<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\tconst wt = await Mutex.loadWorkerThreads();\n\n\t\t// Re-check after the await: another coroutine that was also waiting on\n\t\t// loadWorkerThreads() may have allocated the buffer while we yielded.\n\t\tif (!Is.empty(locks[key])) {\n\t\t\treturn locks[key];\n\t\t}\n\n\t\tif (Is.empty(wt) || wt.isMainThread) {\n\t\t\t// Main thread, fork-mode process, or browser: 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(wt.parentPort)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t}\n\n\t\tconst { port1, port2 } = new wt.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\twt.parentPort.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 = wt.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\t/**\n\t * Lazily loads node:worker_threads, returning null in environments where it is unavailable.\n\t * @returns The worker_threads module or null.\n\t * @internal\n\t */\n\t// false positive: this is a type not an actual import\n\t// eslint-disable-next-line @typescript-eslint/consistent-type-imports\n\tprivate static async loadWorkerThreads(): Promise<typeof import(\"node:worker_threads\") | null> {\n\t\tif (Mutex._workerThreadsModule === undefined) {\n\t\t\ttry {\n\t\t\t\tMutex._workerThreadsModule = await import(\"node:worker_threads\");\n\t\t\t} catch {\n\t\t\t\tMutex._workerThreadsModule = null;\n\t\t\t}\n\t\t}\n\t\treturn Mutex._workerThreadsModule;\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 type { MessagePort } 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 * SharedStore key for the default timeout in milliseconds.\n\t * @internal\n\t */\n\tprivate static readonly _DEFAULT_TIMEOUT_KEY = \"mutexDefaultTimeoutMs\";\n\n\t/**\n\t * Cached reference to the node:worker_threads module, null if unavailable (browser).\n\t * @internal\n\t */\n\t// false positive: this is a type not an actual import\n\t// eslint-disable-next-line @typescript-eslint/consistent-type-imports\n\tprivate static _workerThreadsModule: typeof import(\"node:worker_threads\") | null | undefined;\n\n\t/**\n\t * Gets the default timeout in milliseconds for lock acquisition.\n\t * @returns The default timeout in milliseconds.\n\t */\n\tpublic static getDefaultTimeoutMs(): number {\n\t\treturn SharedStore.get<number>(Mutex._DEFAULT_TIMEOUT_KEY) ?? 5000;\n\t}\n\n\t/**\n\t * Sets the default timeout in milliseconds for lock acquisition.\n\t * @param timeoutMs The default timeout in milliseconds.\n\t * @throws GeneralError if timeoutMs is not a non-negative integer.\n\t */\n\tpublic static setDefaultTimeoutMs(timeoutMs: number): void {\n\t\tGuards.integer(Mutex.CLASS_NAME, nameof(timeoutMs), timeoutMs);\n\t\tif (timeoutMs < 0) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"invalidTimeout\", { timeoutMs });\n\t\t}\n\t\tSharedStore.set(Mutex._DEFAULT_TIMEOUT_KEY, timeoutMs);\n\t}\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, defaults to getDefaultTimeoutMs().\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\t\tif (!Is.empty(options?.timeoutMs)) {\n\t\t\tGuards.integer(Mutex.CLASS_NAME, nameof(options.timeoutMs), options.timeoutMs);\n\t\t\tif (options.timeoutMs < 0) {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"invalidTimeout\", {\n\t\t\t\t\ttimeoutMs: options.timeoutMs\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tconst timeoutMs = options?.timeoutMs ?? Mutex.getDefaultTimeoutMs();\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 = await 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 before updating the signal so it is guaranteed to be in\n\t\t// port1's receive queue when Atomics.wait returns on the worker side.\n\t\tmsg.port.postMessage({ buffer: locks[msg.key].buffer });\n\t\t// Set signal[0] = 1 before notifying. If the OS scheduled the main thread\n\t\t// to process this request before the worker reached Atomics.wait, the notify\n\t\t// would fire with no waiters (lost wakeup). Setting the value first means\n\t\t// Atomics.wait(signal, 0, 0) sees a non-zero value and returns \"not-equal\"\n\t\t// immediately instead of blocking indefinitely.\n\t\tconst signalArr = new Int32Array(msg.signal);\n\t\tAtomics.store(signalArr, 0, 1);\n\t\tAtomics.notify(signalArr, 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 * @param deadline The deadline to use while waiting for the main thread to provide the lock.\n\t * @returns The Int32Array backed by a SharedArrayBuffer for this key.\n\t * @internal\n\t */\n\tprivate static async getOrFetchLock(key: string, deadline: number): Promise<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\tconst wt = await Mutex.loadWorkerThreads();\n\n\t\t// Re-check after the await: another coroutine that was also waiting on\n\t\t// loadWorkerThreads() may have allocated the buffer while we yielded.\n\t\tif (!Is.empty(locks[key])) {\n\t\t\treturn locks[key];\n\t\t}\n\n\t\tif (Is.empty(wt) || wt.isMainThread) {\n\t\t\t// Main thread, fork-mode process, or browser: 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(wt.parentPort)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t}\n\n\t\tconst { port1, port2 } = new wt.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\twt.parentPort.postMessage(msg, [port2]);\n\n\t\ttry {\n\t\t\t// Block until the main thread signals readiness. The main thread sets\n\t\t\t// signal[0] = 1 before calling notify, so if the notify fired before this\n\t\t\t// wait call (lost-wakeup scenario with concurrent workers), Atomics.wait\n\t\t\t// sees a non-zero value and returns \"not-equal\" immediately.\n\t\t\t// Either way the port message is already in port1's receive queue.\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 = wt.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\t/**\n\t * Lazily loads node:worker_threads, returning null in environments where it is unavailable.\n\t * @returns The worker_threads module or null.\n\t * @internal\n\t */\n\t// false positive: this is a type not an actual import\n\t// eslint-disable-next-line @typescript-eslint/consistent-type-imports\n\tprivate static async loadWorkerThreads(): Promise<typeof import(\"node:worker_threads\") | null> {\n\t\tif (Mutex._workerThreadsModule === undefined) {\n\t\t\ttry {\n\t\t\t\tMutex._workerThreadsModule = await import(\"node:worker_threads\");\n\t\t\t} catch {\n\t\t\t\tMutex._workerThreadsModule = null;\n\t\t\t}\n\t\t}\n\t\treturn Mutex._workerThreadsModule;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Is } from "./is.js";
|
|
2
|
+
import { SharedStore } from "./sharedStore.js";
|
|
3
|
+
import { GeneralError } from "../errors/generalError.js";
|
|
4
|
+
import { ObjectHelper } from "../helpers/objectHelper.js";
|
|
5
|
+
import { SharedObjectBufferMessageTypes } from "../models/sharedObjectBufferMessageTypes.js";
|
|
6
|
+
/**
|
|
7
|
+
* Manages per-object SharedArrayBuffers that store objects as UTF-8 JSON.
|
|
8
|
+
* Buffer layout: 4-byte Int32 header (current data byte length) followed by the JSON-encoded object.
|
|
9
|
+
* Buffers are explicitly created with create and cached in SharedStore.
|
|
10
|
+
* On a worker thread an existing buffer is fetched via a MessagePort handshake
|
|
11
|
+
* (same protocol as Mutex) and cached locally.
|
|
12
|
+
* Buffers grow automatically when the payload exceeds capacity; when the payload drops well below
|
|
13
|
+
* capacity the buffer is replaced with a smaller one. The caller must hold the objectId-keyed Mutex
|
|
14
|
+
* around every read and write. The main thread's worker message handler must forward messages to
|
|
15
|
+
* both Mutex.handleWorkerMessage and SharedObjectBuffer.handleWorkerMessage.
|
|
16
|
+
*/
|
|
17
|
+
export class SharedObjectBuffer {
|
|
18
|
+
/**
|
|
19
|
+
* Runtime name for the class.
|
|
20
|
+
*/
|
|
21
|
+
static CLASS_NAME = "SharedObjectBuffer";
|
|
22
|
+
/**
|
|
23
|
+
* Default payload capacity per object (1 MiB). The first caller that creates the buffer
|
|
24
|
+
* for an object determines its initial capacity; later callers that pass a different value
|
|
25
|
+
* are ignored.
|
|
26
|
+
*/
|
|
27
|
+
static DEFAULT_CAPACITY_BYTES = 1 * 1024 * 1024;
|
|
28
|
+
/**
|
|
29
|
+
* Default upper bound for how large a buffer may grow (256 MiB).
|
|
30
|
+
* Override per-object via the maxCapacityBytes option on create.
|
|
31
|
+
*/
|
|
32
|
+
static MAX_CAPACITY_BYTES = 256 * 1024 * 1024;
|
|
33
|
+
/**
|
|
34
|
+
* Bytes reserved for the data-length header at the start of each buffer.
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
static _HEADER_BYTES = 4;
|
|
38
|
+
/**
|
|
39
|
+
* Multiplier applied to the current capacity when growing or sizing a replacement buffer.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
static _GROW_FACTOR = 2;
|
|
43
|
+
/**
|
|
44
|
+
* Fraction of payload capacity below which a buffer is replaced with a smaller one.
|
|
45
|
+
* A value of 0.25 means the buffer is shrunk when the payload is below 25 % of capacity.
|
|
46
|
+
* Shrinking only applies when the capacity already exceeds DEFAULT_CAPACITY_BYTES.
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
static _SHRINK_THRESHOLD = 0.25;
|
|
50
|
+
/**
|
|
51
|
+
* SharedStore key under which the per-object buffer map lives on each thread.
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
static _STORE_KEY = "sharedObjectBuffers";
|
|
55
|
+
/**
|
|
56
|
+
* Cached worker_threads module; undefined = not yet loaded, null = unavailable.
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
// false positive: this is a type not an actual import
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
61
|
+
static _workerThreadsModule;
|
|
62
|
+
/**
|
|
63
|
+
* Create the buffer for the given objectId if it does not already exist.
|
|
64
|
+
* Must be called while holding Mutex.lock(objectId).
|
|
65
|
+
* @param objectId The object id that identifies the buffer.
|
|
66
|
+
* @param options Optional capacity configuration used when creating the buffer.
|
|
67
|
+
*/
|
|
68
|
+
static async create(objectId, options) {
|
|
69
|
+
await SharedObjectBuffer.getOrFetchBuffer(objectId, options ?? {});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read and decode the object stored for the given objectId.
|
|
73
|
+
* Must be called while holding Mutex.lock(objectId).
|
|
74
|
+
* @param objectId The object id that identifies the buffer.
|
|
75
|
+
* @returns The stored object, or undefined when nothing has been written yet.
|
|
76
|
+
*/
|
|
77
|
+
static async read(objectId) {
|
|
78
|
+
const buf = await SharedObjectBuffer.getOrFetchBuffer(objectId);
|
|
79
|
+
if (Is.undefined(buf)) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return SharedObjectBuffer.decode(buf);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Encode and write the object into the buffer for the given objectId.
|
|
86
|
+
* The buffer must already exist, usually by calling create first.
|
|
87
|
+
* When the encoded payload exceeds the current buffer capacity the buffer is grown
|
|
88
|
+
* in-place via SharedArrayBuffer.grow so every thread with a reference sees the
|
|
89
|
+
* new size without any pointer swap. When the payload is smaller than
|
|
90
|
+
* _SHRINK_THRESHOLD of the current capacity and the capacity exceeds
|
|
91
|
+
* DEFAULT_CAPACITY_BYTES, the buffer is replaced with a smaller one on the calling thread.
|
|
92
|
+
* Must be called while holding Mutex.lock(objectId).
|
|
93
|
+
* @param objectId The object id that identifies the buffer.
|
|
94
|
+
* @param value The object to persist.
|
|
95
|
+
*/
|
|
96
|
+
static async write(objectId, value) {
|
|
97
|
+
const encoded = ObjectHelper.toBytes(value);
|
|
98
|
+
let buf = await SharedObjectBuffer.getOrFetchBuffer(objectId);
|
|
99
|
+
if (Is.undefined(buf)) {
|
|
100
|
+
throw new GeneralError(SharedObjectBuffer.CLASS_NAME, "notCreated", {
|
|
101
|
+
objectId
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const payloadCapacity = buf.byteLength - SharedObjectBuffer._HEADER_BYTES;
|
|
105
|
+
const maxPayloadCapacity = buf.maxByteLength - SharedObjectBuffer._HEADER_BYTES;
|
|
106
|
+
if (encoded.length > payloadCapacity) {
|
|
107
|
+
// Grow the buffer in-place. SharedArrayBuffer.grow is atomic: all threads that
|
|
108
|
+
// hold a reference see the enlarged buffer without a pointer swap.
|
|
109
|
+
const newPayloadCapacity = Math.max(encoded.length, payloadCapacity * SharedObjectBuffer._GROW_FACTOR);
|
|
110
|
+
const newByteLength = Math.min(SharedObjectBuffer._HEADER_BYTES + newPayloadCapacity, buf.maxByteLength);
|
|
111
|
+
if (encoded.length > newByteLength - SharedObjectBuffer._HEADER_BYTES) {
|
|
112
|
+
throw new GeneralError(SharedObjectBuffer.CLASS_NAME, "capacityExceeded", {
|
|
113
|
+
objectId,
|
|
114
|
+
required: encoded.length,
|
|
115
|
+
capacity: maxPayloadCapacity
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
buf.grow(newByteLength);
|
|
119
|
+
}
|
|
120
|
+
else if (payloadCapacity > SharedObjectBuffer.DEFAULT_CAPACITY_BYTES &&
|
|
121
|
+
encoded.length < payloadCapacity * SharedObjectBuffer._SHRINK_THRESHOLD) {
|
|
122
|
+
// Replace with a smaller buffer when far below capacity. The caller writes
|
|
123
|
+
// the payload into the returned buffer immediately after.
|
|
124
|
+
buf = SharedObjectBuffer.shrinkBuffer(objectId, encoded.length, buf.maxByteLength);
|
|
125
|
+
}
|
|
126
|
+
new Uint8Array(buf, SharedObjectBuffer._HEADER_BYTES).set(encoded);
|
|
127
|
+
Atomics.store(new Int32Array(buf, 0, 1), 0, encoded.length);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Remove the stored object and release the buffer for the given objectId.
|
|
131
|
+
* The entry is deleted from the local cache so subsequent reads or writes will
|
|
132
|
+
* create or fetch a fresh buffer. Worker threads that have cached the old buffer
|
|
133
|
+
* reference continue using it until they restart or re-request via the worker protocol.
|
|
134
|
+
* Must be called while holding Mutex.lock(objectId).
|
|
135
|
+
* @param objectId The object id that identifies the buffer.
|
|
136
|
+
*/
|
|
137
|
+
static remove(objectId) {
|
|
138
|
+
delete SharedObjectBuffer.getBuffers()[objectId];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Inspect a message from a worker thread and, if it is a SharedObjectBuffer
|
|
142
|
+
* buffer-fetch request, respond to it synchronously.
|
|
143
|
+
* Call this from the main thread's worker message handler alongside
|
|
144
|
+
* Mutex.handleWorkerMessage.
|
|
145
|
+
* @param msg The raw message received from the worker.
|
|
146
|
+
* @returns True if the message was a SharedObjectBuffer protocol message, false otherwise.
|
|
147
|
+
*/
|
|
148
|
+
static handleWorkerMessage(msg) {
|
|
149
|
+
if (!Is.object(msg) ||
|
|
150
|
+
msg.type !== SharedObjectBufferMessageTypes.GetBuffer) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const { objectId: objectName, port, signal, options } = msg;
|
|
154
|
+
const buf = Is.object(options)
|
|
155
|
+
? SharedObjectBuffer.getOrCreateBuffer(objectName, options)
|
|
156
|
+
: SharedObjectBuffer.getBuffers()[objectName];
|
|
157
|
+
// Deliver the buffer before waking the worker so it is in the port's receive
|
|
158
|
+
// queue when Atomics.wait returns (same ordering guarantee as Mutex).
|
|
159
|
+
port.postMessage({ buffer: buf });
|
|
160
|
+
Atomics.notify(new Int32Array(signal), 0, 1);
|
|
161
|
+
port.close();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Decode the object from a SharedArrayBuffer.
|
|
166
|
+
* @param buf The SharedArrayBuffer to decode.
|
|
167
|
+
* @returns The decoded object, or undefined when nothing has been written.
|
|
168
|
+
* @internal
|
|
169
|
+
*/
|
|
170
|
+
static decode(buf) {
|
|
171
|
+
const dataLen = Atomics.load(new Int32Array(buf, 0, 1), 0);
|
|
172
|
+
if (dataLen === 0) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
const bytes = new Uint8Array(buf, SharedObjectBuffer._HEADER_BYTES, dataLen);
|
|
176
|
+
return ObjectHelper.fromBytes(bytes);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Return the cached buffer for the object, creating or fetching it if needed.
|
|
180
|
+
* Applies a double-check after the async loadWorkerThreads call to handle the
|
|
181
|
+
* case where another coroutine populated the cache while this one yielded.
|
|
182
|
+
* @param objectId The object id that identifies the buffer.
|
|
183
|
+
* @param options Optional capacity configuration when creating the buffer.
|
|
184
|
+
* @returns The SharedArrayBuffer for the object.
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
static async getOrFetchBuffer(objectId, options) {
|
|
188
|
+
const buffers = SharedObjectBuffer.getBuffers();
|
|
189
|
+
if (!Is.undefined(buffers[objectId])) {
|
|
190
|
+
return buffers[objectId];
|
|
191
|
+
}
|
|
192
|
+
const wt = await SharedObjectBuffer.loadWorkerThreads();
|
|
193
|
+
// Re-check after the await: another coroutine may have populated the cache.
|
|
194
|
+
if (!Is.undefined(buffers[objectId])) {
|
|
195
|
+
return buffers[objectId];
|
|
196
|
+
}
|
|
197
|
+
if (Is.empty(wt) || wt.isMainThread) {
|
|
198
|
+
if (!Is.object(options)) {
|
|
199
|
+
return buffers[objectId];
|
|
200
|
+
}
|
|
201
|
+
return SharedObjectBuffer.getOrCreateBuffer(objectId, options);
|
|
202
|
+
}
|
|
203
|
+
// Worker thread: synchronously request the SharedArrayBuffer from the main thread.
|
|
204
|
+
if (Is.empty(wt.parentPort)) {
|
|
205
|
+
throw new GeneralError(SharedObjectBuffer.CLASS_NAME, "bufferFetchFailed", { objectId });
|
|
206
|
+
}
|
|
207
|
+
const { port1, port2 } = new wt.MessageChannel();
|
|
208
|
+
const signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
209
|
+
const request = {
|
|
210
|
+
type: SharedObjectBufferMessageTypes.GetBuffer,
|
|
211
|
+
objectId,
|
|
212
|
+
signal: signal.buffer,
|
|
213
|
+
port: port2,
|
|
214
|
+
options
|
|
215
|
+
};
|
|
216
|
+
wt.parentPort.postMessage(request, [port2]);
|
|
217
|
+
try {
|
|
218
|
+
// Block until the main thread posts the buffer and fires Atomics.notify.
|
|
219
|
+
// The response is guaranteed to be in port1's queue when wait returns because
|
|
220
|
+
// port.postMessage executes before Atomics.notify on the main thread.
|
|
221
|
+
const waitResult = Atomics.wait(signal, 0, 0, 30_000);
|
|
222
|
+
if (waitResult === "timed-out") {
|
|
223
|
+
throw new GeneralError(SharedObjectBuffer.CLASS_NAME, "bufferFetchFailed", { objectId });
|
|
224
|
+
}
|
|
225
|
+
const response = wt.receiveMessageOnPort(port1);
|
|
226
|
+
if (Is.empty(response)) {
|
|
227
|
+
throw new GeneralError(SharedObjectBuffer.CLASS_NAME, "bufferFetchFailed", { objectId });
|
|
228
|
+
}
|
|
229
|
+
if (Is.undefined(response.message.buffer)) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
buffers[objectId] = response.message.buffer;
|
|
233
|
+
return buffers[objectId];
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
port1.close();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get or create the growable buffer on the main thread (or in a fork-mode process).
|
|
241
|
+
* @param objectId The object id that identifies the buffer.
|
|
242
|
+
* @param options Optional capacity configuration for new buffers.
|
|
243
|
+
* @returns The existing or newly created SharedArrayBuffer.
|
|
244
|
+
* @internal
|
|
245
|
+
*/
|
|
246
|
+
static getOrCreateBuffer(objectId, options) {
|
|
247
|
+
const buffers = SharedObjectBuffer.getBuffers();
|
|
248
|
+
if (Is.undefined(buffers[objectId])) {
|
|
249
|
+
const maxCapacity = options?.maxCapacityBytes ?? SharedObjectBuffer.MAX_CAPACITY_BYTES;
|
|
250
|
+
const initialCapacity = Math.min(options?.initialCapacityBytes ?? SharedObjectBuffer.DEFAULT_CAPACITY_BYTES, maxCapacity);
|
|
251
|
+
buffers[objectId] = new SharedArrayBuffer(SharedObjectBuffer._HEADER_BYTES + initialCapacity, {
|
|
252
|
+
maxByteLength: SharedObjectBuffer._HEADER_BYTES + maxCapacity
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return buffers[objectId];
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Create a new smaller SharedArrayBuffer, register it in SharedStore, and return it.
|
|
259
|
+
* The new buffer is empty; write writes the current payload into it immediately
|
|
260
|
+
* after this call returns. Callers on other threads that have cached the old buffer
|
|
261
|
+
* reference continue using it until they restart or re-request via the worker protocol.
|
|
262
|
+
* @param objectId The object id that identifies the buffer.
|
|
263
|
+
* @param dataLen Byte length of the payload that will be written next.
|
|
264
|
+
* @param maxByteLength The maxByteLength to preserve from the old buffer.
|
|
265
|
+
* @returns The new, smaller SharedArrayBuffer registered in SharedStore.
|
|
266
|
+
* @internal
|
|
267
|
+
*/
|
|
268
|
+
static shrinkBuffer(objectId, dataLen, maxByteLength) {
|
|
269
|
+
const newCapacity = Math.max(dataLen * SharedObjectBuffer._GROW_FACTOR, SharedObjectBuffer.DEFAULT_CAPACITY_BYTES);
|
|
270
|
+
const newBuf = new SharedArrayBuffer(SharedObjectBuffer._HEADER_BYTES + newCapacity, {
|
|
271
|
+
maxByteLength
|
|
272
|
+
});
|
|
273
|
+
SharedObjectBuffer.getBuffers()[objectId] = newBuf;
|
|
274
|
+
return newBuf;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Return the per-thread buffer map from SharedStore, creating it if absent.
|
|
278
|
+
* @returns The map of object id to SharedArrayBuffer.
|
|
279
|
+
* @internal
|
|
280
|
+
*/
|
|
281
|
+
static getBuffers() {
|
|
282
|
+
let buffers = SharedStore.get(SharedObjectBuffer._STORE_KEY);
|
|
283
|
+
if (Is.undefined(buffers)) {
|
|
284
|
+
buffers = {};
|
|
285
|
+
SharedStore.set(SharedObjectBuffer._STORE_KEY, buffers);
|
|
286
|
+
}
|
|
287
|
+
return buffers;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Lazily load node:worker_threads, returning null in environments where it is unavailable.
|
|
291
|
+
* @returns The worker_threads module or null.
|
|
292
|
+
* @internal
|
|
293
|
+
*/
|
|
294
|
+
// false positive: this is a type not an actual import
|
|
295
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
296
|
+
static async loadWorkerThreads() {
|
|
297
|
+
if (SharedObjectBuffer._workerThreadsModule === undefined) {
|
|
298
|
+
try {
|
|
299
|
+
SharedObjectBuffer._workerThreadsModule = await import("node:worker_threads");
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
SharedObjectBuffer._workerThreadsModule = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return SharedObjectBuffer._workerThreadsModule;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=sharedObjectBuffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sharedObjectBuffer.js","sourceRoot":"","sources":["../../../src/utils/sharedObjectBuffer.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { MessagePort } from \"node:worker_threads\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\nimport { GeneralError } from \"../errors/generalError.js\";\nimport { ObjectHelper } from \"../helpers/objectHelper.js\";\nimport type { ISharedObjectBufferOptions } from \"../models/ISharedObjectBufferOptions.js\";\nimport type { ISharedObjectBufferWorkerMessage } from \"../models/ISharedObjectBufferWorkerMessage.js\";\nimport { SharedObjectBufferMessageTypes } from \"../models/sharedObjectBufferMessageTypes.js\";\n\n/**\n * Manages per-object SharedArrayBuffers that store objects as UTF-8 JSON.\n * Buffer layout: 4-byte Int32 header (current data byte length) followed by the JSON-encoded object.\n * Buffers are explicitly created with create and cached in SharedStore.\n * On a worker thread an existing buffer is fetched via a MessagePort handshake\n * (same protocol as Mutex) and cached locally.\n * Buffers grow automatically when the payload exceeds capacity; when the payload drops well below\n * capacity the buffer is replaced with a smaller one. The caller must hold the objectId-keyed Mutex\n * around every read and write. The main thread's worker message handler must forward messages to\n * both Mutex.handleWorkerMessage and SharedObjectBuffer.handleWorkerMessage.\n */\nexport class SharedObjectBuffer {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<SharedObjectBuffer>();\n\n\t/**\n\t * Default payload capacity per object (1 MiB). The first caller that creates the buffer\n\t * for an object determines its initial capacity; later callers that pass a different value\n\t * are ignored.\n\t */\n\tpublic static readonly DEFAULT_CAPACITY_BYTES = 1 * 1024 * 1024;\n\n\t/**\n\t * Default upper bound for how large a buffer may grow (256 MiB).\n\t * Override per-object via the maxCapacityBytes option on create.\n\t */\n\tpublic static readonly MAX_CAPACITY_BYTES = 256 * 1024 * 1024;\n\n\t/**\n\t * Bytes reserved for the data-length header at the start of each buffer.\n\t * @internal\n\t */\n\tprivate static readonly _HEADER_BYTES = 4;\n\n\t/**\n\t * Multiplier applied to the current capacity when growing or sizing a replacement buffer.\n\t * @internal\n\t */\n\tprivate static readonly _GROW_FACTOR = 2;\n\n\t/**\n\t * Fraction of payload capacity below which a buffer is replaced with a smaller one.\n\t * A value of 0.25 means the buffer is shrunk when the payload is below 25 % of capacity.\n\t * Shrinking only applies when the capacity already exceeds DEFAULT_CAPACITY_BYTES.\n\t * @internal\n\t */\n\tprivate static readonly _SHRINK_THRESHOLD = 0.25;\n\n\t/**\n\t * SharedStore key under which the per-object buffer map lives on each thread.\n\t * @internal\n\t */\n\tprivate static readonly _STORE_KEY = \"sharedObjectBuffers\";\n\n\t/**\n\t * Cached worker_threads module; undefined = not yet loaded, null = unavailable.\n\t * @internal\n\t */\n\t// false positive: this is a type not an actual import\n\t// eslint-disable-next-line @typescript-eslint/consistent-type-imports\n\tprivate static _workerThreadsModule: typeof import(\"node:worker_threads\") | null | undefined;\n\n\t/**\n\t * Create the buffer for the given objectId if it does not already exist.\n\t * Must be called while holding Mutex.lock(objectId).\n\t * @param objectId The object id that identifies the buffer.\n\t * @param options Optional capacity configuration used when creating the buffer.\n\t */\n\tpublic static async create(\n\t\tobjectId: string,\n\t\toptions?: ISharedObjectBufferOptions\n\t): Promise<void> {\n\t\tawait SharedObjectBuffer.getOrFetchBuffer(objectId, options ?? {});\n\t}\n\n\t/**\n\t * Read and decode the object stored for the given objectId.\n\t * Must be called while holding Mutex.lock(objectId).\n\t * @param objectId The object id that identifies the buffer.\n\t * @returns The stored object, or undefined when nothing has been written yet.\n\t */\n\tpublic static async read<T>(objectId: string): Promise<T | undefined> {\n\t\tconst buf = await SharedObjectBuffer.getOrFetchBuffer(objectId);\n\t\tif (Is.undefined(buf)) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn SharedObjectBuffer.decode<T>(buf);\n\t}\n\n\t/**\n\t * Encode and write the object into the buffer for the given objectId.\n\t * The buffer must already exist, usually by calling create first.\n\t * When the encoded payload exceeds the current buffer capacity the buffer is grown\n\t * in-place via SharedArrayBuffer.grow so every thread with a reference sees the\n\t * new size without any pointer swap. When the payload is smaller than\n\t * _SHRINK_THRESHOLD of the current capacity and the capacity exceeds\n\t * DEFAULT_CAPACITY_BYTES, the buffer is replaced with a smaller one on the calling thread.\n\t * Must be called while holding Mutex.lock(objectId).\n\t * @param objectId The object id that identifies the buffer.\n\t * @param value The object to persist.\n\t */\n\tpublic static async write<T>(objectId: string, value: T): Promise<void> {\n\t\tconst encoded = ObjectHelper.toBytes(value);\n\t\tlet buf = await SharedObjectBuffer.getOrFetchBuffer(objectId);\n\n\t\tif (Is.undefined(buf)) {\n\t\t\tthrow new GeneralError(SharedObjectBuffer.CLASS_NAME, \"notCreated\", {\n\t\t\t\tobjectId\n\t\t\t});\n\t\t}\n\n\t\tconst payloadCapacity = buf.byteLength - SharedObjectBuffer._HEADER_BYTES;\n\t\tconst maxPayloadCapacity = buf.maxByteLength - SharedObjectBuffer._HEADER_BYTES;\n\n\t\tif (encoded.length > payloadCapacity) {\n\t\t\t// Grow the buffer in-place. SharedArrayBuffer.grow is atomic: all threads that\n\t\t\t// hold a reference see the enlarged buffer without a pointer swap.\n\t\t\tconst newPayloadCapacity = Math.max(\n\t\t\t\tencoded.length,\n\t\t\t\tpayloadCapacity * SharedObjectBuffer._GROW_FACTOR\n\t\t\t);\n\t\t\tconst newByteLength = Math.min(\n\t\t\t\tSharedObjectBuffer._HEADER_BYTES + newPayloadCapacity,\n\t\t\t\tbuf.maxByteLength\n\t\t\t);\n\t\t\tif (encoded.length > newByteLength - SharedObjectBuffer._HEADER_BYTES) {\n\t\t\t\tthrow new GeneralError(SharedObjectBuffer.CLASS_NAME, \"capacityExceeded\", {\n\t\t\t\t\tobjectId,\n\t\t\t\t\trequired: encoded.length,\n\t\t\t\t\tcapacity: maxPayloadCapacity\n\t\t\t\t});\n\t\t\t}\n\t\t\tbuf.grow(newByteLength);\n\t\t} else if (\n\t\t\tpayloadCapacity > SharedObjectBuffer.DEFAULT_CAPACITY_BYTES &&\n\t\t\tencoded.length < payloadCapacity * SharedObjectBuffer._SHRINK_THRESHOLD\n\t\t) {\n\t\t\t// Replace with a smaller buffer when far below capacity. The caller writes\n\t\t\t// the payload into the returned buffer immediately after.\n\t\t\tbuf = SharedObjectBuffer.shrinkBuffer(objectId, encoded.length, buf.maxByteLength);\n\t\t}\n\n\t\tnew Uint8Array(buf, SharedObjectBuffer._HEADER_BYTES).set(encoded);\n\t\tAtomics.store(new Int32Array(buf, 0, 1), 0, encoded.length);\n\t}\n\n\t/**\n\t * Remove the stored object and release the buffer for the given objectId.\n\t * The entry is deleted from the local cache so subsequent reads or writes will\n\t * create or fetch a fresh buffer. Worker threads that have cached the old buffer\n\t * reference continue using it until they restart or re-request via the worker protocol.\n\t * Must be called while holding Mutex.lock(objectId).\n\t * @param objectId The object id that identifies the buffer.\n\t */\n\tpublic static remove(objectId: string): void {\n\t\tdelete SharedObjectBuffer.getBuffers()[objectId];\n\t}\n\n\t/**\n\t * Inspect a message from a worker thread and, if it is a SharedObjectBuffer\n\t * buffer-fetch request, respond to it synchronously.\n\t * Call this from the main thread's worker message handler alongside\n\t * Mutex.handleWorkerMessage.\n\t * @param msg The raw message received from the worker.\n\t * @returns True if the message was a SharedObjectBuffer protocol message, false otherwise.\n\t */\n\tpublic static handleWorkerMessage(msg: unknown): boolean {\n\t\tif (\n\t\t\t!Is.object<ISharedObjectBufferWorkerMessage>(msg) ||\n\t\t\tmsg.type !== SharedObjectBufferMessageTypes.GetBuffer\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst {\n\t\t\tobjectId: objectName,\n\t\t\tport,\n\t\t\tsignal,\n\t\t\toptions\n\t\t} = msg as ISharedObjectBufferWorkerMessage & {\n\t\t\tport: MessagePort;\n\t\t};\n\n\t\tconst buf = Is.object(options)\n\t\t\t? SharedObjectBuffer.getOrCreateBuffer(objectName, options)\n\t\t\t: SharedObjectBuffer.getBuffers()[objectName];\n\n\t\t// Deliver the buffer before waking the worker so it is in the port's receive\n\t\t// queue when Atomics.wait returns (same ordering guarantee as Mutex).\n\t\tport.postMessage({ buffer: buf });\n\t\tAtomics.notify(new Int32Array(signal), 0, 1);\n\t\tport.close();\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Decode the object from a SharedArrayBuffer.\n\t * @param buf The SharedArrayBuffer to decode.\n\t * @returns The decoded object, or undefined when nothing has been written.\n\t * @internal\n\t */\n\tprivate static decode<T>(buf: SharedArrayBuffer): T | undefined {\n\t\tconst dataLen = Atomics.load(new Int32Array(buf, 0, 1), 0);\n\t\tif (dataLen === 0) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst bytes = new Uint8Array(buf, SharedObjectBuffer._HEADER_BYTES, dataLen);\n\t\treturn ObjectHelper.fromBytes<T>(bytes);\n\t}\n\n\t/**\n\t * Return the cached buffer for the object, creating or fetching it if needed.\n\t * Applies a double-check after the async loadWorkerThreads call to handle the\n\t * case where another coroutine populated the cache while this one yielded.\n\t * @param objectId The object id that identifies the buffer.\n\t * @param options Optional capacity configuration when creating the buffer.\n\t * @returns The SharedArrayBuffer for the object.\n\t * @internal\n\t */\n\tprivate static async getOrFetchBuffer(\n\t\tobjectId: string,\n\t\toptions?: ISharedObjectBufferOptions\n\t): Promise<SharedArrayBuffer | undefined> {\n\t\tconst buffers = SharedObjectBuffer.getBuffers();\n\n\t\tif (!Is.undefined(buffers[objectId])) {\n\t\t\treturn buffers[objectId];\n\t\t}\n\n\t\tconst wt = await SharedObjectBuffer.loadWorkerThreads();\n\n\t\t// Re-check after the await: another coroutine may have populated the cache.\n\t\tif (!Is.undefined(buffers[objectId])) {\n\t\t\treturn buffers[objectId];\n\t\t}\n\n\t\tif (Is.empty(wt) || wt.isMainThread) {\n\t\t\tif (!Is.object(options)) {\n\t\t\t\treturn buffers[objectId];\n\t\t\t}\n\t\t\treturn SharedObjectBuffer.getOrCreateBuffer(objectId, options);\n\t\t}\n\n\t\t// Worker thread: synchronously request the SharedArrayBuffer from the main thread.\n\t\tif (Is.empty(wt.parentPort)) {\n\t\t\tthrow new GeneralError(SharedObjectBuffer.CLASS_NAME, \"bufferFetchFailed\", { objectId });\n\t\t}\n\n\t\tconst { port1, port2 } = new wt.MessageChannel();\n\t\tconst signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\n\t\tconst request: ISharedObjectBufferWorkerMessage = {\n\t\t\ttype: SharedObjectBufferMessageTypes.GetBuffer,\n\t\t\tobjectId,\n\t\t\tsignal: signal.buffer,\n\t\t\tport: port2,\n\t\t\toptions\n\t\t};\n\n\t\twt.parentPort.postMessage(request, [port2]);\n\n\t\ttry {\n\t\t\t// Block until the main thread posts the buffer and fires Atomics.notify.\n\t\t\t// The response is guaranteed to be in port1's queue when wait returns because\n\t\t\t// port.postMessage executes before Atomics.notify on the main thread.\n\t\t\tconst waitResult = Atomics.wait(signal, 0, 0, 30_000);\n\t\t\tif (waitResult === \"timed-out\") {\n\t\t\t\tthrow new GeneralError(SharedObjectBuffer.CLASS_NAME, \"bufferFetchFailed\", { objectId });\n\t\t\t}\n\n\t\t\tconst response = wt.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(SharedObjectBuffer.CLASS_NAME, \"bufferFetchFailed\", { objectId });\n\t\t\t}\n\n\t\t\tif (Is.undefined(response.message.buffer)) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\tbuffers[objectId] = response.message.buffer;\n\t\t\treturn buffers[objectId];\n\t\t} finally {\n\t\t\tport1.close();\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the growable buffer on the main thread (or in a fork-mode process).\n\t * @param objectId The object id that identifies the buffer.\n\t * @param options Optional capacity configuration for new buffers.\n\t * @returns The existing or newly created SharedArrayBuffer.\n\t * @internal\n\t */\n\tprivate static getOrCreateBuffer(\n\t\tobjectId: string,\n\t\toptions?: ISharedObjectBufferOptions\n\t): SharedArrayBuffer {\n\t\tconst buffers = SharedObjectBuffer.getBuffers();\n\t\tif (Is.undefined(buffers[objectId])) {\n\t\t\tconst maxCapacity = options?.maxCapacityBytes ?? SharedObjectBuffer.MAX_CAPACITY_BYTES;\n\t\t\tconst initialCapacity = Math.min(\n\t\t\t\toptions?.initialCapacityBytes ?? SharedObjectBuffer.DEFAULT_CAPACITY_BYTES,\n\t\t\t\tmaxCapacity\n\t\t\t);\n\t\t\tbuffers[objectId] = new SharedArrayBuffer(\n\t\t\t\tSharedObjectBuffer._HEADER_BYTES + initialCapacity,\n\t\t\t\t{\n\t\t\t\t\tmaxByteLength: SharedObjectBuffer._HEADER_BYTES + maxCapacity\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t\treturn buffers[objectId];\n\t}\n\n\t/**\n\t * Create a new smaller SharedArrayBuffer, register it in SharedStore, and return it.\n\t * The new buffer is empty; write writes the current payload into it immediately\n\t * after this call returns. Callers on other threads that have cached the old buffer\n\t * reference continue using it until they restart or re-request via the worker protocol.\n\t * @param objectId The object id that identifies the buffer.\n\t * @param dataLen Byte length of the payload that will be written next.\n\t * @param maxByteLength The maxByteLength to preserve from the old buffer.\n\t * @returns The new, smaller SharedArrayBuffer registered in SharedStore.\n\t * @internal\n\t */\n\tprivate static shrinkBuffer(\n\t\tobjectId: string,\n\t\tdataLen: number,\n\t\tmaxByteLength: number\n\t): SharedArrayBuffer {\n\t\tconst newCapacity = Math.max(\n\t\t\tdataLen * SharedObjectBuffer._GROW_FACTOR,\n\t\t\tSharedObjectBuffer.DEFAULT_CAPACITY_BYTES\n\t\t);\n\t\tconst newBuf = new SharedArrayBuffer(SharedObjectBuffer._HEADER_BYTES + newCapacity, {\n\t\t\tmaxByteLength\n\t\t});\n\t\tSharedObjectBuffer.getBuffers()[objectId] = newBuf;\n\t\treturn newBuf;\n\t}\n\n\t/**\n\t * Return the per-thread buffer map from SharedStore, creating it if absent.\n\t * @returns The map of object id to SharedArrayBuffer.\n\t * @internal\n\t */\n\tprivate static getBuffers(): { [objectId: string]: SharedArrayBuffer } {\n\t\tlet buffers = SharedStore.get<{ [objectId: string]: SharedArrayBuffer }>(\n\t\t\tSharedObjectBuffer._STORE_KEY\n\t\t);\n\t\tif (Is.undefined(buffers)) {\n\t\t\tbuffers = {};\n\t\t\tSharedStore.set(SharedObjectBuffer._STORE_KEY, buffers);\n\t\t}\n\t\treturn buffers;\n\t}\n\n\t/**\n\t * Lazily load node:worker_threads, returning null in environments where it is unavailable.\n\t * @returns The worker_threads module or null.\n\t * @internal\n\t */\n\t// false positive: this is a type not an actual import\n\t// eslint-disable-next-line @typescript-eslint/consistent-type-imports\n\tprivate static async loadWorkerThreads(): Promise<typeof import(\"node:worker_threads\") | null> {\n\t\tif (SharedObjectBuffer._workerThreadsModule === undefined) {\n\t\t\ttry {\n\t\t\t\tSharedObjectBuffer._workerThreadsModule = await import(\"node:worker_threads\");\n\t\t\t} catch {\n\t\t\t\tSharedObjectBuffer._workerThreadsModule = null;\n\t\t\t}\n\t\t}\n\t\treturn SharedObjectBuffer._workerThreadsModule;\n\t}\n}\n"]}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
3
|
import { Is } from "./is.js";
|
|
4
4
|
/**
|
|
5
|
-
* Provide a store for shared objects which can be
|
|
6
|
-
* instance loads of a
|
|
5
|
+
* Provide a store for shared objects which can be accessed through multiple
|
|
6
|
+
* instance loads of a package.
|
|
7
7
|
*/
|
|
8
8
|
export class SharedStore {
|
|
9
9
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharedStore.js","sourceRoot":"","sources":["../../../src/utils/sharedStore.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B;;;GAGG;AACH,MAAM,OAAO,WAAW;IACvB;;;;OAIG;IACI,MAAM,CAAC,GAAG,CAAc,IAAY;QAC1C,8DAA8D;QAC9D,MAAM,MAAM,GAAI,UAAkB,CAAC,eAAe,CAAC;QACnD,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,OAAO;QACR,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,CAAM,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,GAAG,CAAc,IAAY,EAAE,KAAQ;QACpD,8DAA8D;QAC9D,IAAI,EAAE,CAAC,SAAS,CAAE,UAAkB,CAAC,eAAe,CAAC,EAAE,CAAC;YACvD,8DAA8D;YAC7D,UAAkB,CAAC,eAAe,GAAG,EAAE,CAAC;QAC1C,CAAC;QAED,8DAA8D;QAC7D,UAAkB,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACnD,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,MAAM,CAAC,IAAY;QAChC,8DAA8D;QAC9D,MAAM,MAAM,GAAI,UAAkB,CAAC,eAAe,CAAC;QACnD,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACF,CAAC;CACD","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is } from \"./is.js\";\n\n/**\n * Provide a store for shared objects which can be
|
|
1
|
+
{"version":3,"file":"sharedStore.js","sourceRoot":"","sources":["../../../src/utils/sharedStore.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B;;;GAGG;AACH,MAAM,OAAO,WAAW;IACvB;;;;OAIG;IACI,MAAM,CAAC,GAAG,CAAc,IAAY;QAC1C,8DAA8D;QAC9D,MAAM,MAAM,GAAI,UAAkB,CAAC,eAAe,CAAC;QACnD,IAAI,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,OAAO;QACR,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,CAAM,CAAC;IAC1B,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,GAAG,CAAc,IAAY,EAAE,KAAQ;QACpD,8DAA8D;QAC9D,IAAI,EAAE,CAAC,SAAS,CAAE,UAAkB,CAAC,eAAe,CAAC,EAAE,CAAC;YACvD,8DAA8D;YAC7D,UAAkB,CAAC,eAAe,GAAG,EAAE,CAAC;QAC1C,CAAC;QAED,8DAA8D;QAC7D,UAAkB,CAAC,eAAe,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IACnD,CAAC;IAED;;;OAGG;IACI,MAAM,CAAC,MAAM,CAAC,IAAY;QAChC,8DAA8D;QAC9D,MAAM,MAAM,GAAI,UAAkB,CAAC,eAAe,CAAC;QACnD,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACF,CAAC;CACD","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is } from \"./is.js\";\n\n/**\n * Provide a store for shared objects which can be accessed through multiple\n * instance loads of a package.\n */\nexport class SharedStore {\n\t/**\n\t * Get a property from the shared store.\n\t * @param prop The name of the property to get.\n\t * @returns The property if it exists.\n\t */\n\tpublic static get<T = unknown>(prop: string): T | undefined {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst shared = (globalThis as any).__TWIN_SHARED__;\n\t\tif (Is.undefined(shared)) {\n\t\t\treturn;\n\t\t}\n\n\t\treturn shared[prop] as T;\n\t}\n\n\t/**\n\t * Set the property in the shared store.\n\t * @param prop The name of the property to set.\n\t * @param value The value to set.\n\t */\n\tpublic static set<T = unknown>(prop: string, value: T): void {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tif (Is.undefined((globalThis as any).__TWIN_SHARED__)) {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\t\t(globalThis as any).__TWIN_SHARED__ = {};\n\t\t}\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\t(globalThis as any).__TWIN_SHARED__[prop] = value;\n\t}\n\n\t/**\n\t * Remove a property from the shared store.\n\t * @param prop The name of the property to remove.\n\t */\n\tpublic static remove(prop: string): void {\n\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\t\tconst shared = (globalThis as any).__TWIN_SHARED__;\n\t\tif (!Is.undefined(shared)) {\n\t\t\tdelete shared[prop];\n\t\t}\n\t}\n}\n"]}
|
|
@@ -14,7 +14,7 @@ export declare class ConflictError extends BaseError {
|
|
|
14
14
|
* @param conflictId The id that has conflicts.
|
|
15
15
|
* @param conflicts The conflicts that occurred.
|
|
16
16
|
* @param properties Any additional information for the error.
|
|
17
|
-
* @param cause The cause
|
|
17
|
+
* @param cause The cause of the error if we have wrapped another error.
|
|
18
18
|
*/
|
|
19
19
|
constructor(source: string, message: string, conflictId?: string, conflicts?: string[], properties?: {
|
|
20
20
|
[id: string]: unknown;
|
|
@@ -6,7 +6,7 @@ export declare class ArrayHelper {
|
|
|
6
6
|
* Do the two arrays match.
|
|
7
7
|
* @param arr1 The first array.
|
|
8
8
|
* @param arr2 The second array.
|
|
9
|
-
* @returns True if both arrays are empty
|
|
9
|
+
* @returns True if both arrays are empty or have the same values.
|
|
10
10
|
*/
|
|
11
11
|
static matches(arr1: unknown, arr2: unknown): boolean;
|
|
12
12
|
/**
|
|
@@ -4,7 +4,7 @@ import type { IError } from "../models/IError.js";
|
|
|
4
4
|
*/
|
|
5
5
|
export declare class ErrorHelper {
|
|
6
6
|
/**
|
|
7
|
-
* Format
|
|
7
|
+
* Format errors and returns just their messages.
|
|
8
8
|
* @param error The error to format.
|
|
9
9
|
* @param options Options for formatting the error.
|
|
10
10
|
* @param options.includeStack Whether to include the stack trace in the output, defaults to false.
|