@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.
@@ -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 includeDetails Whether to include error details, defaults to false.
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, includeDetails) {
19
+ static formatErrors(error, options) {
18
20
  const localizedErrors = ErrorHelper.localizeErrors(error);
19
- if (includeDetails ?? false) {
20
- const output = [];
21
- for (const err of localizedErrors) {
22
- let detailedError = err.message;
23
- if (Is.stringValue(err.stack)) {
24
- detailedError += `\n${err.stack}`;
25
- }
26
- output.push(detailedError);
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
- return output;
32
+ output.push(detailedError);
29
33
  }
30
- return localizedErrors.map(e => e.message);
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.stringValue(additional)) {
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.join("\n");
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":"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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 includeDetails Whether to include error details, defaults to false.\n\t * @returns The error formatted including any causes errors.\n\t */\n\tpublic static formatErrors(error: unknown, includeDetails?: boolean): string[] {\n\t\tconst localizedErrors = ErrorHelper.localizeErrors(error);\n\t\tif (includeDetails ?? false) {\n\t\t\tconst output: string[] = [];\n\t\t\tfor (const err of localizedErrors) {\n\t\t\t\tlet detailedError = err.message;\n\t\t\t\tif (Is.stringValue(err.stack)) {\n\t\t\t\t\tdetailedError += `\\n${err.stack}`;\n\t\t\t\t}\n\t\t\t\toutput.push(detailedError);\n\t\t\t}\n\t\t\treturn output;\n\t\t}\n\t\treturn localizedErrors.map(e => e.message);\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[] {\n\t\tconst formattedErrors: IError[] = [];\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.stringValue(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 = [];\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.join(\"\\n\");\n\t\t}\n\t}\n}\n"]}
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
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=IMutexWorkerMessage.js.map
@@ -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: Date.now() + ttlMs
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
- if (!Is.empty(cache[key]?.result)) {
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 cache[key].result;
148
+ return entry.result;
119
149
  }
120
- if (!Is.empty(cache[key]?.error)) {
150
+ if (!Is.empty(entry?.error)) {
121
151
  // If the cache has already resulted in an error, reject it
122
- throw cache[key].error;
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 ms, defaults to 1s.
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: Date.now() + (ttlMs ?? 1000)
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("asyncCache", {});
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 cache = AsyncCache.getSharedCache();
170
- for (const entry in cache) {
171
- if (cache[entry].expires > 0 &&
172
- cache[entry].expires < Date.now() &&
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("asyncCache");
237
+ let sharedCache = SharedStore.get(AsyncCache._CACHE_KEY);
185
238
  if (Is.undefined(sharedCache)) {
186
239
  sharedCache = {};
187
- SharedStore.set("asyncCache", sharedCache);
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 includeDetails Whether to include error details, defaults to false.
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, includeDetails?: boolean): string[];
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
  }
@@ -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 ms, defaults to 1s.
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 ms, defaults to 1s.
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`, `includeDetails?`): `string`[]
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
- ##### includeDetails?
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 details, defaults to false.
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.
@@ -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,5 @@
1
+ # Type Alias: MutexMessageTypes
2
+
3
+ > **MutexMessageTypes** = *typeof* [`MutexMessageTypes`](../variables/MutexMessageTypes.md)\[keyof *typeof* [`MutexMessageTypes`](../variables/MutexMessageTypes.md)\]
4
+
5
+ Mutex message types.
@@ -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.45",
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.45",
17
+ "@twin.org/nameof": "0.0.3-next.47",
18
18
  "intl-messageformat": "11.2.6",
19
19
  "rfc6902": "5.2.0"
20
20
  },