@twin.org/core 0.0.3-next.45 → 0.0.3-next.47
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/helpers/errorHelper.js +18 -14
- package/dist/es/helpers/errorHelper.js.map +1 -1
- package/dist/es/index.js +3 -0
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IMutexWorkerMessage.js +2 -0
- package/dist/es/models/IMutexWorkerMessage.js.map +1 -0
- package/dist/es/models/mutexMessageTypes.js +13 -0
- package/dist/es/models/mutexMessageTypes.js.map +1 -0
- package/dist/es/utils/asyncCache.js +139 -18
- package/dist/es/utils/asyncCache.js.map +1 -1
- package/dist/es/utils/mutex.js +185 -0
- package/dist/es/utils/mutex.js.map +1 -0
- package/dist/types/helpers/errorHelper.d.ts +11 -4
- package/dist/types/index.d.ts +3 -0
- package/dist/types/models/IMutexWorkerMessage.d.ts +23 -0
- package/dist/types/models/mutexMessageTypes.d.ts +13 -0
- package/dist/types/utils/asyncCache.d.ts +3 -2
- package/dist/types/utils/mutex.d.ts +53 -0
- package/docs/changelog.md +34 -0
- package/docs/reference/classes/AsyncCache.md +3 -2
- package/docs/reference/classes/ErrorHelper.md +17 -7
- package/docs/reference/classes/Mutex.md +128 -0
- package/docs/reference/index.md +4 -0
- package/docs/reference/interfaces/IMutexWorkerMessage.md +35 -0
- package/docs/reference/type-aliases/MutexMessageTypes.md +5 -0
- package/docs/reference/variables/MutexMessageTypes.md +13 -0
- package/locales/en.json +6 -0
- package/package.json +2 -2
|
@@ -11,23 +11,27 @@ export class ErrorHelper {
|
|
|
11
11
|
/**
|
|
12
12
|
* Format Errors and returns just their messages.
|
|
13
13
|
* @param error The error to format.
|
|
14
|
-
* @param
|
|
14
|
+
* @param options Options for formatting the error.
|
|
15
|
+
* @param options.includeStack Whether to include the stack trace in the output, defaults to false.
|
|
16
|
+
* @param options.includeAdditional Whether to include additional error information in the output, defaults to false.
|
|
15
17
|
* @returns The error formatted including any causes errors.
|
|
16
18
|
*/
|
|
17
|
-
static formatErrors(error,
|
|
19
|
+
static formatErrors(error, options) {
|
|
18
20
|
const localizedErrors = ErrorHelper.localizeErrors(error);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
21
|
+
const output = [];
|
|
22
|
+
const includeAdditional = options?.includeAdditional ?? false;
|
|
23
|
+
const includeStack = options?.includeStack ?? false;
|
|
24
|
+
for (const err of localizedErrors) {
|
|
25
|
+
let detailedError = err.message;
|
|
26
|
+
if (includeAdditional && Is.arrayValue(err.additional)) {
|
|
27
|
+
detailedError += `\n${err.additional.join("\n")}`;
|
|
28
|
+
}
|
|
29
|
+
if (includeStack && Is.stringValue(err.stack)) {
|
|
30
|
+
detailedError += `\n${err.stack}`;
|
|
27
31
|
}
|
|
28
|
-
|
|
32
|
+
output.push(detailedError);
|
|
29
33
|
}
|
|
30
|
-
return
|
|
34
|
+
return output;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Localize the content of an error and any causes.
|
|
@@ -62,7 +66,7 @@ export class ErrorHelper {
|
|
|
62
66
|
localizedError.stack = lines.join("\n");
|
|
63
67
|
}
|
|
64
68
|
const additional = ErrorHelper.formatValidationErrors(err);
|
|
65
|
-
if (Is.
|
|
69
|
+
if (Is.arrayValue(additional)) {
|
|
66
70
|
localizedError.additional = additional;
|
|
67
71
|
}
|
|
68
72
|
formattedErrors.push(localizedError);
|
|
@@ -93,7 +97,7 @@ export class ErrorHelper {
|
|
|
93
97
|
}
|
|
94
98
|
validationErrors.push(v);
|
|
95
99
|
}
|
|
96
|
-
return validationErrors
|
|
100
|
+
return validationErrors;
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errorHelper.js","sourceRoot":"","sources":["../../../src/helpers/errorHelper.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAEpC;;GAEG;AACH,MAAM,OAAO,WAAW;IACvB
|
|
1
|
+
{"version":3,"file":"errorHelper.js","sourceRoot":"","sources":["../../../src/helpers/errorHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { StringHelper } from \"./stringHelper.js\";\nimport { BaseError } from \"../errors/baseError.js\";\nimport type { IError } from \"../models/IError.js\";\nimport type { IValidationFailure } from \"../models/IValidationFailure.js\";\nimport { I18n } from \"../utils/i18n.js\";\nimport { Is } from \"../utils/is.js\";\n\n/**\n * Error helper functions.\n */\nexport class ErrorHelper {\n\t/**\n\t * Format Errors and returns just their messages.\n\t * @param error The error to format.\n\t * @param options Options for formatting the error.\n\t * @param options.includeStack Whether to include the stack trace in the output, defaults to false.\n\t * @param options.includeAdditional Whether to include additional error information in the output, defaults to false.\n\t * @returns The error formatted including any causes errors.\n\t */\n\tpublic static formatErrors(\n\t\terror: unknown,\n\t\toptions?: {\n\t\t\tincludeStack?: boolean;\n\t\t\tincludeAdditional?: boolean;\n\t\t}\n\t): string[] {\n\t\tconst localizedErrors = ErrorHelper.localizeErrors(error);\n\n\t\tconst output: string[] = [];\n\n\t\tconst includeAdditional = options?.includeAdditional ?? false;\n\t\tconst includeStack = options?.includeStack ?? false;\n\n\t\tfor (const err of localizedErrors) {\n\t\t\tlet detailedError = err.message;\n\t\t\tif (includeAdditional && Is.arrayValue(err.additional)) {\n\t\t\t\tdetailedError += `\\n${err.additional.join(\"\\n\")}`;\n\t\t\t}\n\t\t\tif (includeStack && Is.stringValue(err.stack)) {\n\t\t\t\tdetailedError += `\\n${err.stack}`;\n\t\t\t}\n\t\t\toutput.push(detailedError);\n\t\t}\n\n\t\treturn output;\n\t}\n\n\t/**\n\t * Localize the content of an error and any causes.\n\t * @param error The error to format.\n\t * @returns The localized version of the errors flattened.\n\t */\n\tpublic static localizeErrors(error: unknown): (IError & { additional?: string[] })[] {\n\t\tconst formattedErrors: (IError & { additional?: string[] })[] = [];\n\n\t\tif (Is.notEmpty(error)) {\n\t\t\tconst errors = BaseError.flatten(error);\n\n\t\t\tfor (const err of errors) {\n\t\t\t\tconst errorNameKey = `errorNames.${StringHelper.camelCase(err.name)}`;\n\t\t\t\tconst errorMessageKey = `error.${err.message}`;\n\n\t\t\t\t// If there is no error message then it is probably\n\t\t\t\t// from a 3rd party lib, so don't format it just display\n\t\t\t\tconst hasErrorName = I18n.hasMessage(errorNameKey);\n\t\t\t\tconst hasErrorMessage = I18n.hasMessage(errorMessageKey);\n\n\t\t\t\tconst localizedError: IError & { additional?: string[] } = {\n\t\t\t\t\tname: I18n.formatMessage(hasErrorName ? errorNameKey : \"errorNames.error\"),\n\t\t\t\t\tmessage: hasErrorMessage\n\t\t\t\t\t\t? I18n.formatMessage(errorMessageKey, err.properties)\n\t\t\t\t\t\t: err.message\n\t\t\t\t};\n\n\t\t\t\tif (Is.stringValue(err.source)) {\n\t\t\t\t\tlocalizedError.source = err.source;\n\t\t\t\t}\n\t\t\t\tif (Is.stringValue(err.stack)) {\n\t\t\t\t\t// Remove the first line from the stack traces as they\n\t\t\t\t\t// just have the error type and message duplicated\n\t\t\t\t\tconst lines = err.stack.split(\"\\n\");\n\t\t\t\t\tlines.shift();\n\t\t\t\t\tlocalizedError.stack = lines.join(\"\\n\");\n\t\t\t\t}\n\n\t\t\t\tconst additional = ErrorHelper.formatValidationErrors(err);\n\t\t\t\tif (Is.arrayValue(additional)) {\n\t\t\t\t\tlocalizedError.additional = additional;\n\t\t\t\t}\n\n\t\t\t\tformattedErrors.push(localizedError);\n\t\t\t}\n\t\t}\n\n\t\treturn formattedErrors;\n\t}\n\n\t/**\n\t * Localize the content of an error and any causes.\n\t * @param error The error to format.\n\t * @returns The localized version of the errors flattened.\n\t */\n\tpublic static formatValidationErrors(error: IError): string[] | undefined {\n\t\tif (\n\t\t\tIs.object(error.properties) &&\n\t\t\tObject.keys(error.properties).length > 0 &&\n\t\t\tIs.object<{ validationFailures: IValidationFailure[] }>(error.properties) &&\n\t\t\tIs.arrayValue(error.properties.validationFailures)\n\t\t) {\n\t\t\tconst validationErrors: string[] = [];\n\t\t\tfor (const validationFailure of error.properties.validationFailures) {\n\t\t\t\tconst errorI18n = `error.${validationFailure.reason}`;\n\t\t\t\tconst errorMessage = I18n.hasMessage(errorI18n)\n\t\t\t\t\t? I18n.formatMessage(errorI18n, validationFailure.properties)\n\t\t\t\t\t: errorI18n;\n\n\t\t\t\tlet v = `${validationFailure.property}: ${errorMessage}`;\n\t\t\t\tif (\n\t\t\t\t\tIs.object<{ value: unknown }>(validationFailure.properties) &&\n\t\t\t\t\tIs.notEmpty(validationFailure.properties.value)\n\t\t\t\t) {\n\t\t\t\t\tv += ` = ${JSON.stringify(validationFailure.properties.value)}`;\n\t\t\t\t}\n\t\t\t\tvalidationErrors.push(v);\n\t\t\t}\n\t\t\treturn validationErrors;\n\t\t}\n\t}\n}\n"]}
|
package/dist/es/index.js
CHANGED
|
@@ -40,9 +40,11 @@ export * from "./models/ILabelledValue.js";
|
|
|
40
40
|
export * from "./models/ILocale.js";
|
|
41
41
|
export * from "./models/ILocaleDictionary.js";
|
|
42
42
|
export * from "./models/ILocalesIndex.js";
|
|
43
|
+
export * from "./models/IMutexWorkerMessage.js";
|
|
43
44
|
export * from "./models/IPatchOperation.js";
|
|
44
45
|
export * from "./models/IUrlParts.js";
|
|
45
46
|
export * from "./models/IValidationFailure.js";
|
|
47
|
+
export * from "./models/mutexMessageTypes.js";
|
|
46
48
|
export * from "./types/bitString.js";
|
|
47
49
|
export * from "./types/objectOrArray.js";
|
|
48
50
|
export * from "./types/singleOccurrenceArray.js";
|
|
@@ -56,6 +58,7 @@ export * from "./utils/converter.js";
|
|
|
56
58
|
export * from "./utils/guards.js";
|
|
57
59
|
export * from "./utils/i18n.js";
|
|
58
60
|
export * from "./utils/is.js";
|
|
61
|
+
export * from "./utils/mutex.js";
|
|
59
62
|
export * from "./utils/sharedStore.js";
|
|
60
63
|
export * from "./utils/validation.js";
|
|
61
64
|
//# sourceMappingURL=index.js.map
|
package/dist/es/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC;AACtC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./encoding/base32.js\";\nexport * from \"./encoding/base58.js\";\nexport * from \"./encoding/base64.js\";\nexport * from \"./encoding/base64Url.js\";\nexport * from \"./errors/alreadyExistsError.js\";\nexport * from \"./errors/baseError.js\";\nexport * from \"./errors/conflictError.js\";\nexport * from \"./errors/generalError.js\";\nexport * from \"./errors/guardError.js\";\nexport * from \"./errors/notFoundError.js\";\nexport * from \"./errors/notImplementedError.js\";\nexport * from \"./errors/notSupportedError.js\";\nexport * from \"./errors/unauthorizedError.js\";\nexport * from \"./errors/unprocessableError.js\";\nexport * from \"./errors/validationError.js\";\nexport * from \"./factories/componentFactory.js\";\nexport * from \"./factories/factory.js\";\nexport * from \"./helpers/arrayHelper.js\";\nexport * from \"./helpers/envHelper.js\";\nexport * from \"./helpers/errorHelper.js\";\nexport * from \"./helpers/filenameHelper.js\";\nexport * from \"./helpers/hexHelper.js\";\nexport * from \"./helpers/jsonHelper.js\";\nexport * from \"./helpers/numberHelper.js\";\nexport * from \"./helpers/objectHelper.js\";\nexport * from \"./helpers/randomHelper.js\";\nexport * from \"./helpers/stringHelper.js\";\nexport * from \"./helpers/uint8ArrayHelper.js\";\nexport * from \"./models/coerceType.js\";\nexport * from \"./models/compressionType.js\";\nexport * from \"./models/healthStatus.js\";\nexport * from \"./models/IComponent.js\";\nexport * from \"./models/IError.js\";\nexport * from \"./models/IHealth.js\";\nexport * from \"./models/II18nShared.js\";\nexport * from \"./models/IKeyValue.js\";\nexport * from \"./models/ILabelledValue.js\";\nexport * from \"./models/ILocale.js\";\nexport * from \"./models/ILocaleDictionary.js\";\nexport * from \"./models/ILocalesIndex.js\";\nexport * from \"./models/IPatchOperation.js\";\nexport * from \"./models/IUrlParts.js\";\nexport * from \"./models/IValidationFailure.js\";\nexport * from \"./types/bitString.js\";\nexport * from \"./types/objectOrArray.js\";\nexport * from \"./types/singleOccurrenceArray.js\";\nexport * from \"./types/singleOccurrenceArrayDepthHelper.js\";\nexport * from \"./types/url.js\";\nexport * from \"./types/urn.js\";\nexport * from \"./utils/asyncCache.js\";\nexport * from \"./utils/coerce.js\";\nexport * from \"./utils/compression.js\";\nexport * from \"./utils/converter.js\";\nexport * from \"./utils/guards.js\";\nexport * from \"./utils/i18n.js\";\nexport * from \"./utils/is.js\";\nexport * from \"./utils/sharedStore.js\";\nexport * from \"./utils/validation.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iCAAiC,CAAC;AAChD,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AACpC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uBAAuB,CAAC;AACtC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kCAAkC,CAAC;AACjD,cAAc,6CAA6C,CAAC;AAC5D,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./encoding/base32.js\";\nexport * from \"./encoding/base58.js\";\nexport * from \"./encoding/base64.js\";\nexport * from \"./encoding/base64Url.js\";\nexport * from \"./errors/alreadyExistsError.js\";\nexport * from \"./errors/baseError.js\";\nexport * from \"./errors/conflictError.js\";\nexport * from \"./errors/generalError.js\";\nexport * from \"./errors/guardError.js\";\nexport * from \"./errors/notFoundError.js\";\nexport * from \"./errors/notImplementedError.js\";\nexport * from \"./errors/notSupportedError.js\";\nexport * from \"./errors/unauthorizedError.js\";\nexport * from \"./errors/unprocessableError.js\";\nexport * from \"./errors/validationError.js\";\nexport * from \"./factories/componentFactory.js\";\nexport * from \"./factories/factory.js\";\nexport * from \"./helpers/arrayHelper.js\";\nexport * from \"./helpers/envHelper.js\";\nexport * from \"./helpers/errorHelper.js\";\nexport * from \"./helpers/filenameHelper.js\";\nexport * from \"./helpers/hexHelper.js\";\nexport * from \"./helpers/jsonHelper.js\";\nexport * from \"./helpers/numberHelper.js\";\nexport * from \"./helpers/objectHelper.js\";\nexport * from \"./helpers/randomHelper.js\";\nexport * from \"./helpers/stringHelper.js\";\nexport * from \"./helpers/uint8ArrayHelper.js\";\nexport * from \"./models/coerceType.js\";\nexport * from \"./models/compressionType.js\";\nexport * from \"./models/healthStatus.js\";\nexport * from \"./models/IComponent.js\";\nexport * from \"./models/IError.js\";\nexport * from \"./models/IHealth.js\";\nexport * from \"./models/II18nShared.js\";\nexport * from \"./models/IKeyValue.js\";\nexport * from \"./models/ILabelledValue.js\";\nexport * from \"./models/ILocale.js\";\nexport * from \"./models/ILocaleDictionary.js\";\nexport * from \"./models/ILocalesIndex.js\";\nexport * from \"./models/IMutexWorkerMessage.js\";\nexport * from \"./models/IPatchOperation.js\";\nexport * from \"./models/IUrlParts.js\";\nexport * from \"./models/IValidationFailure.js\";\nexport * from \"./models/mutexMessageTypes.js\";\nexport * from \"./types/bitString.js\";\nexport * from \"./types/objectOrArray.js\";\nexport * from \"./types/singleOccurrenceArray.js\";\nexport * from \"./types/singleOccurrenceArrayDepthHelper.js\";\nexport * from \"./types/url.js\";\nexport * from \"./types/urn.js\";\nexport * from \"./utils/asyncCache.js\";\nexport * from \"./utils/coerce.js\";\nexport * from \"./utils/compression.js\";\nexport * from \"./utils/converter.js\";\nexport * from \"./utils/guards.js\";\nexport * from \"./utils/i18n.js\";\nexport * from \"./utils/is.js\";\nexport * from \"./utils/mutex.js\";\nexport * from \"./utils/sharedStore.js\";\nexport * from \"./utils/validation.js\";\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IMutexWorkerMessage.js","sourceRoot":"","sources":["../../../src/models/IMutexWorkerMessage.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { MessagePort } from \"node:worker_threads\";\nimport type { MutexMessageTypes } from \"./mutexMessageTypes.js\";\n\n/**\n * Message sent from a worker thread to the main thread to request a SharedArrayBuffer for a given mutex key.\n */\nexport interface IMutexWorkerMessage {\n\t/**\n\t * The message type.\n\t */\n\ttype: typeof MutexMessageTypes.GetBuffer;\n\n\t/**\n\t * The mutex key for which the buffer is requested.\n\t */\n\tkey: string;\n\n\t/**\n\t * The SharedArrayBuffer for the mutex, sent from the worker to the main thread.\n\t */\n\tsignal: SharedArrayBuffer;\n\n\t/**\n\t * The MessagePort for the main thread to respond with the buffer, sent from the worker to the main thread.\n\t */\n\tport: MessagePort;\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Copyright 2026 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
/**
|
|
4
|
+
* Mutex message types.
|
|
5
|
+
*/
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
7
|
+
export const MutexMessageTypes = {
|
|
8
|
+
/**
|
|
9
|
+
* Get buffer.
|
|
10
|
+
*/
|
|
11
|
+
GetBuffer: "twin:mutex:getBuffer"
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=mutexMessageTypes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutexMessageTypes.js","sourceRoot":"","sources":["../../../src/models/mutexMessageTypes.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AAEvC;;GAEG;AACH,gEAAgE;AAChE,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAChC;;OAEG;IACH,SAAS,EAAE,sBAAsB;CACxB,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Mutex message types.\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport const MutexMessageTypes = {\n\t/**\n\t * Get buffer.\n\t */\n\tGetBuffer: \"twin:mutex:getBuffer\"\n} as const;\n\n/**\n * Mutex message types.\n */\nexport type MutexMessageTypes = (typeof MutexMessageTypes)[keyof typeof MutexMessageTypes];\n"]}
|
|
@@ -6,6 +6,26 @@ import { SharedStore } from "./sharedStore.js";
|
|
|
6
6
|
* Cache the results from asynchronous requests.
|
|
7
7
|
*/
|
|
8
8
|
export class AsyncCache {
|
|
9
|
+
/**
|
|
10
|
+
* Cache key for the shared cache object in the SharedStore.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
static _CACHE_KEY = "asyncCache";
|
|
14
|
+
/**
|
|
15
|
+
* Cache key for the entry key of the soonest-expiring cache entry, used to optimize cleanup.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
static _NEXT_EXPIRY_CACHE_KEY = "asyncCacheNextExpiryKey";
|
|
19
|
+
/**
|
|
20
|
+
* Cache key for the timestamp of the last full cleanup scan.
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
static _LAST_CLEANUP_KEY = "asyncCacheLastCleanup";
|
|
24
|
+
/**
|
|
25
|
+
* Minimum interval in ms between full cleanup scans.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
static _CLEANUP_INTERVAL_MS = 5000;
|
|
9
29
|
/**
|
|
10
30
|
* Execute an async request and cache the result.
|
|
11
31
|
* @param key The key for the entry in the cache.
|
|
@@ -21,6 +41,9 @@ export class AsyncCache {
|
|
|
21
41
|
return requestMethod();
|
|
22
42
|
}
|
|
23
43
|
AsyncCache.cleanupExpired();
|
|
44
|
+
// Cleanup will not necessarily remove the entry for the key we are requesting
|
|
45
|
+
// as it is throttled, so we also check and evict if expired here to ensure we don't return stale data.
|
|
46
|
+
AsyncCache.evictIfExpired(key);
|
|
24
47
|
const cache = AsyncCache.getSharedCache();
|
|
25
48
|
const cachedEntry = cache[key];
|
|
26
49
|
// Do we have a cache entry for the key
|
|
@@ -52,12 +75,14 @@ export class AsyncCache {
|
|
|
52
75
|
return wait;
|
|
53
76
|
}
|
|
54
77
|
// If we don't have a cache entry, create a new one
|
|
78
|
+
const expires = Date.now() + ttlMs;
|
|
55
79
|
const cacheEntry = {
|
|
56
80
|
inProgress: true,
|
|
57
81
|
promiseQueue: [],
|
|
58
|
-
expires
|
|
82
|
+
expires
|
|
59
83
|
};
|
|
60
84
|
cache[key] = cacheEntry;
|
|
85
|
+
AsyncCache.updateNextExpiryKey(key, expires);
|
|
61
86
|
// Return a promise that wraps the original request method
|
|
62
87
|
// so that we can store any results or errors in the cache
|
|
63
88
|
return new Promise((resolve, reject) => {
|
|
@@ -109,33 +134,40 @@ export class AsyncCache {
|
|
|
109
134
|
/**
|
|
110
135
|
* Get an entry from the cache.
|
|
111
136
|
* @param key The key to get from the cache.
|
|
112
|
-
* @returns The item from the cache if it exists
|
|
137
|
+
* @returns The item from the cache if it exists, or undefined if the key is missing, expired, or
|
|
138
|
+
* its request is still in-progress. Throws if a cached failure exists for the key.
|
|
113
139
|
*/
|
|
114
140
|
static async get(key) {
|
|
141
|
+
if (AsyncCache.evictIfExpired(key)) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
115
144
|
const cache = AsyncCache.getSharedCache();
|
|
116
|
-
|
|
145
|
+
const entry = cache[key];
|
|
146
|
+
if (!Is.empty(entry?.result)) {
|
|
117
147
|
// If the cache has already resulted in a value, resolve it
|
|
118
|
-
return
|
|
148
|
+
return entry.result;
|
|
119
149
|
}
|
|
120
|
-
if (!Is.empty(
|
|
150
|
+
if (!Is.empty(entry?.error)) {
|
|
121
151
|
// If the cache has already resulted in an error, reject it
|
|
122
|
-
throw
|
|
152
|
+
throw entry.error;
|
|
123
153
|
}
|
|
124
154
|
}
|
|
125
155
|
/**
|
|
126
156
|
* Set an entry into the cache.
|
|
127
157
|
* @param key The key to set in the cache.
|
|
128
158
|
* @param value The value to set in the cache.
|
|
129
|
-
* @param ttlMs The TTL of the entry in the cache in
|
|
159
|
+
* @param ttlMs The TTL of the entry in the cache in milliseconds. Defaults to 1000 (1 second).
|
|
130
160
|
* @returns Nothing.
|
|
131
161
|
*/
|
|
132
162
|
static async set(key, value, ttlMs) {
|
|
163
|
+
const expires = Date.now() + (ttlMs ?? 1000);
|
|
133
164
|
const cache = AsyncCache.getSharedCache();
|
|
134
165
|
cache[key] = {
|
|
135
166
|
result: value,
|
|
136
167
|
promiseQueue: [],
|
|
137
|
-
expires
|
|
168
|
+
expires
|
|
138
169
|
};
|
|
170
|
+
AsyncCache.updateNextExpiryKey(key, expires);
|
|
139
171
|
}
|
|
140
172
|
/**
|
|
141
173
|
* Remove an entry from the cache.
|
|
@@ -144,6 +176,10 @@ export class AsyncCache {
|
|
|
144
176
|
static remove(key) {
|
|
145
177
|
const cache = AsyncCache.getSharedCache();
|
|
146
178
|
delete cache[key];
|
|
179
|
+
const nextKey = SharedStore.get(AsyncCache._NEXT_EXPIRY_CACHE_KEY);
|
|
180
|
+
if (nextKey === key) {
|
|
181
|
+
AsyncCache.recalculateNextKey();
|
|
182
|
+
}
|
|
147
183
|
}
|
|
148
184
|
/**
|
|
149
185
|
* Clear the cache.
|
|
@@ -152,28 +188,45 @@ export class AsyncCache {
|
|
|
152
188
|
static clearCache(prefix) {
|
|
153
189
|
const cache = AsyncCache.getSharedCache();
|
|
154
190
|
if (Is.stringValue(prefix)) {
|
|
191
|
+
const nextKey = SharedStore.get(AsyncCache._NEXT_EXPIRY_CACHE_KEY);
|
|
192
|
+
let nextKeyRemoved = false;
|
|
155
193
|
for (const entry in cache) {
|
|
156
194
|
if (entry.startsWith(prefix)) {
|
|
195
|
+
if (entry === nextKey) {
|
|
196
|
+
nextKeyRemoved = true;
|
|
197
|
+
}
|
|
157
198
|
delete cache[entry];
|
|
158
199
|
}
|
|
159
200
|
}
|
|
201
|
+
if (nextKeyRemoved) {
|
|
202
|
+
AsyncCache.recalculateNextKey();
|
|
203
|
+
}
|
|
160
204
|
}
|
|
161
205
|
else {
|
|
162
|
-
SharedStore.set(
|
|
206
|
+
SharedStore.set(AsyncCache._CACHE_KEY, {});
|
|
207
|
+
SharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);
|
|
208
|
+
SharedStore.set(AsyncCache._LAST_CLEANUP_KEY, Date.now());
|
|
163
209
|
}
|
|
164
210
|
}
|
|
165
211
|
/**
|
|
166
212
|
* Perform a cleanup of the expired entries in the cache.
|
|
167
213
|
*/
|
|
168
214
|
static cleanupExpired() {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
cache[entry].inProgress !== true) {
|
|
174
|
-
delete cache[entry];
|
|
175
|
-
}
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
const lastCleanup = SharedStore.get(AsyncCache._LAST_CLEANUP_KEY) ?? 0;
|
|
217
|
+
if (now - lastCleanup < AsyncCache._CLEANUP_INTERVAL_MS) {
|
|
218
|
+
return;
|
|
176
219
|
}
|
|
220
|
+
const expiry = AsyncCache.getNextExpiry();
|
|
221
|
+
if (expiry !== undefined && expiry > now) {
|
|
222
|
+
// Nothing is expired yet. Stamp the time so the throttle suppresses further checks
|
|
223
|
+
// for the next interval. Per-key freshness is still guaranteed because exec() and
|
|
224
|
+
// get() call evictIfExpired() directly before reading the cache.
|
|
225
|
+
SharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
SharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);
|
|
229
|
+
AsyncCache.recalculateNextKey();
|
|
177
230
|
}
|
|
178
231
|
/**
|
|
179
232
|
* Get the shared cache.
|
|
@@ -181,10 +234,10 @@ export class AsyncCache {
|
|
|
181
234
|
* @internal
|
|
182
235
|
*/
|
|
183
236
|
static getSharedCache() {
|
|
184
|
-
let sharedCache = SharedStore.get(
|
|
237
|
+
let sharedCache = SharedStore.get(AsyncCache._CACHE_KEY);
|
|
185
238
|
if (Is.undefined(sharedCache)) {
|
|
186
239
|
sharedCache = {};
|
|
187
|
-
SharedStore.set(
|
|
240
|
+
SharedStore.set(AsyncCache._CACHE_KEY, sharedCache);
|
|
188
241
|
}
|
|
189
242
|
return sharedCache;
|
|
190
243
|
}
|
|
@@ -202,5 +255,73 @@ export class AsyncCache {
|
|
|
202
255
|
reject(waitErr);
|
|
203
256
|
}
|
|
204
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Scan all cache entries, delete any that have expired, and record the key of the
|
|
260
|
+
* soonest-expiring remaining entry.
|
|
261
|
+
* @internal
|
|
262
|
+
*/
|
|
263
|
+
static recalculateNextKey() {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const cache = AsyncCache.getSharedCache();
|
|
266
|
+
if (Object.keys(cache).length === 0) {
|
|
267
|
+
SharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let newNextKey;
|
|
271
|
+
let newNextExpiry = Number.MAX_SAFE_INTEGER;
|
|
272
|
+
for (const entryKey in cache) {
|
|
273
|
+
const { expires, inProgress } = cache[entryKey];
|
|
274
|
+
if (expires > 0 && expires < now && inProgress !== true) {
|
|
275
|
+
delete cache[entryKey];
|
|
276
|
+
}
|
|
277
|
+
else if (expires > 0 && expires < newNextExpiry) {
|
|
278
|
+
newNextExpiry = expires;
|
|
279
|
+
newNextKey = entryKey;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
SharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, newNextKey);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Update the tracked next-expiry entry key if the given entry expires sooner than the current one.
|
|
286
|
+
* @param key The cache entry key.
|
|
287
|
+
* @param expires The expiry timestamp of the entry.
|
|
288
|
+
* @internal
|
|
289
|
+
*/
|
|
290
|
+
static updateNextExpiryKey(key, expires) {
|
|
291
|
+
const expiry = AsyncCache.getNextExpiry();
|
|
292
|
+
if (expiry !== undefined && expiry <= expires) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
SharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, key);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Deletes the given key from the cache if it has expired and is not in-progress.
|
|
299
|
+
* Returns true if the entry was evicted.
|
|
300
|
+
* @param key The cache entry key to check.
|
|
301
|
+
* @internal
|
|
302
|
+
*/
|
|
303
|
+
static evictIfExpired(key) {
|
|
304
|
+
const cache = AsyncCache.getSharedCache();
|
|
305
|
+
const entry = cache[key];
|
|
306
|
+
if (!Is.empty(entry) && entry.expires > 0 && entry.expires < Date.now() && !entry.inProgress) {
|
|
307
|
+
delete cache[key];
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Returns the expiry timestamp of the tracked next-expiry entry, or undefined if there
|
|
314
|
+
* is no tracked entry or it is no longer present in the cache.
|
|
315
|
+
* @internal
|
|
316
|
+
*/
|
|
317
|
+
static getNextExpiry() {
|
|
318
|
+
const nextKey = SharedStore.get(AsyncCache._NEXT_EXPIRY_CACHE_KEY);
|
|
319
|
+
if (Is.empty(nextKey)) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const cache = AsyncCache.getSharedCache();
|
|
323
|
+
const nextEntry = cache[nextKey];
|
|
324
|
+
return Is.empty(nextEntry) ? undefined : nextEntry.expires;
|
|
325
|
+
}
|
|
205
326
|
}
|
|
206
327
|
//# sourceMappingURL=asyncCache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asyncCache.js","sourceRoot":"","sources":["../../../src/utils/asyncCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\n\n/**\n * Cache the results from asynchronous requests.\n */\nexport class AsyncCache {\n\t/**\n\t * Execute an async request and cache the result.\n\t * @param key The key for the entry in the cache.\n\t * @param ttlMs The TTL of the entry in the cache.\n\t * @param requestMethod The method to call if not cached.\n\t * @param cacheFailures Cache failure results, defaults to false.\n\t * @returns The response.\n\t */\n\tpublic static async exec<T = unknown>(\n\t\tkey: string,\n\t\tttlMs: number | undefined,\n\t\trequestMethod: () => Promise<T>,\n\t\tcacheFailures?: boolean\n\t): Promise<T> {\n\t\tconst cacheEnabled = Is.integer(ttlMs) && ttlMs > 0;\n\t\tif (!cacheEnabled) {\n\t\t\t// No caching, just execute the request method\n\t\t\treturn requestMethod();\n\t\t}\n\n\t\tAsyncCache.cleanupExpired();\n\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tconst cachedEntry = cache[key];\n\n\t\t// Do we have a cache entry for the key\n\t\tif (cachedEntry) {\n\t\t\tif (!Is.empty(cachedEntry.result)) {\n\t\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\t\treturn Promise.resolve(cachedEntry.result);\n\t\t\t} else if (!Is.empty(cachedEntry.error)) {\n\t\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\t\treturn Promise.reject(cachedEntry.error);\n\t\t\t}\n\n\t\t\t// Otherwise create a promise to return and store the resolver\n\t\t\t// and rejector in the cache entry, so that we can call then\n\t\t\t// when the request is done\n\n\t\t\tlet storedResolve: ((value: T | PromiseLike<T>) => void) | undefined;\n\t\t\tlet storedReject: ((reason?: unknown) => void) | undefined;\n\t\t\tconst wait = new Promise<T>((resolve, reject) => {\n\t\t\t\tstoredResolve = resolve;\n\t\t\t\tstoredReject = reject;\n\t\t\t});\n\t\t\tif (!Is.empty(storedResolve) && !Is.empty(storedReject)) {\n\t\t\t\tcachedEntry.promiseQueue.push({\n\t\t\t\t\trequestMethod,\n\t\t\t\t\tresolve: storedResolve,\n\t\t\t\t\treject: storedReject\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn wait;\n\t\t}\n\n\t\t// If we don't have a cache entry, create a new one\n\t\tconst cacheEntry: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t} = {\n\t\t\tinProgress: true,\n\t\t\tpromiseQueue: [],\n\t\t\texpires: Date.now() + ttlMs\n\t\t};\n\t\tcache[key] = cacheEntry;\n\n\t\t// Return a promise that wraps the original request method\n\t\t// so that we can store any results or errors in the cache\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// Call the request method and store the result\n\t\t\trequestMethod()\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.then(res => {\n\t\t\t\t\t// If the request was successful, store the result\n\t\t\t\t\tcacheEntry.inProgress = false;\n\t\t\t\t\tcacheEntry.result = res;\n\n\t\t\t\t\t// and resolve both this promise and all the waiters\n\t\t\t\t\tresolve(res);\n\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\twait.resolve(res);\n\t\t\t\t\t}\n\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\treturn res;\n\t\t\t\t})\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\t// Reject the promise\n\t\t\t\t\treject(err);\n\t\t\t\t\tcacheEntry.inProgress = false;\n\n\t\t\t\t\t// Handle the waiters based on the cacheFailures flag\n\t\t\t\t\tif (cacheFailures ?? false) {\n\t\t\t\t\t\t// If we are caching failures, store the error and reject the waiters\n\t\t\t\t\t\tcacheEntry.error = err;\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\twait.reject(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Clear the waiters so we don't call them again\n\t\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If not caching failures for any queued requests we\n\t\t\t\t\t\t// have no value to either resolve or reject, so we\n\t\t\t\t\t\t// just resolve with the original request method\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-floating-promises\n\t\t\t\t\t\t\tAsyncCache.resolveWaiter(wait.requestMethod, wait.resolve, wait.reject);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (cache[key] === cacheEntry) {\n\t\t\t\t\t\t\tdelete cache[key];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Get an entry from the cache.\n\t * @param key The key to get from the cache.\n\t * @returns The item from the cache if it exists.\n\t */\n\tpublic static async get<T = unknown>(key: string): Promise<T | undefined> {\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tif (!Is.empty(cache[key]?.result)) {\n\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\treturn cache[key].result;\n\t\t}\n\n\t\tif (!Is.empty(cache[key]?.error)) {\n\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\tthrow cache[key].error as Error;\n\t\t}\n\t}\n\n\t/**\n\t * Set an entry into the cache.\n\t * @param key The key to set in the cache.\n\t * @param value The value to set in the cache.\n\t * @param ttlMs The TTL of the entry in the cache in ms, defaults to 1s.\n\t * @returns Nothing.\n\t */\n\tpublic static async set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void> {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tcache[key] = {\n\t\t\tresult: value,\n\t\t\tpromiseQueue: [],\n\t\t\texpires: Date.now() + (ttlMs ?? 1000)\n\t\t};\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * @param key The key to remove from the cache.\n\t */\n\tpublic static remove(key: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tdelete cache[key];\n\t}\n\n\t/**\n\t * Clear the cache.\n\t * @param prefix Optional prefix to clear only entries with that prefix.\n\t */\n\tpublic static clearCache(prefix?: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tif (Is.stringValue(prefix)) {\n\t\t\tfor (const entry in cache) {\n\t\t\t\tif (entry.startsWith(prefix)) {\n\t\t\t\t\tdelete cache[entry];\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tSharedStore.set(\"asyncCache\", {});\n\t\t}\n\t}\n\n\t/**\n\t * Perform a cleanup of the expired entries in the cache.\n\t */\n\tpublic static cleanupExpired(): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tfor (const entry in cache) {\n\t\t\tif (\n\t\t\t\tcache[entry].expires > 0 &&\n\t\t\t\tcache[entry].expires < Date.now() &&\n\t\t\t\tcache[entry].inProgress !== true\n\t\t\t) {\n\t\t\t\tdelete cache[entry];\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get the shared cache.\n\t * @returns The shared cache.\n\t * @internal\n\t */\n\tprivate static getSharedCache<T = unknown>(): {\n\t\t[url: string]: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t};\n\t} {\n\t\tlet sharedCache = SharedStore.get<{\n\t\t\t[url: string]: {\n\t\t\t\tresult?: T;\n\t\t\t\terror?: unknown;\n\t\t\t\tinProgress?: boolean;\n\t\t\t\tpromiseQueue: {\n\t\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t\t}[];\n\t\t\t\texpires: number;\n\t\t\t};\n\t\t}>(\"asyncCache\");\n\n\t\tif (Is.undefined(sharedCache)) {\n\t\t\tsharedCache = {};\n\t\t\tSharedStore.set(\"asyncCache\", sharedCache);\n\t\t}\n\n\t\treturn sharedCache;\n\t}\n\n\t/**\n\t * Resolve a waiter by re-running its request method safely.\n\t * @param requestMethod The method to execute.\n\t * @param resolve The resolver for the waiter.\n\t * @param reject The rejector for the waiter.\n\t */\n\tprivate static async resolveWaiter<T>(\n\t\trequestMethod: () => Promise<T>,\n\t\tresolve: (value: T | PromiseLike<T>) => void,\n\t\treject: (reason?: unknown) => void\n\t): Promise<void> {\n\t\ttry {\n\t\t\tresolve(await requestMethod());\n\t\t} catch (waitErr) {\n\t\t\treject(waitErr);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asyncCache.js","sourceRoot":"","sources":["../../../src/utils/asyncCache.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\n\n/**\n * Cache the results from asynchronous requests.\n */\nexport class AsyncCache {\n\t/**\n\t * Cache key for the shared cache object in the SharedStore.\n\t * @internal\n\t */\n\tprivate static readonly _CACHE_KEY = \"asyncCache\";\n\n\t/**\n\t * Cache key for the entry key of the soonest-expiring cache entry, used to optimize cleanup.\n\t * @internal\n\t */\n\tprivate static readonly _NEXT_EXPIRY_CACHE_KEY = \"asyncCacheNextExpiryKey\";\n\n\t/**\n\t * Cache key for the timestamp of the last full cleanup scan.\n\t * @internal\n\t */\n\tprivate static readonly _LAST_CLEANUP_KEY = \"asyncCacheLastCleanup\";\n\n\t/**\n\t * Minimum interval in ms between full cleanup scans.\n\t * @internal\n\t */\n\tprivate static readonly _CLEANUP_INTERVAL_MS = 5000;\n\n\t/**\n\t * Execute an async request and cache the result.\n\t * @param key The key for the entry in the cache.\n\t * @param ttlMs The TTL of the entry in the cache.\n\t * @param requestMethod The method to call if not cached.\n\t * @param cacheFailures Cache failure results, defaults to false.\n\t * @returns The response.\n\t */\n\tpublic static async exec<T = unknown>(\n\t\tkey: string,\n\t\tttlMs: number | undefined,\n\t\trequestMethod: () => Promise<T>,\n\t\tcacheFailures?: boolean\n\t): Promise<T> {\n\t\tconst cacheEnabled = Is.integer(ttlMs) && ttlMs > 0;\n\t\tif (!cacheEnabled) {\n\t\t\t// No caching, just execute the request method\n\t\t\treturn requestMethod();\n\t\t}\n\n\t\tAsyncCache.cleanupExpired();\n\t\t// Cleanup will not necessarily remove the entry for the key we are requesting\n\t\t// as it is throttled, so we also check and evict if expired here to ensure we don't return stale data.\n\t\tAsyncCache.evictIfExpired(key);\n\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tconst cachedEntry = cache[key];\n\n\t\t// Do we have a cache entry for the key\n\t\tif (cachedEntry) {\n\t\t\tif (!Is.empty(cachedEntry.result)) {\n\t\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\t\treturn Promise.resolve(cachedEntry.result);\n\t\t\t} else if (!Is.empty(cachedEntry.error)) {\n\t\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\t\treturn Promise.reject(cachedEntry.error);\n\t\t\t}\n\n\t\t\t// Otherwise create a promise to return and store the resolver\n\t\t\t// and rejector in the cache entry, so that we can call then\n\t\t\t// when the request is done\n\n\t\t\tlet storedResolve: ((value: T | PromiseLike<T>) => void) | undefined;\n\t\t\tlet storedReject: ((reason?: unknown) => void) | undefined;\n\t\t\tconst wait = new Promise<T>((resolve, reject) => {\n\t\t\t\tstoredResolve = resolve;\n\t\t\t\tstoredReject = reject;\n\t\t\t});\n\t\t\tif (!Is.empty(storedResolve) && !Is.empty(storedReject)) {\n\t\t\t\tcachedEntry.promiseQueue.push({\n\t\t\t\t\trequestMethod,\n\t\t\t\t\tresolve: storedResolve,\n\t\t\t\t\treject: storedReject\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn wait;\n\t\t}\n\n\t\t// If we don't have a cache entry, create a new one\n\t\tconst expires = Date.now() + ttlMs;\n\t\tconst cacheEntry: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t} = {\n\t\t\tinProgress: true,\n\t\t\tpromiseQueue: [],\n\t\t\texpires\n\t\t};\n\t\tcache[key] = cacheEntry;\n\t\tAsyncCache.updateNextExpiryKey(key, expires);\n\n\t\t// Return a promise that wraps the original request method\n\t\t// so that we can store any results or errors in the cache\n\t\treturn new Promise((resolve, reject) => {\n\t\t\t// Call the request method and store the result\n\t\t\trequestMethod()\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.then(res => {\n\t\t\t\t\t// If the request was successful, store the result\n\t\t\t\t\tcacheEntry.inProgress = false;\n\t\t\t\t\tcacheEntry.result = res;\n\n\t\t\t\t\t// and resolve both this promise and all the waiters\n\t\t\t\t\tresolve(res);\n\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\twait.resolve(res);\n\t\t\t\t\t}\n\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\treturn res;\n\t\t\t\t})\n\t\t\t\t// eslint-disable-next-line promise/prefer-await-to-then\n\t\t\t\t.catch((err: unknown) => {\n\t\t\t\t\t// Reject the promise\n\t\t\t\t\treject(err);\n\t\t\t\t\tcacheEntry.inProgress = false;\n\n\t\t\t\t\t// Handle the waiters based on the cacheFailures flag\n\t\t\t\t\tif (cacheFailures ?? false) {\n\t\t\t\t\t\t// If we are caching failures, store the error and reject the waiters\n\t\t\t\t\t\tcacheEntry.error = err;\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\twait.reject(err);\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Clear the waiters so we don't call them again\n\t\t\t\t\t\tcacheEntry.promiseQueue = [];\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// If not caching failures for any queued requests we\n\t\t\t\t\t\t// have no value to either resolve or reject, so we\n\t\t\t\t\t\t// just resolve with the original request method\n\t\t\t\t\t\tfor (const wait of cacheEntry.promiseQueue) {\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-floating-promises\n\t\t\t\t\t\t\tAsyncCache.resolveWaiter(wait.requestMethod, wait.resolve, wait.reject);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (cache[key] === cacheEntry) {\n\t\t\t\t\t\t\tdelete cache[key];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t});\n\t}\n\n\t/**\n\t * Get an entry from the cache.\n\t * @param key The key to get from the cache.\n\t * @returns The item from the cache if it exists, or undefined if the key is missing, expired, or\n\t * its request is still in-progress. Throws if a cached failure exists for the key.\n\t */\n\tpublic static async get<T = unknown>(key: string): Promise<T | undefined> {\n\t\tif (AsyncCache.evictIfExpired(key)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst cache = AsyncCache.getSharedCache<T>();\n\t\tconst entry = cache[key];\n\n\t\tif (!Is.empty(entry?.result)) {\n\t\t\t// If the cache has already resulted in a value, resolve it\n\t\t\treturn entry.result;\n\t\t}\n\n\t\tif (!Is.empty(entry?.error)) {\n\t\t\t// If the cache has already resulted in an error, reject it\n\t\t\tthrow entry.error as Error;\n\t\t}\n\t}\n\n\t/**\n\t * Set an entry into the cache.\n\t * @param key The key to set in the cache.\n\t * @param value The value to set in the cache.\n\t * @param ttlMs The TTL of the entry in the cache in milliseconds. Defaults to 1000 (1 second).\n\t * @returns Nothing.\n\t */\n\tpublic static async set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void> {\n\t\tconst expires = Date.now() + (ttlMs ?? 1000);\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tcache[key] = {\n\t\t\tresult: value,\n\t\t\tpromiseQueue: [],\n\t\t\texpires\n\t\t};\n\t\tAsyncCache.updateNextExpiryKey(key, expires);\n\t}\n\n\t/**\n\t * Remove an entry from the cache.\n\t * @param key The key to remove from the cache.\n\t */\n\tpublic static remove(key: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tdelete cache[key];\n\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\tif (nextKey === key) {\n\t\t\tAsyncCache.recalculateNextKey();\n\t\t}\n\t}\n\n\t/**\n\t * Clear the cache.\n\t * @param prefix Optional prefix to clear only entries with that prefix.\n\t */\n\tpublic static clearCache(prefix?: string): void {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tif (Is.stringValue(prefix)) {\n\t\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\t\tlet nextKeyRemoved = false;\n\t\t\tfor (const entry in cache) {\n\t\t\t\tif (entry.startsWith(prefix)) {\n\t\t\t\t\tif (entry === nextKey) {\n\t\t\t\t\t\tnextKeyRemoved = true;\n\t\t\t\t\t}\n\t\t\t\t\tdelete cache[entry];\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (nextKeyRemoved) {\n\t\t\t\tAsyncCache.recalculateNextKey();\n\t\t\t}\n\t\t} else {\n\t\t\tSharedStore.set(AsyncCache._CACHE_KEY, {});\n\t\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);\n\t\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, Date.now());\n\t\t}\n\t}\n\n\t/**\n\t * Perform a cleanup of the expired entries in the cache.\n\t */\n\tpublic static cleanupExpired(): void {\n\t\tconst now = Date.now();\n\t\tconst lastCleanup = SharedStore.get<number>(AsyncCache._LAST_CLEANUP_KEY) ?? 0;\n\t\tif (now - lastCleanup < AsyncCache._CLEANUP_INTERVAL_MS) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst expiry = AsyncCache.getNextExpiry();\n\t\tif (expiry !== undefined && expiry > now) {\n\t\t\t// Nothing is expired yet. Stamp the time so the throttle suppresses further checks\n\t\t\t// for the next interval. Per-key freshness is still guaranteed because exec() and\n\t\t\t// get() call evictIfExpired() directly before reading the cache.\n\t\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);\n\t\t\treturn;\n\t\t}\n\n\t\tSharedStore.set(AsyncCache._LAST_CLEANUP_KEY, now);\n\t\tAsyncCache.recalculateNextKey();\n\t}\n\n\t/**\n\t * Get the shared cache.\n\t * @returns The shared cache.\n\t * @internal\n\t */\n\tprivate static getSharedCache<T = unknown>(): {\n\t\t[url: string]: {\n\t\t\tresult?: T;\n\t\t\terror?: unknown;\n\t\t\tinProgress?: boolean;\n\t\t\tpromiseQueue: {\n\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t}[];\n\t\t\texpires: number;\n\t\t};\n\t} {\n\t\tlet sharedCache = SharedStore.get<{\n\t\t\t[url: string]: {\n\t\t\t\tresult?: T;\n\t\t\t\terror?: unknown;\n\t\t\t\tinProgress?: boolean;\n\t\t\t\tpromiseQueue: {\n\t\t\t\t\trequestMethod: () => Promise<T>;\n\t\t\t\t\tresolve: (value: T | PromiseLike<T>) => void;\n\t\t\t\t\treject: (reason?: unknown) => void;\n\t\t\t\t}[];\n\t\t\t\texpires: number;\n\t\t\t};\n\t\t}>(AsyncCache._CACHE_KEY);\n\n\t\tif (Is.undefined(sharedCache)) {\n\t\t\tsharedCache = {};\n\t\t\tSharedStore.set(AsyncCache._CACHE_KEY, sharedCache);\n\t\t}\n\n\t\treturn sharedCache;\n\t}\n\n\t/**\n\t * Resolve a waiter by re-running its request method safely.\n\t * @param requestMethod The method to execute.\n\t * @param resolve The resolver for the waiter.\n\t * @param reject The rejector for the waiter.\n\t */\n\tprivate static async resolveWaiter<T>(\n\t\trequestMethod: () => Promise<T>,\n\t\tresolve: (value: T | PromiseLike<T>) => void,\n\t\treject: (reason?: unknown) => void\n\t): Promise<void> {\n\t\ttry {\n\t\t\tresolve(await requestMethod());\n\t\t} catch (waitErr) {\n\t\t\treject(waitErr);\n\t\t}\n\t}\n\n\t/**\n\t * Scan all cache entries, delete any that have expired, and record the key of the\n\t * soonest-expiring remaining entry.\n\t * @internal\n\t */\n\tprivate static recalculateNextKey(): void {\n\t\tconst now = Date.now();\n\t\tconst cache = AsyncCache.getSharedCache();\n\n\t\tif (Object.keys(cache).length === 0) {\n\t\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, undefined);\n\t\t\treturn;\n\t\t}\n\n\t\tlet newNextKey: string | undefined;\n\t\tlet newNextExpiry = Number.MAX_SAFE_INTEGER;\n\t\tfor (const entryKey in cache) {\n\t\t\tconst { expires, inProgress } = cache[entryKey];\n\t\t\tif (expires > 0 && expires < now && inProgress !== true) {\n\t\t\t\tdelete cache[entryKey];\n\t\t\t} else if (expires > 0 && expires < newNextExpiry) {\n\t\t\t\tnewNextExpiry = expires;\n\t\t\t\tnewNextKey = entryKey;\n\t\t\t}\n\t\t}\n\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, newNextKey);\n\t}\n\n\t/**\n\t * Update the tracked next-expiry entry key if the given entry expires sooner than the current one.\n\t * @param key The cache entry key.\n\t * @param expires The expiry timestamp of the entry.\n\t * @internal\n\t */\n\tprivate static updateNextExpiryKey(key: string, expires: number): void {\n\t\tconst expiry = AsyncCache.getNextExpiry();\n\t\tif (expiry !== undefined && expiry <= expires) {\n\t\t\treturn;\n\t\t}\n\t\tSharedStore.set(AsyncCache._NEXT_EXPIRY_CACHE_KEY, key);\n\t}\n\n\t/**\n\t * Deletes the given key from the cache if it has expired and is not in-progress.\n\t * Returns true if the entry was evicted.\n\t * @param key The cache entry key to check.\n\t * @internal\n\t */\n\tprivate static evictIfExpired(key: string): boolean {\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tconst entry = cache[key];\n\t\tif (!Is.empty(entry) && entry.expires > 0 && entry.expires < Date.now() && !entry.inProgress) {\n\t\t\tdelete cache[key];\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Returns the expiry timestamp of the tracked next-expiry entry, or undefined if there\n\t * is no tracked entry or it is no longer present in the cache.\n\t * @internal\n\t */\n\tprivate static getNextExpiry(): number | undefined {\n\t\tconst nextKey = SharedStore.get<string>(AsyncCache._NEXT_EXPIRY_CACHE_KEY);\n\t\tif (Is.empty(nextKey)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst cache = AsyncCache.getSharedCache();\n\t\tconst nextEntry = cache[nextKey];\n\t\treturn Is.empty(nextEntry) ? undefined : nextEntry.expires;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Copyright 2026 IOTA Stiftung.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { MessageChannel, isMainThread, parentPort, receiveMessageOnPort } from "node:worker_threads";
|
|
4
|
+
import { Guards } from "./guards.js";
|
|
5
|
+
import { Is } from "./is.js";
|
|
6
|
+
import { SharedStore } from "./sharedStore.js";
|
|
7
|
+
import { GeneralError } from "../errors/generalError.js";
|
|
8
|
+
import { MutexMessageTypes } from "../models/mutexMessageTypes.js";
|
|
9
|
+
/**
|
|
10
|
+
* A cross-thread mutex built on Atomics and SharedArrayBuffer.
|
|
11
|
+
*
|
|
12
|
+
* When isMainThread is true (main thread or fork-mode child process) the class acts as
|
|
13
|
+
* the authoritative registry: it creates a SharedArrayBuffer-backed Int32Array for each
|
|
14
|
+
* key on first use and never discards it, because worker threads may hold references to
|
|
15
|
+
* the same underlying memory.
|
|
16
|
+
*
|
|
17
|
+
* When isMainThread is false (a true worker thread) the class synchronously negotiates
|
|
18
|
+
* the shared buffer with the main thread on first use of each key, then caches it locally.
|
|
19
|
+
* The main thread must call Mutex.handleWorkerMessage(msg) from its worker message handler
|
|
20
|
+
* before that worker first calls Mutex.lock().
|
|
21
|
+
*
|
|
22
|
+
* The lock is not re-entrant: a thread that already holds a key and calls lock() again on
|
|
23
|
+
* the same key will block until the timeout elapses.
|
|
24
|
+
*/
|
|
25
|
+
export class Mutex {
|
|
26
|
+
/**
|
|
27
|
+
* Runtime name for the class.
|
|
28
|
+
*/
|
|
29
|
+
static CLASS_NAME = "Mutex";
|
|
30
|
+
/**
|
|
31
|
+
* SharedStore key for the per-thread sparse map from lock key strings to Int32Arrays.
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
static _LOCKS_KEY = "mutexLocks";
|
|
35
|
+
/**
|
|
36
|
+
* Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.
|
|
37
|
+
* The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.
|
|
38
|
+
*
|
|
39
|
+
* WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the
|
|
40
|
+
* duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a
|
|
41
|
+
* buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.
|
|
42
|
+
* @param key The key to lock on.
|
|
43
|
+
* @param options Lock options.
|
|
44
|
+
* @param options.timeoutMs The maximum time to wait for the lock in milliseconds, default is 5000.
|
|
45
|
+
* @param options.throwOnTimeout Whether to throw an error if the lock could not be acquired within the timeout, default is false.
|
|
46
|
+
* @returns True if the lock was acquired, false if it timed out and throwOnTimeout is false.
|
|
47
|
+
* @throws GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.
|
|
48
|
+
*/
|
|
49
|
+
static lock(key, options) {
|
|
50
|
+
Guards.stringValue(Mutex.CLASS_NAME, "key", key);
|
|
51
|
+
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
52
|
+
const throwOnTimeout = options?.throwOnTimeout ?? false;
|
|
53
|
+
const deadline = Date.now() + timeoutMs;
|
|
54
|
+
const lock = Mutex.getOrFetchLock(key, deadline);
|
|
55
|
+
for (;;) {
|
|
56
|
+
// Atomically swap 0 → 1; if the previous value was 0 we acquired the lock.
|
|
57
|
+
const previous = Atomics.compareExchange(lock, 0, 0, 1);
|
|
58
|
+
if (previous === 0) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
// Otherwise, the lock is held by someone else. Check if we've already timed out before blocking.
|
|
62
|
+
const remaining = deadline - Date.now();
|
|
63
|
+
if (remaining <= 0) {
|
|
64
|
+
if (throwOnTimeout) {
|
|
65
|
+
throw new GeneralError(Mutex.CLASS_NAME, "lockTimeout", { key, timeoutMs });
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// Block until the value changes away from 1 (i.e. the holder calls unlock)
|
|
70
|
+
// or until the remaining timeout elapses.
|
|
71
|
+
const waitResult = Atomics.wait(lock, 0, 1, remaining);
|
|
72
|
+
if (waitResult === "timed-out") {
|
|
73
|
+
if (throwOnTimeout) {
|
|
74
|
+
throw new GeneralError(Mutex.CLASS_NAME, "lockTimeout", { key, timeoutMs });
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Releases the lock for the given key.
|
|
82
|
+
* @param key The key to unlock.
|
|
83
|
+
* @throws GeneralError if the key is invalid or the lock is not currently held.
|
|
84
|
+
*/
|
|
85
|
+
static unlock(key) {
|
|
86
|
+
Guards.stringValue(Mutex.CLASS_NAME, "key", key);
|
|
87
|
+
const locks = Mutex.getLocks();
|
|
88
|
+
const lock = locks[key];
|
|
89
|
+
if (Is.empty(lock)) {
|
|
90
|
+
throw new GeneralError(Mutex.CLASS_NAME, "lockNotFound", { key });
|
|
91
|
+
}
|
|
92
|
+
const previous = Atomics.compareExchange(lock, 0, 1, 0);
|
|
93
|
+
if (previous !== 1) {
|
|
94
|
+
throw new GeneralError(Mutex.CLASS_NAME, "lockAlreadyReleased", { key });
|
|
95
|
+
}
|
|
96
|
+
Atomics.notify(lock, 0, 1);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Inspect a message received from a worker and, if it is a Mutex buffer-fetch request,
|
|
100
|
+
* respond to it synchronously. Call from the main thread's worker message handler.
|
|
101
|
+
* @param msg The raw message received from the worker.
|
|
102
|
+
* @returns True if the message was a Mutex protocol message and was handled, false otherwise.
|
|
103
|
+
*/
|
|
104
|
+
static handleWorkerMessage(msg) {
|
|
105
|
+
if (!Is.object(msg) || msg.type !== MutexMessageTypes.GetBuffer) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
Guards.stringValue(Mutex.CLASS_NAME, "msg.key", msg.key);
|
|
109
|
+
Guards.object(Mutex.CLASS_NAME, "msg.signal", msg.signal);
|
|
110
|
+
Guards.object(Mutex.CLASS_NAME, "msg.port", msg.port);
|
|
111
|
+
const locks = Mutex.getLocks();
|
|
112
|
+
locks[msg.key] ??= new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
113
|
+
// Send the buffer to the worker before notifying so that it is guaranteed
|
|
114
|
+
// to be in port1's receive queue when Atomics.wait returns on the worker side.
|
|
115
|
+
msg.port.postMessage({ buffer: locks[msg.key].buffer });
|
|
116
|
+
Atomics.notify(new Int32Array(msg.signal), 0, 1);
|
|
117
|
+
msg.port.close();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Returns the Int32Array for the given key, fetching it from the main thread if this
|
|
122
|
+
* is a worker thread and the key is not yet in the local cache.
|
|
123
|
+
* @param key The lock key.
|
|
124
|
+
* @returns The Int32Array backed by a SharedArrayBuffer for this key.
|
|
125
|
+
* @internal
|
|
126
|
+
*/
|
|
127
|
+
static getOrFetchLock(key, deadline) {
|
|
128
|
+
const locks = Mutex.getLocks();
|
|
129
|
+
if (!Is.empty(locks[key])) {
|
|
130
|
+
return locks[key];
|
|
131
|
+
}
|
|
132
|
+
if (isMainThread) {
|
|
133
|
+
// Main thread or fork-mode process: own the registry entry.
|
|
134
|
+
locks[key] = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
135
|
+
return locks[key];
|
|
136
|
+
}
|
|
137
|
+
// Worker thread: synchronously request the SharedArrayBuffer from the main thread.
|
|
138
|
+
// Mutex.handleWorkerMessage(msg) must be called on the main thread's worker message handler.
|
|
139
|
+
if (Is.empty(parentPort)) {
|
|
140
|
+
throw new GeneralError(Mutex.CLASS_NAME, "bufferFetchFailed", { key });
|
|
141
|
+
}
|
|
142
|
+
const { port1, port2 } = new MessageChannel();
|
|
143
|
+
const signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
144
|
+
const msg = {
|
|
145
|
+
type: MutexMessageTypes.GetBuffer,
|
|
146
|
+
key,
|
|
147
|
+
signal: signal.buffer,
|
|
148
|
+
port: port2
|
|
149
|
+
};
|
|
150
|
+
parentPort.postMessage(msg, [port2]);
|
|
151
|
+
try {
|
|
152
|
+
// Block until the main thread posts the response and fires Atomics.notify.
|
|
153
|
+
// The response is guaranteed to be in port1's queue at this point because
|
|
154
|
+
// port.postMessage executes before Atomics.notify on the main thread.
|
|
155
|
+
// Use the lock deadline so the buffer fetch is bounded by the same timeout.
|
|
156
|
+
const waitResult = Atomics.wait(signal, 0, 0, Math.max(0, deadline - Date.now()));
|
|
157
|
+
if (waitResult === "timed-out") {
|
|
158
|
+
throw new GeneralError(Mutex.CLASS_NAME, "bufferFetchFailed", { key });
|
|
159
|
+
}
|
|
160
|
+
const response = receiveMessageOnPort(port1);
|
|
161
|
+
if (Is.empty(response)) {
|
|
162
|
+
throw new GeneralError(Mutex.CLASS_NAME, "bufferFetchFailed", { key });
|
|
163
|
+
}
|
|
164
|
+
locks[key] = new Int32Array(response.message.buffer);
|
|
165
|
+
return locks[key];
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
port1.close();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the shared locks map, creating it if it does not exist.
|
|
173
|
+
* @returns The shared locks map.
|
|
174
|
+
* @internal
|
|
175
|
+
*/
|
|
176
|
+
static getLocks() {
|
|
177
|
+
let locks = SharedStore.get(Mutex._LOCKS_KEY);
|
|
178
|
+
if (Is.undefined(locks)) {
|
|
179
|
+
locks = {};
|
|
180
|
+
SharedStore.set(Mutex._LOCKS_KEY, locks);
|
|
181
|
+
}
|
|
182
|
+
return locks;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=mutex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutex.js","sourceRoot":"","sources":["../../../src/utils/mutex.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EACN,cAAc,EAEd,YAAY,EACZ,UAAU,EACV,oBAAoB,EACpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,KAAK;IACjB;;OAEG;IACI,MAAM,CAAU,UAAU,WAA2B;IAE5D;;;OAGG;IACK,MAAM,CAAU,UAAU,GAAG,YAAY,CAAC;IAElD;;;;;;;;;;;;;OAaG;IACI,MAAM,CAAC,IAAI,CACjB,GAAW,EACX,OAA0D;QAE1D,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,SAAe,GAAG,CAAC,CAAC;QAEvD,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,IAAI,CAAC;QAC7C,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,KAAK,CAAC;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;QAExC,MAAM,IAAI,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAEjD,SAAS,CAAC;YACT,2EAA2E;YAC3E,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACxD,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;gBACpB,OAAO,IAAI,CAAC;YACb,CAAC;YAED,iGAAiG;YACjG,MAAM,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACxC,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;gBACpB,IAAI,cAAc,EAAE,CAAC;oBACpB,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;gBAC7E,CAAC;gBACD,OAAO,KAAK,CAAC;YACd,CAAC;YAED,2EAA2E;YAC3E,0CAA0C;YAC1C,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;YACvD,IAAI,UAAU,KAAK,WAAW,EAAE,CAAC;gBAChC,IAAI,cAAc,EAAE,CAAC;oBACpB,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;gBAC7E,CAAC;gBACD,OAAO,KAAK,CAAC;YACd,CAAC;QACF,CAAC;IACF,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,MAAM,CAAC,GAAW;QAC/B,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,SAAe,GAAG,CAAC,CAAC;QAEvD,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACnE,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACxD,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,qBAAqB,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1E,CAAC;QAED,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,mBAAmB,CAAC,GAAY;QAC7C,IAAI,CAAC,EAAE,CAAC,MAAM,CAAsB,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,iBAAiB,CAAC,SAAS,EAAE,CAAC;YACtF,OAAO,KAAK,CAAC;QACd,CAAC;QAED,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,aAAmB,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAoB,KAAK,CAAC,UAAU,gBAAsB,GAAG,CAAC,MAAM,CAAC,CAAC;QACnF,MAAM,CAAC,MAAM,CAAc,KAAK,CAAC,UAAU,cAAoB,GAAG,CAAC,IAAI,CAAC,CAAC;QAEzE,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/B,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC;QACvF,0EAA0E;QAC1E,+EAA+E;QAC/E,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACxD,OAAO,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QACjD,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAEjB,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;;;OAMG;IACK,MAAM,CAAC,cAAc,CAAC,GAAW,EAAE,QAAgB;QAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAC/B,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,IAAI,YAAY,EAAE,CAAC;YAClB,4DAA4D;YAC5D,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC;YACjF,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,mFAAmF;QACnF,6FAA6F;QAC7F,IAAI,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,mBAAmB,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,cAAc,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,iBAAiB,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC;QAEnF,MAAM,GAAG,GAAwB;YAChC,IAAI,EAAE,iBAAiB,CAAC,SAAS;YACjC,GAAG;YACH,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,IAAI,EAAE,KAAK;SACX,CAAC;QAEF,UAAU,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAErC,IAAI,CAAC;YACJ,2EAA2E;YAC3E,0EAA0E;YAC1E,sEAAsE;YACtE,4EAA4E;YAC5E,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAClF,IAAI,UAAU,KAAK,WAAW,EAAE,CAAC;gBAChC,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,mBAAmB,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;YACxE,CAAC;YAED,MAAM,QAAQ,GAAG,oBAAoB,CAAC,KAAK,CAEnC,CAAC;YAET,IAAI,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,mBAAmB,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;YACxE,CAAC;YAED,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACrD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;gBAAS,CAAC;YACV,KAAK,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;IACF,CAAC;IAED;;;;OAIG;IACK,MAAM,CAAC,QAAQ;QACtB,IAAI,KAAK,GAAG,WAAW,CAAC,GAAG,CAAgC,KAAK,CAAC,UAAU,CAAC,CAAC;QAC7E,IAAI,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YACzB,KAAK,GAAG,EAAE,CAAC;YACX,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,KAAK,CAAC;IACd,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport {\n\tMessageChannel,\n\ttype MessagePort,\n\tisMainThread,\n\tparentPort,\n\treceiveMessageOnPort\n} from \"node:worker_threads\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { Guards } from \"./guards.js\";\nimport { Is } from \"./is.js\";\nimport { SharedStore } from \"./sharedStore.js\";\nimport { GeneralError } from \"../errors/generalError.js\";\nimport type { IMutexWorkerMessage } from \"../models/IMutexWorkerMessage.js\";\nimport { MutexMessageTypes } from \"../models/mutexMessageTypes.js\";\n\n/**\n * A cross-thread mutex built on Atomics and SharedArrayBuffer.\n *\n * When isMainThread is true (main thread or fork-mode child process) the class acts as\n * the authoritative registry: it creates a SharedArrayBuffer-backed Int32Array for each\n * key on first use and never discards it, because worker threads may hold references to\n * the same underlying memory.\n *\n * When isMainThread is false (a true worker thread) the class synchronously negotiates\n * the shared buffer with the main thread on first use of each key, then caches it locally.\n * The main thread must call Mutex.handleWorkerMessage(msg) from its worker message handler\n * before that worker first calls Mutex.lock().\n *\n * The lock is not re-entrant: a thread that already holds a key and calls lock() again on\n * the same key will block until the timeout elapses.\n */\nexport class Mutex {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<Mutex>();\n\n\t/**\n\t * SharedStore key for the per-thread sparse map from lock key strings to Int32Arrays.\n\t * @internal\n\t */\n\tprivate static readonly _LOCKS_KEY = \"mutexLocks\";\n\n\t/**\n\t * Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.\n\t * The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.\n\t *\n\t * WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the\n\t * duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a\n\t * buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.\n\t * @param key The key to lock on.\n\t * @param options Lock options.\n\t * @param options.timeoutMs The maximum time to wait for the lock in milliseconds, default is 5000.\n\t * @param options.throwOnTimeout Whether to throw an error if the lock could not be acquired within the timeout, default is false.\n\t * @returns True if the lock was acquired, false if it timed out and throwOnTimeout is false.\n\t * @throws GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.\n\t */\n\tpublic static lock(\n\t\tkey: string,\n\t\toptions?: { timeoutMs?: number; throwOnTimeout?: boolean }\n\t): boolean {\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(key), key);\n\n\t\tconst timeoutMs = options?.timeoutMs ?? 5000;\n\t\tconst throwOnTimeout = options?.throwOnTimeout ?? false;\n\t\tconst deadline = Date.now() + timeoutMs;\n\n\t\tconst lock = Mutex.getOrFetchLock(key, deadline);\n\n\t\tfor (;;) {\n\t\t\t// Atomically swap 0 → 1; if the previous value was 0 we acquired the lock.\n\t\t\tconst previous = Atomics.compareExchange(lock, 0, 0, 1);\n\t\t\tif (previous === 0) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\t// Otherwise, the lock is held by someone else. Check if we've already timed out before blocking.\n\t\t\tconst remaining = deadline - Date.now();\n\t\t\tif (remaining <= 0) {\n\t\t\t\tif (throwOnTimeout) {\n\t\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Block until the value changes away from 1 (i.e. the holder calls unlock)\n\t\t\t// or until the remaining timeout elapses.\n\t\t\tconst waitResult = Atomics.wait(lock, 0, 1, remaining);\n\t\t\tif (waitResult === \"timed-out\") {\n\t\t\t\tif (throwOnTimeout) {\n\t\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockTimeout\", { key, timeoutMs });\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Releases the lock for the given key.\n\t * @param key The key to unlock.\n\t * @throws GeneralError if the key is invalid or the lock is not currently held.\n\t */\n\tpublic static unlock(key: string): void {\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(key), key);\n\n\t\tconst locks = Mutex.getLocks();\n\t\tconst lock = locks[key];\n\t\tif (Is.empty(lock)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockNotFound\", { key });\n\t\t}\n\n\t\tconst previous = Atomics.compareExchange(lock, 0, 1, 0);\n\t\tif (previous !== 1) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"lockAlreadyReleased\", { key });\n\t\t}\n\n\t\tAtomics.notify(lock, 0, 1);\n\t}\n\n\t/**\n\t * Inspect a message received from a worker and, if it is a Mutex buffer-fetch request,\n\t * respond to it synchronously. Call from the main thread's worker message handler.\n\t * @param msg The raw message received from the worker.\n\t * @returns True if the message was a Mutex protocol message and was handled, false otherwise.\n\t */\n\tpublic static handleWorkerMessage(msg: unknown): boolean {\n\t\tif (!Is.object<IMutexWorkerMessage>(msg) || msg.type !== MutexMessageTypes.GetBuffer) {\n\t\t\treturn false;\n\t\t}\n\n\t\tGuards.stringValue(Mutex.CLASS_NAME, nameof(msg.key), msg.key);\n\t\tGuards.object<SharedArrayBuffer>(Mutex.CLASS_NAME, nameof(msg.signal), msg.signal);\n\t\tGuards.object<MessagePort>(Mutex.CLASS_NAME, nameof(msg.port), msg.port);\n\n\t\tconst locks = Mutex.getLocks();\n\t\tlocks[msg.key] ??= new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\t\t// Send the buffer to the worker before notifying so that it is guaranteed\n\t\t// to be in port1's receive queue when Atomics.wait returns on the worker side.\n\t\tmsg.port.postMessage({ buffer: locks[msg.key].buffer });\n\t\tAtomics.notify(new Int32Array(msg.signal), 0, 1);\n\t\tmsg.port.close();\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Returns the Int32Array for the given key, fetching it from the main thread if this\n\t * is a worker thread and the key is not yet in the local cache.\n\t * @param key The lock key.\n\t * @returns The Int32Array backed by a SharedArrayBuffer for this key.\n\t * @internal\n\t */\n\tprivate static getOrFetchLock(key: string, deadline: number): Int32Array {\n\t\tconst locks = Mutex.getLocks();\n\t\tif (!Is.empty(locks[key])) {\n\t\t\treturn locks[key];\n\t\t}\n\n\t\tif (isMainThread) {\n\t\t\t// Main thread or fork-mode process: own the registry entry.\n\t\t\tlocks[key] = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\t\t\treturn locks[key];\n\t\t}\n\n\t\t// Worker thread: synchronously request the SharedArrayBuffer from the main thread.\n\t\t// Mutex.handleWorkerMessage(msg) must be called on the main thread's worker message handler.\n\t\tif (Is.empty(parentPort)) {\n\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t}\n\n\t\tconst { port1, port2 } = new MessageChannel();\n\t\tconst signal = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\n\t\tconst msg: IMutexWorkerMessage = {\n\t\t\ttype: MutexMessageTypes.GetBuffer,\n\t\t\tkey,\n\t\t\tsignal: signal.buffer,\n\t\t\tport: port2\n\t\t};\n\n\t\tparentPort.postMessage(msg, [port2]);\n\n\t\ttry {\n\t\t\t// Block until the main thread posts the response and fires Atomics.notify.\n\t\t\t// The response is guaranteed to be in port1's queue at this point because\n\t\t\t// port.postMessage executes before Atomics.notify on the main thread.\n\t\t\t// Use the lock deadline so the buffer fetch is bounded by the same timeout.\n\t\t\tconst waitResult = Atomics.wait(signal, 0, 0, Math.max(0, deadline - Date.now()));\n\t\t\tif (waitResult === \"timed-out\") {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t\t}\n\n\t\t\tconst response = receiveMessageOnPort(port1) as {\n\t\t\t\tmessage: { buffer: SharedArrayBuffer };\n\t\t\t} | null;\n\n\t\t\tif (Is.empty(response)) {\n\t\t\t\tthrow new GeneralError(Mutex.CLASS_NAME, \"bufferFetchFailed\", { key });\n\t\t\t}\n\n\t\t\tlocks[key] = new Int32Array(response.message.buffer);\n\t\t\treturn locks[key];\n\t\t} finally {\n\t\t\tport1.close();\n\t\t}\n\t}\n\n\t/**\n\t * Get the shared locks map, creating it if it does not exist.\n\t * @returns The shared locks map.\n\t * @internal\n\t */\n\tprivate static getLocks(): { [key: string]: Int32Array } {\n\t\tlet locks = SharedStore.get<{ [key: string]: Int32Array }>(Mutex._LOCKS_KEY);\n\t\tif (Is.undefined(locks)) {\n\t\t\tlocks = {};\n\t\t\tSharedStore.set(Mutex._LOCKS_KEY, locks);\n\t\t}\n\t\treturn locks;\n\t}\n}\n"]}
|
|
@@ -6,20 +6,27 @@ export declare class ErrorHelper {
|
|
|
6
6
|
/**
|
|
7
7
|
* Format Errors and returns just their messages.
|
|
8
8
|
* @param error The error to format.
|
|
9
|
-
* @param
|
|
9
|
+
* @param options Options for formatting the error.
|
|
10
|
+
* @param options.includeStack Whether to include the stack trace in the output, defaults to false.
|
|
11
|
+
* @param options.includeAdditional Whether to include additional error information in the output, defaults to false.
|
|
10
12
|
* @returns The error formatted including any causes errors.
|
|
11
13
|
*/
|
|
12
|
-
static formatErrors(error: unknown,
|
|
14
|
+
static formatErrors(error: unknown, options?: {
|
|
15
|
+
includeStack?: boolean;
|
|
16
|
+
includeAdditional?: boolean;
|
|
17
|
+
}): string[];
|
|
13
18
|
/**
|
|
14
19
|
* Localize the content of an error and any causes.
|
|
15
20
|
* @param error The error to format.
|
|
16
21
|
* @returns The localized version of the errors flattened.
|
|
17
22
|
*/
|
|
18
|
-
static localizeErrors(error: unknown): IError
|
|
23
|
+
static localizeErrors(error: unknown): (IError & {
|
|
24
|
+
additional?: string[];
|
|
25
|
+
})[];
|
|
19
26
|
/**
|
|
20
27
|
* Localize the content of an error and any causes.
|
|
21
28
|
* @param error The error to format.
|
|
22
29
|
* @returns The localized version of the errors flattened.
|
|
23
30
|
*/
|
|
24
|
-
static formatValidationErrors(error: IError): string | undefined;
|
|
31
|
+
static formatValidationErrors(error: IError): string[] | undefined;
|
|
25
32
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -38,9 +38,11 @@ export * from "./models/ILabelledValue.js";
|
|
|
38
38
|
export * from "./models/ILocale.js";
|
|
39
39
|
export * from "./models/ILocaleDictionary.js";
|
|
40
40
|
export * from "./models/ILocalesIndex.js";
|
|
41
|
+
export * from "./models/IMutexWorkerMessage.js";
|
|
41
42
|
export * from "./models/IPatchOperation.js";
|
|
42
43
|
export * from "./models/IUrlParts.js";
|
|
43
44
|
export * from "./models/IValidationFailure.js";
|
|
45
|
+
export * from "./models/mutexMessageTypes.js";
|
|
44
46
|
export * from "./types/bitString.js";
|
|
45
47
|
export * from "./types/objectOrArray.js";
|
|
46
48
|
export * from "./types/singleOccurrenceArray.js";
|
|
@@ -54,5 +56,6 @@ export * from "./utils/converter.js";
|
|
|
54
56
|
export * from "./utils/guards.js";
|
|
55
57
|
export * from "./utils/i18n.js";
|
|
56
58
|
export * from "./utils/is.js";
|
|
59
|
+
export * from "./utils/mutex.js";
|
|
57
60
|
export * from "./utils/sharedStore.js";
|
|
58
61
|
export * from "./utils/validation.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MessagePort } from "node:worker_threads";
|
|
2
|
+
import type { MutexMessageTypes } from "./mutexMessageTypes.js";
|
|
3
|
+
/**
|
|
4
|
+
* Message sent from a worker thread to the main thread to request a SharedArrayBuffer for a given mutex key.
|
|
5
|
+
*/
|
|
6
|
+
export interface IMutexWorkerMessage {
|
|
7
|
+
/**
|
|
8
|
+
* The message type.
|
|
9
|
+
*/
|
|
10
|
+
type: typeof MutexMessageTypes.GetBuffer;
|
|
11
|
+
/**
|
|
12
|
+
* The mutex key for which the buffer is requested.
|
|
13
|
+
*/
|
|
14
|
+
key: string;
|
|
15
|
+
/**
|
|
16
|
+
* The SharedArrayBuffer for the mutex, sent from the worker to the main thread.
|
|
17
|
+
*/
|
|
18
|
+
signal: SharedArrayBuffer;
|
|
19
|
+
/**
|
|
20
|
+
* The MessagePort for the main thread to respond with the buffer, sent from the worker to the main thread.
|
|
21
|
+
*/
|
|
22
|
+
port: MessagePort;
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutex message types.
|
|
3
|
+
*/
|
|
4
|
+
export declare const MutexMessageTypes: {
|
|
5
|
+
/**
|
|
6
|
+
* Get buffer.
|
|
7
|
+
*/
|
|
8
|
+
readonly GetBuffer: "twin:mutex:getBuffer";
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Mutex message types.
|
|
12
|
+
*/
|
|
13
|
+
export type MutexMessageTypes = (typeof MutexMessageTypes)[keyof typeof MutexMessageTypes];
|
|
@@ -14,14 +14,15 @@ export declare class AsyncCache {
|
|
|
14
14
|
/**
|
|
15
15
|
* Get an entry from the cache.
|
|
16
16
|
* @param key The key to get from the cache.
|
|
17
|
-
* @returns The item from the cache if it exists
|
|
17
|
+
* @returns The item from the cache if it exists, or undefined if the key is missing, expired, or
|
|
18
|
+
* its request is still in-progress. Throws if a cached failure exists for the key.
|
|
18
19
|
*/
|
|
19
20
|
static get<T = unknown>(key: string): Promise<T | undefined>;
|
|
20
21
|
/**
|
|
21
22
|
* Set an entry into the cache.
|
|
22
23
|
* @param key The key to set in the cache.
|
|
23
24
|
* @param value The value to set in the cache.
|
|
24
|
-
* @param ttlMs The TTL of the entry in the cache in
|
|
25
|
+
* @param ttlMs The TTL of the entry in the cache in milliseconds. Defaults to 1000 (1 second).
|
|
25
26
|
* @returns Nothing.
|
|
26
27
|
*/
|
|
27
28
|
static set<T = unknown>(key: string, value: T, ttlMs?: number): Promise<void>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cross-thread mutex built on Atomics and SharedArrayBuffer.
|
|
3
|
+
*
|
|
4
|
+
* When isMainThread is true (main thread or fork-mode child process) the class acts as
|
|
5
|
+
* the authoritative registry: it creates a SharedArrayBuffer-backed Int32Array for each
|
|
6
|
+
* key on first use and never discards it, because worker threads may hold references to
|
|
7
|
+
* the same underlying memory.
|
|
8
|
+
*
|
|
9
|
+
* When isMainThread is false (a true worker thread) the class synchronously negotiates
|
|
10
|
+
* the shared buffer with the main thread on first use of each key, then caches it locally.
|
|
11
|
+
* The main thread must call Mutex.handleWorkerMessage(msg) from its worker message handler
|
|
12
|
+
* before that worker first calls Mutex.lock().
|
|
13
|
+
*
|
|
14
|
+
* The lock is not re-entrant: a thread that already holds a key and calls lock() again on
|
|
15
|
+
* the same key will block until the timeout elapses.
|
|
16
|
+
*/
|
|
17
|
+
export declare class Mutex {
|
|
18
|
+
/**
|
|
19
|
+
* Runtime name for the class.
|
|
20
|
+
*/
|
|
21
|
+
static readonly CLASS_NAME: string;
|
|
22
|
+
/**
|
|
23
|
+
* Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.
|
|
24
|
+
* The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.
|
|
25
|
+
*
|
|
26
|
+
* WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the
|
|
27
|
+
* duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a
|
|
28
|
+
* buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.
|
|
29
|
+
* @param key The key to lock on.
|
|
30
|
+
* @param options Lock options.
|
|
31
|
+
* @param options.timeoutMs The maximum time to wait for the lock in milliseconds, default is 5000.
|
|
32
|
+
* @param options.throwOnTimeout Whether to throw an error if the lock could not be acquired within the timeout, default is false.
|
|
33
|
+
* @returns True if the lock was acquired, false if it timed out and throwOnTimeout is false.
|
|
34
|
+
* @throws GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.
|
|
35
|
+
*/
|
|
36
|
+
static lock(key: string, options?: {
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
throwOnTimeout?: boolean;
|
|
39
|
+
}): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Releases the lock for the given key.
|
|
42
|
+
* @param key The key to unlock.
|
|
43
|
+
* @throws GeneralError if the key is invalid or the lock is not currently held.
|
|
44
|
+
*/
|
|
45
|
+
static unlock(key: string): void;
|
|
46
|
+
/**
|
|
47
|
+
* Inspect a message received from a worker and, if it is a Mutex buffer-fetch request,
|
|
48
|
+
* respond to it synchronously. Call from the main thread's worker message handler.
|
|
49
|
+
* @param msg The raw message received from the worker.
|
|
50
|
+
* @returns True if the message was a Mutex protocol message and was handled, false otherwise.
|
|
51
|
+
*/
|
|
52
|
+
static handleWorkerMessage(msg: unknown): boolean;
|
|
53
|
+
}
|
package/docs/changelog.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.3-next.47](https://github.com/iotaledger/twin-framework/compare/core-v0.0.3-next.46...core-v0.0.3-next.47) (2026-05-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add mutex ([#316](https://github.com/iotaledger/twin-framework/issues/316)) ([1027e5a](https://github.com/iotaledger/twin-framework/commit/1027e5ac77aa9804178b4253abdeefd6f1c3d521))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* The following workspace dependencies were updated
|
|
14
|
+
* dependencies
|
|
15
|
+
* @twin.org/nameof bumped from 0.0.3-next.46 to 0.0.3-next.47
|
|
16
|
+
* devDependencies
|
|
17
|
+
* @twin.org/nameof-transformer bumped from 0.0.3-next.46 to 0.0.3-next.47
|
|
18
|
+
* @twin.org/nameof-vitest-plugin bumped from 0.0.3-next.46 to 0.0.3-next.47
|
|
19
|
+
|
|
20
|
+
## [0.0.3-next.46](https://github.com/iotaledger/twin-framework/compare/core-v0.0.3-next.45...core-v0.0.3-next.46) (2026-05-22)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* improve error formatting ([#313](https://github.com/iotaledger/twin-framework/issues/313)) ([5a19623](https://github.com/iotaledger/twin-framework/commit/5a196231bcbf088bf9ba92a93b7478d3b8c5593f))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Dependencies
|
|
29
|
+
|
|
30
|
+
* The following workspace dependencies were updated
|
|
31
|
+
* dependencies
|
|
32
|
+
* @twin.org/nameof bumped from 0.0.3-next.45 to 0.0.3-next.46
|
|
33
|
+
* devDependencies
|
|
34
|
+
* @twin.org/nameof-transformer bumped from 0.0.3-next.45 to 0.0.3-next.46
|
|
35
|
+
* @twin.org/nameof-vitest-plugin bumped from 0.0.3-next.45 to 0.0.3-next.46
|
|
36
|
+
|
|
3
37
|
## [0.0.3-next.45](https://github.com/iotaledger/twin-framework/compare/core-v0.0.3-next.44...core-v0.0.3-next.45) (2026-05-21)
|
|
4
38
|
|
|
5
39
|
|
|
@@ -84,7 +84,8 @@ The key to get from the cache.
|
|
|
84
84
|
|
|
85
85
|
`Promise`\<`T` \| `undefined`\>
|
|
86
86
|
|
|
87
|
-
The item from the cache if it exists
|
|
87
|
+
The item from the cache if it exists, or undefined if the key is missing, expired, or
|
|
88
|
+
its request is still in-progress. Throws if a cached failure exists for the key.
|
|
88
89
|
|
|
89
90
|
***
|
|
90
91
|
|
|
@@ -118,7 +119,7 @@ The value to set in the cache.
|
|
|
118
119
|
|
|
119
120
|
`number`
|
|
120
121
|
|
|
121
|
-
The TTL of the entry in the cache in
|
|
122
|
+
The TTL of the entry in the cache in milliseconds. Defaults to 1000 (1 second).
|
|
122
123
|
|
|
123
124
|
#### Returns
|
|
124
125
|
|
|
@@ -16,7 +16,7 @@ Error helper functions.
|
|
|
16
16
|
|
|
17
17
|
### formatErrors() {#formaterrors}
|
|
18
18
|
|
|
19
|
-
> `static` **formatErrors**(`error`, `
|
|
19
|
+
> `static` **formatErrors**(`error`, `options?`): `string`[]
|
|
20
20
|
|
|
21
21
|
Format Errors and returns just their messages.
|
|
22
22
|
|
|
@@ -28,11 +28,21 @@ Format Errors and returns just their messages.
|
|
|
28
28
|
|
|
29
29
|
The error to format.
|
|
30
30
|
|
|
31
|
-
#####
|
|
31
|
+
##### options?
|
|
32
|
+
|
|
33
|
+
Options for formatting the error.
|
|
34
|
+
|
|
35
|
+
###### includeStack?
|
|
36
|
+
|
|
37
|
+
`boolean`
|
|
38
|
+
|
|
39
|
+
Whether to include the stack trace in the output, defaults to false.
|
|
40
|
+
|
|
41
|
+
###### includeAdditional?
|
|
32
42
|
|
|
33
43
|
`boolean`
|
|
34
44
|
|
|
35
|
-
Whether to include error
|
|
45
|
+
Whether to include additional error information in the output, defaults to false.
|
|
36
46
|
|
|
37
47
|
#### Returns
|
|
38
48
|
|
|
@@ -44,7 +54,7 @@ The error formatted including any causes errors.
|
|
|
44
54
|
|
|
45
55
|
### localizeErrors() {#localizeerrors}
|
|
46
56
|
|
|
47
|
-
> `static` **localizeErrors**(`error`): [`IError`](../interfaces/IError.md)[]
|
|
57
|
+
> `static` **localizeErrors**(`error`): [`IError`](../interfaces/IError.md) & `object`[]
|
|
48
58
|
|
|
49
59
|
Localize the content of an error and any causes.
|
|
50
60
|
|
|
@@ -58,7 +68,7 @@ The error to format.
|
|
|
58
68
|
|
|
59
69
|
#### Returns
|
|
60
70
|
|
|
61
|
-
[`IError`](../interfaces/IError.md)[]
|
|
71
|
+
[`IError`](../interfaces/IError.md) & `object`[]
|
|
62
72
|
|
|
63
73
|
The localized version of the errors flattened.
|
|
64
74
|
|
|
@@ -66,7 +76,7 @@ The localized version of the errors flattened.
|
|
|
66
76
|
|
|
67
77
|
### formatValidationErrors() {#formatvalidationerrors}
|
|
68
78
|
|
|
69
|
-
> `static` **formatValidationErrors**(`error`): `string` \| `undefined`
|
|
79
|
+
> `static` **formatValidationErrors**(`error`): `string`[] \| `undefined`
|
|
70
80
|
|
|
71
81
|
Localize the content of an error and any causes.
|
|
72
82
|
|
|
@@ -80,6 +90,6 @@ The error to format.
|
|
|
80
90
|
|
|
81
91
|
#### Returns
|
|
82
92
|
|
|
83
|
-
`string` \| `undefined`
|
|
93
|
+
`string`[] \| `undefined`
|
|
84
94
|
|
|
85
95
|
The localized version of the errors flattened.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Class: Mutex
|
|
2
|
+
|
|
3
|
+
A cross-thread mutex built on Atomics and SharedArrayBuffer.
|
|
4
|
+
|
|
5
|
+
When isMainThread is true (main thread or fork-mode child process) the class acts as
|
|
6
|
+
the authoritative registry: it creates a SharedArrayBuffer-backed Int32Array for each
|
|
7
|
+
key on first use and never discards it, because worker threads may hold references to
|
|
8
|
+
the same underlying memory.
|
|
9
|
+
|
|
10
|
+
When isMainThread is false (a true worker thread) the class synchronously negotiates
|
|
11
|
+
the shared buffer with the main thread on first use of each key, then caches it locally.
|
|
12
|
+
The main thread must call Mutex.handleWorkerMessage(msg) from its worker message handler
|
|
13
|
+
before that worker first calls Mutex.lock().
|
|
14
|
+
|
|
15
|
+
The lock is not re-entrant: a thread that already holds a key and calls lock() again on
|
|
16
|
+
the same key will block until the timeout elapses.
|
|
17
|
+
|
|
18
|
+
## Constructors
|
|
19
|
+
|
|
20
|
+
### Constructor
|
|
21
|
+
|
|
22
|
+
> **new Mutex**(): `Mutex`
|
|
23
|
+
|
|
24
|
+
#### Returns
|
|
25
|
+
|
|
26
|
+
`Mutex`
|
|
27
|
+
|
|
28
|
+
## Properties
|
|
29
|
+
|
|
30
|
+
### CLASS\_NAME {#class_name}
|
|
31
|
+
|
|
32
|
+
> `readonly` `static` **CLASS\_NAME**: `string`
|
|
33
|
+
|
|
34
|
+
Runtime name for the class.
|
|
35
|
+
|
|
36
|
+
## Methods
|
|
37
|
+
|
|
38
|
+
### lock() {#lock}
|
|
39
|
+
|
|
40
|
+
> `static` **lock**(`key`, `options?`): `boolean`
|
|
41
|
+
|
|
42
|
+
Acquires a lock for the given key. If the lock is already held, it will wait until it is released or until the timeout is reached.
|
|
43
|
+
The lock is not re-entrant: if the same thread tries to acquire the same lock again, it will deadlock until the timeout is reached.
|
|
44
|
+
|
|
45
|
+
WARNING: this method calls Atomics.wait internally. On the main thread this blocks the Node.js event loop for the
|
|
46
|
+
duration of the wait. Do not call from the main thread while a worker thread may simultaneously need to fetch a
|
|
47
|
+
buffer for a new mutex key, as that fetch requires the main thread's message loop to be running and will deadlock.
|
|
48
|
+
|
|
49
|
+
#### Parameters
|
|
50
|
+
|
|
51
|
+
##### key
|
|
52
|
+
|
|
53
|
+
`string`
|
|
54
|
+
|
|
55
|
+
The key to lock on.
|
|
56
|
+
|
|
57
|
+
##### options?
|
|
58
|
+
|
|
59
|
+
Lock options.
|
|
60
|
+
|
|
61
|
+
###### timeoutMs?
|
|
62
|
+
|
|
63
|
+
`number`
|
|
64
|
+
|
|
65
|
+
The maximum time to wait for the lock in milliseconds, default is 5000.
|
|
66
|
+
|
|
67
|
+
###### throwOnTimeout?
|
|
68
|
+
|
|
69
|
+
`boolean`
|
|
70
|
+
|
|
71
|
+
Whether to throw an error if the lock could not be acquired within the timeout, default is false.
|
|
72
|
+
|
|
73
|
+
#### Returns
|
|
74
|
+
|
|
75
|
+
`boolean`
|
|
76
|
+
|
|
77
|
+
True if the lock was acquired, false if it timed out and throwOnTimeout is false.
|
|
78
|
+
|
|
79
|
+
#### Throws
|
|
80
|
+
|
|
81
|
+
GeneralError if the key is invalid or if the lock could not be acquired within the timeout and throwOnTimeout is true.
|
|
82
|
+
|
|
83
|
+
***
|
|
84
|
+
|
|
85
|
+
### unlock() {#unlock}
|
|
86
|
+
|
|
87
|
+
> `static` **unlock**(`key`): `void`
|
|
88
|
+
|
|
89
|
+
Releases the lock for the given key.
|
|
90
|
+
|
|
91
|
+
#### Parameters
|
|
92
|
+
|
|
93
|
+
##### key
|
|
94
|
+
|
|
95
|
+
`string`
|
|
96
|
+
|
|
97
|
+
The key to unlock.
|
|
98
|
+
|
|
99
|
+
#### Returns
|
|
100
|
+
|
|
101
|
+
`void`
|
|
102
|
+
|
|
103
|
+
#### Throws
|
|
104
|
+
|
|
105
|
+
GeneralError if the key is invalid or the lock is not currently held.
|
|
106
|
+
|
|
107
|
+
***
|
|
108
|
+
|
|
109
|
+
### handleWorkerMessage() {#handleworkermessage}
|
|
110
|
+
|
|
111
|
+
> `static` **handleWorkerMessage**(`msg`): `boolean`
|
|
112
|
+
|
|
113
|
+
Inspect a message received from a worker and, if it is a Mutex buffer-fetch request,
|
|
114
|
+
respond to it synchronously. Call from the main thread's worker message handler.
|
|
115
|
+
|
|
116
|
+
#### Parameters
|
|
117
|
+
|
|
118
|
+
##### msg
|
|
119
|
+
|
|
120
|
+
`unknown`
|
|
121
|
+
|
|
122
|
+
The raw message received from the worker.
|
|
123
|
+
|
|
124
|
+
#### Returns
|
|
125
|
+
|
|
126
|
+
`boolean`
|
|
127
|
+
|
|
128
|
+
True if the message was a Mutex protocol message and was handled, false otherwise.
|
package/docs/reference/index.md
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
- [Guards](classes/Guards.md)
|
|
40
40
|
- [I18n](classes/I18n.md)
|
|
41
41
|
- [Is](classes/Is.md)
|
|
42
|
+
- [Mutex](classes/Mutex.md)
|
|
42
43
|
- [SharedStore](classes/SharedStore.md)
|
|
43
44
|
- [Validation](classes/Validation.md)
|
|
44
45
|
|
|
@@ -53,6 +54,7 @@
|
|
|
53
54
|
- [ILocale](interfaces/ILocale.md)
|
|
54
55
|
- [ILocaleDictionary](interfaces/ILocaleDictionary.md)
|
|
55
56
|
- [ILocalesIndex](interfaces/ILocalesIndex.md)
|
|
57
|
+
- [IMutexWorkerMessage](interfaces/IMutexWorkerMessage.md)
|
|
56
58
|
- [IPatchOperation](interfaces/IPatchOperation.md)
|
|
57
59
|
- [IUrlParts](interfaces/IUrlParts.md)
|
|
58
60
|
- [IValidationFailure](interfaces/IValidationFailure.md)
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
- [CoerceType](type-aliases/CoerceType.md)
|
|
63
65
|
- [CompressionType](type-aliases/CompressionType.md)
|
|
64
66
|
- [HealthStatus](type-aliases/HealthStatus.md)
|
|
67
|
+
- [MutexMessageTypes](type-aliases/MutexMessageTypes.md)
|
|
65
68
|
- [ObjectOrArray](type-aliases/ObjectOrArray.md)
|
|
66
69
|
- [SingleOccurrenceArray](type-aliases/SingleOccurrenceArray.md)
|
|
67
70
|
- [SingleOccurrenceArrayDepthHelper](type-aliases/SingleOccurrenceArrayDepthHelper.md)
|
|
@@ -72,3 +75,4 @@
|
|
|
72
75
|
- [CoerceType](variables/CoerceType.md)
|
|
73
76
|
- [CompressionType](variables/CompressionType.md)
|
|
74
77
|
- [HealthStatus](variables/HealthStatus.md)
|
|
78
|
+
- [MutexMessageTypes](variables/MutexMessageTypes.md)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Interface: IMutexWorkerMessage
|
|
2
|
+
|
|
3
|
+
Message sent from a worker thread to the main thread to request a SharedArrayBuffer for a given mutex key.
|
|
4
|
+
|
|
5
|
+
## Properties
|
|
6
|
+
|
|
7
|
+
### type {#type}
|
|
8
|
+
|
|
9
|
+
> **type**: `"twin:mutex:getBuffer"`
|
|
10
|
+
|
|
11
|
+
The message type.
|
|
12
|
+
|
|
13
|
+
***
|
|
14
|
+
|
|
15
|
+
### key {#key}
|
|
16
|
+
|
|
17
|
+
> **key**: `string`
|
|
18
|
+
|
|
19
|
+
The mutex key for which the buffer is requested.
|
|
20
|
+
|
|
21
|
+
***
|
|
22
|
+
|
|
23
|
+
### signal {#signal}
|
|
24
|
+
|
|
25
|
+
> **signal**: `SharedArrayBuffer`
|
|
26
|
+
|
|
27
|
+
The SharedArrayBuffer for the mutex, sent from the worker to the main thread.
|
|
28
|
+
|
|
29
|
+
***
|
|
30
|
+
|
|
31
|
+
### port {#port}
|
|
32
|
+
|
|
33
|
+
> **port**: `MessagePort`
|
|
34
|
+
|
|
35
|
+
The MessagePort for the main thread to respond with the buffer, sent from the worker to the main thread.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Variable: MutexMessageTypes
|
|
2
|
+
|
|
3
|
+
> `const` **MutexMessageTypes**: `object`
|
|
4
|
+
|
|
5
|
+
Mutex message types.
|
|
6
|
+
|
|
7
|
+
## Type Declaration
|
|
8
|
+
|
|
9
|
+
### GetBuffer {#getbuffer}
|
|
10
|
+
|
|
11
|
+
> `readonly` **GetBuffer**: `"twin:mutex:getBuffer"` = `"twin:mutex:getBuffer"`
|
|
12
|
+
|
|
13
|
+
Get buffer.
|
package/locales/en.json
CHANGED
|
@@ -101,6 +101,12 @@
|
|
|
101
101
|
},
|
|
102
102
|
"jsonHelper": {
|
|
103
103
|
"failedPatch": "Failed to patch the JSON object, patch index \"{index}\" failed"
|
|
104
|
+
},
|
|
105
|
+
"mutex": {
|
|
106
|
+
"lockNotFound": "The key \"{key}\" has no active lock",
|
|
107
|
+
"lockAlreadyReleased": "The key \"{key}\" is not currently locked",
|
|
108
|
+
"lockTimeout": "Failed to acquire lock for key \"{key}\" within the timeout of {timeout} milliseconds",
|
|
109
|
+
"bufferFetchFailed": "Failed to retrieve shared buffer for key \"{key}\" from the main thread"
|
|
104
110
|
}
|
|
105
111
|
},
|
|
106
112
|
"errorNames": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/core",
|
|
3
|
-
"version": "0.0.3-next.
|
|
3
|
+
"version": "0.0.3-next.47",
|
|
4
4
|
"description": "Helper methods/classes for data type checking/validation/guarding/error handling",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": ">=20.0.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@twin.org/nameof": "0.0.3-next.
|
|
17
|
+
"@twin.org/nameof": "0.0.3-next.47",
|
|
18
18
|
"intl-messageformat": "11.2.6",
|
|
19
19
|
"rfc6902": "5.2.0"
|
|
20
20
|
},
|