@twin.org/core 0.9.3-next.1 → 0.9.3-next.10
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/factories/facadeFactory.js +9 -0
- package/dist/es/factories/facadeFactory.js.map +1 -0
- package/dist/es/factories/factory.js +137 -6
- package/dist/es/factories/factory.js.map +1 -1
- package/dist/es/helpers/timeoutHelper.js +46 -0
- package/dist/es/helpers/timeoutHelper.js.map +1 -0
- package/dist/es/index.js +4 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IFacade.js +4 -0
- package/dist/es/models/IFacade.js.map +1 -0
- package/dist/es/models/IMutexWaiter.js +4 -0
- package/dist/es/models/IMutexWaiter.js.map +1 -0
- package/dist/es/utils/lfuCache.js +91 -13
- package/dist/es/utils/lfuCache.js.map +1 -1
- package/dist/es/utils/lruCache.js +83 -10
- package/dist/es/utils/lruCache.js.map +1 -1
- package/dist/es/utils/mutex.js +170 -23
- package/dist/es/utils/mutex.js.map +1 -1
- package/dist/types/factories/facadeFactory.d.ts +6 -0
- package/dist/types/factories/factory.d.ts +22 -2
- package/dist/types/helpers/timeoutHelper.d.ts +17 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/models/IFacade.d.ts +12 -0
- package/dist/types/models/IMutexWaiter.d.ts +17 -0
- package/dist/types/utils/lfuCache.d.ts +8 -2
- package/dist/types/utils/lruCache.d.ts +8 -2
- package/dist/types/utils/mutex.d.ts +9 -0
- package/docs/changelog.md +153 -0
- package/docs/examples.md +38 -1
- package/docs/reference/classes/Factory.md +80 -2
- package/docs/reference/classes/LfuCache.md +18 -2
- package/docs/reference/classes/LruCache.md +18 -2
- package/docs/reference/classes/Mutex.md +9 -0
- package/docs/reference/classes/TimeoutHelper.md +57 -0
- package/docs/reference/index.md +4 -0
- package/docs/reference/interfaces/IFacade.md +32 -0
- package/docs/reference/interfaces/IMutexWaiter.md +37 -0
- package/docs/reference/variables/FacadeFactory.md +5 -0
- package/locales/en.json +5 -2
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lruCache.js","sourceRoot":"","sources":["../../../src/utils/lruCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { nameof } from \"@twin.org/nameof\";\nimport { Guards } from \"./guards.js\";\nimport { Is } from \"./is.js\";\nimport { Mutex } from \"./mutex.js\";\nimport { Validation } from \"./validation.js\";\nimport { RandomHelper } from \"../helpers/randomHelper.js\";\nimport type { IValidationFailure } from \"../models/IValidationFailure.js\";\n\n/**\n * A fixed-capacity LRU cache with time-to-idle eviction.\n *\n * Entries are removed in two ways:\n * - Capacity eviction: when the cache is full the least-recently-used entry is removed first.\n * - TTI eviction: a background timer sweeps idle entries every ttiMs milliseconds.\n * The timer only runs while there are entries; it stops automatically when the cache empties.\n *\n * `get` and `set` both update an entry's LRU position and reset its idle timer.\n * `has` is a pure peek it evicts idle entries but does not refresh a live entry's TTI.\n * Call `destroy` when the cache is no longer needed to stop the background timer.\n */\nexport class LruCache<T = unknown> {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<LruCache>();\n\n\t/**\n\t * Default capacity.\n\t */\n\tpublic static readonly DEFAULT_CAPACITY = 1000;\n\n\t/**\n\t * Default time-to-idle in milliseconds.\n\t */\n\tpublic static readonly DEFAULT_TTI_MS = 10000;\n\n\t/**\n\t * The maximum number of entries the cache will hold.\n\t * @internal\n\t */\n\tprivate readonly _capacity: number;\n\n\t/**\n\t * The idle duration in milliseconds after which an untouched entry is evicted.\n\t * @internal\n\t */\n\tprivate readonly _ttiMs: number;\n\n\t/**\n\t * Optional timeout in milliseconds for mutex acquisition.\n\t * @internal\n\t */\n\tprivate readonly _mutexTimeoutMs: number | undefined;\n\n\t/**\n\t * Per-instance namespace prefix for mutex keys.\n\t * @internal\n\t */\n\tprivate readonly _mutexScope: string;\n\n\t/**\n\t * Underlying storage; Map iteration order tracks LRU position (first = oldest).\n\t * @internal\n\t */\n\tprivate readonly _cache: Map<string, { value: T; lastAccessed: number }>;\n\n\t/**\n\t * Handle for the pending idle-sweep timeout, or undefined if no timer is scheduled.\n\t * @internal\n\t */\n\tprivate _sweepTimer: ReturnType<typeof setTimeout> | undefined;\n\n\t/**\n\t * Create a new instance of LruCache.\n\t * @param options The cache options.\n\t * @param options.capacity Maximum number of entries. Defaults to 1000. Must be a positive integer.\n\t * @param options.ttiMs Time-to-idle in milliseconds. Defaults to 10000. Must be a positive integer.\n\t * @param options.mutexTimeoutMs Maximum time in milliseconds to wait for getOrSet mutex acquisition.\n\t * @throws ValidationError if capacity or ttiMs is not a positive integer.\n\t */\n\tconstructor(options?: { capacity?: number; ttiMs?: number; mutexTimeoutMs?: number }) {\n\t\tconst capacity = options?.capacity ?? LruCache.DEFAULT_CAPACITY;\n\t\tconst ttiMs = options?.ttiMs ?? LruCache.DEFAULT_TTI_MS;\n\t\tconst mutexTimeoutMs = options?.mutexTimeoutMs;\n\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(capacity), capacity);\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(ttiMs), ttiMs);\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tGuards.integer(LruCache.CLASS_NAME, nameof(mutexTimeoutMs), mutexTimeoutMs);\n\t\t}\n\n\t\tconst failures: IValidationFailure[] = [];\n\t\tValidation.integer(nameof(capacity), capacity, failures, undefined, { minValue: 1 });\n\t\tValidation.integer(nameof(ttiMs), ttiMs, failures, undefined, { minValue: 1 });\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tValidation.integer(nameof(mutexTimeoutMs), mutexTimeoutMs, failures, undefined, {\n\t\t\t\tminValue: 0\n\t\t\t});\n\t\t}\n\t\tValidation.asValidationError(LruCache.CLASS_NAME, nameof<LruCache>(), failures);\n\n\t\tthis._capacity = capacity;\n\t\tthis._ttiMs = ttiMs;\n\t\tthis._mutexTimeoutMs = mutexTimeoutMs;\n\t\tthis._mutexScope = `${LruCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;\n\t\tthis._cache = new Map();\n\t\tthis._sweepTimer = undefined;\n\t}\n\n\t/**\n\t * The number of entries currently held in the cache.\n\t * @returns The number of entries in the cache.\n\t */\n\tpublic count(): number {\n\t\treturn this._cache.size;\n\t}\n\n\t/**\n\t * Get a value from the cache.\n\t * Returns undefined if the key is absent or the entry has idled out.\n\t * A successful hit resets the entry's idle timer and moves it to most-recently-used.\n\t * @param key The key to retrieve.\n\t * @returns The cached value, or undefined on a miss or idle eviction.\n\t */\n\tpublic get(key: string): T | undefined {\n\t\tconst entry = this._cache.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = Date.now();\n\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\tthis._cache.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\t\t// Move to end of Map (most-recently-used) via delete + re-insert\n\t\tthis._cache.delete(key);\n\t\tentry.lastAccessed = now;\n\t\tthis._cache.set(key, entry);\n\t\treturn entry.value;\n\t}\n\n\t/**\n\t * Store a value in the cache.\n\t * If the key already exists its value and idle timer are refreshed.\n\t * When the cache is at capacity, idle entries are swept first; if it is still full the\n\t * least-recently-used entry is evicted.\n\t * @param key The key to store.\n\t * @param value The value to cache.\n\t */\n\tpublic set(key: string, value: T): void {\n\t\tconst now = Date.now();\n\t\t// Remove any existing entry so the refreshed version is inserted at the end\n\t\tthis._cache.delete(key);\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tthis.sweepIdle();\n\t\t}\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tconst lruKey = this._cache.keys().next().value;\n\t\t\tif (lruKey !== undefined) {\n\t\t\t\tthis._cache.delete(lruKey);\n\t\t\t}\n\t\t}\n\t\tthis._cache.set(key, { value, lastAccessed: now });\n\t\tthis.startTimer();\n\t}\n\n\t/**\n\t * Atomically get an existing value or create and store it once using an async factory.\n\t * Concurrent calls for the same key are serialized via a mutex.\n\t * @param key The key to get or create.\n\t * @param valueFactory Async callback used to build a value when the key is absent.\n\t * @returns The existing or newly created value.\n\t */\n\tpublic async getOrSet(key: string, valueFactory: () => Promise<T>): Promise<T> {\n\t\tGuards.stringValue(LruCache.CLASS_NAME, nameof(key), key);\n\t\tGuards.function(LruCache.CLASS_NAME, nameof(valueFactory), valueFactory);\n\n\t\tconst mutexKey = `${this._mutexScope}:${key}`;\n\t\tawait Mutex.lock(mutexKey, {\n\t\t\ttimeoutMs: this._mutexTimeoutMs,\n\t\t\tthrowOnTimeout: true\n\t\t});\n\n\t\ttry {\n\t\t\tif (this.has(key)) {\n\t\t\t\treturn this.get(key) as T;\n\t\t\t}\n\n\t\t\tconst value = await valueFactory();\n\t\t\tthis.set(key, value);\n\t\t\treturn value;\n\t\t} finally {\n\t\t\tMutex.unlock(mutexKey);\n\t\t}\n\t}\n\n\t/**\n\t * Check whether a key exists in the cache and has not idled out.\n\t * Idle entries are evicted on peek, but a live entry's TTI is not reset.\n\t * @param key The key to test.\n\t * @returns True if the key is present and not idle.\n\t */\n\tpublic has(key: string): boolean {\n\t\tconst entry = this._cache.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn false;\n\t\t}\n\t\tif (Date.now() - entry.lastAccessed >= this._ttiMs) {\n\t\t\tthis._cache.delete(key);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return all keys for entries that have not idled out.\n\t * Idle entries encountered during iteration are evicted.\n\t * @returns An array of live keys in least-recently-used to most-recently-used order.\n\t */\n\tpublic keys(): string[] {\n\t\tconst now = Date.now();\n\t\tconst result: string[] = [];\n\t\tfor (const [k, entry] of this._cache) {\n\t\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\t\tthis._cache.delete(k);\n\t\t\t} else {\n\t\t\t\tresult.push(k);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * Cancels the background timer if the cache becomes empty.\n\t * @param key The key to remove.\n\t */\n\tpublic delete(key: string): void {\n\t\tthis._cache.delete(key);\n\t\tif (this._cache.size === 0) {\n\t\t\tthis.cancelTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Remove all entries from the cache and cancel the background timer.\n\t */\n\tpublic clear(): void {\n\t\tthis.cancelTimer();\n\t\tthis._cache.clear();\n\t}\n\n\t/**\n\t * Stop the background idle-sweep timer and release all entries.\n\t * The cache must not be used after this call.\n\t */\n\tpublic destroy(): void {\n\t\tthis.cancelTimer();\n\t\tthis._cache.clear();\n\t}\n\n\t/**\n\t * Delete all entries whose idle time has been exceeded, then restart the timer\n\t * if any entries remain.\n\t * @internal\n\t */\n\tprivate sweepIdle(): void {\n\t\tthis.cancelTimer();\n\t\tconst now = Date.now();\n\t\tfor (const [k, entry] of this._cache) {\n\t\t\tif (now - entry.lastAccessed >= this._ttiMs) {\n\t\t\t\tthis._cache.delete(k);\n\t\t\t}\n\t\t}\n\t\tif (this._cache.size > 0) {\n\t\t\tthis.startTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Schedule the next idle sweep if no timer is already pending.\n\t * @internal\n\t */\n\tprivate startTimer(): void {\n\t\tthis._sweepTimer ??= setTimeout(() => this.sweepIdle(), this._ttiMs);\n\t}\n\n\t/**\n\t * Cancel the pending idle-sweep timer.\n\t * @internal\n\t */\n\tprivate cancelTimer(): void {\n\t\tif (Is.notEmpty(this._sweepTimer)) {\n\t\t\tclearTimeout(this._sweepTimer);\n\t\t\tthis._sweepTimer = undefined;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lruCache.js","sourceRoot":"","sources":["../../../src/utils/lruCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { nameof } from \"@twin.org/nameof\";\nimport { Guards } from \"./guards.js\";\nimport { Is } from \"./is.js\";\nimport { Mutex } from \"./mutex.js\";\nimport { Validation } from \"./validation.js\";\nimport { RandomHelper } from \"../helpers/randomHelper.js\";\nimport type { IValidationFailure } from \"../models/IValidationFailure.js\";\n\n/**\n * A fixed-capacity LRU cache with time-to-idle eviction.\n *\n * Entries are removed in two ways:\n * - Capacity eviction: when the cache is full the least-recently-used entry is removed first.\n * - TTI eviction: a background timer sweeps idle entries every ttiMs milliseconds.\n * The timer only runs while there are entries; it stops automatically when the cache empties.\n *\n * `get` and `set` both update an entry's LRU position and reset its idle timer.\n * `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that\n * time is reached however recently it was used, and the TTI still applies alongside it.\n * `has` is a pure peek it evicts idle entries but does not refresh a live entry's TTI.\n * Call `destroy` when the cache is no longer needed to stop the background timer.\n */\nexport class LruCache<T = unknown> {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<LruCache>();\n\n\t/**\n\t * Default capacity.\n\t */\n\tpublic static readonly DEFAULT_CAPACITY = 1000;\n\n\t/**\n\t * Default time-to-idle in milliseconds.\n\t */\n\tpublic static readonly DEFAULT_TTI_MS = 10000;\n\n\t/**\n\t * The maximum number of entries the cache will hold.\n\t * @internal\n\t */\n\tprivate readonly _capacity: number;\n\n\t/**\n\t * The idle duration in milliseconds after which an untouched entry is evicted.\n\t * @internal\n\t */\n\tprivate readonly _ttiMs: number;\n\n\t/**\n\t * Optional timeout in milliseconds for mutex acquisition.\n\t * @internal\n\t */\n\tprivate readonly _mutexTimeoutMs: number | undefined;\n\n\t/**\n\t * Per-instance namespace prefix for mutex keys.\n\t * @internal\n\t */\n\tprivate readonly _mutexScope: string;\n\n\t/**\n\t * Underlying storage; Map iteration order tracks LRU position (first = oldest).\n\t * @internal\n\t */\n\tprivate readonly _cache: Map<\n\t\tstring,\n\t\t{ value: T; lastAccessed: number; expires: number | undefined }\n\t>;\n\n\t/**\n\t * The earliest hard expiry timestamp among the live entries, used to pace the sweep timer.\n\t * @internal\n\t */\n\tprivate _nextExpires: number | undefined;\n\n\t/**\n\t * The timestamp the pending sweep is due to run at.\n\t * @internal\n\t */\n\tprivate _scheduledDueAt: number;\n\n\t/**\n\t * Handle for the pending idle-sweep timeout, or undefined if no timer is scheduled.\n\t * @internal\n\t */\n\tprivate _sweepTimer: ReturnType<typeof setTimeout> | undefined;\n\n\t/**\n\t * Create a new instance of LruCache.\n\t * @param options The cache options.\n\t * @param options.capacity Maximum number of entries. Defaults to 1000. Must be a positive integer.\n\t * @param options.ttiMs Time-to-idle in milliseconds. Defaults to 10000. Must be a positive integer.\n\t * @param options.mutexTimeoutMs Maximum time in milliseconds to wait for getOrSet mutex acquisition.\n\t * @throws ValidationError if capacity or ttiMs is not a positive integer.\n\t */\n\tconstructor(options?: { capacity?: number; ttiMs?: number; mutexTimeoutMs?: number }) {\n\t\tconst capacity = options?.capacity ?? LruCache.DEFAULT_CAPACITY;\n\t\tconst ttiMs = options?.ttiMs ?? LruCache.DEFAULT_TTI_MS;\n\t\tconst mutexTimeoutMs = options?.mutexTimeoutMs;\n\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(capacity), capacity);\n\t\tGuards.integer(LruCache.CLASS_NAME, nameof(ttiMs), ttiMs);\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tGuards.integer(LruCache.CLASS_NAME, nameof(mutexTimeoutMs), mutexTimeoutMs);\n\t\t}\n\n\t\tconst failures: IValidationFailure[] = [];\n\t\tValidation.integer(nameof(capacity), capacity, failures, undefined, { minValue: 1 });\n\t\tValidation.integer(nameof(ttiMs), ttiMs, failures, undefined, { minValue: 1 });\n\t\tif (Is.notEmpty(mutexTimeoutMs)) {\n\t\t\tValidation.integer(nameof(mutexTimeoutMs), mutexTimeoutMs, failures, undefined, {\n\t\t\t\tminValue: 0\n\t\t\t});\n\t\t}\n\t\tValidation.asValidationError(LruCache.CLASS_NAME, nameof<LruCache>(), failures);\n\n\t\tthis._capacity = capacity;\n\t\tthis._ttiMs = ttiMs;\n\t\tthis._mutexTimeoutMs = mutexTimeoutMs;\n\t\tthis._mutexScope = `${LruCache.CLASS_NAME}:${RandomHelper.generateUuidV7()}`;\n\t\tthis._cache = new Map();\n\t\tthis._nextExpires = undefined;\n\t\tthis._scheduledDueAt = 0;\n\t\tthis._sweepTimer = undefined;\n\t}\n\n\t/**\n\t * The number of entries currently held in the cache.\n\t * @returns The number of entries in the cache.\n\t */\n\tpublic count(): number {\n\t\treturn this._cache.size;\n\t}\n\n\t/**\n\t * Get a value from the cache.\n\t * Returns undefined if the key is absent or the entry has idled out.\n\t * A successful hit resets the entry's idle timer and moves it to most-recently-used.\n\t * @param key The key to retrieve.\n\t * @returns The cached value, or undefined on a miss or idle eviction.\n\t */\n\tpublic get(key: string): T | undefined {\n\t\tGuards.stringValue(LruCache.CLASS_NAME, nameof(key), key);\n\n\t\tconst entry = this._cache.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst now = Date.now();\n\t\tif (this.isExpired(entry, now)) {\n\t\t\tthis._cache.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\t\t// Move to end of Map (most-recently-used) via delete + re-insert\n\t\tthis._cache.delete(key);\n\t\tentry.lastAccessed = now;\n\t\tthis._cache.set(key, entry);\n\n\t\treturn entry.value;\n\t}\n\n\t/**\n\t * Store a value in the cache.\n\t * If the key already exists its value and idle timer are refreshed.\n\t * When the cache is at capacity, idle entries are swept first; if it is still full the\n\t * least-recently-used entry is evicted.\n\t * @param key The key to store.\n\t * @param value The value to cache.\n\t * @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed\n\t * once this time is reached regardless of how recently it was used. Must be an integer.\n\t */\n\tpublic set(key: string, value: T, expires?: number): void {\n\t\tGuards.stringValue(LruCache.CLASS_NAME, nameof(key), key);\n\n\t\tif (!Is.empty(expires)) {\n\t\t\tGuards.integer(LruCache.CLASS_NAME, nameof(expires), expires);\n\t\t}\n\n\t\tconst now = Date.now();\n\t\t// Remove any existing entry so the refreshed version is inserted at the end\n\t\tthis._cache.delete(key);\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tthis.sweepIdle();\n\t\t}\n\t\tif (this._cache.size >= this._capacity) {\n\t\t\tconst lruKey = this._cache.keys().next().value;\n\t\t\tif (lruKey !== undefined) {\n\t\t\t\tthis._cache.delete(lruKey);\n\t\t\t}\n\t\t}\n\t\tthis._cache.set(key, { value, lastAccessed: now, expires });\n\t\tthis.trackExpires(expires);\n\t\tthis.startTimer();\n\t}\n\n\t/**\n\t * Atomically get an existing value or create and store it once using an async factory.\n\t * Concurrent calls for the same key are serialized via a mutex.\n\t * @param key The key to get or create.\n\t * @param valueFactory Async callback used to build a value when the key is absent.\n\t * @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry\n\t * when one is created. Must be an integer.\n\t * @returns The existing or newly created value.\n\t */\n\tpublic async getOrSet(key: string, valueFactory: () => Promise<T>, expires?: number): Promise<T> {\n\t\tGuards.stringValue(LruCache.CLASS_NAME, nameof(key), key);\n\t\tGuards.function(LruCache.CLASS_NAME, nameof(valueFactory), valueFactory);\n\t\tif (!Is.empty(expires)) {\n\t\t\tGuards.integer(LruCache.CLASS_NAME, nameof(expires), expires);\n\t\t}\n\n\t\tconst mutexKey = `${this._mutexScope}:${key}`;\n\t\tawait Mutex.lock(mutexKey, {\n\t\t\ttimeoutMs: this._mutexTimeoutMs,\n\t\t\tthrowOnTimeout: true\n\t\t});\n\n\t\ttry {\n\t\t\tif (this.has(key)) {\n\t\t\t\treturn this.get(key) as T;\n\t\t\t}\n\n\t\t\tconst value = await valueFactory();\n\t\t\tthis.set(key, value, expires);\n\t\t\treturn value;\n\t\t} finally {\n\t\t\tMutex.unlock(mutexKey);\n\t\t}\n\t}\n\n\t/**\n\t * Check whether a key exists in the cache and has not idled out.\n\t * Idle entries are evicted on peek, but a live entry's TTI is not reset.\n\t * @param key The key to test.\n\t * @returns True if the key is present and not idle.\n\t */\n\tpublic has(key: string): boolean {\n\t\tGuards.stringValue(LruCache.CLASS_NAME, nameof(key), key);\n\t\tconst entry = this._cache.get(key);\n\t\tif (entry === undefined) {\n\t\t\treturn false;\n\t\t}\n\t\tif (this.isExpired(entry, Date.now())) {\n\t\t\tthis._cache.delete(key);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\t/**\n\t * Return all keys for entries that have not idled out.\n\t * Idle entries encountered during iteration are evicted.\n\t * @returns An array of live keys in least-recently-used to most-recently-used order.\n\t */\n\tpublic keys(): string[] {\n\t\tconst now = Date.now();\n\t\tconst result: string[] = [];\n\t\tfor (const [k, entry] of this._cache) {\n\t\t\tif (this.isExpired(entry, now)) {\n\t\t\t\tthis._cache.delete(k);\n\t\t\t} else {\n\t\t\t\tresult.push(k);\n\t\t\t}\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * Cancels the background timer if the cache becomes empty.\n\t * @param key The key to remove.\n\t */\n\tpublic delete(key: string): void {\n\t\tGuards.stringValue(LruCache.CLASS_NAME, nameof(key), key);\n\t\tthis._cache.delete(key);\n\t\tif (this._cache.size === 0) {\n\t\t\tthis.cancelTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Remove all entries from the cache and cancel the background timer.\n\t */\n\tpublic clear(): void {\n\t\tthis.cancelTimer();\n\t\tthis._cache.clear();\n\t\tthis._nextExpires = undefined;\n\t}\n\n\t/**\n\t * Stop the background idle-sweep timer and release all entries.\n\t * The cache must not be used after this call.\n\t */\n\tpublic destroy(): void {\n\t\tthis.cancelTimer();\n\t\tthis._cache.clear();\n\t\tthis._nextExpires = undefined;\n\t}\n\n\t/**\n\t * Delete all entries whose idle time has been exceeded, then restart the timer\n\t * if any entries remain.\n\t * @internal\n\t */\n\tprivate sweepIdle(): void {\n\t\tthis.cancelTimer();\n\t\tconst now = Date.now();\n\t\tlet nextExpires: number | undefined;\n\t\tfor (const [k, entry] of this._cache) {\n\t\t\tif (this.isExpired(entry, now)) {\n\t\t\t\tthis._cache.delete(k);\n\t\t\t} else if (\n\t\t\t\tIs.notEmpty(entry.expires) &&\n\t\t\t\t(Is.empty(nextExpires) || entry.expires < nextExpires)\n\t\t\t) {\n\t\t\t\tnextExpires = entry.expires;\n\t\t\t}\n\t\t}\n\t\tthis._nextExpires = nextExpires;\n\t\tif (this._cache.size > 0) {\n\t\t\tthis.startTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Record an entry expiry timestamp if it is earlier than the currently tracked one.\n\t * @param expires The expiry timestamp in milliseconds, or undefined for none.\n\t * @internal\n\t */\n\tprivate trackExpires(expires: number | undefined): void {\n\t\tif (Is.notEmpty(expires) && (Is.empty(this._nextExpires) || expires < this._nextExpires)) {\n\t\t\tthis._nextExpires = expires;\n\t\t}\n\t}\n\n\t/**\n\t * Determine whether an entry has idled out or reached its hard expiry timestamp.\n\t * @param entry The entry to test.\n\t * @param entry.lastAccessed The last-accessed timestamp in milliseconds.\n\t * @param entry.expires The hard expiry timestamp in milliseconds, or undefined for none.\n\t * @param now The current time in milliseconds.\n\t * @returns True if the entry should be removed.\n\t * @internal\n\t */\n\tprivate isExpired(\n\t\tentry: { lastAccessed: number; expires: number | undefined },\n\t\tnow: number\n\t): boolean {\n\t\treturn (\n\t\t\tnow - entry.lastAccessed >= this._ttiMs ||\n\t\t\t(Is.notEmpty(entry.expires) && now >= entry.expires)\n\t\t);\n\t}\n\n\t/**\n\t * Schedule the next sweep if no timer is already pending, bringing a pending one forward\n\t * when an entry with an earlier hard expiry has since been added.\n\t * @internal\n\t */\n\tprivate startTimer(): void {\n\t\tconst now = Date.now();\n\t\tlet delay = this._ttiMs;\n\t\tif (Is.notEmpty(this._nextExpires)) {\n\t\t\tdelay = Math.min(delay, Math.max(0, this._nextExpires - now));\n\t\t}\n\t\tif (Is.empty(this._sweepTimer)) {\n\t\t\tthis._scheduledDueAt = now + delay;\n\t\t\tthis._sweepTimer = setTimeout(() => this.sweepIdle(), delay);\n\t\t} else if (now + delay < this._scheduledDueAt) {\n\t\t\tthis.cancelTimer();\n\t\t\tthis.startTimer();\n\t\t}\n\t}\n\n\t/**\n\t * Cancel the pending idle-sweep timer.\n\t * @internal\n\t */\n\tprivate cancelTimer(): void {\n\t\tif (Is.notEmpty(this._sweepTimer)) {\n\t\t\tclearTimeout(this._sweepTimer);\n\t\t\tthis._sweepTimer = undefined;\n\t\t}\n\t}\n}\n"]}
|
package/dist/es/utils/mutex.js
CHANGED
|
@@ -16,6 +16,13 @@ import { MutexMessageTypes } from "../models/mutexMessageTypes.js";
|
|
|
16
16
|
* The main thread must call Mutex.handleWorkerMessage(msg) from its worker message handler
|
|
17
17
|
* before that worker first calls Mutex.lock().
|
|
18
18
|
*
|
|
19
|
+
* Callers on the same thread are served in the order they arrived. Each key has a FIFO
|
|
20
|
+
* queue of waiters, unlock() hands the lock directly to the waiter at the front, and a new
|
|
21
|
+
* caller only takes the lock outright when that queue is empty. Without this a caller that
|
|
22
|
+
* arrives while a waiter is being woken can take the lock first, which lets a busy key
|
|
23
|
+
* starve a waiter until its timeout elapses. Threads still contend with each other for the
|
|
24
|
+
* shared lock, so the ordering guarantee is per thread rather than global.
|
|
25
|
+
*
|
|
19
26
|
* The lock is not re-entrant: a thread that already holds a key and calls lock() again on
|
|
20
27
|
* the same key will block until the timeout elapses.
|
|
21
28
|
*/
|
|
@@ -34,6 +41,23 @@ export class Mutex {
|
|
|
34
41
|
* @internal
|
|
35
42
|
*/
|
|
36
43
|
static _DEFAULT_TIMEOUT_KEY = "mutexDefaultTimeoutMs";
|
|
44
|
+
/**
|
|
45
|
+
* SharedStore key for the per-thread map from lock key strings to FIFO waiter queues.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
static _WAITERS_KEY = "mutexWaiters";
|
|
49
|
+
/**
|
|
50
|
+
* SharedStore key for the per-thread map from lock key strings to the running watch task.
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
static _WATCHERS_KEY = "mutexWatchers";
|
|
54
|
+
/**
|
|
55
|
+
* How long the watch task waits on the shared lock before re-reading the queue, in
|
|
56
|
+
* milliseconds. It only bounds how quickly the task notices that the queue has drained,
|
|
57
|
+
* releases from other threads wake it immediately.
|
|
58
|
+
* @internal
|
|
59
|
+
*/
|
|
60
|
+
static _WATCH_TIMEOUT_MS = 250;
|
|
37
61
|
/**
|
|
38
62
|
* Cached reference to the node:worker_threads module, null if unavailable (browser).
|
|
39
63
|
* @internal
|
|
@@ -65,6 +89,8 @@ export class Mutex {
|
|
|
65
89
|
* held, it suspends the current async task until the lock is released or the timeout is reached.
|
|
66
90
|
* Use this in async single-threaded contexts (e.g. the main thread or a Fastify route handler)
|
|
67
91
|
* where calling the synchronous lock() would freeze the event loop and deadlock.
|
|
92
|
+
* Callers on the same thread are served in the order they arrived, so a contended key
|
|
93
|
+
* cannot starve an earlier caller.
|
|
68
94
|
* The lock is not re-entrant: if the same context holds the key and calls lockAsync() again on
|
|
69
95
|
* the same key, it will suspend until the timeout elapses.
|
|
70
96
|
* @param key The key to lock on.
|
|
@@ -90,31 +116,39 @@ export class Mutex {
|
|
|
90
116
|
// getOrFetchLock may block once per key on worker threads to negotiate the
|
|
91
117
|
// shared buffer with the main thread; that one-time fetch is acceptable here.
|
|
92
118
|
const lock = await Mutex.getOrFetchLock(key, deadline);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
throw new GeneralError(Mutex.CLASS_NAME, "lockTimeout", { key, timeoutMs });
|
|
104
|
-
}
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
// Suspend without blocking the event loop so the lock holder's async
|
|
108
|
-
// continuations can run and eventually call unlock().
|
|
109
|
-
const waitResult = Atomics.waitAsync(lock, 0, 1, remaining);
|
|
110
|
-
const outcome = waitResult.async ? await waitResult.value : waitResult.value;
|
|
111
|
-
if (outcome === "timed-out") {
|
|
112
|
-
if (throwOnTimeout) {
|
|
113
|
-
throw new GeneralError(Mutex.CLASS_NAME, "lockTimeout", { key, timeoutMs });
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
119
|
+
const queue = Mutex.getQueue(key);
|
|
120
|
+
// Atomically swap 0 → 1; if the previous value was 0 we acquired the lock. Only take
|
|
121
|
+
// it outright when nothing on this thread is already queued, otherwise this caller
|
|
122
|
+
// would barge in front of waiters that arrived earlier.
|
|
123
|
+
if (queue.length === 0 && Atomics.compareExchange(lock, 0, 0, 1) === 0) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (deadline - Date.now() <= 0) {
|
|
127
|
+
if (throwOnTimeout) {
|
|
128
|
+
throw new GeneralError(Mutex.CLASS_NAME, "lockTimeout", { key, timeoutMs });
|
|
116
129
|
}
|
|
130
|
+
return false;
|
|
117
131
|
}
|
|
132
|
+
// Join the back of the queue and suspend without blocking the event loop, so the
|
|
133
|
+
// holder's async continuations can run and eventually call unlock(). The lock is
|
|
134
|
+
// handed over either by unlock() on this thread or by the watch task below when
|
|
135
|
+
// another thread releases it.
|
|
136
|
+
const waiter = { settled: false };
|
|
137
|
+
const granted = new Promise(resolve => {
|
|
138
|
+
waiter.resolve = resolve;
|
|
139
|
+
});
|
|
140
|
+
queue.push(waiter);
|
|
141
|
+
// The deadline is enforced by a timer rather than only by the atomic wait, so a
|
|
142
|
+
// congested event loop cannot stretch the wait well beyond the requested timeout.
|
|
143
|
+
waiter.timer = setTimeout(() => Mutex.settleWaiter(key, waiter, false), deadline - Date.now());
|
|
144
|
+
Mutex.startWatching(key, lock);
|
|
145
|
+
if (await granted) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
if (throwOnTimeout) {
|
|
149
|
+
throw new GeneralError(Mutex.CLASS_NAME, "lockTimeout", { key, timeoutMs });
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
118
152
|
}
|
|
119
153
|
/**
|
|
120
154
|
* Releases the lock for the given key.
|
|
@@ -132,6 +166,14 @@ export class Mutex {
|
|
|
132
166
|
if (previous !== 1) {
|
|
133
167
|
throw new GeneralError(Mutex.CLASS_NAME, "lockAlreadyReleased", { key });
|
|
134
168
|
}
|
|
169
|
+
// Hand the lock straight to the caller at the front of this thread's queue. Nothing
|
|
170
|
+
// else on this thread can run between the release above and the re-acquire below, so
|
|
171
|
+
// no later caller can see the released state and take it first.
|
|
172
|
+
const next = Mutex.getWaiters()[key]?.[0];
|
|
173
|
+
if (!Is.empty(next) && Atomics.compareExchange(lock, 0, 0, 1) === 0) {
|
|
174
|
+
Mutex.settleWaiter(key, next, true);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
135
177
|
Atomics.notify(lock, 0, 1);
|
|
136
178
|
}
|
|
137
179
|
/**
|
|
@@ -223,6 +265,111 @@ export class Mutex {
|
|
|
223
265
|
port1.close();
|
|
224
266
|
}
|
|
225
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Settle a queued waiter, remove it from the queue and resume its caller.
|
|
270
|
+
* @param key The lock key.
|
|
271
|
+
* @param waiter The waiter to settle.
|
|
272
|
+
* @param granted True when the waiter has been handed the lock and now owns it.
|
|
273
|
+
* @internal
|
|
274
|
+
*/
|
|
275
|
+
static settleWaiter(key, waiter, granted) {
|
|
276
|
+
if (waiter.settled) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
waiter.settled = true;
|
|
280
|
+
if (!Is.empty(waiter.timer)) {
|
|
281
|
+
clearTimeout(waiter.timer);
|
|
282
|
+
waiter.timer = undefined;
|
|
283
|
+
}
|
|
284
|
+
const queue = Mutex.getWaiters()[key];
|
|
285
|
+
const index = queue?.indexOf(waiter) ?? -1;
|
|
286
|
+
if (index >= 0) {
|
|
287
|
+
queue.splice(index, 1);
|
|
288
|
+
}
|
|
289
|
+
waiter.resolve?.(granted);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Start the watch task for a key if one is not already running.
|
|
293
|
+
* @param key The lock key.
|
|
294
|
+
* @param lock The shared lock for the key.
|
|
295
|
+
* @internal
|
|
296
|
+
*/
|
|
297
|
+
static startWatching(key, lock) {
|
|
298
|
+
const watchers = Mutex.getWatchers();
|
|
299
|
+
watchers[key] ??= Mutex.runWatcher(key, lock);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Run the watch task for a key and clear its registry entry once it ends. The entry is
|
|
303
|
+
* only cleared after an await, so it cannot be removed before startWatching has recorded
|
|
304
|
+
* it, even when the task itself completes without suspending.
|
|
305
|
+
* @param key The lock key.
|
|
306
|
+
* @param lock The shared lock for the key.
|
|
307
|
+
* @returns A promise that resolves when the task ends.
|
|
308
|
+
* @internal
|
|
309
|
+
*/
|
|
310
|
+
static async runWatcher(key, lock) {
|
|
311
|
+
try {
|
|
312
|
+
await Mutex.watchQueue(key, lock);
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
delete Mutex.getWatchers()[key];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Watch the shared lock for a key and hand it to the waiter at the front of the queue.
|
|
320
|
+
* This covers releases from other threads, a release on this thread hands the lock over
|
|
321
|
+
* directly in unlock(). The task ends once the queue for the key has drained.
|
|
322
|
+
* @param key The lock key.
|
|
323
|
+
* @param lock The shared lock for the key.
|
|
324
|
+
* @returns A promise that resolves when the task ends.
|
|
325
|
+
* @internal
|
|
326
|
+
*/
|
|
327
|
+
static async watchQueue(key, lock) {
|
|
328
|
+
for (;;) {
|
|
329
|
+
// Re-read the front of the queue on every pass, waiters that reached their
|
|
330
|
+
// deadline have already removed themselves.
|
|
331
|
+
const head = Mutex.getWaiters()[key]?.[0];
|
|
332
|
+
if (Is.empty(head)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (Atomics.compareExchange(lock, 0, 0, 1) === 0) {
|
|
336
|
+
Mutex.settleWaiter(key, head, true);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const waitResult = Atomics.waitAsync(lock, 0, 1, Mutex._WATCH_TIMEOUT_MS);
|
|
340
|
+
if (waitResult.async) {
|
|
341
|
+
await waitResult.value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Get the FIFO waiter queue for a key, creating it if it does not exist.
|
|
348
|
+
* @param key The lock key.
|
|
349
|
+
* @returns The waiter queue for the key.
|
|
350
|
+
* @internal
|
|
351
|
+
*/
|
|
352
|
+
static getQueue(key) {
|
|
353
|
+
const waiters = Mutex.getWaiters();
|
|
354
|
+
waiters[key] ??= [];
|
|
355
|
+
return waiters[key];
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get the shared waiter queues map, creating it if it does not exist.
|
|
359
|
+
* @returns The shared waiter queues map.
|
|
360
|
+
* @internal
|
|
361
|
+
*/
|
|
362
|
+
static getWaiters() {
|
|
363
|
+
return SharedStore.get(Mutex._WAITERS_KEY, () => ({}));
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Get the shared watch tasks map, creating it if it does not exist.
|
|
367
|
+
* @returns The shared watch tasks map.
|
|
368
|
+
* @internal
|
|
369
|
+
*/
|
|
370
|
+
static getWatchers() {
|
|
371
|
+
return SharedStore.get(Mutex._WATCHERS_KEY, () => ({}));
|
|
372
|
+
}
|
|
226
373
|
/**
|
|
227
374
|
* Get the shared locks map, creating it if it does not exist.
|
|
228
375
|
* @returns The shared locks map.
|
|
@@ -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 * 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\treturn SharedStore.get<{ [key: string]: Int32Array }>(Mutex._LOCKS_KEY, () => ({}));\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 { IMutexWaiter } from \"../models/IMutexWaiter.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 * Callers on the same thread are served in the order they arrived. Each key has a FIFO\n * queue of waiters, unlock() hands the lock directly to the waiter at the front, and a new\n * caller only takes the lock outright when that queue is empty. Without this a caller that\n * arrives while a waiter is being woken can take the lock first, which lets a busy key\n * starve a waiter until its timeout elapses. Threads still contend with each other for the\n * shared lock, so the ordering guarantee is per thread rather than global.\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 * SharedStore key for the per-thread map from lock key strings to FIFO waiter queues.\n\t * @internal\n\t */\n\tprivate static readonly _WAITERS_KEY = \"mutexWaiters\";\n\n\t/**\n\t * SharedStore key for the per-thread map from lock key strings to the running watch task.\n\t * @internal\n\t */\n\tprivate static readonly _WATCHERS_KEY = \"mutexWatchers\";\n\n\t/**\n\t * How long the watch task waits on the shared lock before re-reading the queue, in\n\t * milliseconds. It only bounds how quickly the task notices that the queue has drained,\n\t * releases from other threads wake it immediately.\n\t * @internal\n\t */\n\tprivate static readonly _WATCH_TIMEOUT_MS = 250;\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 * Callers on the same thread are served in the order they arrived, so a contended key\n\t * cannot starve an earlier caller.\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\t\tconst queue = Mutex.getQueue(key);\n\n\t\t// Atomically swap 0 → 1; if the previous value was 0 we acquired the lock. Only take\n\t\t// it outright when nothing on this thread is already queued, otherwise this caller\n\t\t// would barge in front of waiters that arrived earlier.\n\t\tif (queue.length === 0 && Atomics.compareExchange(lock, 0, 0, 1) === 0) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (deadline - Date.now() <= 0) {\n\t\t\tif (throwOnTimeout) {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\n\t\t// Join the back of the queue and suspend without blocking the event loop, so the\n\t\t// holder's async continuations can run and eventually call unlock(). The lock is\n\t\t// handed over either by unlock() on this thread or by the watch task below when\n\t\t// another thread releases it.\n\t\tconst waiter: IMutexWaiter = { settled: false };\n\t\tconst granted = new Promise<boolean>(resolve => {\n\t\t\twaiter.resolve = resolve;\n\t\t});\n\t\tqueue.push(waiter);\n\n\t\t// The deadline is enforced by a timer rather than only by the atomic wait, so a\n\t\t// congested event loop cannot stretch the wait well beyond the requested timeout.\n\t\twaiter.timer = setTimeout(() => Mutex.settleWaiter(key, waiter, false), deadline - Date.now());\n\n\t\tMutex.startWatching(key, lock);\n\n\t\tif (await granted) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (throwOnTimeout) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t}\n\t\treturn false;\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\t// Hand the lock straight to the caller at the front of this thread's queue. Nothing\n\t\t// else on this thread can run between the release above and the re-acquire below, so\n\t\t// no later caller can see the released state and take it first.\n\t\tconst next = Mutex.getWaiters()[key]?.[0];\n\t\tif (!Is.empty(next) && Atomics.compareExchange(lock, 0, 0, 1) === 0) {\n\t\t\tMutex.settleWaiter(key, next, true);\n\t\t\treturn;\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 * Settle a queued waiter, remove it from the queue and resume its caller.\n\t * @param key The lock key.\n\t * @param waiter The waiter to settle.\n\t * @param granted True when the waiter has been handed the lock and now owns it.\n\t * @internal\n\t */\n\tprivate static settleWaiter(key: string, waiter: IMutexWaiter, granted: boolean): void {\n\t\tif (waiter.settled) {\n\t\t\treturn;\n\t\t}\n\t\twaiter.settled = true;\n\n\t\tif (!Is.empty(waiter.timer)) {\n\t\t\tclearTimeout(waiter.timer);\n\t\t\twaiter.timer = undefined;\n\t\t}\n\n\t\tconst queue = Mutex.getWaiters()[key];\n\t\tconst index = queue?.indexOf(waiter) ?? -1;\n\t\tif (index >= 0) {\n\t\t\tqueue.splice(index, 1);\n\t\t}\n\n\t\twaiter.resolve?.(granted);\n\t}\n\n\t/**\n\t * Start the watch task for a key if one is not already running.\n\t * @param key The lock key.\n\t * @param lock The shared lock for the key.\n\t * @internal\n\t */\n\tprivate static startWatching(key: string, lock: Int32Array): void {\n\t\tconst watchers = Mutex.getWatchers();\n\t\twatchers[key] ??= Mutex.runWatcher(key, lock);\n\t}\n\n\t/**\n\t * Run the watch task for a key and clear its registry entry once it ends. The entry is\n\t * only cleared after an await, so it cannot be removed before startWatching has recorded\n\t * it, even when the task itself completes without suspending.\n\t * @param key The lock key.\n\t * @param lock The shared lock for the key.\n\t * @returns A promise that resolves when the task ends.\n\t * @internal\n\t */\n\tprivate static async runWatcher(key: string, lock: Int32Array): Promise<void> {\n\t\ttry {\n\t\t\tawait Mutex.watchQueue(key, lock);\n\t\t} finally {\n\t\t\tdelete Mutex.getWatchers()[key];\n\t\t}\n\t}\n\n\t/**\n\t * Watch the shared lock for a key and hand it to the waiter at the front of the queue.\n\t * This covers releases from other threads, a release on this thread hands the lock over\n\t * directly in unlock(). The task ends once the queue for the key has drained.\n\t * @param key The lock key.\n\t * @param lock The shared lock for the key.\n\t * @returns A promise that resolves when the task ends.\n\t * @internal\n\t */\n\tprivate static async watchQueue(key: string, lock: Int32Array): Promise<void> {\n\t\tfor (;;) {\n\t\t\t// Re-read the front of the queue on every pass, waiters that reached their\n\t\t\t// deadline have already removed themselves.\n\t\t\tconst head = Mutex.getWaiters()[key]?.[0];\n\t\t\tif (Is.empty(head)) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (Atomics.compareExchange(lock, 0, 0, 1) === 0) {\n\t\t\t\tMutex.settleWaiter(key, head, true);\n\t\t\t} else {\n\t\t\t\tconst waitResult = Atomics.waitAsync(lock, 0, 1, Mutex._WATCH_TIMEOUT_MS);\n\t\t\t\tif (waitResult.async) {\n\t\t\t\t\tawait waitResult.value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the FIFO waiter queue for a key, creating it if it does not exist.\n\t * @param key The lock key.\n\t * @returns The waiter queue for the key.\n\t * @internal\n\t */\n\tprivate static getQueue(key: string): IMutexWaiter[] {\n\t\tconst waiters = Mutex.getWaiters();\n\t\twaiters[key] ??= [];\n\t\treturn waiters[key];\n\t}\n\n\t/**\n\t * Get the shared waiter queues map, creating it if it does not exist.\n\t * @returns The shared waiter queues map.\n\t * @internal\n\t */\n\tprivate static getWaiters(): { [key: string]: IMutexWaiter[] } {\n\t\treturn SharedStore.get<{ [key: string]: IMutexWaiter[] }>(Mutex._WAITERS_KEY, () => ({}));\n\t}\n\n\t/**\n\t * Get the shared watch tasks map, creating it if it does not exist.\n\t * @returns The shared watch tasks map.\n\t * @internal\n\t */\n\tprivate static getWatchers(): { [key: string]: Promise<void> } {\n\t\treturn SharedStore.get<{ [key: string]: Promise<void> }>(Mutex._WATCHERS_KEY, () => ({}));\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\treturn SharedStore.get<{ [key: string]: Int32Array }>(Mutex._LOCKS_KEY, () => ({}));\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"]}
|
|
@@ -92,18 +92,38 @@ export declare class Factory<T> {
|
|
|
92
92
|
* Remove all the instances and the generators.
|
|
93
93
|
*/
|
|
94
94
|
clear(): void;
|
|
95
|
+
/**
|
|
96
|
+
* Activate a facade for this factory, so every instance it produces is wrapped by it.
|
|
97
|
+
* An instance is passed through the facades in the order they were activated, so the facade
|
|
98
|
+
* activated first is the outermost. Activating a facade which is already active does nothing.
|
|
99
|
+
* @param name The name of the facade, as registered with the facade factory.
|
|
100
|
+
* @param excludeTypes The instance types the facade is not applied to, named as they are
|
|
101
|
+
* registered with this factory.
|
|
102
|
+
* @throws GuardError if the parameters are invalid.
|
|
103
|
+
* @throws GeneralError if no facade is registered with the name, or the factory is the facade
|
|
104
|
+
* factory itself.
|
|
105
|
+
*/
|
|
106
|
+
useFacade(name: string, excludeTypes?: string[]): void;
|
|
107
|
+
/**
|
|
108
|
+
* Deactivate a facade for this factory. Deactivating a facade which is not active does nothing.
|
|
109
|
+
* @param name The name of the facade to deactivate.
|
|
110
|
+
* @throws GuardError if the parameters are invalid.
|
|
111
|
+
*/
|
|
112
|
+
unuseFacade(name: string): void;
|
|
95
113
|
/**
|
|
96
114
|
* Get all the instances as a map.
|
|
115
|
+
* @param withFacade Return the instances with the active facades applied, defaults to false.
|
|
97
116
|
* @returns The instances as a map.
|
|
98
117
|
*/
|
|
99
|
-
instancesMap(): {
|
|
118
|
+
instancesMap(withFacade?: boolean): {
|
|
100
119
|
[name: string]: T;
|
|
101
120
|
};
|
|
102
121
|
/**
|
|
103
122
|
* Get all the instances as a list in the order they were registered.
|
|
123
|
+
* @param withFacade Return the instances with the active facades applied, defaults to false.
|
|
104
124
|
* @returns The instances as a list in the order they were registered.
|
|
105
125
|
*/
|
|
106
|
-
instancesList(): T[];
|
|
126
|
+
instancesList(withFacade?: boolean): T[];
|
|
107
127
|
/**
|
|
108
128
|
* Get all the generator names in the order they were registered.
|
|
109
129
|
* @returns The ordered generator names.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for bounding operations which can fail to settle.
|
|
3
|
+
*/
|
|
4
|
+
export declare class TimeoutHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Stop waiting for an operation which has not settled within the given time.
|
|
7
|
+
* The operation itself cannot be cancelled, so a timed out operation is abandoned, which is
|
|
8
|
+
* the only option available when the code being called can leave the promise it returned
|
|
9
|
+
* pending forever.
|
|
10
|
+
* @param operation The operation to bound.
|
|
11
|
+
* @param timeoutMs The maximum time to wait in milliseconds, 0 or less waits indefinitely.
|
|
12
|
+
* @param onTimeout Called when the wait expires, throw from it to fail the operation, or
|
|
13
|
+
* return a value to complete it with that value instead.
|
|
14
|
+
* @returns The result of the operation, or the value returned by onTimeout.
|
|
15
|
+
*/
|
|
16
|
+
static withTimeout<T>(operation: Promise<T>, timeoutMs: number, onTimeout: () => T): Promise<T>;
|
|
17
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./errors/unauthorizedError.js";
|
|
|
14
14
|
export * from "./errors/unprocessableError.js";
|
|
15
15
|
export * from "./errors/validationError.js";
|
|
16
16
|
export * from "./factories/componentFactory.js";
|
|
17
|
+
export * from "./factories/facadeFactory.js";
|
|
17
18
|
export * from "./factories/factory.js";
|
|
18
19
|
export * from "./helpers/arrayHelper.js";
|
|
19
20
|
export * from "./helpers/envHelper.js";
|
|
@@ -25,11 +26,13 @@ export * from "./helpers/numberHelper.js";
|
|
|
25
26
|
export * from "./helpers/objectHelper.js";
|
|
26
27
|
export * from "./helpers/randomHelper.js";
|
|
27
28
|
export * from "./helpers/stringHelper.js";
|
|
29
|
+
export * from "./helpers/timeoutHelper.js";
|
|
28
30
|
export * from "./helpers/uint8ArrayHelper.js";
|
|
29
31
|
export * from "./models/coerceType.js";
|
|
30
32
|
export * from "./models/IDuration.js";
|
|
31
33
|
export * from "./models/compressionType.js";
|
|
32
34
|
export * from "./models/IComponent.js";
|
|
35
|
+
export * from "./models/IFacade.js";
|
|
33
36
|
export * from "./models/IError.js";
|
|
34
37
|
export * from "./models/II18nShared.js";
|
|
35
38
|
export * from "./models/IKeyValue.js";
|
|
@@ -37,6 +40,7 @@ export * from "./models/ILabelledValue.js";
|
|
|
37
40
|
export * from "./models/ILocale.js";
|
|
38
41
|
export * from "./models/ILocaleDictionary.js";
|
|
39
42
|
export * from "./models/ILocalesIndex.js";
|
|
43
|
+
export * from "./models/IMutexWaiter.js";
|
|
40
44
|
export * from "./models/IMutexWorkerMessage.js";
|
|
41
45
|
export * from "./models/IPatchOperation.js";
|
|
42
46
|
export * from "./models/ISharedObjectBufferOptions.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A facade wraps a component so a cross cutting concern can be applied to it without the
|
|
3
|
+
* component being modified. The wrapped component is returned in place of the original.
|
|
4
|
+
*/
|
|
5
|
+
export interface IFacade<T = unknown> {
|
|
6
|
+
/**
|
|
7
|
+
* Wrap the target, returning a replacement.
|
|
8
|
+
* @param target The component to wrap.
|
|
9
|
+
* @returns The wrapped component.
|
|
10
|
+
*/
|
|
11
|
+
wrap(target: T): T;
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A caller queued on a mutex key, waiting to be handed the lock.
|
|
3
|
+
*/
|
|
4
|
+
export interface IMutexWaiter {
|
|
5
|
+
/**
|
|
6
|
+
* Has the waiter already been granted the lock or given up waiting.
|
|
7
|
+
*/
|
|
8
|
+
settled: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the queued caller, true when it now owns the lock.
|
|
11
|
+
*/
|
|
12
|
+
resolve?: (granted: boolean) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Timer which enforces the waiter's deadline.
|
|
15
|
+
*/
|
|
16
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
17
|
+
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* The timer only runs while there are entries; it stops automatically when the cache empties.
|
|
10
10
|
*
|
|
11
11
|
* `get` and `set` increment an entry's access frequency and reset its idle timer.
|
|
12
|
+
* `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that
|
|
13
|
+
* time is reached however recently it was used, and the TTI still applies alongside it.
|
|
12
14
|
* `has` and `keys` are pure peeks they evict idle entries but do not affect frequency or TTI.
|
|
13
15
|
* Call `destroy` when the cache is no longer needed to stop the background timer.
|
|
14
16
|
*/
|
|
@@ -58,16 +60,20 @@ export declare class LfuCache<T> {
|
|
|
58
60
|
* least-frequently-used entry is evicted (LRU among ties).
|
|
59
61
|
* @param key The key to store.
|
|
60
62
|
* @param value The value to cache.
|
|
63
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed
|
|
64
|
+
* once this time is reached regardless of how recently it was used. Must be an integer.
|
|
61
65
|
*/
|
|
62
|
-
set(key: string, value: T): void;
|
|
66
|
+
set(key: string, value: T, expires?: number): void;
|
|
63
67
|
/**
|
|
64
68
|
* Atomically get an existing value or create and store it once using an async factory.
|
|
65
69
|
* Concurrent calls for the same key are serialized via a mutex.
|
|
66
70
|
* @param key The key to get or create.
|
|
67
71
|
* @param valueFactory Async callback used to build a value when the key is absent.
|
|
72
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry
|
|
73
|
+
* when one is created. Must be an integer.
|
|
68
74
|
* @returns The existing or newly created value.
|
|
69
75
|
*/
|
|
70
|
-
getOrSet(key: string, valueFactory: () => Promise<T
|
|
76
|
+
getOrSet(key: string, valueFactory: () => Promise<T>, expires?: number): Promise<T>;
|
|
71
77
|
/**
|
|
72
78
|
* Check whether a key exists in the cache and has not idled out.
|
|
73
79
|
* Idle entries are evicted on peek, but a live entry's frequency and TTI are not updated.
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* The timer only runs while there are entries; it stops automatically when the cache empties.
|
|
8
8
|
*
|
|
9
9
|
* `get` and `set` both update an entry's LRU position and reset its idle timer.
|
|
10
|
+
* `set` and `getOrSet` accept an optional hard expiry timestamp; the entry is removed once that
|
|
11
|
+
* time is reached however recently it was used, and the TTI still applies alongside it.
|
|
10
12
|
* `has` is a pure peek it evicts idle entries but does not refresh a live entry's TTI.
|
|
11
13
|
* Call `destroy` when the cache is no longer needed to stop the background timer.
|
|
12
14
|
*/
|
|
@@ -56,16 +58,20 @@ export declare class LruCache<T = unknown> {
|
|
|
56
58
|
* least-recently-used entry is evicted.
|
|
57
59
|
* @param key The key to store.
|
|
58
60
|
* @param value The value to cache.
|
|
61
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch. The entry is removed
|
|
62
|
+
* once this time is reached regardless of how recently it was used. Must be an integer.
|
|
59
63
|
*/
|
|
60
|
-
set(key: string, value: T): void;
|
|
64
|
+
set(key: string, value: T, expires?: number): void;
|
|
61
65
|
/**
|
|
62
66
|
* Atomically get an existing value or create and store it once using an async factory.
|
|
63
67
|
* Concurrent calls for the same key are serialized via a mutex.
|
|
64
68
|
* @param key The key to get or create.
|
|
65
69
|
* @param valueFactory Async callback used to build a value when the key is absent.
|
|
70
|
+
* @param expires Hard expiry timestamp in milliseconds since the epoch, applied to the entry
|
|
71
|
+
* when one is created. Must be an integer.
|
|
66
72
|
* @returns The existing or newly created value.
|
|
67
73
|
*/
|
|
68
|
-
getOrSet(key: string, valueFactory: () => Promise<T
|
|
74
|
+
getOrSet(key: string, valueFactory: () => Promise<T>, expires?: number): Promise<T>;
|
|
69
75
|
/**
|
|
70
76
|
* Check whether a key exists in the cache and has not idled out.
|
|
71
77
|
* Idle entries are evicted on peek, but a live entry's TTI is not reset.
|