diff --git a/.editorconfig b/.editorconfig index e81eb91..55cf59a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -35,6 +35,7 @@ indent_size = 2 [*.json] indent_style = space indent_size = 4 +insert_final_newline = false [*.yml] indent_style = space diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bacc24f..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,799 +0,0 @@ -{ - "env": { - "es6": true, - "node": true - }, - "parserOptions": { - "sourceType": "script", - "ecmaVersion": 2018 - }, - "rules": { - "accessor-pairs": "off", - "array-callback-return": "error", - "block-scoped-var": "error", - "complexity": [ - "off", - 11 - ], - "class-methods-use-this": [ - "warn" - ], - "consistent-return": "warn", - "curly": [ - "warn", - "multi-or-nest", - "consistent" - ], - "default-case": [ - "error", - { - "commentPattern": "^no default$" - } - ], - "dot-notation": [ - "error", - { - "allowKeywords": true - } - ], - "dot-location": [ - "error", - "property" - ], - "eqeqeq": [ - "warn", - "allow-null" - ], - "guard-for-in": "error", - "no-alert": "warn", - "no-caller": "error", - "no-case-declarations": "error", - "no-div-regex": "off", - "no-else-return": "warn", - "no-empty-function": [ - "warn", - { - "allow": [ - "arrowFunctions", - "methods", - "getters" - ] - } - ], - "no-empty-pattern": "error", - "no-eq-null": "off", - "no-eval": "error", - "no-extend-native": "error", - "no-extra-bind": "error", - "no-extra-label": "error", - "no-fallthrough": "error", - "no-floating-decimal": "error", - "no-global-assign": [ - "error", - { - "exceptions": [] - } - ], - "no-native-reassign": "off", - "no-implicit-coercion": [ - "off", - { - "boolean": false, - "number": true, - "string": true, - "allow": [] - } - ], - "no-implicit-globals": "off", - "no-implied-eval": "error", - "no-invalid-this": "off", - "no-iterator": "error", - "no-labels": [ - "error", - { - "allowLoop": false, - "allowSwitch": false - } - ], - "no-lone-blocks": "error", - "no-loop-func": "error", - "no-magic-numbers": [ - "off", - { - "ignore": [], - "ignoreArrayIndexes": true, - "enforceConst": true, - "detectObjects": false - } - ], - "no-multi-spaces": "warn", - "no-multi-str": "error", - "no-new": "error", - "no-new-func": "error", - "no-new-wrappers": "error", - "no-octal": "error", - "no-octal-escape": "error", - "no-param-reassign": [ - "warn", - { - "props": false - } - ], - "no-proto": "error", - "no-redeclare": "error", - "no-restricted-properties": [ - "error", - { - "object": "arguments", - "property": "callee", - "message": "arguments.callee is deprecated" - }, - { - "property": "__defineGetter__", - "message": "Please use Object.defineProperty instead." - }, - { - "property": "__defineSetter__", - "message": "Please use Object.defineProperty instead." - }, - { - "object": "Math", - "property": "pow", - "message": "Use the exponentiation operator (**) instead." - } - ], - "no-return-assign": "error", - "no-return-await": "error", - "no-script-url": "error", - "no-self-assign": "error", - "no-self-compare": "error", - "no-sequences": "error", - "no-throw-literal": "error", - "no-unmodified-loop-condition": "off", - "no-unused-expressions": [ - "error", - { - "allowShortCircuit": false, - "allowTernary": false - } - ], - "no-unused-labels": "error", - "no-useless-call": "off", - "no-useless-concat": "error", - "no-useless-escape": "error", - "no-useless-return": "error", - "no-void": "error", - "no-warning-comments": [ - "off", - { - "terms": [ - "todo", - "fixme", - "xxx" - ], - "location": "start" - } - ], - "no-with": "error", - "radix": "error", - "vars-on-top": "error", - "wrap-iife": [ - "error", - "outside", - { - "functionPrototypeMethods": false - } - ], - "yoda": "error", - "no-mixed-requires": "warn", - "callback-return": "off", - "global-require": "error", - "handle-callback-err": "off", - "no-new-require": "error", - "no-path-concat": "error", - "no-process-env": "off", - "no-process-exit": "off", - "no-restricted-modules": "off", - "no-sync": "off", - "arrow-body-style": [ - "warn", - "as-needed" - ], - "arrow-parens": [ - "error", - "as-needed" - ], - "arrow-spacing": [ - "error", - { - "before": true, - "after": true - } - ], - "constructor-super": "error", - "generator-star-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "no-class-assign": "error", - "no-confusing-arrow": [ - "error", - { - "allowParens": true - } - ], - "no-const-assign": "error", - "no-dupe-class-members": "error", - "no-duplicate-imports": "error", - "no-new-symbol": "error", - "no-restricted-imports": "off", - "no-this-before-super": "error", - "no-useless-computed-key": "error", - "no-useless-constructor": "error", - "no-useless-rename": [ - "error", - { - "ignoreDestructuring": false, - "ignoreImport": false, - "ignoreExport": false - } - ], - "no-var": "error", - "object-shorthand": [ - "warn", - "always", - { - "ignoreConstructors": false, - "avoidQuotes": true - } - ], - "prefer-arrow-callback": [ - "warn", - { - "allowNamedFunctions": false, - "allowUnboundThis": true - } - ], - "prefer-const": [ - "error", - { - "destructuring": "any", - "ignoreReadBeforeAssign": true - } - ], - "prefer-numeric-literals": "error", - "prefer-reflect": "off", - "prefer-rest-params": "warn", - "prefer-spread": "error", - "prefer-template": "warn", - "require-yield": "error", - "rest-spread-spacing": [ - "error", - "never" - ], - "sort-imports": [ - "off", - { - "ignoreCase": false, - "ignoreMemberSort": false, - "memberSyntaxSortOrder": [ - "none", - "all", - "multiple", - "single" - ] - } - ], - "symbol-description": "warn", - "template-curly-spacing": "error", - "yield-star-spacing": [ - "error", - "after" - ], - "comma-dangle": [ - "warn", - "never" - ], - "no-cond-assign": [ - "error", - "always" - ], - "no-console": "warn", - "no-constant-condition": "warn", - "no-control-regex": "error", - "no-debugger": "error", - "no-dupe-args": "error", - "no-dupe-keys": "error", - "no-duplicate-case": "error", - "no-empty": "warn", - "no-empty-character-class": "error", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", - "no-extra-parens": [ - "off", - "all", - { - "conditionalAssign": true, - "nestedBinaryExpressions": false, - "returnAssign": false - } - ], - "no-extra-semi": "error", - "no-func-assign": "error", - "no-inner-declarations": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-obj-calls": "error", - "no-prototype-builtins": "error", - "no-regex-spaces": "error", - "no-sparse-arrays": "error", - "no-template-curly-in-string": "error", - "no-unexpected-multiline": "error", - "no-unsafe-finally": "error", - "no-unsafe-negation": "error", - "no-negated-in-lhs": "off", - "use-isnan": "error", - "valid-jsdoc": "off", - "valid-typeof": [ - "error", - { - "requireStringLiterals": true - } - ], - "array-bracket-spacing": [ - "error", - "never" - ], - "block-spacing": [ - "error", - "always" - ], - "brace-style": [ - "warn", - "stroustrup", - { - "allowSingleLine": false - } - ], - "camelcase": [ - "warn", - { - "properties": "never" - } - ], - "comma-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "comma-style": [ - "error", - "last" - ], - "computed-property-spacing": [ - "error", - "never" - ], - "consistent-this": "off", - "eol-last": [ - "error", - "always" - ], - "func-call-spacing": [ - "error", - "never" - ], - "func-name-matching": [ - "off", - "always", - { - "includeCommonJSModuleExports": false - } - ], - "func-names": "warn", - "func-style": [ - "off", - "expression" - ], - "id-blacklist": "off", - "id-length": "off", - "id-match": "off", - "indent": [ - "warn", - "tab", - { - "SwitchCase": 1, - "VariableDeclarator": 1, - "outerIIFEBody": 1, - "FunctionDeclaration": { - "parameters": 1, - "body": 1 - }, - "FunctionExpression": { - "parameters": 1, - "body": 1 - } - } - ], - "jsx-quotes": [ - "off", - "prefer-double" - ], - "key-spacing": [ - "error", - { - "beforeColon": false, - "afterColon": true - } - ], - "keyword-spacing": [ - "error", - { - "before": true, - "after": true, - "overrides": { - "return": { - "after": true - }, - "throw": { - "after": true - }, - "case": { - "after": true - } - } - } - ], - "line-comment-position": [ - "off", - { - "position": "above", - "ignorePattern": "", - "applyDefaultPatterns": true - } - ], - "linebreak-style": [ - "error", - "unix" - ], - "lines-around-comment": [ - "warn", - { - "beforeBlockComment": true, - "afterBlockComment": false, - "beforeLineComment": true, - "afterLineComment": false, - "allowBlockStart": true, - "allowObjectStart": true, - "allowArrayStart": true - } - ], - "lines-around-directive": [ - "warn", - { - "before": "never", - "after": "always" - } - ], - "max-depth": [ - "off", - 4 - ], - "max-len": [ - "warn", - 120, - 2, - { - "ignoreUrls": true, - "ignoreComments": false, - "ignoreRegExpLiterals": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true - } - ], - "max-lines": [ - "off", - { - "max": 300, - "skipBlankLines": true, - "skipComments": true - } - ], - "max-nested-callbacks": "off", - "max-params": [ - "off", - 3 - ], - "max-statements": [ - "off", - 10 - ], - "max-statements-per-line": [ - "off", - { - "max": 1 - } - ], - "multiline-ternary": [ - "off", - "never" - ], - "new-cap": [ - "error", - { - "newIsCap": true, - "newIsCapExceptions": [], - "capIsNew": false, - "capIsNewExceptions": [ - "Immutable.Map", - "Immutable.Set", - "Immutable.List" - ] - } - ], - "new-parens": "error", - "newline-after-var": "off", - "newline-before-return": "off", - "newline-per-chained-call": [ - "error", - { - "ignoreChainWithDepth": 4 - } - ], - "no-array-constructor": "error", - "no-bitwise": "error", - "no-continue": "off", - "no-inline-comments": "off", - "no-lonely-if": "error", - "no-mixed-operators": [ - "warn", - { - "groups": [ - [ - "+", - "-", - "*", - "/", - "%", - "**" - ], - [ - "&", - "|", - "^", - "~", - "<<", - ">>", - ">>>" - ], - [ - "==", - "!=", - "===", - "!==", - ">", - ">=", - "<", - "<=" - ], - [ - "&&", - "||" - ], - [ - "in", - "instanceof" - ] - ], - "allowSamePrecedence": false - } - ], - "no-mixed-spaces-and-tabs": "warn", - "no-multiple-empty-lines": [ - "warn", - { - "max": 2, - "maxEOF": 1 - } - ], - "no-negated-condition": "off", - "no-nested-ternary": "warn", - "no-new-object": "error", - "no-plusplus": [ - "warn", - { - "allowForLoopAfterthoughts": true - } - ], - "no-restricted-syntax": [ - "error", - "ForInStatement", - "LabeledStatement", - "WithStatement" - ], - "no-spaced-func": "error", - "no-ternary": "off", - "no-trailing-spaces": "warn", - "no-underscore-dangle": [ - "off", - { - "allowAfterThis": true - } - ], - "no-unneeded-ternary": [ - "error", - { - "defaultAssignment": false - } - ], - "no-whitespace-before-property": "error", - "object-curly-spacing": [ - "warn", - "always" - ], - "object-curly-newline": [ - "off", - { - "ObjectExpression": { - "minProperties": 0, - "multiline": true - }, - "ObjectPattern": { - "minProperties": 0, - "multiline": true - } - } - ], - "object-property-newline": [ - "error", - { - "allowMultiplePropertiesPerLine": true - } - ], - "one-var": [ - "error", - "never" - ], - "one-var-declaration-per-line": [ - "error", - "always" - ], - "operator-assignment": [ - "error", - "always" - ], - "operator-linebreak": "off", - "padded-blocks": [ - "off", - "never" - ], - "quote-props": [ - "error", - "as-needed", - { - "keywords": false, - "unnecessary": true, - "numbers": false - } - ], - "quotes": [ - "warn", - "single", - { - "avoidEscape": true - } - ], - "require-jsdoc": [ - "warn", - { - "require": { - "FunctionDeclaration": false, - "MethodDefinition": true, - "ClassDeclaration": false, - "ArrowFunctionExpression": false - } - } - ], - "semi": [ - "error", - "always" - ], - "semi-spacing": [ - "error", - { - "before": false, - "after": true - } - ], - "sort-keys": [ - "off", - "asc", - { - "caseSensitive": false, - "natural": true - } - ], - "sort-vars": "off", - "space-before-blocks": "error", - "space-before-function-paren": [ - "error", - { - "anonymous": "always", - "named": "never", - "asyncArrow": "always" - } - ], - "space-in-parens": [ - "error", - "never" - ], - "space-infix-ops": "error", - "space-unary-ops": [ - "error", - { - "words": true, - "nonwords": false, - "overrides": {} - } - ], - "spaced-comment": [ - "error", - "always", - { - "line": { - "exceptions": [ - "-", - "+" - ], - "markers": [ - "=", - "!" - ] - }, - "block": { - "exceptions": [ - "-", - "+" - ], - "markers": [ - "=", - "!" - ], - "balanced": false - } - } - ], - "unicode-bom": [ - "error", - "never" - ], - "wrap-regex": "off", - "init-declarations": "off", - "no-catch-shadow": "off", - "no-delete-var": "error", - "no-label-var": "error", - "no-restricted-globals": "off", - "no-shadow": "error", - "no-shadow-restricted-names": "error", - "no-undef": "error", - "no-undef-init": "error", - "no-undefined": "off", - "no-unused-vars": [ - "warn", - { - "vars": "local", - "args": "after-used" - } - ], - "no-use-before-define": "error", - "strict": [ - "error", - "global" - ] - } -} diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 3df82f2..9ad2354 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm audit --parseable --production --audit-level=moderate diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index f092473..413b288 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci --no-optional diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 058a839..2cee9cd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,12 +11,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' registry-url: 'https://registry.npmjs.org' - name: Install dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b35bb3..4003a5b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,13 +13,12 @@ jobs: matrix: node-version: [18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci --no-optional env: CI: true - run: npm run test - - run: npm run test:integration diff --git a/.gitignore b/.gitignore index ea0b7b1..041d411 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ app/tests/coverage/ coverage/ .nyc_output/ dist/ +types/ *.tgz # IDE's diff --git a/CHANGELOG.md b/CHANGELOG.md index e4cacbd..26db4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,112 +1,255 @@ -# [0.17.0](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.5...v0.17.0) (2025-08-12) +# [1.0.0-rc.33](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.32...v1.0.0-rc.33) (2026-01-26) +### Fixes + +* Concurrent operations handling in SqliteObjectStorage updateEnforcingNew ([bab7807](https://github.com/snatalenko/node-cqrs/commit/bab78078de52bd88bb86c293adb87eeb974241d5)) + + +# [1.0.0-rc.32](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.31...v1.0.0-rc.32) (2026-01-09) + -# [1.0.0-rc.5](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.4...v1.0.0-rc.5) (2024-10-27) +### Internal Fixes + +* Update Lock interface to support resource management with `using` keyword ([196332e](https://github.com/snatalenko/node-cqrs/commit/196332e1f382880161e0f7192966e2fb4f222be7)) +* Expose connection state events on RabbitMqGateway ([42fe349](https://github.com/snatalenko/node-cqrs/commit/42fe3497ce886bc4e20efa6008b97104380a8ba5)) + + +# [1.0.0-rc.31](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.30...v1.0.0-rc.31) (2025-12-22) ### Changes -* Add `InMemoryView.prototype.getSync` method ([5d4adb9](https://github.com/snatalenko/node-cqrs/commit/5d4adb9109c4c85edae2b0f3dfd995e8c51aef06)) +* Auto-reconnect to RabbitMQ ([ba80536](https://github.com/snatalenko/node-cqrs/commit/ba8053697fb271a57fde7fc236d0f15c7d497c8e)) + + +# [1.0.0-rc.30](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.29...v1.0.0-rc.30) (2025-12-22) + + +### Internal Fixes + +* MQ consumption starts before handler is properly recorded ([35a974b](https://github.com/snatalenko/node-cqrs/commit/35a974b15ab650728768d1efd655b45a6df052fb)) +* RabbitMQ connection not auto-closing on SIGTERM ([63b4f48](https://github.com/snatalenko/node-cqrs/commit/63b4f48f1abc6936472db66e821de2543dbc874b)) + + +# [1.0.0-rc.29](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.28...v1.0.0-rc.29) (2025-12-21) + +### Internal Fixes -# [1.0.0-rc.4](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.3...v1.0.0-rc.4) (2024-10-02) +* Enhance logging in RabbitMqGateway and AbstractProjection for better traceability ([57d3f30](https://github.com/snatalenko/node-cqrs/commit/57d3f3099cc52c19963279a2b4a66c79e5fbd3ee)) +# [1.0.0-rc.28](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.27...v1.0.0-rc.28) (2025-12-17) -# [1.0.0-rc.3](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.2...v1.0.0-rc.3) (2024-09-23) +### Internal Fixes +* Refactor subscription handling, improve logging on subscription removing ([72c5370](https://github.com/snatalenko/node-cqrs/commit/72c537092c435fe68e343c33ad46d99a1f474b06)) -# [1.0.0-rc.2](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.1...v1.0.0-rc.2) (2024-08-03) +# [1.0.0-rc.27](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.26...v1.0.0-rc.27) (2025-12-17) -# [1.0.0-rc.1](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.0...v1.0.0-rc.1) (2024-08-03) +### Internal Fixes +* Close rabbitmq connection on SIGINT/SIGTERM ([21686be](https://github.com/snatalenko/node-cqrs/commit/21686bebb6a0ca5901263f3d382ffe369d62ef85)) + + +# [1.0.0-rc.26](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.25...v1.0.0-rc.26) (2025-12-05) + + +### Changes + +* Move reconnect logic to rabbitMqConnectionFactory; re-establish subscriptions on reconnect ([a42d138](https://github.com/snatalenko/node-cqrs/commit/a42d138fc93bc767ae5d7fac75f5582cb3936103)) ### Build System -* Add NPM publishing script ([3372990](https://github.com/snatalenko/node-cqrs/commit/3372990ba2549695398e0949e35009396e660005)) -* Suppress audit and test for tags ([574a00c](https://github.com/snatalenko/node-cqrs/commit/574a00cc53af009994ca4dd3278cb764743b4ad6)) +* Update changelog titles and commit message prefixes ([8c6ead0](https://github.com/snatalenko/node-cqrs/commit/8c6ead0a9b4f3feba7bbfba539082eeb0b09b9f9)) + +### Internal Fixes + +* Use "quorum" type for durable queues ([f617149](https://github.com/snatalenko/node-cqrs/commit/f6171498db544d820e876d550421eef75c66088f)) +* Vulnerability in js-yaml dev dependency ([0e9b25e](https://github.com/snatalenko/node-cqrs/commit/0e9b25edd0a81581fb084256638c9ab56afb4115)) +* Ensure proper subscription management in TerminationHandler ([506acc2](https://github.com/snatalenko/node-cqrs/commit/506acc2dde02dd4d83cb8e8d6079dc63fa992651)) + + +# [1.0.0-rc.25](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.24...v1.0.0-rc.25) (2025-10-31) + + + +# [1.0.0-rc.24](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.23...v1.0.0-rc.24) (2025-10-30) + + +# [1.0.0-rc.23](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.22...v1.0.0-rc.23) (2025-10-26) -# [1.0.0-rc.0](https://github.com/snatalenko/node-cqrs/compare/v0.16.4...v1.0.0-rc.0) (2024-08-02) + + +# [1.0.0-rc.22](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.21...v1.0.0-rc.22) (2025-10-23) + + + +# [1.0.0-rc.21](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.20...v1.0.0-rc.21) (2025-10-14) ### Fixes -* Vulnerability in minimist dependency ([07b8c68](https://github.com/snatalenko/node-cqrs/commit/07b8c682fae4278965aa13a06caa994c037934e9)) +* Proper milliseconds calculation for Event Locker ([ca4016a](https://github.com/snatalenko/node-cqrs/commit/ca4016a486a7b2a010f86174140bd21e0a1c0d08)) -### Refactoring -* Migrate to TS and Jest ([6737d55](https://github.com/snatalenko/node-cqrs/commit/6737d5566a9dc6314df0b20a65d32414fc503e54)) +# [1.0.0-rc.20](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.19...v1.0.0-rc.20) (2025-10-13) -## [0.16.4](https://github.com/snatalenko/node-cqrs/compare/v0.16.3...v0.16.4) (2022-08-28) +### Changes +* Enhance type safety in CqrsContainerBuilder with generics ([025765c](https://github.com/snatalenko/node-cqrs/commit/025765cc31eec5a004142dff5cafd8264af10ea9)) -### Refactoring -* Use di package from npm ([0e8db91](https://github.com/snatalenko/node-cqrs/commit/0e8db91636541e95f804e2c266e2d8bbf0f49a8b)) +# [1.0.0-rc.19](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.18...v1.0.0-rc.19) (2025-09-24) + + + +# [1.0.0-rc.18](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.17...v1.0.0-rc.18) (2025-09-11) + + + +# [1.0.0-rc.17](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.16...v1.0.0-rc.17) (2025-08-29) -## [0.16.3](https://github.com/snatalenko/node-cqrs/compare/v0.16.2...v0.16.3) (2022-01-28) + +# [1.0.0-rc.16](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.15...v1.0.0-rc.16) (2025-08-29) + + + +# [1.0.0-rc.15](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.14...v1.0.0-rc.15) (2025-08-24) + + + +# [1.0.0-rc.14](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.13...v1.0.0-rc.14) (2025-08-15) + + + +# [1.0.0-rc.13](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.12...v1.0.0-rc.13) (2025-08-14) + + + +# [1.0.0-rc.12](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.11...v1.0.0-rc.12) (2025-08-11) + + + +# [1.0.0-rc.11](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.10...v1.0.0-rc.11) (2025-05-09) ### Changes -* Update dev dependencies ([e76db7b](https://github.com/snatalenko/node-cqrs/commit/e76db7be66b53afeb619bda459686e490530556f)) -* Remove InMemoryView data size calculation ([fb4260b](https://github.com/snatalenko/node-cqrs/commit/fb4260b94170e371c02be5b6867ba5b1cf7e428f)) +* Cache immediate aggregates to handle concurrent commands ([e193c4c](https://github.com/snatalenko/node-cqrs/commit/e193c4c8dc7b91de6cbc84e2ac668170ddb48bc0)) +### Internal Fixes + +* Use `structuredClone` for snapshot creation ([1d0e827](https://github.com/snatalenko/node-cqrs/commit/1d0e827da71c760739588a37ae6afe63a4fa8d34)) +* Simplify aggregate interface ([3e141fd](https://github.com/snatalenko/node-cqrs/commit/3e141fd217c4a094a57fefe8788816d474020ffe)) -## [0.16.2](https://github.com/snatalenko/node-cqrs/compare/v0.16.1...v0.16.2) (2021-07-06) + +# [1.0.0-rc.10](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.9...v1.0.0-rc.10) (2025-04-13) ### Fixes -* Vulnerabilities in dependencies ([1bdd491](https://github.com/snatalenko/node-cqrs/commit/1bdd4916e3080bd96b15d87c947f6b85e44d6d40)) +* Asserting db connection in prolongLock and unlock methods ([b272473](https://github.com/snatalenko/node-cqrs/commit/b2724739b3ff483b13c0cfeea30c73c7d8ab8b94)) -## [0.16.1](https://github.com/snatalenko/node-cqrs/compare/v0.16.0...v0.16.1) (2021-05-28) +# [1.0.0-rc.9](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.8...v1.0.0-rc.9) (2025-04-13) -### Fixes -* Mark aggregateId optional on command send ([f496ecf](https://github.com/snatalenko/node-cqrs/commit/f496ecfbd5413e8e2a4c69af7848ecc3f1a5365a)) +# [1.0.0-rc.8](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.7...v1.0.0-rc.8) (2025-04-13) + + +### Features + +* RabbitMQ integration classes to support event publishing and subscription ([991c223](https://github.com/snatalenko/node-cqrs/commit/991c2233185d3610a2b8930f6930a03c0cdea01d)) ### Changes -* Postpone view.get responses to next loop iteration ([950c2e4](https://github.com/snatalenko/node-cqrs/commit/950c2e42f62d7388b0cc668e81fb4f6718656fca)) +* Move validation, snapshot and event persistence to EventDispatcher pipeline ([e781f7c](https://github.com/snatalenko/node-cqrs/commit/e781f7c6c2e4f7c9f8c4615b170d0d29d3e8f133)) + + +# [1.0.0-rc.7](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.6...v1.0.0-rc.7) (2025-04-13) + + +### Changes + +* Remove `publishAsync` setting, simplify publishing sequence ([79257e5](https://github.com/snatalenko/node-cqrs/commit/79257e59d322df5dd8e41bedf5273c97ae77b609)) -# [0.16.0](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-5...v0.16.0) (2020-03-18) +# [1.0.0-rc.6](https://github.com/snatalenko/node-cqrs/compare/v0.16.4...v1.0.0-rc.6) (2025-03-21) +### Changes + +* Add `InMemoryView.prototype.getSync` method ([5d4adb9](https://github.com/snatalenko/node-cqrs/commit/5d4adb9109c4c85edae2b0f3dfd995e8c51aef06)) +* Support persistent views; Add SQLite infrastructure ([c235573](https://github.com/snatalenko/node-cqrs/commit/c235573678be349d031d1a696cab3993224979a2)) + ### Fixes -* Moderate security issue in "minimist" dev dependency ([579d523](https://github.com/snatalenko/node-cqrs/commit/579d523745a6d33902a5245bc7e9f3fe843abc2b)) +* Vulnerability in minimist dependency ([07b8c68](https://github.com/snatalenko/node-cqrs/commit/07b8c682fae4278965aa13a06caa994c037934e9)) +### Build System -# [0.16.0-5](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-4...v0.16.0-5) (2020-02-19) +* Add NPM publishing script ([3372990](https://github.com/snatalenko/node-cqrs/commit/3372990ba2549695398e0949e35009396e660005)) +* Suppress audit and test for tags ([574a00c](https://github.com/snatalenko/node-cqrs/commit/574a00cc53af009994ca4dd3278cb764743b4ad6)) +### Internal Fixes +* Migrate to TS and Jest ([6737d55](https://github.com/snatalenko/node-cqrs/commit/6737d5566a9dc6314df0b20a65d32414fc503e54)) +* EventStore not subscribing to events emitted by `storage` ([84eaea1](https://github.com/snatalenko/node-cqrs/commit/84eaea17650589717af1720921716246762fec86)) -# [0.16.0-4](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-3...v0.16.0-4) (2020-02-19) +## [0.16.4](https://github.com/snatalenko/node-cqrs/compare/v0.16.3...v0.16.4) (2022-08-28) -# [0.16.0-3](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-2...v0.16.0-3) (2020-01-28) +### Internal Fixes +* Use di package from npm ([0e8db91](https://github.com/snatalenko/node-cqrs/commit/0e8db91636541e95f804e2c266e2d8bbf0f49a8b)) -### Features -* Detect circular dependencies in DI container ([1490b51](https://github.com/snatalenko/node-cqrs/commit/1490b519c7581b1de6cd084d91f61875751d773b)) +## [0.16.3](https://github.com/snatalenko/node-cqrs/compare/v0.16.2...v0.16.3) (2022-01-28) + + +### Changes + +* Update dev dependencies ([e76db7b](https://github.com/snatalenko/node-cqrs/commit/e76db7be66b53afeb619bda459686e490530556f)) +* Remove InMemoryView data size calculation ([fb4260b](https://github.com/snatalenko/node-cqrs/commit/fb4260b94170e371c02be5b6867ba5b1cf7e428f)) + + +## [0.16.2](https://github.com/snatalenko/node-cqrs/compare/v0.16.1...v0.16.2) (2021-07-06) + ### Fixes -* Debug output on one time subscriptions ([2fd7601](https://github.com/snatalenko/node-cqrs/commit/2fd7601b6b8e8059f0b777af6c1294cc78cb787b)) -* Correctly set type of the extended container builder created from container ([1f2f632](https://github.com/snatalenko/node-cqrs/commit/1f2f6325ceab65c4c81494d145261668125d03b1)) +* Vulnerabilities in dependencies ([1bdd491](https://github.com/snatalenko/node-cqrs/commit/1bdd4916e3080bd96b15d87c947f6b85e44d6d40)) + + +## [0.16.1](https://github.com/snatalenko/node-cqrs/compare/v0.16.0...v0.16.1) (2021-05-28) + + +### Changes + +* Postpone view.get responses to next loop iteration ([950c2e4](https://github.com/snatalenko/node-cqrs/commit/950c2e42f62d7388b0cc668e81fb4f6718656fca)) + +### Fixes + +* Mark aggregateId optional on command send ([f496ecf](https://github.com/snatalenko/node-cqrs/commit/f496ecfbd5413e8e2a4c69af7848ecc3f1a5365a)) + + +# [0.16.0](https://github.com/snatalenko/node-cqrs/compare/v0.15.1...v0.16.0) (2020-03-18) + + +### Features + +* Accept logger as an optional dependency ([65fe5ad](https://github.com/snatalenko/node-cqrs/commit/65fe5ad8a9de48d548715a2bd651f6d9c4cb0af1)) +* Detect circular dependencies in DI container ([1490b51](https://github.com/snatalenko/node-cqrs/commit/1490b519c7581b1de6cd084d91f61875751d773b)) ### Changes @@ -117,128 +260,226 @@ * Remove dependency to nodejs EventEmitter ([3fd7cd8](https://github.com/snatalenko/node-cqrs/commit/3fd7cd84bb3c20ec4189bd0083ef83bc07dc62d5)) * Wrap types in NodeCqrs namespace ([74e9b67](https://github.com/snatalenko/node-cqrs/commit/74e9b67833592c030d67fe605f160f99664d9b6c)) +### Fixes + +* Debug output not using toString in Node 12 ([ca0d32f](https://github.com/snatalenko/node-cqrs/commit/ca0d32f78a676faf45a342f4198ef4a93a3d0702)) +* Debug output on one time subscriptions ([2fd7601](https://github.com/snatalenko/node-cqrs/commit/2fd7601b6b8e8059f0b777af6c1294cc78cb787b)) +* Correctly set type of the extended container builder created from container ([1f2f632](https://github.com/snatalenko/node-cqrs/commit/1f2f6325ceab65c4c81494d145261668125d03b1)) +* Moderate security issue in "minimist" dev dependency ([579d523](https://github.com/snatalenko/node-cqrs/commit/579d523745a6d33902a5245bc7e9f3fe843abc2b)) + ### Documentation * Add saga documentation ([e27d1e3](https://github.com/snatalenko/node-cqrs/commit/e27d1e34a0792bec7098535ebec20c97c0f01ed4)) ### Tests +* Fix tests in Node 12 ([beeb471](https://github.com/snatalenko/node-cqrs/commit/beeb471faee9e1259f11b4c1c65877cd27309637)) * Run example domain tests with unit tests ([5ffdb43](https://github.com/snatalenko/node-cqrs/commit/5ffdb43c0398fc6650a7a1d62a5f07870ee20bfd)) * Run eslint for entire project folder ([d9055a1](https://github.com/snatalenko/node-cqrs/commit/d9055a158faa67dc9ece4f77b01517a5480b0a18)) ### Build System +* Prevent git push on version ([3ea9e38](https://github.com/snatalenko/node-cqrs/commit/3ea9e38babf440ab384235e69d248fd92a2dfdff)) +* Add conventional-changelog script ([da26a1c](https://github.com/snatalenko/node-cqrs/commit/da26a1cf6db0a609fcb3f1ba3a29ce6db6d0ab95)) +* Run tests in NodeJS 12 env ([1d4239c](https://github.com/snatalenko/node-cqrs/commit/1d4239cf0f48e64105bfd6b28ab9a22f3fd23e7e)) +* Replace changelog eslint preset with custom one ([8507262](https://github.com/snatalenko/node-cqrs/commit/8507262eeb7c367bbb8bd52b74e04c678bfcf956)) * Exclude unnecessary files from package ([47b6797](https://github.com/snatalenko/node-cqrs/commit/47b679750780c0d7840d4d45a1296dc9bef7d674)) * Do not install global dependencies ([158783c](https://github.com/snatalenko/node-cqrs/commit/158783c299720e709b8a34f3ef74fba1390d03ad)) -# [0.16.0-2](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-1...v0.16.0-2) (2019-12-18) +## [0.15.1](https://github.com/snatalenko/node-cqrs/compare/v0.15.0...v0.15.1) (2019-08-26) -### Features +### Changes -* Accept logger as an optional dependency ([65fe5ad](https://github.com/snatalenko/node-cqrs/commit/65fe5ad8a9de48d548715a2bd651f6d9c4cb0af1)) +* Upgrade dev dependencies to fix audit script ([ef01cc3](https://github.com/snatalenko/node-cqrs/commit/ef01cc33b63a95a8783a83b34c4fcb3f4830fe52)) -### Build System -* Replace changelog eslint preset with custom one ([8507262](https://github.com/snatalenko/node-cqrs/commit/8507262eeb7c367bbb8bd52b74e04c678bfcf956)) +# [0.15.0](https://github.com/snatalenko/node-cqrs/compare/v0.14.2...v0.15.0) (2019-08-25) -## [0.15.1](https://github.com/snatalenko/node-cqrs/compare/v0.15.0...v0.15.1) (2019-08-26) +## [0.14.2](https://github.com/snatalenko/node-cqrs/compare/v0.14.1...v0.14.2) (2018-07-29) -### Changes -* Upgrade dev dependencies to fix audit script ([ef01cc3](https://github.com/snatalenko/node-cqrs/commit/ef01cc33b63a95a8783a83b34c4fcb3f4830fe52)) +## [0.14.1](https://github.com/snatalenko/node-cqrs/compare/v0.14.0...v0.14.1) (2018-07-14) -# [0.16.0-1](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-0...v0.16.0-1) (2019-11-28) -### Changes -* EventStore to return async event generators (requires NodeJS version 10+) +# [0.14.0](https://github.com/snatalenko/node-cqrs/compare/v0.13.0...v0.14.0) (2018-05-17) -### Build -* Add conventional-changelog script ([da26a1c](https://github.com/snatalenko/node-cqrs/commit/da26a1cf6db0a609fcb3f1ba3a29ce6db6d0ab95)) -* Prevent git push on version ([3ea9e38](https://github.com/snatalenko/node-cqrs/commit/3ea9e38babf440ab384235e69d248fd92a2dfdff)) -* Run tests in NodeJS 12 env ([1d4239c](https://github.com/snatalenko/node-cqrs/commit/1d4239cf0f48e64105bfd6b28ab9a22f3fd23e7e)) -### Fix +# [0.13.0](https://github.com/snatalenko/node-cqrs/compare/v0.12.6...v0.13.0) (2017-10-04) -* Debug output not using toString in Node 12 ([ca0d32f](https://github.com/snatalenko/node-cqrs/commit/ca0d32f78a676faf45a342f4198ef4a93a3d0702)) -### Tests -* Fix tests in Node 12 ([beeb471](https://github.com/snatalenko/node-cqrs/commit/beeb471faee9e1259f11b4c1c65877cd27309637)) +## [0.12.6](https://github.com/snatalenko/node-cqrs/compare/v0.12.5...v0.12.6) (2017-08-23) -### Upgrade -* debug, mocha, sinon ([ac80c27](https://github.com/snatalenko/node-cqrs/commit/ac80c27653828904cf7b80d37b0ecade860b7490)) -## 0.15.0 - 2018-08-25 +## [0.12.5](https://github.com/snatalenko/node-cqrs/compare/v0.12.4...v0.12.5) (2017-06-23) -### Features -* `InMemoryView.prototype.getAll` as an alternative to the deprecated `state` property +## [0.12.4](https://github.com/snatalenko/node-cqrs/compare/v0.12.3...v0.12.4) (2017-04-25) -### Changes -* `InMemoryView.prototype.create` 2nd parameter must be an instance of an Object, not a factory function -* `InMemoryView.prototype.updateEnforcingNew` does not pass an empty object as a parameter when record does not exist -* Observable `on(,,{queueName})` replaced with `queue(name).on(,)`; -* separated IProjectionView and IConcurrentView interfaces -* `IProjectionView.prototype.shouldRestore` can return Promise -* Projection `restore` process flow to support async concurrent views -### Fixes +## [0.12.3](https://github.com/snatalenko/node-cqrs/compare/v0.12.1...v0.12.3) (2017-04-24) -* Typings -* Call stack overflow in EventStream constructor on large number of events -## 0.14.2 (2018-07-29) +## [0.12.1](https://github.com/snatalenko/node-cqrs/compare/v0.12.0...v0.12.1) (2017-04-24) -### Fixes -* `Container.prototype.registerInstance` requires an Object as first parameter -## 0.14.1 (2018-07-14) +# [0.12.0](https://github.com/snatalenko/node-cqrs/compare/v0.11.1...v0.12.0) (2017-04-22) -### Features -* `Aggregate.prototype.makeEvent` as a separate method for testing purposes -### Fixes +## [0.11.1](https://github.com/snatalenko/node-cqrs/compare/v0.11.0...v0.11.1) (2017-03-01) -* Aggregate snapshot modification thru Aggregate state -* Tests with NodeJS@^10 -## 0.14.0 (2018-05-17) +# [0.11.0](https://github.com/snatalenko/node-cqrs/compare/v0.10.0...v0.11.0) (2017-01-18) -### Features -* examples/user-domain -* typings -* changelog -### Changes +# [0.10.0](https://github.com/snatalenko/node-cqrs/compare/v0.9.3...v0.10.0) (2017-01-16) -* snapshotStorage moved to a separate interface/entity -* named queues handling moved out of EventStore to InMemoryMessageBus implementation -* command-to-event context copying moved out of EventStore to AbstractAggregate.prototype.emit, which frees up road for a concurrent operations on same aggregate implementation -* EventStream is immutable -* `AbstractProjection.prototype.shouldRestoreView` can be overriden in projection for own view implementations -## 0.13.0 (2017-10-04) -### Documentation +## [0.9.3](https://github.com/snatalenko/node-cqrs/compare/v0.9.2...v0.9.3) (2017-01-06) + + + +## [0.9.2](https://github.com/snatalenko/node-cqrs/compare/v0.9.1...v0.9.2) (2016-12-19) + + + +## [0.9.1](https://github.com/snatalenko/node-cqrs/compare/v0.9.0...v0.9.1) (2016-12-17) + + + +# [0.9.0](https://github.com/snatalenko/node-cqrs/compare/v0.8.0...v0.9.0) (2016-12-17) + + + +# [0.8.0](https://github.com/snatalenko/node-cqrs/compare/v0.7.8...v0.8.0) (2016-12-07) + + + +## [0.7.8](https://github.com/snatalenko/node-cqrs/compare/v0.7.7...v0.7.8) (2016-12-05) + + + +## [0.7.7](https://github.com/snatalenko/node-cqrs/compare/v0.7.6...v0.7.7) (2016-12-04) + + + +## [0.7.6](https://github.com/snatalenko/node-cqrs/compare/v0.7.5...v0.7.6) (2016-12-01) + + + +## [0.7.5](https://github.com/snatalenko/node-cqrs/compare/v0.7.4...v0.7.5) (2016-12-01) + + + +## [0.7.4](https://github.com/snatalenko/node-cqrs/compare/v0.7.3...v0.7.4) (2016-11-30) + + + +## [0.7.3](https://github.com/snatalenko/node-cqrs/compare/v0.7.2...v0.7.3) (2016-11-29) + + + +## [0.7.2](https://github.com/snatalenko/node-cqrs/compare/v0.7.1...v0.7.2) (2016-11-25) + + + +## [0.7.1](https://github.com/snatalenko/node-cqrs/compare/v0.7.0...v0.7.1) (2016-11-20) + + + +# [0.7.0](https://github.com/snatalenko/node-cqrs/compare/v0.6.10...v0.7.0) (2016-11-18) + + + +## [0.6.10](https://github.com/snatalenko/node-cqrs/compare/v0.6.9...v0.6.10) (2016-10-24) + + + +## [0.6.9](https://github.com/snatalenko/node-cqrs/compare/v0.6.8...v0.6.9) (2016-10-24) + + + +## [0.6.8](https://github.com/snatalenko/node-cqrs/compare/v0.6.7...v0.6.8) (2016-10-23) + + + +## [0.6.7](https://github.com/snatalenko/node-cqrs/compare/v0.6.6...v0.6.7) (2016-10-23) + + + +## [0.6.6](https://github.com/snatalenko/node-cqrs/compare/v0.6.5...v0.6.6) (2016-08-23) + + + +## [0.6.5](https://github.com/snatalenko/node-cqrs/compare/v0.6.4...v0.6.5) (2016-08-23) + + + +## [0.6.4](https://github.com/snatalenko/node-cqrs/compare/v0.6.3...v0.6.4) (2016-07-24) + + + +## [0.6.3](https://github.com/snatalenko/node-cqrs/compare/v0.6.2...v0.6.3) (2016-07-06) + + + +## [0.6.2](https://github.com/snatalenko/node-cqrs/compare/v0.6.1...v0.6.2) (2016-07-02) + + + +## [0.6.1](https://github.com/snatalenko/node-cqrs/compare/v0.6.0...v0.6.1) (2016-05-31) + + + +# [0.6.0](https://github.com/snatalenko/node-cqrs/compare/v0.5.0...v0.6.0) (2016-03-06) + + + +# [0.5.0](https://github.com/snatalenko/node-cqrs/compare/v0.4.0...v0.5.0) (2016-03-03) + + + +# [0.4.0](https://github.com/snatalenko/node-cqrs/compare/v0.3.2...v0.4.0) (2016-03-03) + + + +## [0.3.2](https://github.com/snatalenko/node-cqrs/compare/v0.3.1...v0.3.2) (2016-02-29) + + + +## [0.3.1](https://github.com/snatalenko/node-cqrs/compare/v0.3.0...v0.3.1) (2016-02-29) + + + +# [0.3.0](https://github.com/snatalenko/node-cqrs/compare/v0.2.2...v0.3.0) (2016-02-29) + + + +## [0.2.2](https://github.com/snatalenko/node-cqrs/compare/v0.2.1...v0.2.2) (2015-12-23) + + + +## [0.2.1](https://github.com/snatalenko/node-cqrs/compare/v0.2.0...v0.2.1) (2015-12-22) + + + +# 0.2.0 (2015-12-22) -* docs publishing to [node-cqrs.org](https://www.node-cqrs.org) -### Changes -* In-Memory views do not respond to get(..) requests until they are restored -* In-Memory views restoring is handled by AbstractProjection diff --git a/docs/entities/Projection/README.md b/docs/entities/Projection/README.md index 3e797c7..0a94cf6 100644 --- a/docs/entities/Projection/README.md +++ b/docs/entities/Projection/README.md @@ -4,7 +4,7 @@ Projection is an Observer, that listens to events and updates an associated View ## Projection View Restoring -By default, an [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryViewStorage.js) is used. That means that upon application start, Projection queries all known events from the EventStore and projects them to the view. Once this process is complete, the view's `ready` property gets switched from *false* to *true*. +By default, an [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryViewStorage.js) is used. That means that upon application start, Projection queries all known events from the EventStore and projects them to the view. Once this process is complete, the view's `ready` property gets switched from *false* to *true*. ## Projection Event Handlers diff --git a/docs/infrastructure/README.md b/docs/infrastructure/README.md index 762deaf..93a194c 100644 --- a/docs/infrastructure/README.md +++ b/docs/infrastructure/README.md @@ -2,9 +2,9 @@ node-cqrs comes with a set of In-Memory infrastructure service implementations. They are suitable for test purposes, since all data is persisted in process memory only: -* [InMemoryEventStorage](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryEventStorage.js) -* [InMemoryMessageBus](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryMessageBus.js) -* [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryView.js) +* [InMemoryEventStorage](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryEventStorage.js) +* [InMemoryMessageBus](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryMessageBus.js) +* [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryView.js) The following storage/bus implementations persist data in external storages and can be used in production: diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..4e2dec9 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,839 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import tsParser from "@typescript-eslint/parser"; +import tsPlugin from "@typescript-eslint/eslint-plugin"; +import jestPlugin from 'eslint-plugin-jest'; +import globals from "globals"; + +export default defineConfig([ + globalIgnores([ + "coverage/*", + "dist/*", + "types/*" + ]), + { + files: [ + "**/*.ts" + ], + languageOptions: { + parser: tsParser, + globals: { + ...globals.node, + NodeJS: true + } + }, + plugins: { + "@typescript-eslint": tsPlugin, + }, + "rules": { + "no-explicit-any": "off", + "no-unused-vars": "off", + "no-use-before-define": "warn", + "strict": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "vars": "local", + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "^(_|err)" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-empty-object-type": "off", + "padding-line-between-statements": [ + "warn", + { + "blankLine": "always", + "prev": "if", + "next": "return" + }, + { + "blankLine": "any", + "prev": "block-like", + "next": "return" + }, + { + "blankLine": "always", + "prev": "if", + "next": "const" + }, + { + "blankLine": "any", + "prev": "block-like", + "next": "const" + } + ], + "nonblock-statement-body-position": [ + "error", + "below" + ], + "accessor-pairs": "off", + "array-callback-return": "error", + "block-scoped-var": "error", + "complexity": [ + "off", + 11 + ], + "class-methods-use-this": "warn", + "consistent-return": "error", + "curly": [ + "error", + "multi-or-nest", + "consistent" + ], + "default-case": [ + "error", + { + "commentPattern": "^no default$" + } + ], + "dot-notation": [ + "error", + { + "allowKeywords": true + } + ], + "dot-location": [ + "error", + "property" + ], + "eqeqeq": [ + "error", + "allow-null" + ], + "guard-for-in": "error", + "no-alert": "error", + "no-caller": "error", + "no-case-declarations": "error", + "no-div-regex": "off", + "no-else-return": "off", + "no-empty-function": [ + "error", + { + "allow": [ + "arrowFunctions", + "methods", + "getters" + ] + } + ], + "no-empty-pattern": "error", + "no-eq-null": "off", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-label": "error", + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-global-assign": [ + "error", + { + "exceptions": [] + } + ], + "no-native-reassign": "off", + "no-implicit-coercion": [ + "off", + { + "boolean": false, + "number": true, + "string": true, + "allow": [] + } + ], + "no-implicit-globals": "off", + "no-implied-eval": "error", + "no-invalid-this": "off", + "no-iterator": "error", + "no-labels": [ + "error", + { + "allowLoop": false, + "allowSwitch": false + } + ], + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-magic-numbers": [ + "off", + { + "ignore": [], + "ignoreArrayIndexes": true, + "enforceConst": true, + "detectObjects": false + } + ], + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-param-reassign": [ + "off", + { + "props": true + } + ], + "no-proto": "error", + "no-redeclare": "error", + "no-restricted-properties": [ + "error", + { + "object": "arguments", + "property": "callee", + "message": "arguments.callee is deprecated" + }, + { + "property": "__defineGetter__", + "message": "Please use Object.defineProperty instead." + }, + { + "property": "__defineSetter__", + "message": "Please use Object.defineProperty instead." + }, + { + "object": "Math", + "property": "pow", + "message": "Use the exponentiation operator (**) instead." + } + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unmodified-loop-condition": "off", + "no-unused-expressions": "off", + "no-unused-labels": "error", + "no-useless-call": "off", + "no-useless-concat": "error", + "no-useless-escape": "error", + "no-useless-return": "error", + "no-void": "error", + "no-warning-comments": [ + "off", + { + "terms": [ + "todo", + "fixme", + "xxx" + ], + "location": "start" + } + ], + "no-with": "error", + "radix": "error", + "vars-on-top": "error", + "wrap-iife": [ + "error", + "outside", + { + "functionPrototypeMethods": false + } + ], + "yoda": "error", + "no-mixed-requires": "error", + "callback-return": "off", + "global-require": "error", + "handle-callback-err": "off", + "no-new-require": "error", + "no-path-concat": "error", + "no-process-env": "off", + "no-process-exit": "off", + "no-restricted-modules": "off", + "no-sync": "off", + "arrow-body-style": [ + "error", + "as-needed" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "constructor-super": "error", + "generator-star-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "no-class-assign": "error", + "no-confusing-arrow": [ + "error", + { + "allowParens": true + } + ], + "no-const-assign": "error", + "no-dupe-class-members": "error", + "no-duplicate-imports": "error", + "no-new-symbol": "error", + "no-restricted-imports": "off", + "no-this-before-super": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": [ + "error", + { + "ignoreDestructuring": false, + "ignoreImport": false, + "ignoreExport": false + } + ], + "no-var": "error", + "object-shorthand": [ + "error", + "always", + { + "ignoreConstructors": false, + "avoidQuotes": true + } + ], + "prefer-arrow-callback": "off", + "prefer-const": [ + "error", + { + "destructuring": "any", + "ignoreReadBeforeAssign": true + } + ], + "prefer-numeric-literals": "error", + "prefer-reflect": "off", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "require-yield": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "off", + { + "ignoreCase": false, + "ignoreMemberSort": false, + "memberSyntaxSortOrder": [ + "none", + "all", + "multiple", + "single" + ] + } + ], + "symbol-description": "error", + "template-curly-spacing": "error", + "yield-star-spacing": [ + "error", + "after" + ], + "comma-dangle": [ + "error", + "never" + ], + "no-cond-assign": [ + "error", + "always" + ], + "no-console": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-debugger": "error", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": [ + "off", + "all", + { + "conditionalAssign": true, + "nestedBinaryExpressions": false, + "returnAssign": false + } + ], + "no-extra-semi": "error", + "no-func-assign": "error", + "no-inner-declarations": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-obj-calls": "error", + "no-prototype-builtins": "error", + "no-regex-spaces": "error", + "no-sparse-arrays": "error", + "no-template-curly-in-string": "error", + "no-unexpected-multiline": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-negated-in-lhs": "off", + "use-isnan": "error", + "valid-jsdoc": "off", + "valid-typeof": [ + "error", + { + "requireStringLiterals": true + } + ], + "array-bracket-spacing": [ + "error", + "never" + ], + "block-spacing": [ + "error", + "always" + ], + "brace-style": [ + "error", + "stroustrup", + { + "allowSingleLine": false + } + ], + "camelcase": [ + "error", + { + "properties": "never" + } + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "consistent-this": "off", + "eol-last": [ + "error", + "always" + ], + "func-call-spacing": [ + "error", + "never" + ], + "func-name-matching": [ + "off", + "always", + { + "includeCommonJSModuleExports": false + } + ], + "func-style": [ + "off", + "expression" + ], + "id-blacklist": "off", + "id-length": "off", + "id-match": "off", + "indent": [ + "error", + "tab", + { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "FunctionDeclaration": { + "parameters": 1, + "body": 1 + }, + "FunctionExpression": { + "parameters": 1, + "body": 1 + } + } + ], + "jsx-quotes": [ + "off", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true, + "overrides": { + "return": { + "after": true + }, + "throw": { + "after": true + }, + "case": { + "after": true + } + } + } + ], + "line-comment-position": [ + "off", + { + "position": "above", + "ignorePattern": "", + "applyDefaultPatterns": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-around-comment": [ + "error", + { + "beforeBlockComment": true, + "afterBlockComment": false, + "beforeLineComment": true, + "afterLineComment": false, + "allowBlockStart": true, + "allowObjectStart": true, + "allowArrayStart": true + } + ], + "lines-around-directive": [ + "error", + { + "before": "never", + "after": "always" + } + ], + "max-depth": [ + "off", + 4 + ], + "max-len": [ + "warn", + 120, + 4, + { + "ignoreUrls": true, + "ignoreComments": false, + "ignoreRegExpLiterals": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true + } + ], + "max-lines": [ + "off", + { + "max": 300, + "skipBlankLines": true, + "skipComments": true + } + ], + "max-nested-callbacks": "off", + "max-params": [ + "warn", + 5 + ], + "max-statements": [ + "off", + 10 + ], + "max-statements-per-line": [ + "off", + { + "max": 1 + } + ], + "multiline-ternary": [ + "off", + "never" + ], + "new-cap": [ + "error", + { + "newIsCap": true, + "newIsCapExceptions": [], + "capIsNew": false, + "capIsNewExceptions": [ + "Immutable.Map", + "Immutable.Set", + "Immutable.List" + ] + } + ], + "new-parens": "error", + "newline-after-var": "off", + "newline-before-return": "off", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 4 + } + ], + "no-array-constructor": "error", + "no-bitwise": "error", + "no-continue": "off", + "no-inline-comments": "off", + "no-lonely-if": "error", + "no-mixed-operators": [ + "error", + { + "groups": [ + [ + "+", + "-", + "*", + "/", + "%", + "**" + ], + [ + "&", + "|", + "^", + "~", + "<<", + ">>", + ">>>" + ], + [ + "==", + "!=", + "===", + "!==", + ">", + ">=", + "<", + "<=" + ], + [ + "&&", + "||" + ], + [ + "in", + "instanceof" + ] + ], + "allowSamePrecedence": true + } + ], + "no-mixed-spaces-and-tabs": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 2, + "maxEOF": 1 + } + ], + "no-negated-condition": "off", + "no-nested-ternary": "error", + "no-new-object": "error", + "no-restricted-syntax": [ + "error", + "ForInStatement", + "LabeledStatement", + "WithStatement" + ], + "no-spaced-func": "error", + "no-ternary": "off", + "no-trailing-spaces": "error", + "no-underscore-dangle": [ + "off", + { + "allowAfterThis": true + } + ], + "no-unneeded-ternary": [ + "error", + { + "defaultAssignment": false + } + ], + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "object-curly-newline": [ + "off", + { + "ObjectExpression": { + "minProperties": 0, + "multiline": true + }, + "ObjectPattern": { + "minProperties": 0, + "multiline": true + } + } + ], + "object-property-newline": [ + "error", + { + "allowMultiplePropertiesPerLine": true + } + ], + "one-var": [ + "error", + "never" + ], + "one-var-declaration-per-line": [ + "error", + "always" + ], + "operator-assignment": [ + "error", + "always" + ], + "operator-linebreak": "off", + "padded-blocks": [ + "off", + "never" + ], + "quote-props": [ + "error", + "as-needed", + { + "keywords": false, + "unnecessary": true, + "numbers": false + } + ], + "quotes": [ + "error", + "single", + { + "avoidEscape": true + } + ], + "require-jsdoc": "off", + "semi": [ + "error", + "always" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "sort-keys": [ + "off", + "asc", + { + "caseSensitive": false, + "natural": true + } + ], + "sort-vars": "off", + "space-before-blocks": "error", + "space-before-function-paren": [ + "error", + { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + } + ], + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-unary-ops": [ + "error", + { + "words": true, + "nonwords": false, + "overrides": {} + } + ], + "spaced-comment": [ + "error", + "always", + { + "line": { + "exceptions": [ + "-", + "+" + ], + "markers": [ + "/", + "=", + "!" + ] + }, + "block": { + "exceptions": [ + "-", + "+" + ], + "markers": [ + "=", + "!" + ], + "balanced": false + } + } + ], + "unicode-bom": [ + "error", + "never" + ], + "wrap-regex": "off", + "init-declarations": "off", + "no-catch-shadow": "off", + "no-delete-var": "error", + "no-label-var": "error", + "no-restricted-globals": "off", + "no-shadow": "error", + "no-shadow-restricted-names": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-undefined": "off" + } + }, { + files: [ + 'tests/**/*.ts' + ], + plugins: { + jest: jestPlugin, + }, + languageOptions: { + globals: jestPlugin.environments.globals.globals, + }, + rules: { + 'jest/no-disabled-tests': 'warn', + 'jest/no-focused-tests': 'error', + 'jest/no-identical-title': 'error', + 'jest/prefer-to-have-length': 'warn', + 'jest/valid-expect': 'error', + 'class-methods-use-this': 'off', + 'no-loop-func': 'off', + 'no-return-assign': 'off', + 'no-console': 'off' + } + } +]); diff --git a/examples/user-domain-tests/index.test.js b/examples/user-domain-tests/index.test.js index 4dcb988..e5f4378 100644 --- a/examples/user-domain-tests/index.test.js +++ b/examples/user-domain-tests/index.test.js @@ -2,7 +2,6 @@ const { expect } = require('chai'); const { createContainer, createBaseInstances } = require('../user-domain'); -const { nextCycle } = require('../../src/infrastructure/utils'); describe('user-domain example', () => { @@ -10,9 +9,6 @@ describe('user-domain example', () => { const { commandBus, eventStore } = container; - // HACK: let projection restoring to start before emitting new events - await nextCycle(); - // we send a command to an aggregate that does not exist yet (userAggregateId = undefined), // a new instance will be created automatically let userAggregateId; @@ -55,8 +51,7 @@ describe('user-domain example', () => { const { commandBus, eventStore, users } = container; - // HACK: let projection restoring to start before emitting new events - await nextCycle(); + const userCreatedPromise = eventStore.once('userCreated'); await commandBus.send('createUser', undefined, { payload: { @@ -65,7 +60,7 @@ describe('user-domain example', () => { } }); - const userCreated = await eventStore.once('userCreated'); + const userCreated = await userCreatedPromise; const viewRecord = await users.get(userCreated.aggregateId); diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index 11e2bee..e276a54 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -6,8 +6,10 @@ const { CommandBus, EventStore, AggregateCommandHandler, - InMemoryMessageBus + InMemoryMessageBus, + EventDispatcher } = require('../..'); // node-cqrs +const { InMemorySnapshotStorage } = require('../..'); const UserAggregate = require('./UserAggregate'); const UsersProjection = require('./UsersProjection'); @@ -17,9 +19,12 @@ const UsersProjection = require('./UsersProjection'); exports.createContainer = () => { const builder = new ContainerBuilder(); - // register infrastructure services - builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('messageBus'); + // register infrastructure services; + // eventStorageWriter and snapshotStorage are automatically added to the event dispatch pipeline + // if they implement IDispatchPipelineProcessor interface (see src/CqrsContainerBuilder.ts) + builder.register(InMemoryEventStorage).as('eventStorageReader').as('eventStorageWriter'); + builder.register(InMemorySnapshotStorage).as('snapshotStorage'); + builder.register(InMemoryMessageBus).as('eventBus'); // register domain entities builder.registerAggregate(UserAggregate); @@ -34,19 +39,22 @@ exports.createContainer = () => { */ exports.createBaseInstances = () => { // create infrastructure services - const messageBus = new InMemoryMessageBus(); + const eventBus = new InMemoryMessageBus(); const storage = new InMemoryEventStorage(); - const eventStore = new EventStore({ storage, messageBus }); - const commandBus = new CommandBus({ messageBus }); + const eventDispatcher = new EventDispatcher({ eventBus }) + eventDispatcher.addPipelineProcessor(storage); - /** @type {IAggregateConstructor} */ + const eventStore = new EventStore({ eventStorageReader: storage, eventBus, eventDispatcher }); + const commandBus = new CommandBus(); + + /** @type {import('../..').IAggregateConstructor} */ const aggregateType = UserAggregate; - /** @type {ICommandHandler} */ + /** @type {import('../..').ICommandHandler} */ const userCommandHandler = new AggregateCommandHandler({ eventStore, aggregateType }); userCommandHandler.subscribe(commandBus); - /** @type {IProjection} */ + /** @type {import('../..').IProjection} */ const usersProjection = new UsersProjection(); usersProjection.subscribe(eventStore); diff --git a/jest.config.ts b/jest.config.ts index 0a40888..478bb43 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,199 +4,39 @@ */ export default { - // All imported modules in your tests should be mocked automatically - // automock: false, - - // Stop running tests after `n` failures - // bail: 0, - - // The directory where Jest should store its cached dependency information - // cacheDirectory: "/private/var/folders/02/h951kmmd00v199hff5fx0mzh0000gn/T/jest_dx", - - // Automatically clear mock calls and instances between every test - // clearMocks: false, - // Indicates whether the coverage information should be collected while executing the test - // collectCoverage: false, + collectCoverage: false, // An array of glob patterns indicating a set of files for which coverage information should be collected - // collectCoverageFrom: undefined, + collectCoverageFrom: [ + 'src/**/*.ts', // Only collect coverage from TypeScript source + '!src/**/*.d.ts' // Ignore TypeScript type declaration files + ], // The directory where Jest should output its coverage files - coverageDirectory: "coverage", + coverageDirectory: 'coverage', // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "/node_modules/" - // ], + coveragePathIgnorePatterns: [ + '/dist/', + '/examples/', + '/node_modules/', + '/src/rabbitmq/', + '/tests/' + ], // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", - - // A list of reporter names that Jest uses when writing coverage reports - // coverageReporters: [ - // "json", - // "text", - // "lcov", - // "clover" - // ], - - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: undefined, - - // A path to a custom dependency extractor - // dependencyExtractor: undefined, - - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, - - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], - - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, - - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, + // coverageProvider: "v8", // A set of global variables that need to be available in all test environments globals: { }, - // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. - // maxWorkers: "50%", - - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], - - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "json", - // "jsx", - // "ts", - // "tsx", - // "node" - // ], - - // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module - // moduleNameMapper: {}, - - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], - - // Activates notifications for test results - // notify: false, - - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", - - // A preset that is used as a base for Jest's configuration - // preset: undefined, - - // Run tests from one or more projects - // projects: undefined, - - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, - - // Automatically reset mock state between every test - // resetMocks: false, - - // Reset the module registry before running each individual test - // resetModules: false, - - // A path to a custom resolver - // resolver: undefined, - - // Automatically restore mock state between every test - // restoreMocks: false, - - // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, - - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "" - // ], - - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", - - // The paths to modules that run some code to configure or set up the testing environment before each test - // setupFiles: [], - - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], - - // The number of seconds after which a test is considered as slow and reported as such in the results. - // slowTestThreshold: 5, - - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], - // The test environment that will be used for testing - testEnvironment: "node", - - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, - - // Adds a location field to test results - // testLocationInResults: false, - - // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], - - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - // testPathIgnorePatterns: [ - // "/node_modules/" - // ], - - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], - - // This option allows the use of a custom results processor - // testResultsProcessor: undefined, - - // This option allows use of a custom test runner - // testRunner: "jasmine2", - - // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href - // testURL: "http://localhost", - - // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" - // timers: "real", + testEnvironment: 'node', // A map from regular expressions to paths to transformers transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - isolatedModules: true - } - ] - }, - - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "/node_modules/", - // "\\.pnp\\.[^\\/]+$" - // ], - - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, - - // Indicates whether each individual test should be reported during the run - // verbose: undefined, - - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], - - // Whether to use watchman for file crawling - // watchman: true, + '^.+\\.tsx?$': ['ts-jest'] + } }; diff --git a/package-lock.json b/package-lock.json index 264c148..6a9bade 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,85 +1,92 @@ { "name": "node-cqrs", - "version": "0.17.0", + "version": "1.0.0-rc.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "0.17.0", + "version": "1.0.0-rc.33", "license": "MIT", "dependencies": { - "di0": "^1.0.0" + "async-iterable-buffer": "^1.1.0", + "async-parallel-pipe": "^1.0.2", + "di0": "^1.2.0" }, "devDependencies": { - "@types/chai": "^4.3.17", - "@types/jest": "^29.5.12", - "@types/node": "^20.14.14", - "@types/sinon": "^10.0.20", + "@stylistic/eslint-plugin-ts": "^4.4.1", + "@types/amqplib": "^0.10.7", + "@types/better-sqlite3": "^7.6.13", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.14", + "@types/md5": "^2.3.5", + "@types/node": "^20.19.21", + "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "coveralls": "^3.1.1", + "eslint": "^9.37.0", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.4.0", "jest": "^29.7.0", - "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "sinon": "^19.0.5", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.1" }, "engines": { - "node": ">=10.3.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "node": ">=18.0.0" }, - "engines": { - "node": ">=6.0.0" + "peerDependencies": { + "amqplib": "^0.10.9", + "better-sqlite3": "^11.10.0", + "md5": "^2.3.0" } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", - "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -94,30 +101,43 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", - "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -130,39 +150,62 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -172,160 +215,67 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -339,6 +289,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -351,6 +302,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -363,6 +315,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -370,11 +323,44 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -387,6 +373,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -395,12 +382,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -414,6 +402,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -426,6 +415,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -438,6 +428,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -450,6 +441,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -462,6 +454,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -474,6 +467,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -481,11 +475,28 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -497,12 +508,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", - "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -512,46 +524,48 @@ } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", - "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -561,13 +575,15 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -580,114 +596,478 @@ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@hutson/parse-repository-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", - "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, + "license": "MIT", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@hutson/parse-repository-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz", + "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -703,6 +1083,7 @@ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -716,6 +1097,7 @@ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, + "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" }, @@ -728,6 +1110,7 @@ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -745,6 +1128,7 @@ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -760,6 +1144,7 @@ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, + "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -803,6 +1188,7 @@ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -815,6 +1201,7 @@ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -829,6 +1216,7 @@ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -844,6 +1232,7 @@ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -859,6 +1248,7 @@ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -885,6 +1275,7 @@ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -898,17 +1289,25 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -916,46 +1315,80 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } @@ -965,6 +1398,7 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -974,74 +1408,91 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", + "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" } }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } + "license": "(Unlicense OR Apache-2.0)" }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-4.4.1.tgz", + "integrity": "sha512-2r6cLcmdF6til66lx8esBYvBvsn7xCmLT50gw/n1rGGlTq/OxeNjBIh4c3VEaDGMa/5TybrZTia6sQUHdIWx1w==", "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.32.1", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0" + }, "engines": { - "node": ">=4" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@types/amqplib": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.7.tgz", + "integrity": "sha512-IVj3avf9AQd2nXCx0PGk/OYq7VmHiyNxWFSb5HhU9ATh+i+gHWvVcljFTcTWQ/dyHJCTrzCixde+r/asL2ErDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -1051,10 +1502,11 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -1064,31 +1516,52 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@types/node": "*" } }, "node_modules/@types/chai": { - "version": "4.3.17", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.17.tgz", - "integrity": "sha512-zmZ21EWzR71B4Sscphjief5djsLre50M6lI622OSySTmn9DB3j+C3kWroHfBQWXbOBwbgg/M8CG/hUxDLIloow==", - "dev": true + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1097,13 +1570,15 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -1113,46 +1588,66 @@ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/md5": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz", + "integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "20.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", + "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/sinon": { - "version": "10.0.20", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.20.tgz", - "integrity": "sha512-2APKKruFNCAZgx3daAyACGzWuJ028VVCUDk6o2rw/Z4PXT0ogwdV4KUegW0MwVs0Zu59auPXbbuBJHF12Sx1Eg==", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", + "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==", "dev": true, + "license": "MIT", "dependencies": { "@types/sinonjs__fake-timers": "*" } @@ -1161,19 +1656,22 @@ "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -1182,13 +1680,237 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1196,11 +1918,22 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, + "license": "MIT", "dependencies": { "acorn": "^8.11.0" }, @@ -1212,13 +1945,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1230,11 +1965,26 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/amqplib": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-more-ints": "~1.0.0", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -1250,6 +2000,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1259,6 +2010,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1274,6 +2026,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1286,91 +2039,61 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } + "license": "Python-2.0" }, "node_modules/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true, - "engines": { - "node": "*" - } + "node_modules/async-iterable-buffer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/async-iterable-buffer/-/async-iterable-buffer-1.1.0.tgz", + "integrity": "sha512-Dg+1qcSvVG72bofflqi0MXUKStuJDU034r8pxWdDZIF17ohU1VcuTp9dPB/Q7b8ZNLhJtOmLgIODP/32a0ygYQ==", + "license": "MIT" }, - "node_modules/aws4": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", - "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", - "dev": true + "node_modules/async-parallel-pipe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/async-parallel-pipe/-/async-parallel-pipe-1.0.2.tgz", + "integrity": "sha512-Ks0JUQJMYNAB4OOmGQJZIYSAuGCU60K2ldhbpDiF8JX8O0MbCWn4mqBM3vMM5i/AkJ5Zh1T+9jcetFKrq9T6lg==", + "license": "MIT" }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -1392,6 +2115,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -1408,6 +2132,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -1419,11 +2144,22 @@ "node": ">=8" } }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -1435,26 +2171,30 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -1462,40 +2202,98 @@ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "peer": true, "dependencies": { - "tweetnacl": "^0.14.3" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1503,6 +2301,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -1511,9 +2310,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -1529,11 +2328,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -1547,6 +2348,7 @@ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, + "license": "MIT", "dependencies": { "fast-json-stable-stringify": "2.x" }, @@ -1559,21 +2361,56 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT", + "peer": true }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1583,6 +2420,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1592,6 +2430,7 @@ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", "dev": true, + "license": "MIT", "dependencies": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", @@ -1605,9 +2444,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001646", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz", - "integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "dev": true, "funding": [ { @@ -1622,19 +2461,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true + ], + "license": "CC-BY-4.0" }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -1653,6 +2488,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1669,15 +2505,27 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -1685,6 +2533,13 @@ "node": "*" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "peer": true + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1696,21 +2551,24 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", - "dev": true + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -1722,6 +2580,7 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, + "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -1731,13 +2590,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1749,25 +2610,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "license": "MIT" }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", "dev": true, + "license": "MIT", "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" @@ -1777,13 +2628,15 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/conventional-changelog": { "version": "3.1.25", "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.25.tgz", "integrity": "sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==", "dev": true, + "license": "MIT", "dependencies": { "conventional-changelog-angular": "^5.0.12", "conventional-changelog-atom": "^2.0.8", @@ -1806,6 +2659,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz", "integrity": "sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==", "dev": true, + "license": "ISC", "dependencies": { "compare-func": "^2.0.0", "q": "^1.5.1" @@ -1819,6 +2673,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz", "integrity": "sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==", "dev": true, + "license": "ISC", "dependencies": { "q": "^1.5.1" }, @@ -1831,6 +2686,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz", "integrity": "sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==", "dev": true, + "license": "ISC", "dependencies": { "q": "^1.5.1" }, @@ -1843,6 +2699,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.6.3.tgz", "integrity": "sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==", "dev": true, + "license": "ISC", "dependencies": { "compare-func": "^2.0.0", "lodash": "^4.17.15", @@ -1857,6 +2714,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz", "integrity": "sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==", "dev": true, + "license": "MIT", "dependencies": { "add-stream": "^1.0.0", "conventional-changelog-writer": "^5.0.0", @@ -1882,6 +2740,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz", "integrity": "sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==", "dev": true, + "license": "ISC", "dependencies": { "q": "^1.5.1" }, @@ -1894,6 +2753,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz", "integrity": "sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==", "dev": true, + "license": "ISC", "dependencies": { "q": "^1.5.1" }, @@ -1906,6 +2766,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz", "integrity": "sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==", "dev": true, + "license": "ISC", "dependencies": { "q": "^1.5.1" }, @@ -1918,6 +2779,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz", "integrity": "sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==", "dev": true, + "license": "ISC", "dependencies": { "q": "^1.5.1" }, @@ -1930,6 +2792,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz", "integrity": "sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==", "dev": true, + "license": "ISC", "dependencies": { "compare-func": "^2.0.0", "q": "^1.5.1" @@ -1943,6 +2806,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz", "integrity": "sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -1952,6 +2816,7 @@ "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz", "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==", "dev": true, + "license": "MIT", "dependencies": { "conventional-commits-filter": "^2.0.7", "dateformat": "^3.0.0", @@ -1970,11 +2835,22 @@ "node": ">=10" } }, + "node_modules/conventional-changelog-writer/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/conventional-commits-filter": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", "integrity": "sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==", "dev": true, + "license": "MIT", "dependencies": { "lodash.ismatch": "^4.4.0", "modify-values": "^1.0.0" @@ -1988,6 +2864,7 @@ "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz", "integrity": "sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==", "dev": true, + "license": "MIT", "dependencies": { "is-text-path": "^1.0.1", "JSONStream": "^1.0.4", @@ -2007,38 +2884,22 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, - "node_modules/coveralls": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.1.1.tgz", - "integrity": "sha512-+dxnG2NHncSD1NrqbSM3dn/lE57O6Qf/koe9+I7c+wzkqRmEvcp0kgJdxKInzYzkICKkFMZsX3Vct3++tsF9ww==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true, - "dependencies": { - "js-yaml": "^3.13.1", - "lcov-parse": "^1.0.0", - "log-driver": "^1.2.7", - "minimist": "^1.2.5", - "request": "^2.88.2" - }, - "bin": { - "coveralls": "bin/coveralls.js" - }, - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -2059,13 +2920,15 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2075,43 +2938,44 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2127,6 +2991,7 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2136,6 +3001,7 @@ "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "dev": true, + "license": "MIT", "dependencies": { "decamelize": "^1.1.0", "map-obj": "^1.0.0" @@ -2152,15 +3018,33 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -2175,6 +3059,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, + "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -2182,22 +3067,41 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "peer": true, "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, "node_modules/detect-newline": { @@ -2205,20 +3109,23 @@ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/di0": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/di0/-/di0-1.0.0.tgz", - "integrity": "sha512-RRZsfbOmxiB0ZI+4ABfw/O7GUOnqmgFJGEPFzj7IX+mpm73Hkd38akjaTagaFmwzzRAqIIVR3uB3zSzwnt8ZFw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/di0/-/di0-1.2.0.tgz", + "integrity": "sha512-9IeKa1bEuwqwZMcAHuCI+YHFS5dHfcmb7/CB8A7GzH6EKIrpz/Du7y5GYrUoC6jEGG4eo9cdVi6gUiY0khWJLQ==", + "license": "MIT" }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2228,6 +3135,7 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -2237,6 +3145,7 @@ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, + "license": "MIT", "dependencies": { "is-obj": "^2.0.0" }, @@ -2244,80 +3153,243 @@ "node": ">=8" } }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "node_modules/electron-to-chromium": { + "version": "1.5.235", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", + "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "peer": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, + "license": "MIT", "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "is-arrayish": "^0.2.1" } }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, + "license": "MIT", "dependencies": { - "jake": "^10.8.5" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" }, "bin": { - "ejs": "bin/cli.js" + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/electron-to-chromium": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", - "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", - "dev": true + "node_modules/eslint-plugin-jest": { + "version": "28.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", + "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, + "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/escalade": { + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=6" + "node": "*" } }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { @@ -2325,6 +3397,7 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -2333,11 +3406,58 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -2365,11 +3485,22 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -2381,77 +3512,103 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ] - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "dependencies": { - "bser": "2.1.1" - } + "license": "MIT" }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, + "license": "ISC", "dependencies": { - "minimatch": "^5.0.1" + "reusify": "^1.0.4" } }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "balanced-match": "^1.0.0" + "bser": "2.1.1" } }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "peer": true + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2460,46 +3617,56 @@ } }, "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "engines": { - "node": "*" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">= 0.12" + "node": ">=16" } }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "peer": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2507,6 +3674,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2520,6 +3688,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2529,6 +3698,7 @@ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -2538,6 +3708,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2547,6 +3718,7 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -2556,6 +3728,7 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.0.0" } @@ -2565,6 +3738,7 @@ "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz", "integrity": "sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==", "dev": true, + "license": "MIT", "dependencies": { "@hutson/parse-repository-url": "^3.0.0", "hosted-git-info": "^4.0.0", @@ -2583,6 +3757,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2597,13 +3772,15 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/get-pkg-repo/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -2613,6 +3790,7 @@ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" @@ -2623,6 +3801,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2630,20 +3809,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - } - }, "node_modules/git-raw-commits": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", "integrity": "sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==", "dev": true, + "license": "MIT", "dependencies": { "dargs": "^7.0.0", "lodash": "^4.17.15", @@ -2663,6 +3834,7 @@ "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", "dev": true, + "license": "MIT", "dependencies": { "gitconfiglocal": "^1.0.0", "pify": "^2.3.0" @@ -2676,6 +3848,7 @@ "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz", "integrity": "sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==", "dev": true, + "license": "MIT", "dependencies": { "meow": "^8.0.0", "semver": "^6.0.0" @@ -2687,21 +3860,40 @@ "node": ">=10" } }, + "node_modules/git-semver-tags/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/gitconfiglocal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", "dev": true, + "license": "BSD", "dependencies": { "ini": "^1.3.2" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "peer": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2717,26 +3909,76 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -2753,34 +3995,12 @@ "uglify-js": "^3.1.4" } }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2790,6 +4010,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2799,6 +4020,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2811,6 +4033,7 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2822,37 +4045,73 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "dev": true, - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } + "license": "MIT" }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -2872,6 +4131,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -2881,6 +4141,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2891,6 +4152,7 @@ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2900,25 +4162,34 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "license": "ISC" }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT", + "peer": true }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2929,11 +4200,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2943,15 +4225,30 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2961,6 +4258,7 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2970,6 +4268,7 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2979,6 +4278,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2991,6 +4291,7 @@ "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", "dev": true, + "license": "MIT", "dependencies": { "text-extensions": "^1.0.0" }, @@ -2998,35 +4299,26 @@ "node": ">=0.10.0" } }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -3036,6 +4328,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -3047,23 +4340,12 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -3078,6 +4360,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -3088,10 +4371,11 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -3100,29 +4384,12 @@ "node": ">=8" } }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "dev": true, - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -3149,6 +4416,7 @@ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -3163,6 +4431,7 @@ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3194,6 +4463,7 @@ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -3227,6 +4497,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -3241,6 +4512,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -3259,6 +4531,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -3268,6 +4541,7 @@ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -3313,6 +4587,7 @@ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -3328,6 +4603,7 @@ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, + "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" }, @@ -3340,6 +4616,7 @@ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -3356,6 +4633,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -3373,6 +4651,7 @@ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -3382,6 +4661,7 @@ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -3407,6 +4687,7 @@ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, + "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -3420,6 +4701,7 @@ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -3435,6 +4717,7 @@ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -3455,6 +4738,7 @@ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3469,6 +4753,7 @@ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -3486,6 +4771,7 @@ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -3495,6 +4781,7 @@ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -3515,6 +4802,7 @@ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, + "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -3528,6 +4816,7 @@ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -3560,6 +4849,7 @@ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -3593,6 +4883,7 @@ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -3619,23 +4910,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3653,6 +4933,7 @@ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -3670,6 +4951,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3682,6 +4964,7 @@ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, + "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -3701,6 +4984,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -3716,6 +5000,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3730,74 +5015,83 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -3812,13 +5106,15 @@ "dev": true, "engines": [ "node >= 0.2.0" - ] + ], + "license": "MIT" }, "node_modules/JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", "dev": true, + "license": "(MIT OR Apache-2.0)", "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" @@ -3830,32 +5126,29 @@ "node": "*" } }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dev": true, - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/just-extend": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3865,39 +5158,48 @@ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/lcov-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", - "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", - "dev": true, - "bin": { - "lcov-parse": "bin/cli.js" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", @@ -3913,6 +5215,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, + "license": "MIT", "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -3926,6 +5229,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3935,60 +5239,61 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { - "p-locate": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "engines": { - "node": ">=0.8.6" - } + "license": "MIT" }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } @@ -3998,6 +5303,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -4010,6 +5316,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -4020,29 +5327,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" } @@ -4052,6 +5349,7 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -4059,11 +5357,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", @@ -4084,17 +5395,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/meow/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/meow/node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/meow/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/meow/node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "dev": true, + "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.0", "normalize-package-data": "^2.5.0", @@ -4110,6 +5479,7 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.1.0", "read-pkg": "^5.2.0", @@ -4127,6 +5497,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" } @@ -4136,6 +5507,7 @@ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -4148,6 +5520,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" } @@ -4157,6 +5530,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -4166,6 +5540,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -4177,40 +5552,31 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } + "license": "MIT" }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 8" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">= 0.6" + "node": ">=8.6" } }, "node_modules/mimic-fn": { @@ -4218,8 +5584,22 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/min-indent": { @@ -4227,27 +5607,32 @@ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4257,6 +5642,7 @@ "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", "dev": true, + "license": "MIT", "dependencies": { "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", @@ -4266,72 +5652,108 @@ "node": ">= 6" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "peer": true + }, "node_modules/modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "peer": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nise": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/text-encoding": "^0.7.2", + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", "just-extend": "^6.2.0", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^8.1.0" } }, "node_modules/nise/node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/node-abi": { + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" } }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^4.0.1", "is-core-module": "^2.5.0", @@ -4342,23 +5764,12 @@ "node": ">=10" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4368,6 +5779,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -4375,20 +5787,11 @@ "node": ">=8" } }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -4398,6 +5801,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -4408,11 +5812,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4424,27 +5847,16 @@ } }, "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4455,6 +5867,20 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, "engines": { "node": ">=6" } @@ -4464,6 +5890,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -4482,6 +5909,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4491,6 +5919,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4500,6 +5929,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4508,19 +5938,26 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^3.0.0" }, @@ -4533,6 +5970,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4542,27 +5980,24 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true - }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -4575,15 +6010,17 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -4593,6 +6030,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -4600,11 +6038,105 @@ "node": ">=8" } }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -4619,6 +6151,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4630,13 +6163,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, + "license": "MIT", "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -4645,17 +6180,23 @@ "node": ">= 6" } }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "peer": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -4674,7 +6215,8 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ] + ], + "license": "MIT" }, "node_modules/q": { "version": "1.5.1", @@ -4682,40 +6224,89 @@ "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" } }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT", + "peer": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, - "engines": { - "node": ">=0.6" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "peer": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, + "license": "MIT", "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", @@ -4730,6 +6321,7 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^2.0.0", "read-pkg": "^3.0.0" @@ -4743,6 +6335,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^2.0.0" }, @@ -4755,6 +6348,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" @@ -4768,6 +6362,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^1.0.0" }, @@ -4780,6 +6375,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^1.1.0" }, @@ -4792,6 +6388,7 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4801,6 +6398,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4809,13 +6407,15 @@ "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/read-pkg/node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -4828,6 +6428,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -4836,7 +6437,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4851,6 +6452,7 @@ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, + "license": "MIT", "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" @@ -4859,60 +6461,40 @@ "node": ">=8" } }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dev": true, - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT", + "peer": true + }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4922,6 +6504,7 @@ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -4929,29 +6512,75 @@ "node": ">=8" } }, - "node_modules/resolve-from": { + "node_modules/resolve-cwd/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -4965,21 +6594,19 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + ], + "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/shebang-command": { @@ -4987,6 +6614,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4999,6 +6627,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5007,20 +6636,68 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "node_modules/sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", - "deprecated": "16.1.1", + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.5.tgz", + "integrity": "sha512-r15s9/s+ub/d4bxNXqIUmwp6imVSdTorIRaxoecYjqTVLZ8RuoXr/4EDGwIBo6Waxn7f2gnURX9zuhAfCwaF6Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.3.0", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.1.0", - "nise": "^5.1.4", + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.5", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", "supports-color": "^7.2.0" }, "funding": { @@ -5028,17 +6705,29 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5048,6 +6737,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5057,6 +6747,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -5067,6 +6758,7 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -5076,29 +6768,33 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.18", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", - "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", - "dev": true + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" }, "node_modules/split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "dev": true, + "license": "MIT", "dependencies": { "through": "2" }, @@ -5111,6 +6807,7 @@ "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", "dev": true, + "license": "ISC", "dependencies": { "readable-stream": "^3.0.0" } @@ -5119,38 +6816,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true - }, - "node_modules/sshpk": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -5158,11 +6832,21 @@ "node": ">=10" } }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -5172,6 +6856,7 @@ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, + "license": "MIT", "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -5185,6 +6870,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5199,6 +6885,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5211,6 +6898,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5220,6 +6908,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5229,6 +6918,7 @@ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", "dev": true, + "license": "MIT", "dependencies": { "min-indent": "^1.0.0" }, @@ -5241,6 +6931,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -5253,6 +6944,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -5265,6 +6957,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5272,11 +6965,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -5286,11 +7010,36 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10" } @@ -5299,13 +7048,15 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "3" } @@ -5314,22 +7065,15 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, - "engines": { - "node": ">=4" - } + "license": "BSD-3-Clause" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -5337,43 +7081,45 @@ "node": ">=8.0" } }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/trim-newlines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-jest": { - "version": "29.2.4", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", - "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", "dev": true, + "license": "MIT", "dependencies": { - "bs-logger": "0.x", - "ejs": "^3.1.10", - "fast-json-stable-stringify": "2.x", - "jest-util": "^29.0.0", + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" @@ -5383,10 +7129,11 @@ }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0", - "@jest/types": "^29.0.0", - "babel-jest": "^29.0.0", - "jest": "^29.0.0", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { @@ -5404,19 +7151,23 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, - "bin": { - "semver": "bin/semver.js" - }, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ts-jest/node_modules/yargs-parser": { @@ -5424,6 +7175,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -5433,6 +7185,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5476,6 +7229,7 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -5484,7 +7238,8 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, + "license": "Apache-2.0", + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -5492,17 +7247,25 @@ "node": "*" } }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } }, "node_modules/type-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -5512,6 +7275,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -5520,10 +7284,11 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5532,11 +7297,36 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz", + "integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/uglify-js": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.1.tgz", - "integrity": "sha512-y/2wiW+ceTYR2TSSptAhfnEtpLaQ4Ups5zrjB2d3kuVxHj16j/QJwPl5PvuGy9uARb39J0+iKxcRPvtpsx4A4A==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -5546,15 +7336,16 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -5570,9 +7361,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -5586,37 +7378,41 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } + "license": "MIT" }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -5631,30 +7427,18 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" } @@ -5664,6 +7448,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -5674,17 +7459,29 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -5701,13 +7498,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -5721,6 +7519,7 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4" } @@ -5730,6 +7529,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -5738,13 +7538,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -5763,6 +7565,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -5772,6 +7575,7 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5781,6 +7585,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 43bf6cc..8e4a29d 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,100 @@ { "name": "node-cqrs", - "version": "0.17.0", + "version": "1.0.0-rc.33", "description": "Basic ES6 backbone for CQRS app development", + "keywords": [ + "cqrs", + "eventsourcing" + ], "repository": { "type": "git", "url": "https://github.com/snatalenko/node-cqrs.git" }, + "main": "./dist/index.js", + "types": "./types/index.d.ts", + "typesVersions": { + "*": { + "rabbitmq": [ + "types/rabbitmq/index.d.ts" + ], + "sqlite": [ + "types/sqlite/index.d.ts" + ] + } + }, + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.js", + "types": "./types/index.d.ts" + }, + "./rabbitmq": { + "require": "./dist/rabbitmq/index.js", + "import": "./dist/rabbitmq/index.js", + "types": "./types/rabbitmq/index.d.ts" + }, + "./sqlite": { + "require": "./dist/sqlite/index.js", + "import": "./dist/sqlite/index.js", + "types": "./types/sqlite/index.d.ts" + } + }, "directories": { "doc": "docs", "example": "examples", "test": "tests" }, - "keywords": [ - "cqrs", - "eventsourcing", - "ddd", - "domain", - "eventstore" - ], - "main": "./dist/index.js", - "types": "./src/index.ts", "engines": { - "node": ">=10.3.0" + "node": ">=18.0.0" }, "scripts": { + "cleanup": "rm -rf ./dist ./types ./coverage && tsc --build --clean", "pretest": "npm run build", - "test": "jest --verbose tests/unit", - "test:coverage": "jest --collect-coverage tests/unit", - "pretest:integration": "npm run build", - "test:integration": "jest --verbose examples/user-domain-tests", - "pretest:coveralls": "npm run test:coverage", - "test:coveralls": "cat ./coverage/lcov.info | coveralls", - "posttest:coveralls": "rm -rf ./coverage", - "changelog": "conventional-changelog -n ./scripts/changelog -i CHANGELOG.md -s", - "clean": "tsc --build --clean", + "test": "jest tests/unit examples/user-domain-tests", + "test:coverage": "npm t -- --collect-coverage", + "test:rabbitmq": "jest --verbose tests/integration/rabbitmq", + "test:sqlite": "jest --verbose tests/integration/sqlite", + "changelog": "conventional-changelog -n ./scripts/changelog -r 0 > CHANGELOG.md", "build": "tsc --build", "prepare": "npm run build", "preversion": "npm test", - "version": "npm run changelog && git add CHANGELOG.md" + "version": "./scripts/cleanup_obsolete_tags.sh v$npm_package_version && npm run changelog && git add CHANGELOG.md", + "lint": "eslint" }, "author": "@snatalenko", "license": "MIT", "homepage": "https://github.com/snatalenko/node-cqrs#readme", "dependencies": { - "di0": "^1.0.0" + "async-iterable-buffer": "^1.1.0", + "async-parallel-pipe": "^1.0.2", + "di0": "^1.2.0" }, "devDependencies": { - "@types/chai": "^4.3.17", - "@types/jest": "^29.5.12", - "@types/node": "^20.14.14", - "@types/sinon": "^10.0.20", + "@stylistic/eslint-plugin-ts": "^4.4.1", + "@types/amqplib": "^0.10.7", + "@types/better-sqlite3": "^7.6.13", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.14", + "@types/md5": "^2.3.5", + "@types/node": "^20.19.21", + "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "coveralls": "^3.1.1", + "eslint": "^9.37.0", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.4.0", "jest": "^29.7.0", - "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "sinon": "^19.0.5", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.1" + }, + "peerDependencies": { + "amqplib": "^0.10.9", + "better-sqlite3": "^11.10.0", + "md5": "^2.3.0" } } diff --git a/scripts/changelog/index.js b/scripts/changelog/index.js index 887d0f0..01eab65 100644 --- a/scripts/changelog/index.js +++ b/scripts/changelog/index.js @@ -6,20 +6,23 @@ const { resolve } = require('path'); const known = require('./commits.json'); const TITLES = [ - { title: 'Features', tags: ['+', 'new', 'feature'] }, - { title: 'Fixes', tags: ['-', 'fix', 'fixes'] }, + { title: 'Features', tags: ['+', 'new', 'feature', 'feat'] }, { title: 'Changes', tags: ['*', 'change'] }, + { title: 'Fixes', tags: ['-', 'fix', 'fixes'] }, { title: 'Performance Improvements', tags: ['perf', 'performance'] }, - { title: 'Refactoring', tags: ['!', 'refactor', 'refactoring'] }, + { title: 'Security', tags: ['security'] }, { title: 'Documentation', tags: ['doc', 'docs'] }, { title: 'Tests', tags: ['test', 'tests'] }, { title: 'Build System', tags: ['build', 'ci'] }, - { title: 'Reverts', tags: ['reverts'] } + { title: 'Reverts', tags: ['reverts', 'revert'] }, + { title: 'Internal Fixes', tags: ['!', 'refactor', 'refactoring', 'internal fix', 'release fix', 'housekeeping', 'chore', 'revert'] } ]; +/** + * @param {Record} commit + */ function transform(commit) { if (known[commit.hash]) - // eslint-disable-next-line no-param-reassign commit = { ...commit, ...known[commit.hash] }; if (!commit.tag) return undefined; @@ -27,34 +30,39 @@ function transform(commit) { let { tag, message } = commit; if (commit.revert) - tag = 'Revert'; + tag = 'revert'; + + const changelogSection = TITLES.find(t => t.tags.includes(tag.toLowerCase())); + if (!changelogSection) + return undefined; if (message) message = message[0].toUpperCase() + message.substr(1); - const matchingTitle = TITLES.find(t => t.tags.includes(tag.toLowerCase())); - if (matchingTitle) - tag = matchingTitle.title; - else - tag = 'Changes'; - return { ...commit, - tag, + tag: changelogSection.title, message, shortHash: commit.hash.substring(0, 7) }; } +/** + * @param {{ title: string}} a + * @param {{ title: string}} b + */ function commitGroupsSort(a, b) { const gRankA = TITLES.findIndex(t => t.title === a.title); const gRankB = TITLES.findIndex(t => t.title === b.title); return gRankA - gRankB; } +/** + * @param {Function} cb + */ async function presetOpts(cb) { const parserOpts = { - headerPattern: /^(\w*):\s*(.*)$/, // /^(\w*:|[+\-*!])\s*(.*)$/, + headerPattern: /^([^:]*):\s*(.*)$/, // /^(\w*:|[+\-*!])\s*(.*)$/, headerCorrespondence: [ 'tag', 'message' diff --git a/scripts/cleanup_obsolete_tags.sh b/scripts/cleanup_obsolete_tags.sh new file mode 100755 index 0000000..56c4f30 --- /dev/null +++ b/scripts/cleanup_obsolete_tags.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e + +comingTag=$1 # can be empty +intermediaryTags=$(git tag -l "v*-*") +tagsToDelete=() + +if [[ ! -z "$comingTag" ]]; then + echo "Creating $comingTag" +fi + +# Check all intemediary tags (containing "-") +# and drop ones that have successor tags created (i.e. "1.80.0" for "1.80.0-0", or "" +for intermediaryTag in $intermediaryTags +do + releaseTag=${intermediaryTag%%-*} + + if [ $(git tag -l "$releaseTag") ]; then + echo "Release tag $releaseTag exists, $intermediaryTag can be removed" + tagsToDelete+=($intermediaryTag) + + elif [[ "$comingTag" = "$releaseTag" ]]; then + echo "Release tag $comingTag is about to be created, $intermediaryTag can be removed" + tagsToDelete+=($intermediaryTag) + + elif [[ "$intermediaryTag" == *"-rc"* ]]; then + echo "No release tag for $intermediaryTag found" + + elif [ $(git tag -l "$releaseTag-rc.*") ]; then + echo "Pre-release tag $releaseTag-rc.* exists, $intermediaryTag can be removed" + tagsToDelete+=($intermediaryTag) + + elif [[ "$comingTag" == "$releaseTag-rc."* ]]; then + echo "Pre-release tag $comingTag is about to be created, $intermediaryTag can be removed" + tagsToDelete+=($intermediaryTag) + + else + echo "No successors for $intermediaryTag found" + fi +done + +if (( ${#tagsToDelete[@]} )); then + echo "Removing tags from remote..." + git push --no-verify --delete origin ${tagsToDelete[@]} || true + + echo "Removing tags locally..." + git tag -d ${tagsToDelete[@]} +fi diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index a7da6ae..4243173 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -5,19 +5,11 @@ import { Identifier, IEvent, IEventSet, - IAggregateConstructorParams -} from "./interfaces"; + IAggregateConstructorParams, + SNAPSHOT_EVENT_TYPE +} from './interfaces'; -import { getClassName, validateHandlers, getHandler } from './utils'; - -/** - * Deep-clone simple JS object - */ -function clone(obj: T): T { - return JSON.parse(JSON.stringify(obj)); -} - -const SNAPSHOT_EVENT_TYPE = 'snapshot'; +import { getClassName, validateHandlers, getHandler, getMessageHandlerNames } from './utils'; /** * Base class for Aggregate definition @@ -25,25 +17,26 @@ const SNAPSHOT_EVENT_TYPE = 'snapshot'; export abstract class AbstractAggregate implements IAggregate { /** - * Optional list of commands handled by Aggregate. - * - * If not overridden in Aggregate implementation, - * `AggregateCommandHandler` will treat all public methods as command handlers + * List of command names handled by the Aggregate. * - * @example - * return ['createUser', 'changePassword']; + * Can be overridden in the Aggregate implementation to explicitly define supported commands. + * If not overridden, all public methods will be treated as command handlers by default. + * + * @example ['createUser', 'changePassword']; */ - static get handles(): string[] | undefined { - return undefined; + static get handles(): string[] { + return getMessageHandlerNames(this); } #id: Identifier; - #changes: IEvent[] = []; #version: number = 0; #snapshotVersion: number | undefined; + /** List of emitted events */ + protected changes: IEvent[] = []; + /** Internal aggregate state */ - protected state: TState; + protected state: TState | undefined; /** Command being handled by aggregate */ protected command?: ICommand; @@ -63,19 +56,16 @@ export abstract class AbstractAggregate 50; */ - get shouldTakeSnapshot(): boolean { + // eslint-disable-next-line class-methods-use-this + protected get shouldTakeSnapshot(): boolean { return false; } @@ -99,24 +89,8 @@ export abstract class AbstractAggregate this.mutate(event)); } - /** Pass command to command handler */ - handle(command: ICommand) { - if (!command) - throw new TypeError('command argument required'); - if (!command.type) - throw new TypeError('command.type argument required'); - - const handler = getHandler(this, command.type); - if (!handler) - throw new Error(`'${command.type}' handler is not defined or not a function`); - - this.command = command; - - return handler.call(this, command.payload, command.context); - } - /** Mutate aggregate state and increment aggregate version */ - mutate(event) { + mutate(event: IEvent) { if (event.aggregateVersion !== undefined) this.#version = event.aggregateVersion; @@ -135,14 +109,67 @@ export abstract class AbstractAggregate this.getUncommittedEvents(eventsOffset)) + .finally(() => { + this.command = undefined; + }); + } + else { // handle synchronous result + const events = this.getUncommittedEvents(eventsOffset); + this.command = undefined; + return events; + } + } + catch (err) { + this.command = undefined; + throw err; + } + } + + /** + * Get the events emitted during commands processing. + * If a snapshot should be taken, the snapshot event is added to the end. + */ + protected getUncommittedEvents(offset?: number): IEventSet { + if (this.shouldTakeSnapshot) + this.takeSnapshot(); + + return this.changes.slice(offset); + } + /** Format and register aggregate event and mutate aggregate state */ - protected emit(type: string, payload?: TPayload) { + protected emit(type: string, payload?: TPayload): IEvent { if (typeof type !== 'string' || !type.length) throw new TypeError('type argument must be a non-empty string'); const event = this.makeEvent(type, payload, this.command); this.emitRaw(event); + + return event; } /** Format event based on a current aggregate state and a command being executed */ @@ -181,22 +208,21 @@ export abstract class AbstractAggregate - 'ready' in view && - 'lock' in view && - 'unlock' in view && - 'once' in view; +export type AbstractProjectionParams = { + + /** + * The default view associated with the projection. + * Can optionally implement IViewLocker and/or IEventLocker. + */ + view?: T, + + /** + * Manages view restoration state to prevent early access to an inconsistent view + * or conflicts from concurrent restoration by other processes. + */ + viewLocker?: IViewLocker, -const asProjectionView = (view: any): IProjectionView | undefined => - (isProjectionView(view) ? view : undefined); + /** + * Tracks event processing state to prevent concurrent handling by multiple processes. + */ + eventLocker?: IEventLocker, + + logger?: ILogger | IExtendableLogger +} /** * Base class for Projection definition */ -export abstract class AbstractProjection implements IProjection { +export abstract class AbstractProjection implements IProjection { /** - * Optional list of event types being handled by projection. - * Can be overridden in projection implementation. - * If not overridden, will detect event types from event handlers declared on the Projection class + * List of event types handled by the projection. Can be overridden in the projection implementation. + * If not overridden, event types will be inferred from handler methods defined on the Projection class. */ - static get handles(): string[] | undefined { - return undefined; + static get handles(): string[] { + return getMessageHandlerNames(this); } + #view?: TView; + #viewLocker?: IViewLocker; + #eventLocker?: IEventLocker; + protected _logger?: ILogger; + /** - * Default view associated with projection + * The default view associated with the projection. + * Can optionally implement IViewLocker and/or IEventLocker. */ - get view(): TView { - if (!this.#view) - this.#view = this.#viewFactory(); - - return this.#view; + public get view(): TView { + return this.#view ?? (this.#view = new InMemoryView() as TView); } - #viewFactory: IViewFactory; - #view?: TView; + protected set view(value: TView) { + this.#view = value; + } - protected _logger?: ILogger; + /** + * Manages view restoration state to prevent early access to an inconsistent view + * or conflicts from concurrent restoration by other processes. + */ + protected get _viewLocker(): IViewLocker | undefined { + return this.#viewLocker ?? (isViewLocker(this.view) ? this.view : undefined); + } - get collectionName(): string { - return getClassName(this); + protected set _viewLocker(value: IViewLocker | undefined) { + this.#viewLocker = value; } /** - * Indicates if view should be restored from EventStore on start. - * Override for custom behavior. + * Tracks event processing state to prevent concurrent handling by multiple processes. */ - get shouldRestoreView(): boolean | Promise { - return (this.view instanceof Map) - || (this.view instanceof InMemoryView); + protected get _eventLocker(): IEventLocker | undefined { + return this.#eventLocker ?? (isEventLocker(this.view) ? this.view : undefined); + } + + protected set _eventLocker(value: IEventLocker | undefined) { + this.#eventLocker = value; } constructor({ view, - viewFactory = InMemoryView.factory, + viewLocker, + eventLocker, logger - }: { - view?: TView, - viewFactory?: IViewFactory, - logger?: ILogger | IExtendableLogger - } = {}) { + }: AbstractProjectionParams = {}) { validateHandlers(this); - this.#viewFactory = view ? - () => view : - viewFactory; + this.#view = view; + this.#viewLocker = viewLocker; + this.#eventLocker = eventLocker; this._logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : @@ -93,7 +115,7 @@ export abstract class AbstractProjection { subscribe(eventStore, this, { - masterHandler: (e: IEvent) => this.project(e) + masterHandler: this.project }); await this.restore(eventStore); @@ -101,9 +123,11 @@ export abstract class AbstractProjection { - const concurrentView = asProjectionView(this.view); - if (concurrentView && !concurrentView.ready) - await concurrentView.once('ready'); + if (this._viewLocker && !this._viewLocker?.ready) { + this._logger?.debug(`view is locked, awaiting until it is ready to process ${describe(event)}`); + await this._viewLocker.once('ready'); + this._logger?.debug(`view is ready, processing ${describe(event)}`); + } return this._project(event); } @@ -114,36 +138,50 @@ export abstract class AbstractProjection { // lock the view to ensure same restoring procedure // won't be performed by another projection instance - const concurrentView = asProjectionView(this.view); - if (concurrentView) - await concurrentView.lock(); + if (this._viewLocker) + await this._viewLocker.lock(); - const shouldRestore = await this.shouldRestoreView; - if (shouldRestore) - await this._restore(eventStore); + await this._restore(eventStore); - if (concurrentView) - concurrentView.unlock(); + if (this._viewLocker) + this._viewLocker.unlock(); } /** Restore projection view from event store */ protected async _restore(eventStore: IEventStore): Promise { if (!eventStore) throw new TypeError('eventStore argument required'); - if (typeof eventStore.getAllEvents !== 'function') - throw new TypeError('eventStore.getAllEvents must be a Function'); + if (typeof eventStore.getEventsByTypes !== 'function') + throw new TypeError('eventStore.getEventsByTypes must be a Function'); + + let lastEvent: IEvent | undefined; + + if (this._eventLocker) { + this._logger?.debug('retrieving last event projected'); + lastEvent = await this._eventLocker.getLastEvent(); + } + + this._logger?.debug(`retrieving ${lastEvent ? `events after ${describe(lastEvent)}` : 'all events'}...`); - this._logger?.debug('retrieving events and restoring projection...'); + const messageTypes = (this.constructor as typeof AbstractProjection).handles; + const eventsIterable = eventStore.getEventsByTypes(messageTypes, { afterEvent: lastEvent }); - const messageTypes = getHandledMessageTypes(this); - const eventsIterable = eventStore.getAllEvents(messageTypes); let eventsCount = 0; const startTs = Date.now(); @@ -152,12 +190,12 @@ export abstract class AbstractProjection implements ICommandHandler { #eventStore: IEventStore; #logger?: ILogger; - - #aggregateFactory: IAggregateFactory; + #aggregateFactory: IAggregateFactory; #handles: string[]; + /** Aggregate instances cache for concurrent command handling */ + #aggregatesCache: MapAssertable> = new MapAssertable(); + + /** Lock for sequential aggregate command execution */ + #executionLock = new Lock(); + constructor({ eventStore, aggregateType, aggregateFactory, handles, logger - }: { - eventStore: IEventStore, - aggregateType?: IAggregateConstructor, - aggregateFactory?: IAggregateFactory, - handles?: string[], - logger?: ILogger | IExtendableLogger + }: Pick & { + aggregateType?: IAggregateConstructor, + aggregateFactory?: IAggregateFactory, + handles?: string[] }) { if (!eventStore) throw new TypeError('eventStore argument required'); @@ -57,7 +56,7 @@ export class AggregateCommandHandler implements ICommandHandler { if (aggregateType) { const AggregateType = aggregateType; this.#aggregateFactory = params => new AggregateType(params); - this.#handles = getHandledMessageTypes(AggregateType); + this.#handles = AggregateType.handles; } else if (aggregateFactory) { if (!Array.isArray(handles) || !handles.length) @@ -72,28 +71,37 @@ export class AggregateCommandHandler implements ICommandHandler { } /** Subscribe to all command types handled by aggregateType */ - subscribe(commandBus: ICommandBus) { - subscribe(commandBus, this, { - messageTypes: this.#handles, - masterHandler: (c: ICommand) => this.execute(c) - }); + subscribe(commandBus: IObservable) { + if (!commandBus) + throw new TypeError('commandBus argument required'); + if (!isIObservable(commandBus)) + throw new TypeError('commandBus argument must implement IObservable interface'); + + for (const commandType of this.#handles) + commandBus.on(commandType, (cmd: ICommand) => this.execute(cmd)); } /** Restore aggregate from event store events */ - async #restoreAggregate(id: Identifier): Promise { + async #restoreAggregate(id: Identifier): Promise { if (!id) throw new TypeError('id argument required'); - const events = await this.#eventStore.getAggregateEvents(id); - const aggregate = this.#aggregateFactory({ id, events }); + const eventsIterable = this.#eventStore.getAggregateEvents(id); + const aggregate = this.#aggregateFactory({ id }); + + let eventCount = 0; + for await (const event of eventsIterable) { + aggregate.mutate(event); + eventCount += 1; + } - this.#logger?.info(`${aggregate} state restored from ${events.length} event(s)`); + this.#logger?.info(`${aggregate} state restored from ${eventCount} event(s)`); return aggregate; } /** Create new aggregate with new Id generated by event store */ - async #createAggregate(): Promise { + async #createAggregate(): Promise { const id = await this.#eventStore.getNewId(); const aggregate = this.#aggregateFactory({ id }); this.#logger?.info(`${aggregate} created`); @@ -101,29 +109,44 @@ export class AggregateCommandHandler implements ICommandHandler { return aggregate; } + async #getAggregateInstance(aggregateId?: Identifier) { + if (!aggregateId) + return this.#createAggregate(); + else + return this.#aggregatesCache.assert(aggregateId, () => this.#restoreAggregate(aggregateId)); + } + /** Pass a command to corresponding aggregate */ async execute(cmd: ICommand): Promise { - if (!cmd) throw new TypeError('cmd argument required'); - if (!cmd.type) throw new TypeError('cmd.type argument required'); + if (!cmd) + throw new TypeError('cmd argument required'); + if (!cmd.type) + throw new TypeError('cmd.type argument required'); - const aggregate = cmd.aggregateId ? - await this.#restoreAggregate(cmd.aggregateId) : - await this.#createAggregate(); + // create new or get cached aggregate instance promise + // multiple concurrent calls to #getAggregateInstance will return the same promise + const aggregate = await this.#getAggregateInstance(cmd.aggregateId); - await aggregate.handle(cmd); + try { + // multiple concurrent commands to a same aggregateId will execute sequentially + if (cmd.aggregateId) + await this.#executionLock.acquire(String(cmd.aggregateId)); - let events = aggregate.changes; - this.#logger?.info(`${aggregate} "${cmd.type}" command processed, ${events.length} event(s) produced`); - if (!events.length) - return events; + // pass command to aggregate instance + const events = await aggregate.handle(cmd); - if (aggregate.shouldTakeSnapshot && this.#eventStore.snapshotsSupported) { - aggregate.takeSnapshot(); - events = aggregate.changes; - } + this.#logger?.info(`${aggregate} "${cmd.type}" command processed, ${events.length} event(s) produced`); - await this.#eventStore.commit(events); + if (events.length) + await this.#eventStore.dispatch(events); - return events; + return events; + } + finally { + if (cmd.aggregateId) { + this.#executionLock.release(String(cmd.aggregateId)); + this.#aggregatesCache.release(cmd.aggregateId); + } + } } } diff --git a/src/CommandBus.ts b/src/CommandBus.ts index 56b08f2..0a2104d 100644 --- a/src/CommandBus.ts +++ b/src/CommandBus.ts @@ -1,4 +1,4 @@ -import { InMemoryMessageBus } from "./infrastructure/InMemoryMessageBus"; +import { InMemoryMessageBus } from './in-memory'; import { ICommand, ICommandBus, @@ -7,25 +7,22 @@ import { ILogger, IMessageBus, IMessageHandler -} from "./interfaces"; +} from './interfaces'; export class CommandBus implements ICommandBus { #logger?: ILogger; #bus: IMessageBus; - /** - * Creates an instance of CommandBus. - */ - constructor({ messageBus, logger }: { + constructor(o?: { messageBus?: IMessageBus, logger?: ILogger | IExtendableLogger }) { - this.#bus = messageBus ?? new InMemoryMessageBus(); + this.#bus = o?.messageBus ?? new InMemoryMessageBus(); - this.#logger = logger && 'child' in logger ? - logger.child({ service: 'CommandBus' }) : - logger; + this.#logger = o?.logger && 'child' in o.logger ? + o.logger.child({ service: 'CommandBus' }) : + o?.logger; } /** @@ -55,7 +52,12 @@ export class CommandBus implements ICommandBus { /** * Format and send a command for execution */ - send(type: string, aggregateId: string, options: { payload: TPayload, context: object }, ...otherArgs: object[]): Promise { + send( + type: string, + aggregateId: string, + options: { payload: TPayload, context: object }, + ...otherArgs: object[] + ): Promise { if (typeof type !== 'string' || !type.length) throw new TypeError('type argument must be a non-empty String'); if (options && typeof options !== 'object') diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index e2c8e0d..8d7f0c7 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -1,47 +1,47 @@ -import { ContainerBuilder, Container, TypeConfig, TClassOrFactory } from 'di0'; - +import { ContainerBuilder, TypeConfig, TClassOrFactory } from 'di0'; import { AggregateCommandHandler } from './AggregateCommandHandler'; import { CommandBus } from './CommandBus'; import { EventStore } from './EventStore'; import { SagaEventHandler } from './SagaEventHandler'; -import { InMemoryMessageBus } from './infrastructure/InMemoryMessageBus'; - -import { - getHandledMessageTypes, - isClass -} from './utils'; - +import { EventDispatcher } from './EventDispatcher'; +import { InMemoryMessageBus } from './in-memory'; +import { isClass } from './utils'; import { IAggregateConstructor, - ICommandBus, ICommandHandler, + IContainer, IEventReceptor, - IEventStore, IProjection, IProjectionConstructor, - ISagaConstructor + ISagaConstructor, + isDispatchPipelineProcessor } from './interfaces'; -interface CqrsContainer extends Container { - eventStore: IEventStore; - commandBus: ICommandBus; -} - -export class CqrsContainerBuilder extends ContainerBuilder { +export class CqrsContainerBuilder + extends ContainerBuilder { constructor(options?: { types: Readonly[]>, singletones: object }) { super(options); + super.register(InMemoryMessageBus).as('eventBus'); super.register(EventStore).as('eventStore'); super.register(CommandBus).as('commandBus'); + super.register(EventDispatcher).as('eventDispatcher'); + + super.register(c => [ + // automatically add `eventStorageWrite` and `snapshotStorage` to the default dispatch pipeline + // if they're registered in the DI container and implement `IDispatchPipelineProcessor` interface + ...isDispatchPipelineProcessor(c.eventStorageWriter) ? [c.eventStorageWriter] : [], + ...isDispatchPipelineProcessor(c.snapshotStorage) ? [c.snapshotStorage] : [] + ]).as('eventDispatchPipeline'); } /** Register command handler, which will be subscribed to commandBus upon instance creation */ - registerCommandHandler(typeOrFactory: TClassOrFactory) { + registerCommandHandler(typeOrFactory: TClassOrFactory) { return super.register( - (container: CqrsContainer) => { + (container: TContainerInterface) => { const handler = container.createInstance(typeOrFactory); handler.subscribe(container.commandBus); return handler; @@ -50,9 +50,9 @@ export class CqrsContainerBuilder extends ContainerBuilder { } /** Register event receptor, which will be subscribed to eventStore upon instance creation */ - registerEventReceptor(typeOrFactory: TClassOrFactory) { + registerEventReceptor(typeOrFactory: TClassOrFactory) { return super.register( - (container: CqrsContainer) => { + (container: TContainerInterface) => { const receptor = container.createInstance(typeOrFactory); receptor.subscribe(container.eventStore); return receptor; @@ -64,11 +64,11 @@ export class CqrsContainerBuilder extends ContainerBuilder { * Register projection, which will expose view and will be subscribed * to eventStore and will restore its state upon instance creation */ - registerProjection(ProjectionType: IProjectionConstructor, exposedViewAlias?: string) { + registerProjection(ProjectionType: IProjectionConstructor, exposedViewAlias?: keyof TContainerInterface) { if (!isClass(ProjectionType)) throw new TypeError('ProjectionType argument must be a constructor function'); - const projectionFactory = (container: CqrsContainer): IProjection => { + const projectionFactory = (container: TContainerInterface): IProjection => { const projection = container.createInstance(ProjectionType); projection.subscribe(container.eventStore); @@ -87,15 +87,15 @@ export class CqrsContainerBuilder extends ContainerBuilder { } /** Register aggregate type in the container */ - registerAggregate(AggregateType: IAggregateConstructor) { + registerAggregate(AggregateType: IAggregateConstructor) { if (!isClass(AggregateType)) throw new TypeError('AggregateType argument must be a constructor function'); - const commandHandlerFactory = (container: CqrsContainer): ICommandHandler => + const commandHandlerFactory = (container: TContainerInterface): ICommandHandler => container.createInstance(AggregateCommandHandler, { aggregateFactory: (options: any) => container.createInstance(AggregateType, options), - handles: getHandledMessageTypes(AggregateType) + handles: AggregateType.handles }); return this.registerCommandHandler(commandHandlerFactory); @@ -107,7 +107,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { if (!isClass(SagaType)) throw new TypeError('SagaType argument must be a constructor function'); - const eventReceptorFactory = (container: CqrsContainer): IEventReceptor => + const eventReceptorFactory = (container: TContainerInterface): IEventReceptor => container.createInstance(SagaEventHandler, { sagaFactory: (options: any) => container.createInstance(SagaType, options), handles: SagaType.handles, diff --git a/src/Event.ts b/src/Event.ts index 83b2043..fecaf16 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -1,11 +1,4 @@ -import { IEvent } from "./interfaces"; -import * as crypto from 'crypto'; - -const md5 = (data: object): string => crypto - .createHash('md5') - .update(JSON.stringify(data)) - .digest('hex') - .replace(/==$/, ''); +import { IEvent } from './interfaces'; /** * Get text description of an event for logging purposes @@ -23,21 +16,3 @@ export function describeMultiple(events: ReadonlyArray): string { return `${events.length} events`; } - -/** - * Validate event structure - */ -export function validate(event: IEvent) { - if (typeof event !== 'object' || !event) - throw new TypeError('event must be an Object'); - if (typeof event.type !== 'string' || !event.type.length) - throw new TypeError('event.type must be a non-empty String'); - if (!event.aggregateId && !event.sagaId) - throw new TypeError('either event.aggregateId or event.sagaId is required'); - if (event.sagaId && typeof event.sagaVersion === 'undefined') - throw new TypeError('event.sagaVersion is required, when event.sagaId is defined'); -} - -export function getId(event: IEvent): string { - return event.id ?? md5(event); -} diff --git a/src/EventDispatchPipeline.ts b/src/EventDispatchPipeline.ts new file mode 100644 index 0000000..340612d --- /dev/null +++ b/src/EventDispatchPipeline.ts @@ -0,0 +1,104 @@ +import { + DispatchPipelineBatch, + IEvent, + IDispatchPipelineProcessor, + IEventBus, + isDispatchPipelineProcessor, + isSnapshotEvent +} from './interfaces'; + +import { parallelPipe } from 'async-parallel-pipe'; +import { AsyncIterableBuffer } from 'async-iterable-buffer'; +import { getClassName } from './utils'; + +export type EventBatchEnvelope = { + data: DispatchPipelineBatch<{ event?: IEvent }>; + error?: Error; + resolve: (event: IEvent[]) => void; + reject: (error: Error) => void; +}; + +export class EventDispatchPipeline { + + #pipelineInput = new AsyncIterableBuffer(); + #processors: Array = []; + #pipeline: AsyncIterableIterator | IterableIterator = this.#pipelineInput; + #processing = false; + + constructor(private readonly eventBus: IEventBus, private readonly concurrentLimit: number) { + } + + addProcessor(preprocessor: IDispatchPipelineProcessor) { + if (!isDispatchPipelineProcessor(preprocessor)) + throw new TypeError(`preprocessor ${getClassName(preprocessor)} does not implement IDispatchPipelineProcessor`); + if (this.#processing) + throw new Error('pipeline processing already started'); + + this.#processors.push(preprocessor); + + // Build a processing pipeline that runs preprocessors concurrently, preserving FIFO ordering + this.#pipeline = parallelPipe(this.#pipeline, this.concurrentLimit, async envelope => { + if (envelope.error) + return envelope; + + try { + return { + ...envelope, + data: await preprocessor.process(envelope.data) + }; + } + catch (error: any) { + return { + ...envelope, + error + }; + } + }); + } + + #ensureProcessingStarted() { + if (this.#processing) + return; + + this.#processing = true; + + (async () => { + for await (const { error, reject, data, resolve } of this.#pipeline) { + try { + if (error) { + await this.revert(data); + reject(error); + continue; + } + + const events: IEvent[] = []; + for (const batch of data) { + const { event, ...meta } = batch as any; + if (!event) + continue; + if (isSnapshotEvent(event)) + continue; + + await this.eventBus.publish(event, meta); + + events.push(event); + } + resolve(events); + } + catch (publishError: any) { + reject(publishError); + } + } + })(); + } + + async revert(batch: DispatchPipelineBatch) { + for (const processor of this.#processors) + await processor.revert?.(batch); + } + + push(envelope: EventBatchEnvelope) { + this.#ensureProcessingStarted(); + this.#pipelineInput.push(envelope); + } +} diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts new file mode 100644 index 0000000..ab11cbe --- /dev/null +++ b/src/EventDispatcher.ts @@ -0,0 +1,126 @@ +import { + IEventDispatcher, + IDispatchPipelineProcessor, + IEventSet, + IEventBus, + isEventSet, + IContainer +} from './interfaces'; +import { InMemoryMessageBus } from './in-memory'; +import { EventBatchEnvelope, EventDispatchPipeline } from './EventDispatchPipeline'; + +export class EventDispatcher implements IEventDispatcher { + + /** Default pipeline name */ + static DEFAULT_PIPELINE = 'default'; + + /** Default maximum number of parallel batches for newly created pipelines */ + static DEFAULT_CONCURRENT_LIMIT = 100; + + /** Default router that uses `meta.origin` as the pipeline name */ + static DEFAULT_ROUTER = (_e: IEventSet, meta?: Record) => meta?.origin; + + /** + * Event bus where dispatched messages are delivered after processing. + * If not provided in the constructor, defaults to an instance of `InMemoryMessageBus`. + */ + eventBus: IEventBus; + + /** + * Default maximum number of parallel batches for newly created pipelines. + */ + concurrentLimit: number; + + /** Router that selects a pipeline name given events and meta */ + eventDispatchRouter?: (events: IEventSet, meta?: Record) => string | undefined; + + #pipelines = new Map(); + + constructor(o?: Pick & { + eventDispatcherConfig?: { + concurrentLimit?: number + }, + eventDispatchPipelines?: Record, + eventDispatchRouter?: (events: IEventSet, meta?: Record) => string | undefined + }) { + this.eventBus = o?.eventBus ?? new InMemoryMessageBus(); + this.concurrentLimit = o?.eventDispatcherConfig?.concurrentLimit ?? EventDispatcher.DEFAULT_CONCURRENT_LIMIT; + this.eventDispatchRouter = o?.eventDispatchRouter ?? EventDispatcher.DEFAULT_ROUTER; + + if (o?.eventDispatchPipelines) { + // Initialize pipelines if provided + for (const [name, processors] of Object.entries(o.eventDispatchPipelines)) + this.addPipeline(name, processors); + } + else if (o?.eventDispatchPipeline) { + // Single pipeline provided becomes the default pipeline + this.addPipeline(EventDispatcher.DEFAULT_PIPELINE, o.eventDispatchPipeline); + } + else { + // Ensure default pipeline exists at minimum + this.addPipeline(EventDispatcher.DEFAULT_PIPELINE, []); + } + } + + /** Add or create the default pipeline processors */ + addPipelineProcessors(eventDispatchPipeline: IDispatchPipelineProcessor[], pipelineName?: string) { + if (!Array.isArray(eventDispatchPipeline)) + throw new TypeError('eventDispatchPipeline argument must be an Array'); + + for (const processor of eventDispatchPipeline) + this.addPipelineProcessor(processor, pipelineName); + } + + /** Adds a single processor to the default pipeline */ + addPipelineProcessor(preprocessor: IDispatchPipelineProcessor, pipelineName?: string) { + const pipeline = this.#pipelines.get(pipelineName ?? EventDispatcher.DEFAULT_PIPELINE); + if (!pipeline) + throw new Error(`Pipeline "${pipelineName ?? EventDispatcher.DEFAULT_PIPELINE}" does not exist`); + + pipeline.addProcessor(preprocessor); + } + + /** Create a named pipeline with processors and optional concurrency limit */ + addPipeline(name: string, processors: IDispatchPipelineProcessor[] = [], options?: { concurrentLimit?: number }) { + if (!name) + throw new TypeError('pipeline name required'); + if (this.#pipelines.has(name)) + throw new Error(`pipeline "${name}" already exists`); + + const pipeline = new EventDispatchPipeline(this.eventBus, options?.concurrentLimit ?? this.concurrentLimit); + for (const p of processors) + pipeline.addProcessor(p); + + this.#pipelines.set(name, pipeline); + + return pipeline; + } + + /** Dispatch events through a routed pipeline and publish to the shared eventBus */ + async dispatch(events: IEventSet, meta?: Record) { + if (!isEventSet(events) || events.length === 0) + throw new TypeError('dispatch requires a non-empty array of events'); + + let resolve!: (value: IEventSet | PromiseLike) => void; + let reject!: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + const envelope: EventBatchEnvelope = { + data: events.map(event => ({ event, ...meta })), + resolve, + reject + }; + + const desired = this.eventDispatchRouter?.(events, meta) ?? EventDispatcher.DEFAULT_PIPELINE; + const pipeline = this.#pipelines.get(desired) ?? this.#pipelines.get(EventDispatcher.DEFAULT_PIPELINE); + if (!pipeline) + throw new Error(`No "${desired}" pipeline configured`); + + pipeline.push(envelope); + + return promise; + } +} diff --git a/src/EventStore.ts b/src/EventStore.ts index 3a0c076..09ba489 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -1,124 +1,99 @@ import { IAggregateSnapshotStorage, - Identifier, IEvent, - IEventQueryFilter, - IEventStorage, + IEventStorageReader, IEventSet, - IExtendableLogger, ILogger, - IMessageBus, IMessageHandler, IObservable, IEventStream, - IEventStore -} from "./interfaces"; -import { getClassName, setupOneTimeEmitterSubscription } from "./utils"; -import * as Event from './Event'; - -const isIEventStorage = (storage: IEventStorage): storage is IEventStorage => - storage - && typeof storage.getNewId === 'function' - && typeof storage.commitEvents === 'function' - && typeof storage.getEvents === 'function' - && typeof storage.getAggregateEvents === 'function' - && typeof storage.getSagaEvents === 'function'; - -const isIObservable = (obj: IObservable | any): obj is IObservable => - obj - && 'on' in obj - && typeof obj.on === 'function' - && 'off' in obj - && typeof obj.off === 'function'; - -const isIMessageBus = (bus: IMessageBus | any): bus is IMessageBus => - bus - && isIObservable(bus) - && 'send' in bus - && typeof bus.send === 'function' - && 'publish' in bus - && typeof bus.publish === 'function'; - -const SNAPSHOT_EVENT_TYPE = 'snapshot'; + IEventStore, + EventQueryAfter, + EventQueryBefore, + Identifier, + IIdentifierProvider, + isIdentifierProvider, + IEventDispatcher, + IEventBus, + isIEventBus, + isIEventStorageReader, + IContainer, + isEventSet +} from './interfaces'; +import { + getClassName, + setupOneTimeEmitterSubscription +} from './utils'; +import { EventDispatcher } from './EventDispatcher'; export class EventStore implements IEventStore { - #publishAsync: boolean; - #validator: (event: IEvent) => void; - #logger?: ILogger; - #storage: IEventStorage; - #messageBus?: IMessageBus; + #identifierProvider: IIdentifierProvider; + #eventStorageReader: IEventStorageReader; #snapshotStorage: IAggregateSnapshotStorage | undefined; - #sagaStarters: string[] = []; - #defaultEventEmitter: IObservable; - - /** Whether storage supports aggregate snapshots */ - get snapshotsSupported(): boolean { - return Boolean(this.#snapshotStorage); - } + eventBus: IEventBus; + #eventDispatcher: IEventDispatcher; + #sagaStarters: Set = new Set(); + #logger?: ILogger; constructor({ - storage, - messageBus, + eventStorageReader, + identifierProvider = isIdentifierProvider(eventStorageReader) ? eventStorageReader : undefined, snapshotStorage, - eventValidator = Event.validate, - eventStoreConfig, + eventBus, + eventDispatcher, logger - }: { - storage: IEventStorage, - messageBus?: IMessageBus, - snapshotStorage?: IAggregateSnapshotStorage, - eventValidator?: IMessageHandler, - eventStoreConfig?: { - publishAsync?: boolean - }, - logger?: ILogger | IExtendableLogger - }) { - if (!storage) - throw new TypeError('storage argument required'); - if (!isIEventStorage(storage)) + }: Pick) { + if (!eventStorageReader) + throw new TypeError('eventStorageReader argument required'); + if (!identifierProvider) + throw new TypeError('identifierProvider argument required'); + if (!isIEventStorageReader(eventStorageReader)) throw new TypeError('storage does not implement IEventStorage interface'); - if (messageBus && !isIMessageBus(messageBus)) - throw new TypeError('messageBus does not implement IMessageBus interface'); - if (messageBus && isIObservable(storage)) - throw new TypeError('both storage and messageBus implement IObservable interface, it is not yet supported'); - - const defaultEventEmitter = isIObservable(storage) ? storage : messageBus; - if (!defaultEventEmitter) - throw new TypeError('storage must implement IObservable if messageBus is not injected'); + if (eventBus && !isIEventBus(eventBus)) + throw new TypeError('eventBus does not implement IMessageBus interface'); - this.#publishAsync = eventStoreConfig?.publishAsync ?? true; - this.#validator = eventValidator; + this.#eventStorageReader = eventStorageReader; + this.#identifierProvider = identifierProvider; + this.#snapshotStorage = snapshotStorage; + this.#eventDispatcher = eventDispatcher ?? new EventDispatcher({ eventBus }); + this.eventBus = eventBus ?? this.#eventDispatcher.eventBus; this.#logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : logger; - this.#storage = storage; - this.#snapshotStorage = snapshotStorage; - this.#messageBus = messageBus; - this.#defaultEventEmitter = defaultEventEmitter; } - /** Retrieve new ID from the storage */ + /** + * Generates and returns a new unique identifier using the configured identifier provider. + * + * @returns A promise resolving to a unique identifier suitable for aggregates, sagas, and events. + */ async getNewId(): Promise { - return this.#storage.getNewId(); + return this.#identifierProvider.getNewId(); } - /** Retrieve all events of specific types */ - async* getAllEvents(eventTypes?: string[]): IEventStream { - if (eventTypes && !Array.isArray(eventTypes)) - throw new TypeError('eventTypes, if specified, must be an Array'); + async* getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream { + if (!Array.isArray(eventTypes)) + throw new TypeError('eventTypes argument must be an Array'); - this.#logger?.debug(`retrieving ${eventTypes ? eventTypes.join(', ') : 'all'} events...`); + this.#logger?.debug(`retrieving ${eventTypes.join(', ')} events...`); - const eventsIterable = await this.#storage.getEvents(eventTypes); + const eventsIterable = await this.#eventStorageReader.getEventsByTypes(eventTypes, options); yield* eventsIterable; - this.#logger?.debug(`${eventTypes ? eventTypes.join(', ') : 'all'} events retrieved`); + this.#logger?.debug(`${eventTypes.join(', ')} events retrieved`); } /** Retrieve all events of specific Aggregate */ - async getAggregateEvents(aggregateId: Identifier): Promise { + async* getAggregateEvents(aggregateId: Identifier): IEventStream { if (!aggregateId) throw new TypeError('aggregateId argument required'); @@ -128,21 +103,18 @@ export class EventStore implements IEventStore { await this.#snapshotStorage.getAggregateSnapshot(aggregateId) : undefined; - const events: IEvent[] = []; if (snapshot) - events.push(snapshot); + yield snapshot; - const eventsIterable = await this.#storage.getAggregateEvents(aggregateId, { snapshot }); - for await (const event of eventsIterable) - events.push(event); + const eventsIterable = await this.#eventStorageReader.getAggregateEvents(aggregateId, { snapshot }); - this.#logger?.debug(`${Event.describeMultiple(events)} retrieved`); + yield* eventsIterable; - return events; + this.#logger?.debug(`all events for aggregate ${aggregateId} retrieved`); } /** Retrieve events of specific Saga */ - async getSagaEvents(sagaId: Identifier, filter: Pick) { + async* getSagaEvents(sagaId: Identifier, filter: EventQueryBefore) { if (!sagaId) throw new TypeError('sagaId argument required'); if (!filter) @@ -154,14 +126,11 @@ export class EventStore implements IEventStore { this.#logger?.debug(`retrieving event stream for saga ${sagaId}, v${filter.beforeEvent.sagaVersion}...`); - const events: IEvent[] = []; - const eventsIterable = await this.#storage.getSagaEvents(sagaId, filter); - for await (const event of eventsIterable) - events.push(event); + const eventsIterable = await this.#eventStorageReader.getSagaEvents(sagaId, filter); - this.#logger?.debug(`${Event.describeMultiple(events)} retrieved`); + yield* eventsIterable; - return events; + this.#logger?.debug(`all events for saga ${sagaId} retrieved`); } /** @@ -169,40 +138,35 @@ export class EventStore implements IEventStore { * Upon such event commit a new sagaId will be assigned */ registerSagaStarters(eventTypes: string[] = []) { - const uniqueEventTypes = eventTypes.filter(e => !this.#sagaStarters.includes(e)); - this.#sagaStarters.push(...uniqueEventTypes); + for (const eventType of eventTypes) + this.#sagaStarters.add(eventType); } /** * Validate events, commit to storage and publish to messageBus, if needed * - * @param {IEventSet} events - a set of events to commit - * @returns {Promise} - resolves to signed and committed events + * @param events - a set of events to commit + * @returns Signed and committed events */ - async commit(events) { - if (!Array.isArray(events)) - throw new TypeError('events argument must be an Array'); + async dispatch(events: IEventSet): Promise { + if (!isEventSet(events) || events.length === 0) + throw new TypeError('dispatch requires a non-empty array of events'); - const containsSagaStarters = this.#sagaStarters.length && events.some(e => this.#sagaStarters.includes(e.type)); - const augmentedEvents = containsSagaStarters ? - await this.#attachSagaIdToSagaStarterEvents(events) : - events; + const augmentedEvents = await this.#attachSagaIdToSagaStarterEvents(events); - const eventStreamWithoutSnapshots = await this.save(augmentedEvents); - - // after events are saved to the persistent storage, - // publish them to the event bus (i.e. RabbitMq) - if (this.#messageBus) - await this.#publish(eventStreamWithoutSnapshots); - - return eventStreamWithoutSnapshots; + return this.#eventDispatcher.dispatch(augmentedEvents, { origin: 'internal' }); } - /** Generate and attach sagaId to events that start new sagas */ + /** + * Generate and attach sagaId to events that start new sagas + */ async #attachSagaIdToSagaStarterEvents(events: IEventSet): Promise { + if (!this.#sagaStarters.size) + return events; + const augmentedEvents: IEvent[] = []; for (const event of events) { - if (this.#sagaStarters.includes(event.type)) { + if (this.#sagaStarters.has(event.type)) { if (event.sagaId) throw new Error(`Event "${event.type}" already contains sagaId. Multiple sagas with same event type are not supported`); @@ -218,98 +182,25 @@ export class EventStore implements IEventStore { return augmentedEvents; } - /** Save events to the persistent storage(s) */ - async save(events: IEventSet): Promise { - if (!Array.isArray(events)) - throw new TypeError('events argument must be an Array'); - - const snapshotEvents = events.filter(e => e.type === SNAPSHOT_EVENT_TYPE); - if (snapshotEvents.length > 1) - throw new Error(`cannot commit a stream with more than 1 ${SNAPSHOT_EVENT_TYPE} event`); - if (snapshotEvents.length && !this.snapshotsSupported) - throw new Error(`${SNAPSHOT_EVENT_TYPE} event type is not supported by the storage`); - - const snapshot = snapshotEvents[0]; - const eventsWithoutSnapshot = events.filter(e => e !== snapshot); - - this.#logger?.debug(`validating ${Event.describeMultiple(eventsWithoutSnapshot)}...`); - eventsWithoutSnapshot.forEach(this.#validator); - - this.#logger?.debug(`saving ${Event.describeMultiple(eventsWithoutSnapshot)}...`); - await Promise.all([ - this.#storage.commitEvents(eventsWithoutSnapshot), - snapshot ? - this.#snapshotStorage?.saveAggregateSnapshot(snapshot) : - undefined - ]); - - return eventsWithoutSnapshot; - } - - async #publish(events: IEventSet) { - if (this.#publishAsync) { - this.#logger?.debug(`publishing ${Event.describeMultiple(events)} asynchronously...`); - setImmediate(() => this.#publishEvents(events)); - } - else { - this.#logger?.debug(`publishing ${Event.describeMultiple(events)} synchronously...`); - await this.#publishEvents(events); - } - } - - async #publishEvents(events: IEventSet) { - if (!this.#messageBus) - return; - - try { - await Promise.all(events.map(event => - this.#messageBus?.publish(event))); - - this.#logger?.debug(`${Event.describeMultiple(events)} published`); - } - catch (error: any) { - this.#logger?.error(`${Event.describeMultiple(events)} publishing failed: ${error.message}`, { - stack: error.stack - }); - throw error; - } - } - - /** Setup a listener for a specific event type */ on(messageType: string, handler: IMessageHandler) { - if (typeof messageType !== 'string' || !messageType.length) - throw new TypeError('messageType argument must be a non-empty String'); - if (typeof handler !== 'function') - throw new TypeError('handler argument must be a Function'); - if (arguments.length !== 2) - throw new TypeError(`2 arguments are expected, but ${arguments.length} received`); - - if (isIObservable(this.#storage)) - this.#storage.on(messageType, handler); - - this.#messageBus?.on(messageType, handler); + this.eventBus.on(messageType, handler); } - /** Remove previously installed listener */ off(messageType: string, handler: IMessageHandler) { - if (isIObservable(this.#storage)) - this.#storage.off(messageType, handler); - - this.#messageBus?.off(messageType, handler); + this.eventBus.off(messageType, handler); } - /** Get or create a named queue, which delivers events to a single handler only */ queue(name: string): IObservable { - if (!this.#defaultEventEmitter.queue) - throw new Error('Named queues are not supported by the underlying message bus'); + if (!this.eventBus.queue) + throw new Error('Injected eventBus does not support named queues'); - return this.#defaultEventEmitter.queue(name); + return this.eventBus.queue(name); } /** Creates one-time subscription for one or multiple events that match a filter */ - once(messageTypes: string | string[], handler: IMessageHandler, filter: (e: IEvent) => boolean): Promise { + once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise { const subscribeTo = Array.isArray(messageTypes) ? messageTypes : [messageTypes]; - return setupOneTimeEmitterSubscription(this.#defaultEventEmitter, subscribeTo, filter, handler, this.#logger); + return setupOneTimeEmitterSubscription(this.eventBus, subscribeTo, filter, handler, this.#logger); } } diff --git a/src/SagaEventHandler.ts b/src/SagaEventHandler.ts index b66c639..6e784ff 100644 --- a/src/SagaEventHandler.ts +++ b/src/SagaEventHandler.ts @@ -1,10 +1,10 @@ import * as Event from './Event'; import { ICommandBus, + IContainer, IEvent, IEventReceptor, IEventStore, - IExtendableLogger, ILogger, IObservable, ISaga, @@ -14,7 +14,8 @@ import { import { subscribe, - getClassName + getClassName, + iteratorToArray } from './utils'; /** @@ -33,12 +34,9 @@ export class SagaEventHandler implements IEventReceptor { #startsWith: string[]; #handles: string[]; - constructor(options: { + constructor(options: Pick & { sagaType?: ISagaConstructor, sagaFactory?: ISagaFactory, - eventStore: IEventStore, - commandBus: ICommandBus, - logger?: ILogger | IExtendableLogger, queueName?: string, startsWith?: string[], handles?: string[] @@ -85,7 +83,7 @@ export class SagaEventHandler implements IEventReceptor { subscribe(eventStore: IObservable) { subscribe(eventStore, this, { messageTypes: [...this.#startsWith, ...this.#handles], - masterHandler: e => this.handle(e), + masterHandler: this.handle, queueName: this.#queueName }); } @@ -151,7 +149,8 @@ export class SagaEventHandler implements IEventReceptor { if (!event.sagaId) throw new TypeError(`${Event.describe(event)} does not contain sagaId`); - const events = await this.#eventStore.getSagaEvents(event.sagaId, { beforeEvent: event }); + const eventsIterable = this.#eventStore.getSagaEvents(event.sagaId, { beforeEvent: event }); + const events = await iteratorToArray(eventsIterable); const saga = this.#sagaFactory.call(null, { id: event.sagaId, events }); this.#logger?.info(`Saga state restored from ${events.length} event(s)`); diff --git a/src/in-memory/InMemoryEventStorage.ts b/src/in-memory/InMemoryEventStorage.ts new file mode 100644 index 0000000..d0b8d95 --- /dev/null +++ b/src/in-memory/InMemoryEventStorage.ts @@ -0,0 +1,108 @@ +import { + IIdentifierProvider, + IEvent, + IEventSet, + EventQueryAfter, + IEventStorageReader, + IEventStream, + IEventStorageWriter, + Identifier, + IDispatchPipelineProcessor, + DispatchPipelineBatch +} from '../interfaces'; +import { nextCycle } from './utils'; + +/** + * A simple event storage implementation intended to use for tests only. + * Storage content resets on each app restart. + */ +export class InMemoryEventStorage implements + IEventStorageReader, + IEventStorageWriter, + IIdentifierProvider, + IDispatchPipelineProcessor { + + #nextId: number = 0; + #events: IEventSet = []; + + getNewId(): string { + this.#nextId += 1; + return String(this.#nextId); + } + + async commitEvents(events: IEventSet): Promise { + await nextCycle(); + + this.#events = this.#events.concat(events); + + await nextCycle(); + + return events; + } + + async* getAggregateEvents(aggregateId: Identifier, options?: { snapshot: IEvent }): IEventStream { + await nextCycle(); + + const afterVersion = options?.snapshot?.aggregateVersion; + const results = !afterVersion ? + this.#events.filter(e => e.aggregateId === aggregateId) : + this.#events.filter(e => + e.aggregateId === aggregateId && + e.aggregateVersion !== undefined && + e.aggregateVersion > afterVersion); + + await nextCycle(); + + yield* results; + } + + async* getSagaEvents(sagaId: Identifier, { beforeEvent }: { beforeEvent: IEvent }): IEventStream { + await nextCycle(); + + const results = this.#events.filter(e => + e.sagaId === sagaId && + e.sagaVersion !== undefined && + beforeEvent.sagaVersion !== undefined && + e.sagaVersion < beforeEvent.sagaVersion); + + await nextCycle(); + + yield* results; + } + + async* getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream { + await nextCycle(); + + const lastEventId = options?.afterEvent?.id; + if (options?.afterEvent && !lastEventId) + throw new TypeError('options.afterEvent.id is required'); + + let offsetFound = !lastEventId; + for (const event of this.#events) { + if (!offsetFound) + offsetFound = event.id === lastEventId; + else if (!eventTypes || eventTypes.includes(event.type)) + yield event; + } + } + + /** + * Processes a batch of dispatch pipeline items, extracts the events, + * commits them to the in-memory storage, and returns the original batch. + * + * This method is part of the `IDispatchPipelineProcessor` interface. + */ + async process(batch: DispatchPipelineBatch): Promise { + const events: IEvent[] = []; + for (const { event } of batch) { + if (!event) + throw new Error('Event batch does not contain `event`'); + + events.push(event); + } + + await this.commitEvents(events); + + return batch; + } +} diff --git a/src/in-memory/InMemoryLock.ts b/src/in-memory/InMemoryLock.ts new file mode 100644 index 0000000..8b853d1 --- /dev/null +++ b/src/in-memory/InMemoryLock.ts @@ -0,0 +1,43 @@ +import { Deferred } from '../utils'; + +export class InMemoryLock { + + #lockMarker: Deferred | undefined; + + /** + * Indicates if lock is acquired + */ + get locked(): boolean { + return !!this.#lockMarker; + } + + /** + * Acquire the lock on the current instance. + * Resolves when the lock is successfully acquired + */ + async lock(): Promise { + while (this.locked) + await this.once('unlocked'); + + this.#lockMarker = new Deferred(); + } + + /** + * Release the lock acquired earlier + */ + async unlock(): Promise { + this.#lockMarker?.resolve(); + this.#lockMarker = undefined; + } + + /** + * Wait until the lock is released. + * Resolves immediately if the lock is not acquired + */ + once(event: 'unlocked'): Promise { + if (event !== 'unlocked') + throw new TypeError(`Unexpected event type: ${event}`); + + return this.#lockMarker?.promise ?? Promise.resolve(); + } +} diff --git a/src/infrastructure/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts similarity index 63% rename from src/infrastructure/InMemoryMessageBus.ts rename to src/in-memory/InMemoryMessageBus.ts index c8f43f3..c2b94a6 100644 --- a/src/infrastructure/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -4,7 +4,7 @@ import { IMessageBus, IMessageHandler, IObservable -} from "../interfaces"; +} from '../interfaces'; /** * Default implementation of the message bus. @@ -12,17 +12,17 @@ import { */ export class InMemoryMessageBus implements IMessageBus { - #handlers: Map> = new Map(); - #name: string | undefined; - #uniqueEventHandlers: boolean; - #queues: Map = new Map(); + protected handlers: Map> = new Map(); + protected uniqueEventHandlers: boolean; + protected queueName: string | undefined; + protected queues: Map = new Map(); - constructor({ name, uniqueEventHandlers = !!name }: { - name?: string, + constructor({ queueName, uniqueEventHandlers = !!queueName }: { + queueName?: string, uniqueEventHandlers?: boolean } = {}) { - this.#name = name; - this.#uniqueEventHandlers = uniqueEventHandlers; + this.queueName = queueName; + this.uniqueEventHandlers = uniqueEventHandlers; } /** @@ -38,25 +38,25 @@ export class InMemoryMessageBus implements IMessageBus { // Events published to a named queue must be consumed only once. // For example, for sending a welcome email, NotificationReceptor will subscribe to "notifications:userCreated". - // Since we use an in-memory bus, there is no need to track message handling by multiple distributed subscribers, - // and we only need to make sure that no more than 1 such subscriber will be created - if (!this.#handlers.has(messageType)) - this.#handlers.set(messageType, new Set()); - else if (this.#uniqueEventHandlers) - throw new Error(`"${messageType}" handler is already set up on the "${this.#name}" queue`); - - this.#handlers.get(messageType)?.add(handler); + // Since we use an in-memory bus, there is no need to track message handling by multiple distributed + // subscribers, and we only need to make sure that no more than 1 such subscriber will be created + if (!this.handlers.has(messageType)) + this.handlers.set(messageType, new Set()); + else if (this.uniqueEventHandlers) + throw new Error(`"${messageType}" handler is already set up on the "${this.queueName}" queue`); + + this.handlers.get(messageType)?.add(handler); } /** * Get or create a named queue. * Named queues support only one handler per event type. */ - queue(name: string): IObservable { - let queue = this.#queues.get(name); + queue(queueName: string): IObservable { + let queue = this.queues.get(queueName); if (!queue) { - queue = new InMemoryMessageBus({ name, uniqueEventHandlers: true }); - this.#queues.set(name, queue); + queue = new InMemoryMessageBus({ queueName, uniqueEventHandlers: true }); + this.queues.set(queueName, queue); } return queue; @@ -72,10 +72,10 @@ export class InMemoryMessageBus implements IMessageBus { throw new TypeError('handler argument must be a Function'); if (arguments.length !== 2) throw new TypeError(`2 arguments are expected, but ${arguments.length} received`); - if (!this.#handlers.has(messageType)) + if (!this.handlers.has(messageType)) throw new Error(`No ${messageType} subscribers found`); - this.#handlers.get(messageType)?.delete(handler); + this.handlers.get(messageType)?.delete(handler); } /** @@ -87,7 +87,7 @@ export class InMemoryMessageBus implements IMessageBus { if (typeof command.type !== 'string' || !command.type.length) throw new TypeError('command.type argument must be a non-empty String'); - const handlers = this.#handlers.get(command.type); + const handlers = this.handlers.get(command.type); if (!handlers || !handlers.size) throw new Error(`No '${command.type}' subscribers found`); if (handlers.size > 1) @@ -95,24 +95,26 @@ export class InMemoryMessageBus implements IMessageBus { const commandHandler = handlers.values().next().value; - return commandHandler(command); + return commandHandler!(command); } /** * Publish event to all subscribers (if any) */ - async publish(event: IEvent): Promise { + async publish(event: IEvent, meta?: Record): Promise { if (typeof event !== 'object' || !event) throw new TypeError('event argument must be an Object'); if (typeof event.type !== 'string' || !event.type.length) throw new TypeError('event.type argument must be a non-empty String'); - const handlers = [ - ...this.#handlers.get(event.type) || [], - ...Array.from(this.#queues.values()).map(namedQueue => - (e: IEvent) => namedQueue.publish(e)) - ]; + const promises: Promise[] = []; - return Promise.all(handlers.map(handler => handler(event))); + for (const handler of this.handlers.get(event.type) ?? []) + promises.push(handler(event, meta)); + + for (const namedQueue of this.queues.values()) + promises.push(namedQueue.publish(event, meta)); + + return Promise.all(promises); } } diff --git a/src/in-memory/InMemorySnapshotStorage.ts b/src/in-memory/InMemorySnapshotStorage.ts new file mode 100644 index 0000000..879dcb2 --- /dev/null +++ b/src/in-memory/InMemorySnapshotStorage.ts @@ -0,0 +1,86 @@ +import { + DispatchPipelineBatch, + IAggregateSnapshotStorage, + IContainer, + Identifier, + IDispatchPipelineProcessor, + IEvent, + ILogger, + isSnapshotEvent +} from '../interfaces'; +import * as Event from '../Event'; + +/** + * In-memory storage for aggregate snapshots. + * Storage content resets on app restart + */ +export class InMemorySnapshotStorage implements IAggregateSnapshotStorage, IDispatchPipelineProcessor { + + #snapshots: Map = new Map(); + #logger: ILogger | undefined; + + constructor(c?: Partial>) { + this.#logger = c?.logger && 'child' in c?.logger ? + c?.logger.child({ service: new.target.name }) : + c?.logger; + } + + /** + * Get latest aggregate snapshot + */ + async getAggregateSnapshot(aggregateId: string): Promise { + return this.#snapshots.get(aggregateId); + } + + /** + * Save new aggregate snapshot + */ + async saveAggregateSnapshot(snapshotEvent: IEvent) { + if (!snapshotEvent.aggregateId) + throw new TypeError('event.aggregateId is required'); + + this.#logger?.debug(`Persisting ${Event.describe(snapshotEvent)}`); + + this.#snapshots.set(snapshotEvent.aggregateId, snapshotEvent); + } + + /** + * Delete aggregate snapshot + */ + deleteAggregateSnapshot(snapshotEvent: IEvent): Promise | void { + if (!snapshotEvent.aggregateId) + throw new TypeError('snapshotEvent.aggregateId argument required'); + + this.#logger?.debug(`Removing ${Event.describe(snapshotEvent)}`); + + this.#snapshots.delete(snapshotEvent.aggregateId); + } + + /** + * Processes a batch of events, saves any snapshot events found, and returns the batch + * without the snapshot events. + * + * This method is part of the `IDispatchPipelineProcessor` interface. + */ + async process(batch: DispatchPipelineBatch): Promise { + const snapshotEvents = batch.map(e => e.event).filter(isSnapshotEvent); + for (const event of snapshotEvents) + await this.saveAggregateSnapshot(event); + + return batch.filter(e => !isSnapshotEvent(e.event)); + } + + /** + * Reverts the snapshots associated with the events in the given batch. + * It filters the batch for snapshot events and deletes the corresponding aggregate snapshots. + * + * This method is part of the `IDispatchPipelineProcessor` interface. + * + * @param batch The batch of events to revert snapshots for. + */ + async revert(batch: DispatchPipelineBatch): Promise { + const snapshotEvents = batch.map(e => e.event).filter(isSnapshotEvent); + for (const snapshotEvent of snapshotEvents) + await this.deleteAggregateSnapshot(snapshotEvent); + } +} diff --git a/src/infrastructure/InMemoryView.ts b/src/in-memory/InMemoryView.ts similarity index 89% rename from src/infrastructure/InMemoryView.ts rename to src/in-memory/InMemoryView.ts index 20f528a..6e2f429 100644 --- a/src/infrastructure/InMemoryView.ts +++ b/src/in-memory/InMemoryView.ts @@ -1,12 +1,12 @@ import { InMemoryLock } from './InMemoryLock'; -import { IProjectionView, Identifier } from "../interfaces"; +import { IViewLocker, Identifier, IObjectStorage } from '../interfaces'; import { nextCycle } from './utils'; /** * Update given value with an update Cb and return updated value. * Wrapper is needed for backward compatibility with update methods that were modifying the passed in objects directly */ -function applyUpdate(view: T | undefined, update: (r?: T) => T | undefined): T | undefined { +function applyUpdate(view: T, update: (r: T) => T): T { const valueReturnedByUpdate = update(view); return valueReturnedByUpdate === undefined ? view : @@ -16,7 +16,7 @@ function applyUpdate(view: T | undefined, update: (r?: T) => T | undefined): /** * In-memory Projection View, which suspends get()'s until it is ready */ -export class InMemoryView implements IProjectionView { +export class InMemoryView implements IViewLocker, IObjectStorage { static factory(): TView { return (new InMemoryView() as unknown) as TView; @@ -26,8 +26,6 @@ export class InMemoryView implements IProjectionView { #lock: InMemoryLock; - #asyncWrites: boolean; - /** Whether the view is restored */ get ready(): boolean { return !this.#lock.locked; @@ -38,12 +36,7 @@ export class InMemoryView implements IProjectionView { return this._map.size; } - constructor(options?: { - /** Indicates if writes should be submitted asynchronously */ - asyncWrites?: boolean - }) { - this.#asyncWrites = options?.asyncWrites ?? false; - + constructor() { this.#lock = new InMemoryLock(); // explicitly bind the `get` method to this object for easier using in Promises @@ -131,9 +124,6 @@ export class InMemoryView implements IProjectionView { if (typeof value === 'function') throw new TypeError('value argument must be an instance of an Object'); - if (this.#asyncWrites) - await nextCycle(); - if (this._map.has(key)) throw new Error(`Key '${key}' already exists`); @@ -180,15 +170,15 @@ export class InMemoryView implements IProjectionView { } /** Update existing record */ - private async _update(key: Identifier, update: (r?: TRecord) => TRecord) { + private async _update(key: Identifier, update: (r: TRecord) => TRecord) { const value = this._map.get(key); + if (!value) + throw new Error(`Key '${key}' does not exist`); + const updatedValue = applyUpdate(value, update); if (updatedValue === undefined) return; - if (this.#asyncWrites) - await nextCycle(); - this._map.set(key, updatedValue); } @@ -197,9 +187,6 @@ export class InMemoryView implements IProjectionView { if (!key) throw new TypeError('key argument required'); - if (this.#asyncWrites) - await nextCycle(); - this._map.delete(key); } diff --git a/src/in-memory/index.ts b/src/in-memory/index.ts new file mode 100644 index 0000000..3ef457a --- /dev/null +++ b/src/in-memory/index.ts @@ -0,0 +1,5 @@ +export * from './InMemoryEventStorage'; +export * from './InMemoryLock'; +export * from './InMemoryMessageBus'; +export * from './InMemorySnapshotStorage'; +export * from './InMemoryView'; diff --git a/src/infrastructure/utils/index.ts b/src/in-memory/utils/index.ts similarity index 50% rename from src/infrastructure/utils/index.ts rename to src/in-memory/utils/index.ts index 3622022..d504dca 100644 --- a/src/infrastructure/utils/index.ts +++ b/src/in-memory/utils/index.ts @@ -1,2 +1 @@ -export * from './Deferred'; export * from './nextCycle'; diff --git a/src/infrastructure/utils/nextCycle.ts b/src/in-memory/utils/nextCycle.ts similarity index 100% rename from src/infrastructure/utils/nextCycle.ts rename to src/in-memory/utils/nextCycle.ts diff --git a/src/index.ts b/src/index.ts index 94b96da..27d5d0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,18 +8,13 @@ export * from './AggregateCommandHandler'; export * from './AbstractSaga'; export * from './SagaEventHandler'; export * from './AbstractProjection'; +export * from './EventDispatcher'; -export * from './infrastructure/InMemoryMessageBus'; -export * from './infrastructure/InMemoryEventStorage'; -export * from './infrastructure/InMemorySnapshotStorage'; -export * from './infrastructure/InMemoryView'; -export * from './infrastructure/InMemoryLock'; -export * from './infrastructure/utils/Deferred'; +export * from './in-memory'; export * as Event from './Event'; export { getMessageHandlerNames, - getHandledMessageTypes, subscribe } from './utils'; diff --git a/src/infrastructure/InMemoryEventStorage.ts b/src/infrastructure/InMemoryEventStorage.ts deleted file mode 100644 index 3fa6d95..0000000 --- a/src/infrastructure/InMemoryEventStorage.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { IEvent, IEventStorage, IEventSet, IEventStream } from "../interfaces"; -import { nextCycle } from "./utils"; - -/** - * A simple event storage implementation intended to use for tests only. - * Storage content resets on each app restart. - * - * @class InMemoryEventStorage - * @implements {IEventStorage} - */ -export class InMemoryEventStorage implements IEventStorage { - - #nextId: number = 0; - #events: IEventSet = []; - - async commitEvents(events: IEventSet): Promise { - await nextCycle(); - - this.#events = this.#events.concat(events); - - await nextCycle(); - - return events; - } - - async getAggregateEvents(aggregateId, options?: { snapshot: IEvent }): Promise { - await nextCycle(); - - const afterVersion = options?.snapshot?.aggregateVersion; - const result = !afterVersion ? - this.#events.filter(e => e.aggregateId == aggregateId) : - this.#events.filter(e => - e.aggregateId == aggregateId && - e.aggregateVersion !== undefined && - e.aggregateVersion > afterVersion); - - await nextCycle(); - - return result; - } - - async getSagaEvents(sagaId, { beforeEvent }): Promise { - await nextCycle(); - - const results = this.#events.filter(e => - e.sagaId == sagaId && - e.sagaVersion !== undefined && - e.sagaVersion < beforeEvent.sagaVersion); - - await nextCycle(); - - return results; - } - - async* getEvents(eventTypes): IEventStream { - await nextCycle(); - - for await (const event of this.#events) { - if (!eventTypes || eventTypes.includes(event.type)) - yield event; - } - } - - getNewId(): number { - this.#nextId += 1; - return this.#nextId; - } -} diff --git a/src/infrastructure/InMemoryLock.ts b/src/infrastructure/InMemoryLock.ts deleted file mode 100644 index dee6195..0000000 --- a/src/infrastructure/InMemoryLock.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { ILockable, ILockableWithIndication } from "../interfaces"; -import { Deferred } from "./utils"; - -export class InMemoryLock implements ILockableWithIndication { - - #lockMarker: Deferred | undefined; - #innerLock: ILockable | undefined; - - /** - * Indicates if lock is acquired - */ - get locked(): boolean { - return !!this.#lockMarker; - } - - /** - * Creates an instance of InMemoryLock - * - * @param innerLock ILockable instance that can persist lock state outside of the current process - */ - constructor(innerLock?: ILockable) { - this.#innerLock = innerLock; - } - - /** - * Acquire the lock on the current instance. - * Resolves when the lock is successfully acquired - */ - async lock(): Promise { - while (this.locked) - await this.once('unlocked'); - - try { - this.#lockMarker = new Deferred(); - if (this.#innerLock) - await this.#innerLock.lock(); - } - catch (err: any) { - try { - await this.unlock(); - } - catch (unlockErr: any) { - // unlocking errors are ignored - } - throw err; - } - } - - /** - * Release the lock acquired earlier - */ - async unlock(): Promise { - try { - if (this.#innerLock) - await this.#innerLock.unlock(); - } - finally { - this.#lockMarker?.resolve(); - this.#lockMarker = undefined; - } - } - - /** - * Wait until the lock is released. - * Resolves immediately if the lock is not acquired - */ - once(event: 'unlocked'): Promise { - if (event !== 'unlocked') - throw new TypeError(`Unexpected event type: ${event}`); - - return this.#lockMarker?.promise ?? Promise.resolve(); - } -} diff --git a/src/infrastructure/InMemorySnapshotStorage.ts b/src/infrastructure/InMemorySnapshotStorage.ts deleted file mode 100644 index d3fd377..0000000 --- a/src/infrastructure/InMemorySnapshotStorage.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { IAggregateSnapshotStorage, Identifier, IEvent } from "../interfaces"; - -/** - * In-memory storage for aggregate snapshots. - * Storage content resets on app restart - */ -export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { - - #snapshots: Map = new Map(); - - /** - * Get latest aggregate snapshot - */ - async getAggregateSnapshot(aggregateId: Identifier): Promise { - return this.#snapshots.get(aggregateId); - } - - /** - * Save new aggregate snapshot - */ - async saveAggregateSnapshot(snapshotEvent: IEvent) { - if (!snapshotEvent.aggregateId) - throw new TypeError('event.aggregateId is required'); - - this.#snapshots.set(snapshotEvent.aggregateId, snapshotEvent); - } -} diff --git a/src/interfaces.ts b/src/interfaces.ts deleted file mode 100644 index b55d4a9..0000000 --- a/src/interfaces.ts +++ /dev/null @@ -1,328 +0,0 @@ -export type Identifier = string | number; - -export interface IMessage { - /** Event or command type */ - type: string; - - aggregateId?: Identifier; - aggregateVersion?: number; - - sagaId?: Identifier; - sagaVersion?: number; - - payload?: TPayload; - context?: any; -} - -export type ICommand = IMessage; - -export type IEvent = IMessage & { - /** Unique event identifier */ - id?: string; -}; - -/** - * @deprecated Try to use `IEventStream` instead - */ -export type IEventSet = ReadonlyArray>; - -export type IEventStream = AsyncIterableIterator>; - - -/** - * Minimum aggregate interface, as it's used by default `AggregateCommandHandler` - */ -export interface IAggregate { - - /** Unique aggregate identifier */ - readonly id: Identifier; - - /** Main entry point for aggregate commands */ - handle(command: ICommand): void | Promise; - - /** List of events emitted by Aggregate as a result of handling command(s) */ - readonly changes: IEventSet; - - /** An indicator if aggregate snapshot should be taken */ - readonly shouldTakeSnapshot?: boolean; - - /** Take an aggregate state snapshot and add it to the changes queue */ - takeSnapshot(): void; -} - -export interface IMutableAggregateState { - // schemaVersion?: number; - // constructor: IAggregateStateConstructor; - mutate(event: IEvent): void; -} - -// export interface IAggregateStateConstructor extends Function { -// schemaVersion?: number; -// new(): IAggregateState; -// } - -export type IAggregateConstructorParams = { - /** Unique aggregate identifier */ - id: Identifier, - - /** Aggregate events, logged after latest snapshot */ - events?: IEventSet, - - /** Aggregate state instance */ - state?: TState -}; - -export interface IAggregateConstructor { - readonly handles?: string[]; - new(options: IAggregateConstructorParams): IAggregate; -} - -export type IAggregateFactory = - (options: IAggregateConstructorParams) => IAggregate; - -export interface ISaga { - /** Unique Saga ID */ - readonly id: Identifier; - - /** List of commands emitted by Saga */ - readonly uncommittedMessages: ICommand[]; - - /** Main entry point for Saga events */ - apply(event: IEvent): void | Promise; - - /** Reset emitted commands when they are not longer needed */ - resetUncommittedMessages(): void; - - onError?(error: Error, options: { event: IEvent, command: ICommand }): void; -} - -export type ISagaConstructorParams = { - id: Identifier, - events?: IEventSet -}; - -export type ISagaFactory = (options: ISagaConstructorParams) => ISaga; - -export interface ISagaConstructor { - new(options: ISagaConstructorParams): ISaga; - - /** List of event types that trigger new saga start */ - readonly startsWith: string[]; - - /** List of events being handled by Saga */ - readonly handles: string[]; -} - -export interface IMessageHandler { - (...args: any[]): any | Promise -}; - -export interface IObservable { - on(type: string, handler: IMessageHandler): void; - - off(type: string, handler: IMessageHandler): void; - - queue?(name: string): IObservable; -} - -export interface IObserver { - subscribe(observable: IObservable): void; -} - -/** Commands */ - -export interface ICommandBus extends IObservable { - send(commandType: string, aggregateId: Identifier, options: { payload?: object, context?: object }): - Promise; - - sendRaw(command: ICommand): - Promise; - - on(type: string, handler: IMessageHandler): void; -} - -export interface ICommandHandler extends IObserver { - subscribe(commandBus: ICommandBus): void; -} - -/** Events */ - -export type IEventQueryFilter = { - /** Get events emitted after this specific event */ - afterEvent?: IEvent; - - /** Get events emitted before this specific event */ - beforeEvent?: IEvent; -} - -export interface IEventStorage { - /** - * Create unique identifier - */ - getNewId(): Identifier | Promise; - - commitEvents(events: IEventSet): Promise; - - getEvents(eventTypes?: Readonly): IEventStream; - - getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): Promise; - - getSagaEvents(sagaId: Identifier, options: Pick): Promise; -} - -export interface IEventStore extends IObservable { - readonly snapshotsSupported?: boolean; - - getNewId(): Identifier | Promise; - - commit(events: IEventSet): Promise; - - getAllEvents(eventTypes?: Readonly): IEventStream; - - getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): Promise; - - getSagaEvents(sagaId: Identifier, options: Pick): Promise; - - once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise; - - queue(name: string): IObservable; - - registerSagaStarters(startsWith: string[] | undefined): void; -} - -export interface IEventReceptor extends IObserver { - subscribe(eventStore: IEventStore): void; -} - -export interface IMessageBus extends IObservable { - send(command: ICommand): Promise; - publish(event: IEvent): Promise; -} - - -/** Projection */ - -export interface IProjection extends IObserver { - readonly view: TView; - - subscribe(eventStore: IEventStore): Promise; - - project(event: IEvent): Promise; -} - -export interface IProjectionConstructor { - new(c?: any): IProjection; - readonly handles?: string[]; -} - -// export type ProjectionViewFactoryParams = { -// schemaVersion: string, -// collectionName: string -// } - -export interface IViewFactory { - (): TView; -} - -export interface ILockable { - lock(): Promise; - unlock(): Promise; -} - -export interface ILockableWithIndication extends ILockable { - locked: Readonly; - once(event: 'unlocked'): Promise; -} - -export interface IProjectionView extends ILockable { - - /** - * Indicates if view is ready for new events projecting - */ - ready: boolean; - - /** - * Lock the view for external reads/writes - */ - lock(): Promise; - - /** - * Unlock external read/write operations - */ - unlock(): Promise; - - /** - * Wait till the view is ready to accept new events - */ - once(eventType: "ready"): Promise; -} - -export interface IPersistentView extends IProjectionView { - - /** - * Get last projected event - */ - getLastEvent(): Promise; - - /** - * Mark event as projecting to prevent its handling by another - * projection instance working with the same storage. - * - * @returns False value if event is already processing or processed - */ - tryMarkAsProjecting(event: IEvent): Promise; - - /** - * Mark event as projected - */ - markAsProjected(event: IEvent): Promise; -} - - -/** Snapshots */ - -type TSnapshot = { - /** - * Schema version of the data stored in `state` property. - * Snapshots with older schema versions must be passed thru a data migration before applying for a newer schema - */ - schemaVersion: string | number; - - /** - * Last event that was processed before making a snapshot - */ - lastEvent: IEvent; - - /** - * Snapshot data - */ - data: TPayload; -} - -interface ISnapshotStorage { - getSnapshot(id: Identifier): Promise; - saveSnapshot(id: Identifier, snapshot: TSnapshot): Promise; -} - -type ISnapshotEvent = IEvent>; - -export interface IAggregateSnapshotStorage { - getAggregateSnapshot(aggregateId: Identifier): Promise | undefined> | IEvent | undefined; - - saveAggregateSnapshot(snapshotEvent: IEvent): Promise | void; -} - - -/** Interfaces */ - -export interface ILogger { - log(level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: { [key: string]: any }): void; - debug(message: string, meta?: { [key: string]: any }): void; - info(message: string, meta?: { [key: string]: any }): void; - warn(message: string, meta?: { [key: string]: any }): void; - error(message: string, meta?: { [key: string]: any }): void; -} - -export interface IExtendableLogger extends ILogger { - child(meta?: { [key: string]: any }): IExtendableLogger; -} diff --git a/src/interfaces/IAggregate.ts b/src/interfaces/IAggregate.ts new file mode 100644 index 0000000..5316773 --- /dev/null +++ b/src/interfaces/IAggregate.ts @@ -0,0 +1,68 @@ +import { ICommand } from './ICommand'; +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; +import { IEventSet } from './IEventSet'; + +/** + * Core interface representing an Aggregate in a CQRS architecture. + * An aggregate encapsulates business logic and state, handling commands + * and applying events to transition between states. + */ +export interface IAggregate { + + /** + * Applies a single event to update the aggregate's internal state. + * + * This method is used primarily when rehydrating the aggregate + * from the persisted sequence of events + * + * @param event - The event to be applied + */ + mutate(event: IEvent): void; + + /** + * Processes a command by executing the aggregate's business logic, + * resulting in new events that capture the state changes. + * It serves as the primary entry point for invoking aggregate behavior + * + * @param command - The command to be processed + * @returns A set of events produced by the command + */ + handle(command: ICommand): IEventSet | Promise; +} + +export interface IMutableAggregateState { + + /** + * Apply a single event to mutate the aggregate's state. + */ + mutate(event: IEvent): void; +} + +export type IAggregateConstructorParams = { + + /** Unique aggregate identifier */ + id: Identifier, + + /** + * @deprecated The aggregate no longer receives all events in the constructor. + * Instead, events are loaded and passed to the `mutate` method after instantiation. + */ + events?: IEventSet, + + /** Aggregate state instance */ + state?: TState +}; + +export interface IAggregateConstructor< + TAggregate extends IAggregate, + TState extends IMutableAggregateState | object | void +> { + readonly handles: string[]; + new(options: IAggregateConstructorParams): TAggregate; +} + +export type IAggregateFactory< + TAggregate extends IAggregate, + TState extends IMutableAggregateState | object | void +> = (options: IAggregateConstructorParams) => TAggregate; diff --git a/src/interfaces/IAggregateSnapshotStorage.ts b/src/interfaces/IAggregateSnapshotStorage.ts new file mode 100644 index 0000000..1fb937a --- /dev/null +++ b/src/interfaces/IAggregateSnapshotStorage.ts @@ -0,0 +1,11 @@ +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; + +export interface IAggregateSnapshotStorage { + getAggregateSnapshot(aggregateId: Identifier): + Promise | undefined> | IEvent | undefined; + + saveAggregateSnapshot(snapshotEvent: IEvent): Promise | void; + + deleteAggregateSnapshot(snapshotEvent: IEvent): Promise | void; +} diff --git a/src/interfaces/ICommand.ts b/src/interfaces/ICommand.ts new file mode 100644 index 0000000..94efa95 --- /dev/null +++ b/src/interfaces/ICommand.ts @@ -0,0 +1,3 @@ +import { IMessage } from './IMessage'; + +export type ICommand = IMessage; diff --git a/src/interfaces/ICommandBus.ts b/src/interfaces/ICommandBus.ts new file mode 100644 index 0000000..53c4a7d --- /dev/null +++ b/src/interfaces/ICommandBus.ts @@ -0,0 +1,16 @@ +import { ICommand } from './ICommand'; +import { IEventSet } from './IEventSet'; +import { IObservable } from './IObservable'; +import { IObserver } from './IObserver'; + +export interface ICommandBus extends IObservable { + send(commandType: string, aggregateId: string | undefined, options: { payload?: object, context?: object }): + Promise; + + sendRaw(command: ICommand): + Promise; +} + +export interface ICommandHandler extends IObserver { + subscribe(commandBus: ICommandBus): void; +} diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts new file mode 100644 index 0000000..1d6bebb --- /dev/null +++ b/src/interfaces/IContainer.ts @@ -0,0 +1,33 @@ +import { Container } from 'di0'; +import { ICommandBus } from './ICommandBus'; +import { IEventDispatcher } from './IEventDispatcher'; +import { IEventStore } from './IEventStore'; +import { IEventBus } from './IEventBus'; +import { IDispatchPipelineProcessor } from './IDispatchPipelineProcessor'; +import { IEventStorageReader } from './IEventStorageReader'; +import { IAggregateSnapshotStorage } from './IAggregateSnapshotStorage'; +import { IIdentifierProvider } from './IIdentifierProvider'; +import { IExtendableLogger, ILogger } from './ILogger'; +import { IEventStorageWriter } from './IEventStorageWriter'; + +export interface IContainer extends Container { + eventBus: IEventBus; + eventStore: IEventStore + eventStorageReader: IEventStorageReader; + eventStorageWriter?: IEventStorageWriter; + identifierProvider?: IIdentifierProvider; + snapshotStorage?: IAggregateSnapshotStorage; + + commandBus: ICommandBus; + eventDispatcher: IEventDispatcher; + + /** Default event dispatch pipeline */ + eventDispatchPipeline?: IDispatchPipelineProcessor[]; + + /** Multiple event dispatch pipelines per origin */ + eventDispatchPipelines?: Record; + + logger?: ILogger | IExtendableLogger; + + process?: NodeJS.Process +} diff --git a/src/interfaces/IDispatchPipelineProcessor.ts b/src/interfaces/IDispatchPipelineProcessor.ts new file mode 100644 index 0000000..958412f --- /dev/null +++ b/src/interfaces/IDispatchPipelineProcessor.ts @@ -0,0 +1,35 @@ +import { IEvent } from './IEvent'; +import { isObject } from './isObject'; + +/** + * Represents a wrapper for an event that can optionally contain additional metadata. + * Used to extend event processing with context-specific data required by processors. + */ +export type DispatchPipelineEnvelope = { + + /** + * Origin of the event. Can be used to distinguish between events coming from different sources. + */ + origin?: string; + + event?: IEvent; +} + +/** + * A batch of event envelopes. Can contain custom envelope types extending EventEnvelope. + */ +export type DispatchPipelineBatch = Readonly>; + +/** + * Defines a processor that operates on a batch of event envelopes. + * Allows transformations, side-effects, or filtering of events during dispatch. + */ +export interface IDispatchPipelineProcessor { + process(batch: DispatchPipelineBatch): Promise>; + revert?(batch: DispatchPipelineBatch): Promise; +} + +export const isDispatchPipelineProcessor = (obj: unknown): obj is IDispatchPipelineProcessor => + isObject(obj) + && 'process' in obj + && typeof (obj as IDispatchPipelineProcessor).process === 'function'; diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts new file mode 100644 index 0000000..0b007c9 --- /dev/null +++ b/src/interfaces/IEvent.ts @@ -0,0 +1,14 @@ +import { IMessage } from './IMessage'; +import { isObject } from './isObject'; + +export type IEvent = IMessage & { + + /** Unique event identifier */ + id?: string; +}; + +export const isEvent = (event: unknown): event is IEvent => + isObject(event) + && 'type' in event + && typeof event.type === 'string' + && event.type.length > 0; diff --git a/src/interfaces/IEventBus.ts b/src/interfaces/IEventBus.ts new file mode 100644 index 0000000..0e37e07 --- /dev/null +++ b/src/interfaces/IEventBus.ts @@ -0,0 +1,11 @@ +import { IEvent } from './IEvent'; +import { IObservable, isIObservable } from './IObservable'; + +export interface IEventBus extends IObservable { + publish(event: IEvent, meta?: Record): Promise; +} + +export const isIEventBus = (obj: unknown) => + isIObservable(obj) + && 'publish' in obj + && typeof obj.publish === 'function'; diff --git a/src/interfaces/IEventDispatcher.ts b/src/interfaces/IEventDispatcher.ts new file mode 100644 index 0000000..60a1ce8 --- /dev/null +++ b/src/interfaces/IEventDispatcher.ts @@ -0,0 +1,7 @@ +import { IEventSet } from './IEventSet'; +import { IEventBus } from './IEventBus'; + +export interface IEventDispatcher { + readonly eventBus: IEventBus; + dispatch(events: IEventSet, meta?: Record): Promise; +} diff --git a/src/interfaces/IEventLocker.ts b/src/interfaces/IEventLocker.ts new file mode 100644 index 0000000..d3388b1 --- /dev/null +++ b/src/interfaces/IEventLocker.ts @@ -0,0 +1,34 @@ +import { IEvent } from './IEvent'; +import { isObject } from './isObject'; + +/** + * Interface for tracking event processing state to prevent concurrent processing + * by multiple processes. + */ +export interface IEventLocker { + + /** + * Retrieves the last projected event, + * allowing the projection state to be restored from subsequent events. + */ + getLastEvent(): Promise | IEvent | undefined; + + /** + * Marks an event as projecting to prevent it from being processed + * by another projection instance using the same storage. + * + * @returns `false` if the event is already being processed or has been processed. + */ + tryMarkAsProjecting(event: IEvent): Promise | boolean; + + /** + * Marks an event as projected. + */ + markAsProjected(event: IEvent): Promise | void; +} + +export const isEventLocker = (view: unknown): view is IEventLocker => + isObject(view) + && 'getLastEvent' in view + && 'tryMarkAsProjecting' in view + && 'markAsProjected' in view; diff --git a/src/interfaces/IEventReceptor.ts b/src/interfaces/IEventReceptor.ts new file mode 100644 index 0000000..722cbde --- /dev/null +++ b/src/interfaces/IEventReceptor.ts @@ -0,0 +1,6 @@ +import { IEventStore } from './IEventStore'; +import { IObserver } from './IObserver'; + +export interface IEventReceptor extends IObserver { + subscribe(eventStore: IEventStore): void; +} diff --git a/src/interfaces/IEventSet.ts b/src/interfaces/IEventSet.ts new file mode 100644 index 0000000..c06ac83 --- /dev/null +++ b/src/interfaces/IEventSet.ts @@ -0,0 +1,7 @@ +import { IEvent, isEvent } from './IEvent'; + +export type IEventSet = ReadonlyArray>; + +export const isEventSet = (arr: unknown): arr is IEventSet => + Array.isArray(arr) + && arr.every(isEvent); diff --git a/src/interfaces/IEventStorageReader.ts b/src/interfaces/IEventStorageReader.ts new file mode 100644 index 0000000..ba124b3 --- /dev/null +++ b/src/interfaces/IEventStorageReader.ts @@ -0,0 +1,44 @@ +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; +import { IEventStream } from './IEventStream'; +import { isObject } from './isObject'; + +export type EventQueryAfter = { + + /** Get events emitted after this specific event */ + afterEvent?: IEvent; +} + +export type EventQueryBefore = { + + /** Get events emitted before this specific event */ + beforeEvent?: IEvent; +} + +export interface IEventStorageReader { + + /** + * Retrieves events of specified types that were emitted after a given event. + */ + getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; + + /** + * Retrieves all events (and optionally a snapshot) associated with a specific aggregate. + */ + getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): IEventStream; + + /** + * Retrieves events associated with a saga, with optional filtering by version or timestamp. + */ + getSagaEvents(sagaId: Identifier, options: EventQueryBefore): IEventStream; +} + + +export const isIEventStorageReader = (storage: unknown): storage is IEventStorageReader => + isObject(storage) + && 'getEventsByTypes' in storage + && typeof storage.getEventsByTypes === 'function' + && 'getAggregateEvents' in storage + && typeof storage.getAggregateEvents === 'function' + && 'getSagaEvents' in storage + && typeof storage.getSagaEvents === 'function'; diff --git a/src/interfaces/IEventStorageWriter.ts b/src/interfaces/IEventStorageWriter.ts new file mode 100644 index 0000000..03521bc --- /dev/null +++ b/src/interfaces/IEventStorageWriter.ts @@ -0,0 +1,10 @@ +import { IEventSet } from './IEventSet'; + +export interface IEventStorageWriter { + + /** + * Persists a set of events to the event store. + * Returns the persisted event set (potentially enriched or normalized). + */ + commitEvents(events: IEventSet): Promise; +} diff --git a/src/interfaces/IEventStore.ts b/src/interfaces/IEventStore.ts new file mode 100644 index 0000000..0ded850 --- /dev/null +++ b/src/interfaces/IEventStore.ts @@ -0,0 +1,13 @@ +import { IEventDispatcher } from './IEventDispatcher'; +import { IEvent } from './IEvent'; +import { IEventStorageReader } from './IEventStorageReader'; +import { IIdentifierProvider } from './IIdentifierProvider'; +import { IMessageHandler, IObservable } from './IObservable'; + +export interface IEventStore + extends IObservable, IEventDispatcher, IEventStorageReader, IIdentifierProvider { + + registerSagaStarters(startsWith: string[] | undefined): void; + + once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise; +} diff --git a/src/interfaces/IEventStream.ts b/src/interfaces/IEventStream.ts new file mode 100644 index 0000000..1f11e35 --- /dev/null +++ b/src/interfaces/IEventStream.ts @@ -0,0 +1,3 @@ +import { IEvent } from './IEvent'; + +export type IEventStream = AsyncIterableIterator>; diff --git a/src/interfaces/IIdentifierProvider.ts b/src/interfaces/IIdentifierProvider.ts new file mode 100644 index 0000000..2c73090 --- /dev/null +++ b/src/interfaces/IIdentifierProvider.ts @@ -0,0 +1,17 @@ +import { Identifier } from './Identifier'; +import { isObject } from './isObject'; + +export interface IIdentifierProvider { + + /** + * Generates and returns a new unique identifier suitable for aggregates, sagas, and events. + * + * @returns A promise resolving to an identifier or an identifier itself. + */ + getNewId(): Identifier | Promise; +} + +export const isIdentifierProvider = (obj: any): obj is IIdentifierProvider => + isObject(obj) + && 'getNewId' in obj + && typeof obj.getNewId === 'function'; diff --git a/src/interfaces/ILogger.ts b/src/interfaces/ILogger.ts new file mode 100644 index 0000000..329e738 --- /dev/null +++ b/src/interfaces/ILogger.ts @@ -0,0 +1,11 @@ +export interface ILogger { + log(level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: { [key: string]: any }): void; + debug(message: string, meta?: { [key: string]: any }): void; + info(message: string, meta?: { [key: string]: any }): void; + warn(message: string, meta?: { [key: string]: any }): void; + error(message: string, meta?: { [key: string]: any }): void; +} + +export interface IExtendableLogger extends ILogger { + child(meta?: { [key: string]: any }): IExtendableLogger; +} diff --git a/src/interfaces/IMessage.ts b/src/interfaces/IMessage.ts new file mode 100644 index 0000000..40c78f0 --- /dev/null +++ b/src/interfaces/IMessage.ts @@ -0,0 +1,22 @@ +import { Identifier } from './Identifier'; +import { isObject } from './isObject'; + +export interface IMessage { + + /** Event or command type */ + type: string; + + aggregateId?: Identifier; + aggregateVersion?: number; + + sagaId?: Identifier; + sagaVersion?: number; + + payload?: TPayload; + context?: any; +} + +export const isMessage = (obj: unknown): obj is IMessage => + isObject(obj) + && 'type' in obj + && typeof obj.type === 'string'; diff --git a/src/interfaces/IMessageBus.ts b/src/interfaces/IMessageBus.ts new file mode 100644 index 0000000..5b626a3 --- /dev/null +++ b/src/interfaces/IMessageBus.ts @@ -0,0 +1,8 @@ +import { ICommand } from './ICommand'; +import { IEvent } from './IEvent'; +import { IObservable } from './IObservable'; + +export interface IMessageBus extends IObservable { + send(command: ICommand): Promise; + publish(event: IEvent, meta?: Record): Promise; +} diff --git a/src/interfaces/IObjectStorage.ts b/src/interfaces/IObjectStorage.ts new file mode 100644 index 0000000..e4b651a --- /dev/null +++ b/src/interfaces/IObjectStorage.ts @@ -0,0 +1,13 @@ +import { Identifier } from './Identifier'; + +export interface IObjectStorage { + get(id: Identifier): Promise | TRecord | undefined; + + create(id: Identifier, r: TRecord): Promise | any; + + update(id: Identifier, cb: (r: TRecord) => TRecord): Promise | any; + + updateEnforcingNew(id: Identifier, cb: (r?: TRecord) => TRecord): Promise | any; + + delete(id: Identifier): Promise | any; +} diff --git a/src/interfaces/IObservable.ts b/src/interfaces/IObservable.ts new file mode 100644 index 0000000..a04387a --- /dev/null +++ b/src/interfaces/IObservable.ts @@ -0,0 +1,31 @@ +import { IMessage } from './IMessage'; +import { isObject } from './isObject'; + +export interface IMessageHandler { + (message: IMessage, meta?: Record): any | Promise +} + +export interface IObservable { + + /** + * Setup a listener for a specific event type + */ + on(type: string, handler: IMessageHandler): void; + + /** + * Remove previously installed listener + */ + off(type: string, handler: IMessageHandler): void; + + /** + * Get or create a named queue, which delivers events to a single handler only + */ + queue?(name: string): IObservable; +} + +export const isIObservable = (obj: unknown): obj is IObservable => + isObject(obj) + && 'on' in obj + && typeof obj.on === 'function' + && 'off' in obj + && typeof obj.off === 'function'; diff --git a/src/interfaces/IObserver.ts b/src/interfaces/IObserver.ts new file mode 100644 index 0000000..d822cea --- /dev/null +++ b/src/interfaces/IObserver.ts @@ -0,0 +1,5 @@ +import { IObservable } from './IObservable'; + +export interface IObserver { + subscribe(observable: IObservable): void; +} diff --git a/src/interfaces/IProjection.ts b/src/interfaces/IProjection.ts new file mode 100644 index 0000000..4c260d7 --- /dev/null +++ b/src/interfaces/IProjection.ts @@ -0,0 +1,20 @@ +import { IEvent } from './IEvent'; +import { IEventStore } from './IEventStore'; +import { IObserver } from './IObserver'; + +export interface IProjection extends IObserver { + readonly view: TView; + + subscribe(eventStore: IEventStore): Promise; + + project(event: IEvent): Promise; +} + +export interface IProjectionConstructor { + new(c?: any): IProjection; + readonly handles?: string[]; +} + +export interface IViewFactory { + (options: { schemaVersion: string }): TView; +} diff --git a/src/interfaces/ISaga.ts b/src/interfaces/ISaga.ts new file mode 100644 index 0000000..f4920ba --- /dev/null +++ b/src/interfaces/ISaga.ts @@ -0,0 +1,38 @@ +import { ICommand } from './ICommand'; +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; +import { IEventSet } from './IEventSet'; + +export interface ISaga { + + /** Unique Saga ID */ + readonly id: Identifier; + + /** List of commands emitted by Saga */ + readonly uncommittedMessages: ICommand[]; + + /** Main entry point for Saga events */ + apply(event: IEvent): void | Promise; + + /** Reset emitted commands when they are not longer needed */ + resetUncommittedMessages(): void; + + onError?(error: Error, options: { event: IEvent, command: ICommand }): void; +} + +export type ISagaConstructorParams = { + id: Identifier, + events?: IEventSet +}; + +export type ISagaFactory = (options: ISagaConstructorParams) => ISaga; + +export interface ISagaConstructor { + new(options: ISagaConstructorParams): ISaga; + + /** List of event types that trigger new saga start */ + readonly startsWith: string[]; + + /** List of events being handled by Saga */ + readonly handles: string[]; +} diff --git a/src/interfaces/ISnapshotEvent.ts b/src/interfaces/ISnapshotEvent.ts new file mode 100644 index 0000000..81b61e6 --- /dev/null +++ b/src/interfaces/ISnapshotEvent.ts @@ -0,0 +1,10 @@ +import { IEvent, isEvent } from './IEvent'; + +export const SNAPSHOT_EVENT_TYPE: 'snapshot' = 'snapshot'; + +export interface ISnapshotEvent extends IEvent { + type: typeof SNAPSHOT_EVENT_TYPE +} + +export const isSnapshotEvent = (event?: unknown): event is ISnapshotEvent => + isEvent(event) && event.type === SNAPSHOT_EVENT_TYPE; diff --git a/src/interfaces/IViewLocker.ts b/src/interfaces/IViewLocker.ts new file mode 100644 index 0000000..fefcd2d --- /dev/null +++ b/src/interfaces/IViewLocker.ts @@ -0,0 +1,46 @@ +import { isObject } from './isObject'; + +/** + * Interface for managing view restoration state to prevent early access to an inconsistent view + * or concurrent restoration by another process. + */ +export interface IViewLocker { + + /** + * Indicates whether the view is fully restored and ready to accept new event projections. + */ + ready: boolean; + + /** + * Locks the view to prevent external read/write operations. + * + * @returns `true` if the lock is successfully acquired, `false` otherwise. + */ + lock(): Promise | boolean; + + /** + * Unlocks the view, allowing external read/write operations to resume. + */ + unlock(): Promise | void; + + /** + * Waits until the view is fully restored and ready to accept new events. + * + * @param eventType The event type to listen for (`"ready"`). + * @returns A promise that resolves when the view is ready. + */ + once(eventType: 'ready'): Promise; +} + +/** + * Checks if a given object conforms to the `IViewLocker` interface. + * + * @param view The object to check. + * @returns `true` if the object implements `IViewLocker`, `false` otherwise. + */ +export const isViewLocker = (view: unknown): view is IViewLocker => + isObject(view) + && 'ready' in view + && 'lock' in view + && 'unlock' in view + && 'once' in view; diff --git a/src/interfaces/Identifier.ts b/src/interfaces/Identifier.ts new file mode 100644 index 0000000..f31f1fb --- /dev/null +++ b/src/interfaces/Identifier.ts @@ -0,0 +1 @@ +export type Identifier = string | number; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..9b9539f --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,28 @@ +export * from './IAggregate'; +export * from './IAggregateSnapshotStorage'; +export * from './ICommand'; +export * from './ICommandBus'; +export * from './IContainer'; +export * from './Identifier'; +export * from './IDispatchPipelineProcessor'; +export * from './IEvent'; +export * from './IEventBus'; +export * from './IEventDispatcher'; +export * from './IEventLocker'; +export * from './IEventReceptor'; +export * from './IEventSet'; +export * from './IEventStorageReader'; +export * from './IEventStorageWriter'; +export * from './IEventStore'; +export * from './IEventStream'; +export * from './IIdentifierProvider'; +export * from './ILogger'; +export * from './IMessage'; +export * from './IMessageBus'; +export * from './IObjectStorage'; +export * from './IObservable'; +export * from './IObserver'; +export * from './IProjection'; +export * from './ISaga'; +export * from './ISnapshotEvent'; +export * from './IViewLocker'; diff --git a/src/interfaces/isObject.ts b/src/interfaces/isObject.ts new file mode 100644 index 0000000..8c26dac --- /dev/null +++ b/src/interfaces/isObject.ts @@ -0,0 +1,5 @@ +export const isObject = (obj: unknown): obj is {} => + typeof obj === 'object' + && obj !== null + && !(obj instanceof Date) + && !Array.isArray(obj); diff --git a/src/rabbitmq/IContainer.ts b/src/rabbitmq/IContainer.ts new file mode 100644 index 0000000..ceac5c4 --- /dev/null +++ b/src/rabbitmq/IContainer.ts @@ -0,0 +1,7 @@ +import { RabbitMqGateway } from './RabbitMqGateway'; + +declare module '../interfaces/IContainer' { + interface IContainer { + rabbitMqGateway?: RabbitMqGateway; + } +} diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts new file mode 100644 index 0000000..50e2e01 --- /dev/null +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -0,0 +1,83 @@ +import { IEvent, IEventBus, IMessageHandler, IObservable } from '../interfaces'; +import { RabbitMqGateway } from './RabbitMqGateway'; + +export class RabbitMqEventBus implements IEventBus { + + static get allEventsWildcard(): string { + return RabbitMqGateway.ALL_EVENTS_WILDCARD; + } + + static DEFAULT_EXCHANGE = 'node-cqrs.events'; + + #gateway: RabbitMqGateway; + #queues = new Map(); + #exchange: string; + #queueName: string | undefined; + + constructor(o: { + rabbitMqGateway: RabbitMqGateway, + exchange?: string, + queueName?: string + }) { + this.#gateway = o.rabbitMqGateway; + this.#exchange = o.exchange ?? RabbitMqEventBus.DEFAULT_EXCHANGE; + this.#queueName = o.queueName; + } + + + /** + * Publishes an event to the fanout exchange. + * The event will be delivered to all subscribers, except this instance's own consumer. + */ + async publish(event: IEvent): Promise { + await this.#gateway.publish(this.#exchange, event); + } + + /** + * Registers a message handler for a specific event type. + * + * @param eventType The event type to listen for. + * @param handler The function to handle incoming messages of the specified type. + */ + async on(eventType: string, handler: IMessageHandler): Promise { + await this.#gateway.subscribe({ + exchange: this.#exchange, + queueName: this.#queueName, + eventType, + handler, + ignoreOwn: !this.#queueName + }); + } + + /** + * Removes a previously registered message handler for a specific event type. + */ + async off(eventType: string, handler: IMessageHandler): Promise { + await this.#gateway.unsubscribe({ + exchange: this.#exchange, + queueName: this.#queueName, + eventType, + handler + }); + } + + /** + * Returns a new instance of RabbitMqGateway that uses a durable queue with the given name. + * This ensures that all messages published to the fanout exchange are also delivered to this queue. + * + * @param name The name of the durable queue. + * @returns A new RabbitMqGateway instance configured to use the specified queue. + */ + queue(name: string): IObservable { + let queue = this.#queues.get(name); + if (!queue) { + queue = new RabbitMqEventBus({ + rabbitMqGateway: this.#gateway, + exchange: this.#exchange, + queueName: name + }); + this.#queues.set(name, queue); + } + return queue; + } +} diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts new file mode 100644 index 0000000..78b49ce --- /dev/null +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -0,0 +1,683 @@ +import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; +import { IContainer, ILogger, IMessage, isMessage } from '../interfaces'; +import * as Event from '../Event'; +import { extractErrorDetails, Lock } from '../utils'; +import { registerExitCleanup } from './utils'; +import { EventEmitter } from 'events'; + +/** Generate a short pseudo-unique identifier using a truncated timestamp and random component */ +const getRandomAppId = () => + `${Date.now().toString(36).slice(-4)}.${Math.random().toString(36).slice(2, 6)}`.toUpperCase(); + +type MessageHandler = (m: IMessage) => Promise | unknown; + +/** + * Represents a subscription to events from a RabbitMQ exchange. + */ +type Subscription = { + + /** Name of the exchange to subscribe to */ + exchange: string; + + /** Optional durable queue name; if omitted, an exclusive temporary queue is used */ + queueName?: string; + + /** Specific event type (routing key) for filtering, defaults to all if omitted */ + eventType?: string; + + /** Callback function to process received messages */ + handler: MessageHandler; + + /** If true, messages originating from this instance are ignored */ + ignoreOwn?: boolean; + + /** Optional limit for concurrent message handling */ + concurrentLimit?: number; + + /** + * If true, the broker won't expect an acknowledgement of messages delivered to this consumer; + * i.e., it will dequeue messages as soon as they've been sent down the wire. + * + * Defaults to `false` - messages are acknowledged after successful handler completion or rejected on exception. + */ + noAck?: boolean; + + /** + * Handler timeout in milliseconds; if the handler does not complete within this time, + * the message is considered failed and is rejected. + * + * Defaults to 1h (`RabbitMqGateway.HANDLER_PROCESS_TIMEOUT`) + */ + handlerProcessTimeout?: number; +}; + +type EstablishedSubscription = Subscription & { + + /** + * Either a durable queue name or an autogenerated exclusive queue name. + * + * Stays empty until a queue is asserted and a subscription is set up successfully. + */ + queueGivenName?: string; +} + +const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); + +const describeSub = (s: EstablishedSubscription) => + `to${s.queueName ?? s.queueGivenName ? ` queue "${s.queueName ?? s.queueGivenName}" of` : ''} exchange "${s.exchange}"`; + +const extractMessageMeta = (msg: ConsumeMessage) => ({ + consumerTag: msg.fields?.consumerTag, + routingKey: msg.fields?.routingKey, + messageId: msg.properties?.messageId, + correlationId: msg.properties?.correlationId, + appId: msg.properties?.appId +}); + +interface RabbitMqGatewayConnected { + get connection(): ChannelModel; +} + +type GatewayEvents = { + connected: []; + disconnected: [reason?: string]; +}; + +/** + * RabbitMqGateway implements the IObservable interface using RabbitMQ. + * + * It uses a fanout exchange to broadcast messages to all connected subscribers. + * The `on` and `off` methods allow you to register and remove handlers for specific event types. + * The `queue(name)` method creates or returns a durable queue with the given name, ensuring that + * all messages delivered to the fanout exchange are also routed to this queue. + */ +export class RabbitMqGateway { + + static HANDLER_PROCESS_TIMEOUT = 60 * 60 * 1000; // 1 hour + static ALL_EVENTS_WILDCARD = '*'; + static RECONNECT_DELAY = 30_000; // 30 sec + + #connectionFactory: () => Promise; + #appId: string; + #logger: ILogger | undefined; + #emitter: EventEmitter; + + #desiredState: 'connected' | 'disconnected' = 'disconnected'; + #stateChangeLock = new Lock(); + + /** + * Established connection to RabbitMQ. + * If empty, the gateway is not connected. + */ + #connection: ChannelModel | undefined; + #pubChannel: ConfirmChannel | undefined; + #exclusiveQueueName: string | undefined; + #queueChannels = new Map(); + #queueConsumers = new Map(); + + #subscriptions: Array = []; + + get connection(): ChannelModel | undefined { + return this.#connection; + } + + isConnected(): this is this & RabbitMqGatewayConnected { + return !!this.#connection; + } + + constructor(o: Partial> & { + rabbitMqConnectionFactory?: () => Promise, + eventEmitterFactory?: () => EventEmitter + }) { + if (!o.rabbitMqConnectionFactory) + throw new TypeError('rabbitMqConnectionFactory argument required'); + + this.#emitter = o?.eventEmitterFactory?.() ?? new EventEmitter(); + this.#connectionFactory = o.rabbitMqConnectionFactory; + this.#appId = getRandomAppId(); + this.#logger = o.logger && 'child' in o.logger ? + o.logger.child({ service: new.target.name, appId: this.#appId }) : + o.logger; + + registerExitCleanup(o.process, () => this.disconnect()); + } + + /** + * Establishes a connection to RabbitMQ. + * If a connection attempt is already in progress, it waits for it to complete. + * If the connection is lost, it attempts to reconnect automatically. + * Upon successful connection, it restores any previously active subscriptions. + * + * This method is called automatically by other methods if a connection is required but not yet established. + * + * @returns A promise that resolves with the ChannelModel representing the established connection. + */ + async connect(): Promise { + this.#desiredState = 'connected'; + this.#clearReconnectTimer(); + + const lease = await this.#stateChangeLock.acquire(); + try { + if (this.#connection) { + this.#logger?.debug('Connection is already established'); + return this.#connection; + } + + return await this.#connect(); + } + finally { + lease.release(); + } + } + + async #connect(): Promise { + try { + const connection = await this.#connectionFactory(); + connection.on('error', err => this.#onConnectionError(err)); + connection.on('close', () => this.#onConnectionClosed(connection)); + + this.#connection = connection; + + this.#logger?.info('Connection established'); + + await this.#restoreSubscriptions(); + + this.#emitter.emit('connected'); + + return this.#connection; + } + catch (err: unknown) { + this.#logger?.error('Connection attempt failed', { + error: extractErrorDetails(err) + }); + + if (this.#desiredState === 'connected') + this.#scheduleReconnect(); + + throw err; + } + } + + #reconnectTimeout: NodeJS.Timeout | undefined; + + #clearReconnectTimer() { + if (!this.#reconnectTimeout) + return; + + clearTimeout(this.#reconnectTimeout); + this.#reconnectTimeout = undefined; + } + + #scheduleReconnect() { + if (this.#desiredState !== 'connected') + return; + + this.#clearReconnectTimer(); + + this.#logger?.debug(`Scheduling reconnect in ${RabbitMqGateway.RECONNECT_DELAY / 1000} seconds`); + this.#reconnectTimeout = setTimeout(() => this.#reconnect(), RabbitMqGateway.RECONNECT_DELAY).unref(); + } + + #reconnect() { + if (this.#desiredState !== 'connected' || this.isConnected()) + return; + + this.connect().catch(() => undefined); + } + + async #restoreSubscriptions() { + for (let i = 0; i < this.#subscriptions.length; i++) { + const subscriptionToRestore = this.#subscriptions.shift(); + if (!subscriptionToRestore) // should never happen; check is for type consistency + continue; + + await this.#subscribe(subscriptionToRestore); + } + + this.#logger?.debug(`${this.#subscriptions.length} subscription(s) restored`); + } + + async disconnect() { + this.#desiredState = 'disconnected'; + this.#clearReconnectTimer(); + + const lease = await this.#stateChangeLock.acquire(); + + try { + if (!this.#connection) { + this.#logger?.debug('Connection is not established'); + return; + } + + this.#logger?.debug('Disconnecting from RabbitMQ...'); + + await this.#stopConsuming(); + await this.#connection.close(); + + if (this.#connection) // clean up in case 'close' event was not triggered + this.#cleanup(); + + this.#logger?.debug('Disconnected from RabbitMQ'); + this.#emitter.emit('disconnected', 'Disconnected by request'); + } + catch (err: unknown) { + this.#logger?.error('Failed to disconnect from RabbitMQ', { + error: extractErrorDetails(err) + }); + } + finally { + lease.release(); + } + } + + async #stopConsuming() { + this.#logger?.debug('Stopping all consumers...'); + + const cancellations = [...this.#queueConsumers.entries()].map(async ([queueName, { channel, consumerTag }]) => { + this.#logger?.debug(`Cancelling consumer "${consumerTag}" for queue "${queueName}"`); + try { + await channel.cancel(consumerTag); + this.#logger?.debug(`Consumer "${consumerTag}" on queue "${queueName}" cancelled successfully`); + this.#queueConsumers.delete(queueName); + } + catch (err: unknown) { + this.#logger?.error(`Failed to cancel consumer "${consumerTag}" for queue "${queueName}"`, { + error: extractErrorDetails(err) + }); + } + }); + + await Promise.all(cancellations); + this.#logger?.info('All consumers stopped'); + } + + #onConnectionError(err: unknown) { + this.#logger?.error('Connection error', { + error: extractErrorDetails(err) + }); + } + + #onConnectionClosed(connection: ChannelModel) { + if (connection !== this.#connection) + return; + + this.#logger?.info('Connection closed'); + this.#cleanup(); + + if (this.#desiredState === 'connected') { + this.#emitter.emit('disconnected', 'Connection closed'); + this.#reconnect(); + } + } + + #cleanup() { + this.#connection = undefined; + this.#pubChannel = undefined; + this.#exclusiveQueueName = undefined; + this.#queueChannels.clear(); + this.#queueConsumers.clear(); + } + + #getHandlers(queueGivenName: string = '', eventType: string = RabbitMqGateway.ALL_EVENTS_WILDCARD) { + return this.#subscriptions.filter(s => + s.queueGivenName === queueGivenName + && ( + !s.eventType + || s.eventType === RabbitMqGateway.ALL_EVENTS_WILDCARD + || s.eventType === eventType + ) + ); + } + + async subscribeToQueue(exchange: string, queueName: string, handler: MessageHandler, + options?: Omit) { + return this.subscribe({ exchange, queueName, handler, ...options }); + } + + /** + * Subscribes to a non-durable, exclusive queue without requiring acknowledgments. + * The queue is deleted when the connection closes. + * Messages are considered "delivered" upon receipt. + * Failed message processing does not result in redelivery or dead-lettering. + */ + async subscribeToFanout(exchange: string, handler: MessageHandler, + options?: Omit) { + return this.subscribe({ exchange, handler, ignoreOwn: true, ...options }); + } + + /** + * Subscribes to events from a specified exchange. + * + * This method sets up the necessary RabbitMQ topology (exchange, queue, bindings) based on the provided details. + * If a `queueName` is provided, it asserts a durable queue with a dead-letter queue for failed messages. + * If `queueName` is omitted, it uses or creates a temporary, exclusive queue for the connection. + * Then it starts consuming messages from the queue with the specified concurrency limit, if specified. + * + * @param subscription - The subscription details. + * @param subscription.exchange - The name of the exchange to subscribe to. + * @param subscription.queueName - Optional. The name of the durable queue. If omitted, an exclusive queue is used. + * @param subscription.eventType - The routing key or pattern to bind the queue with. + * @param subscription.concurrentLimit - Optional. The maximum number of concurrent messages to process. + * @returns A promise that resolves when the subscription is successfully set up. + */ + async subscribe(subscription: Subscription) { + this.#desiredState = 'connected'; + this.#clearReconnectTimer(); + + const lease = await this.#stateChangeLock.acquire(); + try { + if (!this.#connection) + await this.#connect(); + + await this.#subscribe(subscription); + } + finally { + lease.release(); + } + } + + async #subscribe(subscription: Subscription) { + const subscriptionExists = !!this.#findSubscription(subscription); + if (subscriptionExists) + throw new Error(`Subscription ${describeSub(subscription)} already exists`); + + // record subscription details to restore it if connection attempt fails or on reconnect + this.#subscriptions.push(subscription); + this.#logger?.debug(`Subscription ${describeSub(subscription)} recorded`); + + const { + exchange, + queueName, + eventType + } = subscription; + + const channel = await this.#assertQueueChannel(queueName); + + let queueGivenName = queueName; + if (!queueGivenName) { + // Handle temporary (exclusive) queue case + if (!this.#exclusiveQueueName) { + // Assert temporary "exclusive" queue that will be destroyed on connection termination + this.#exclusiveQueueName = await this.#assetQueue(channel, exchange, '', eventType, { + exclusive: true, + durable: false + }); + } + else { + // If exclusive queue already exists, ensure it is bound with the current event type + await this.#assertBinding(channel, exchange, this.#exclusiveQueueName, eventType); + } + queueGivenName = this.#exclusiveQueueName; + } + else { + // Handle durable queue case + const deadLetterExchangeName = `${exchange}.failed`; + + // Assert dead letter queue for rejected or timed out messages + await this.#assetQueue(channel, deadLetterExchangeName, `${queueGivenName}.failed`); + + // Assert durable queue that will survive broker restart + await this.#assetQueue(channel, exchange, queueGivenName, eventType, { deadLetterExchangeName }); + } + + const subscriptionRecord = this.#findSubscription(subscription); + if (subscriptionRecord) + subscriptionRecord.queueGivenName = queueGivenName; + + await this.#assertConsumer(queueGivenName, channel, subscription); + + this.#logger?.debug(`Subscription ${describeSub(subscription)} established`); + } + + #findSubscription(subscription: Pick) { + return this.#subscriptions.find(s => + s.exchange === subscription.exchange && + s.queueName === subscription.queueName && + s.eventType === subscription.eventType && + s.handler === subscription.handler); + } + + async unsubscribe(subscription: Pick) { + const subscriptionToRemove = this.#findSubscription(subscription); + if (!subscriptionToRemove) + throw new Error('Such subscription does not exist'); + + this.#subscriptions = this.#subscriptions.filter(s => s !== subscriptionToRemove); + + await this.#tryDropConsumer(subscriptionToRemove); + + this.#logger?.debug(`Subscription ${describeSub(subscriptionToRemove)} removed`); + } + + async #assertConnection() { + return this.#connection ?? this.connect(); + } + + /** Get existing or open a new channel for a given queue name */ + async #assertQueueChannel(queueName: string = ''): Promise { + let channel = this.#queueChannels.get(queueName); + if (!channel) { + if (!this.#connection) + throw new Error('No connection established to create channel'); + + channel = await this.#connection.createChannel(); + this.#queueChannels.set(queueName, channel); + } + return channel; + } + + /** + * Ensure queue, exchange, and binding exist + */ + async #assetQueue(channel: Channel, exchange: string, queueName: string, eventType?: string, options?: { + + /** The queue will survive a broker restart */ + durable?: boolean, + + /** Used by only one connection and the queue will be deleted when that connection closes */ + exclusive?: boolean, + + /** Exchange where rejected or timed out messages will be delivered */ + deadLetterExchangeName?: string, + }) { + const { + durable = true, + exclusive = false, + deadLetterExchangeName + } = options ?? {}; + + await channel.assertExchange(exchange, 'topic', { durable: true }); + const { queue: queueGivenName } = await channel.assertQueue(queueName, { + exclusive, + durable, + arguments: { + ...deadLetterExchangeName && { + 'x-dead-letter-exchange': deadLetterExchangeName + }, + ...durable && { + // Use quorum queues (Raft-replicated, HA alternative to classic queues) for durable workloads + 'x-queue-type': 'quorum' + } + } + }); + + await this.#assertBinding(channel, exchange, queueGivenName, eventType); + + return queueGivenName; + } + + async #assertBinding(channel: Channel, exchange: string, queueGivenName: string, eventType?: string) { + if (!eventType || eventType === RabbitMqGateway.ALL_EVENTS_WILDCARD) + eventType = '#'; + + await channel.bindQueue(queueGivenName, exchange, eventType); + + this.#logger?.debug(`Queue "${queueGivenName}" bound to exchange "${exchange}" with pattern "${eventType}"`); + } + + async #assertConsumer( + queueGivenName: string, + channel: Channel, + options?: Pick + ) { + if (this.#queueConsumers.has(queueGivenName)) + return; + + if (options?.concurrentLimit !== undefined) + await channel.prefetch(options.concurrentLimit); + + const c = await channel.consume(queueGivenName, async (msg: ConsumeMessage | null) => { + if (!msg) + return; + + // Keep the process alive while waiting for the handler to finish + const handlerProcessTimeout = options?.handlerProcessTimeout ?? RabbitMqGateway.HANDLER_PROCESS_TIMEOUT; + const keepAliveTimeout = setTimeout(() => { + this.#logger?.error('Message processing timed out', { + queueName: queueGivenName, + msg: extractMessageMeta(msg) + }); + this.#rejectMessage(channel, msg); + }, handlerProcessTimeout); + + try { + this.#logger?.debug('Message received', { + queueName: queueGivenName, + msg: extractMessageMeta(msg) + }); + + const jsonContent = msg.content.toString(); + const message: IMessage = JSON.parse(jsonContent); + + const handlers = this.#getHandlers(queueGivenName, message.type); + if (!handlers.length && !isSystemQueue(queueGivenName)) + throw new Error(`Message from queue "${queueGivenName}" was delivered to a consumer that does not handle type "${message.type}"`); + + for (const { handler, ignoreOwn } of handlers) { + if (ignoreOwn && msg.properties.appId === this.#appId) + continue; + + await handler(message); + } + + channel.ack(msg); + } + catch (err: unknown) { + this.#logger?.error('Message processing failed', { + error: extractErrorDetails(err), + queueName: queueGivenName, + msg: extractMessageMeta(msg) + }); + + this.#rejectMessage(channel, msg); + } + finally { + clearTimeout(keepAliveTimeout); + } + }, { + noAck: options?.noAck + }); + + this.#logger?.debug(`Consumer "${c.consumerTag}" registered on queue "${queueGivenName}"`); + + this.#queueConsumers.set(queueGivenName, { + channel, + consumerTag: c.consumerTag + }); + } + + /** Reject a message, causing it to be dead-lettered or discarded */ + #rejectMessage(channel: Channel, msg: ConsumeMessage) { + try { + channel.nack(msg, false, false); + } + catch (err: unknown) { + this.#logger?.warn('Failed to reject message', { + error: extractErrorDetails(err), + msg: extractMessageMeta(msg) + }); + } + } + + async #tryDropConsumer(subscription: EstablishedSubscription) { + if (!subscription.queueGivenName) { + this.#logger?.warn(`Subscription ${describeSub(subscription)} is not bound to a queue`); + return; + } + const queueStillUsed = this.#subscriptions.some(s => s.queueGivenName === subscription.queueGivenName); + if (queueStillUsed) { + this.#logger?.warn(`Queue "${subscription.queueGivenName}" has other consumers in this process`); + return; + } + + const consumer = this.#queueConsumers.get(subscription.queueGivenName); + if (!consumer) { + this.#logger?.warn(`Queue "${subscription.queueGivenName}" does not have consumers`); + return; + } + + this.#queueConsumers.delete(subscription.queueGivenName); + await consumer.channel.cancel(consumer.consumerTag); + + this.#logger?.info(`Consumer "${consumer.consumerTag}" removed from subscription ${describeSub(subscription)}`); + } + + /** + * Publishes an event to the fanout exchange. + * The event will be delivered to all subscribers, except this instance's own consumer. + */ + async publish(exchange: string, message: IMessage): Promise { + if (typeof exchange !== 'string' || !exchange.length) + throw new TypeError('exchange argument must be a non-empty String'); + if (!isMessage(message)) + throw new TypeError('valid message argument is required'); + + if (!this.#pubChannel) { + const connection = await this.#assertConnection(); + this.#pubChannel = await connection.createConfirmChannel(); + + await this.#pubChannel.assertExchange(exchange, 'topic', { durable: true }); + } + + const content = Buffer.from(JSON.stringify(message), 'utf8'); + const properties = { + contentType: 'application/json', + contentEncoding: 'utf8', + persistent: true, + timestamp: message.context?.ts ?? Date.now(), + appId: this.#appId, + type: message.type, + messageId: 'id' in message && typeof message.id === 'string' ? + message.id : + undefined, + correlationId: message.sagaId?.toString() + }; + + return new Promise((resolve, reject) => { + if (!this.#pubChannel) + throw new Error('No channel available for publishing'); + + this.#logger?.debug(`Publishing message "${Event.describe(message)}" to exchange "${exchange}"`); + + const published = this.#pubChannel.publish(exchange, message.type, content, properties, err => + (err ? reject(err) : resolve())); + if (!published) + throw new Error(`Failed to send event ${Event.describe(message)}, channel buffer is full`); + }); + } + + on(event: K, fn: (...args: GatewayEvents[K]) => void) { + this.#emitter.on(event, fn as any); + return this; + } + + once(event: K, fn: (...args: GatewayEvents[K]) => void) { + this.#emitter.once(event, fn as any); + return this; + } + + off(event: K, fn: (...args: GatewayEvents[K]) => void) { + this.#emitter.off(event, fn as any); + return this; + } +} diff --git a/src/rabbitmq/index.ts b/src/rabbitmq/index.ts new file mode 100644 index 0000000..79404df --- /dev/null +++ b/src/rabbitmq/index.ts @@ -0,0 +1,2 @@ +export * from './RabbitMqEventBus'; +export * from './RabbitMqGateway'; diff --git a/src/rabbitmq/utils/index.ts b/src/rabbitmq/utils/index.ts new file mode 100644 index 0000000..807ea15 --- /dev/null +++ b/src/rabbitmq/utils/index.ts @@ -0,0 +1 @@ +export * from './registerExitCleanup'; diff --git a/src/rabbitmq/utils/registerExitCleanup.ts b/src/rabbitmq/utils/registerExitCleanup.ts new file mode 100644 index 0000000..7f3982d --- /dev/null +++ b/src/rabbitmq/utils/registerExitCleanup.ts @@ -0,0 +1,29 @@ +/** + * Registers cleanup handlers for SIGINT and SIGTERM signals on a Node.js process. + * Executes the provided cleanup procedure when one of these signals is received, + * then removes the listeners to allow the process to exit gracefully. + * + * @returns An object with a `dispose` method to manually remove the registered signal handlers. + */ +export const registerExitCleanup = ( + process: NodeJS.Process | undefined, + cleanupProcedure: () => Promise | unknown +) => { + const handler = async () => { + // remove listeners to allow the process to exit + process?.off('SIGINT', handler); + process?.off('SIGTERM', handler); + + await cleanupProcedure(); + }; + + process?.once('SIGINT', handler); + process?.once('SIGTERM', handler); + + return { + dispose: () => { + process?.off('SIGINT', handler); + process?.off('SIGTERM', handler); + } + }; +}; diff --git a/src/sqlite/AbstractSqliteAccessor.ts b/src/sqlite/AbstractSqliteAccessor.ts new file mode 100644 index 0000000..27d3c08 --- /dev/null +++ b/src/sqlite/AbstractSqliteAccessor.ts @@ -0,0 +1,58 @@ +import { IContainer } from '../interfaces'; +import { Lock } from '../utils'; +import { Database } from 'better-sqlite3'; + +/** + * Abstract base class for accessing a SQLite database. + * + * Manages the database connection lifecycle, ensuring initialization via `assertDb`. + * Supports providing a database instance directly or a factory function for lazy initialization. + * + * Subclasses must implement the `initialize` method for specific setup tasks. + */ +export abstract class AbstractSqliteAccessor { + + protected db: Database | undefined; + #dbFactory: (() => Promise | Database) | undefined; + #initLocker = new Lock(); + #initialized = false; + + constructor(c: Partial>) { + if (!c.viewModelSqliteDb && !c.viewModelSqliteDbFactory) + throw new TypeError('either viewModelSqliteDb or viewModelSqliteDbFactory argument required'); + + this.db = c.viewModelSqliteDb; + this.#dbFactory = c.viewModelSqliteDbFactory; + } + + protected abstract initialize(db: Database): Promise | void; + + /** + * Ensures that the database connection is initialized. + * Uses a lock to prevent race conditions during concurrent initialization attempts. + * If the database is not already initialized, it creates the database connection + * using the provided factory and calls the `initialize` method. + * + * This method is idempotent and safe to call multiple times. + */ + async assertConnection() { + if (this.#initialized) + return; + + try { + await this.#initLocker.acquire(); + if (this.#initialized) + return; + + if (!this.db) + this.db = await this.#dbFactory!(); + + await this.initialize(this.db); + + this.#initialized = true; + } + finally { + this.#initLocker.release(); + } + } +} diff --git a/src/sqlite/AbstractSqliteObjectProjection.ts b/src/sqlite/AbstractSqliteObjectProjection.ts new file mode 100644 index 0000000..acc3c2b --- /dev/null +++ b/src/sqlite/AbstractSqliteObjectProjection.ts @@ -0,0 +1,31 @@ +import { AbstractProjection } from '../AbstractProjection'; +import { IContainer } from '../interfaces'; +import { SqliteObjectView } from './SqliteObjectView'; + +export abstract class AbstractSqliteObjectProjection extends AbstractProjection> { + + static get tableName(): string { + throw new Error('tableName is not defined'); + } + + static get schemaVersion(): string { + throw new Error('schemaVersion is not defined'); + } + + constructor({ viewModelSqliteDb, viewModelSqliteDbFactory, logger }: Pick) { + super({ logger }); + + this.view = new SqliteObjectView({ + schemaVersion: new.target.schemaVersion, + projectionName: new.target.name, + viewModelSqliteDb, + viewModelSqliteDbFactory, + tableNamePrefix: new.target.tableName, + logger + }); + } +} diff --git a/src/sqlite/AbstractSqliteView.ts b/src/sqlite/AbstractSqliteView.ts new file mode 100644 index 0000000..52aa020 --- /dev/null +++ b/src/sqlite/AbstractSqliteView.ts @@ -0,0 +1,53 @@ +import { IContainer, IEvent, IEventLocker, ILogger, IViewLocker } from '../interfaces'; +import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker'; +import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; + +export abstract class AbstractSqliteView extends AbstractSqliteAccessor implements IViewLocker, IEventLocker { + + protected readonly schemaVersion: string; + protected readonly viewLocker: SqliteViewLocker; + protected readonly eventLocker: SqliteEventLocker; + protected logger: ILogger | undefined; + + get ready(): boolean { + return this.viewLocker.ready; + } + + constructor(options: Partial> + & SqliteEventLockerParams + & SqliteViewLockerParams) { + super(options); + + this.schemaVersion = options.schemaVersion; + this.viewLocker = new SqliteViewLocker(options); + this.eventLocker = new SqliteEventLocker(options); + this.logger = options.logger && 'child' in options.logger ? + options.logger.child({ serviceName: new.target.name }) : + options.logger; + } + + async lock() { + return this.viewLocker.lock(); + } + + unlock(): void { + this.viewLocker.unlock(); + } + + once(event: 'ready') { + return this.viewLocker.once(event); + } + + getLastEvent() { + return this.eventLocker.getLastEvent(); + } + + tryMarkAsProjecting(event: IEvent) { + return this.eventLocker.tryMarkAsProjecting(event); + } + + markAsProjected(event: IEvent) { + return this.eventLocker.markAsProjected(event); + } +} diff --git a/src/sqlite/IContainer.ts b/src/sqlite/IContainer.ts new file mode 100644 index 0000000..f24f6d6 --- /dev/null +++ b/src/sqlite/IContainer.ts @@ -0,0 +1,8 @@ +import { Database } from 'better-sqlite3'; + +declare module '../interfaces/IContainer' { + interface IContainer { + viewModelSqliteDbFactory?: () => Promise | Database; + viewModelSqliteDb?: Database; + } +} diff --git a/src/sqlite/SqliteEventLocker.ts b/src/sqlite/SqliteEventLocker.ts new file mode 100644 index 0000000..316f028 --- /dev/null +++ b/src/sqlite/SqliteEventLocker.ts @@ -0,0 +1,137 @@ +import { Database, Statement } from 'better-sqlite3'; +import { IContainer, IEvent, IEventLocker } from '../interfaces'; +import { getEventId } from './utils'; +import { viewLockTableInit, eventLockTableInit } from './queries'; +import { SqliteViewLockerParams } from './SqliteViewLocker'; +import { SqliteProjectionDataParams } from './SqliteProjectionDataParams'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; + +export type SqliteEventLockerParams = + SqliteProjectionDataParams + & Pick + & { + + /** + * (Optional) SQLite table name where event locks are stored + * + * @default "tbl_event_lock" + */ + eventLockTableName?: string; + + /** + * (Optional) Time-to-live (TTL) duration in milliseconds + * for which an event remains in the "processing" state until released. + * + * @default 15_000 + */ + eventLockTtl?: number; + }; + +export class SqliteEventLocker extends AbstractSqliteAccessor implements IEventLocker { + + #projectionName: string; + #schemaVersion: string; + #viewLockTableName: string; + #eventLockTableName: string; + #eventLockTtl: number; + + #upsertLastEventQuery!: Statement<[string, string, string], void>; + #getLastEventQuery!: Statement<[string, string], { last_event: string }>; + #lockEventQuery!: Statement<[string, string, Buffer], void>; + #finalizeEventLockQuery!: Statement<[string, string, Buffer], void>; + + constructor(o: Pick & SqliteEventLockerParams) { + super(o); + + if (!o.projectionName) + throw new TypeError('projectionName argument required'); + if (!o.schemaVersion) + throw new TypeError('schemaVersion argument required'); + + this.#projectionName = o.projectionName; + this.#schemaVersion = o.schemaVersion; + this.#viewLockTableName = o.viewLockTableName ?? 'tbl_view_lock'; + this.#eventLockTableName = o.eventLockTableName ?? 'tbl_event_lock'; + this.#eventLockTtl = o.eventLockTtl ?? 15_000; + } + + protected initialize(db: Database) { + db.exec(viewLockTableInit(this.#viewLockTableName)); + db.exec(eventLockTableInit(this.#eventLockTableName)); + + this.#upsertLastEventQuery = db.prepare(` + INSERT INTO ${this.#viewLockTableName} (projection_name, schema_version, last_event) + VALUES (?, ?, ?) + ON CONFLICT (projection_name, schema_version) + DO UPDATE SET + last_event = excluded.last_event + `); + + this.#getLastEventQuery = db.prepare(` + SELECT + last_event + FROM ${this.#viewLockTableName} + WHERE + projection_name = ? + AND schema_version =? + `); + + this.#lockEventQuery = db.prepare(` + INSERT INTO ${this.#eventLockTableName} (projection_name, schema_version, event_id) + VALUES (?, ?, ?) + ON CONFLICT (projection_name, schema_version, event_id) + DO UPDATE SET + processing_at = cast(unixepoch('subsec') * 1000 as INTEGER) + WHERE + processed_at IS NULL + AND processing_at <= cast(unixepoch('subsec') * 1000 as INTEGER) - ${this.#eventLockTtl} + `); + + this.#finalizeEventLockQuery = db.prepare(` + UPDATE ${this.#eventLockTableName} + SET + processed_at = cast(unixepoch('subsec') * 1000 as INTEGER) + WHERE + projection_name = ? + AND schema_version = ? + AND event_id = ? + AND processed_at IS NULL + `); + } + + async tryMarkAsProjecting(event: IEvent) { + await this.assertConnection(); + + const eventId = getEventId(event); + + const r = this.#lockEventQuery.run(this.#projectionName, this.#schemaVersion, eventId); + + return r.changes !== 0; + } + + async markAsProjected(event: IEvent) { + await this.assertConnection(); + + const eventId = getEventId(event); + + const transaction = this.db!.transaction(() => { + const updateResult = this.#finalizeEventLockQuery.run(this.#projectionName, this.#schemaVersion, eventId); + if (updateResult.changes === 0) + throw new Error(`Event ${event.id} could not be marked as processed`); + + this.#upsertLastEventQuery.run(this.#projectionName, this.#schemaVersion, JSON.stringify(event)); + }); + + transaction(); + } + + async getLastEvent(): Promise | undefined> { + await this.assertConnection(); + + const viewInfoRecord = this.#getLastEventQuery.get(this.#projectionName, this.#schemaVersion); + if (!viewInfoRecord?.last_event) + return undefined; + + return JSON.parse(viewInfoRecord.last_event); + } +} diff --git a/src/sqlite/SqliteObjectStorage.ts b/src/sqlite/SqliteObjectStorage.ts new file mode 100644 index 0000000..8f16142 --- /dev/null +++ b/src/sqlite/SqliteObjectStorage.ts @@ -0,0 +1,153 @@ +import { Statement, Database } from 'better-sqlite3'; +import { guid } from './utils'; +import { IContainer, IObjectStorage } from '../interfaces'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; + +export class SqliteObjectStorage extends AbstractSqliteAccessor implements IObjectStorage { + + #tableName: string; + #getQuery!: Statement<[Buffer], { data: string, version: number }>; + #insertQuery!: Statement<[Buffer, string], void>; + #updateByIdAndVersionQuery!: Statement<[string, Buffer, number], void>; + #deleteQuery!: Statement<[Buffer], void>; + + constructor(o: Pick & { + tableName: string + }) { + super(o); + + this.#tableName = o.tableName; + } + + protected initialize(db: Database) { + db.exec(`CREATE TABLE IF NOT EXISTS ${this.#tableName} ( + id BLOB PRIMARY KEY, + version INTEGER DEFAULT 1, + data TEXT NOT NULL + );`); + + this.#getQuery = db.prepare(` + SELECT data, version + FROM ${this.#tableName} + WHERE id = ? + `); + + this.#insertQuery = db.prepare(` + INSERT INTO ${this.#tableName} (id, data) + VALUES (?, ?) + `); + + this.#updateByIdAndVersionQuery = db.prepare(` + UPDATE ${this.#tableName} + SET + data = ?, + version = version + 1 + WHERE + id = ? + AND version = ? + `); + + this.#deleteQuery = db.prepare(` + DELETE FROM ${this.#tableName} + WHERE id = ? + `); + } + + async get(id: string): Promise { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + await this.assertConnection(); + + const r = this.#getQuery.get(guid(id)); + if (!r) + return undefined; + + return JSON.parse(r.data); + } + + getSync(id: string): TRecord | undefined { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + const r = this.#getQuery.get(guid(id)); + if (!r) + return undefined; + + return JSON.parse(r.data); + } + + async create(id: string, data: TRecord) { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + await this.assertConnection(); + + this.#createSync(id, data); + } + + #createSync(id: string, data: TRecord) { + const r = this.#insertQuery.run(guid(id), JSON.stringify(data)); + if (r.changes !== 1) + throw new Error(`Record '${id}' could not be created`); + } + + async update(id: string, update: (r: TRecord) => TRecord) { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + if (typeof update !== 'function') + throw new TypeError('update argument must be a Function'); + + await this.assertConnection(); + + this.#updateSync(id, update); + } + + #updateSync(id: string, update: (r: TRecord) => TRecord) { + const gid = guid(id); + const record = this.#getQuery.get(gid); + if (!record) + throw new Error(`Record '${id}' does not exist`); + + this.#updateExistingSync(id, record, update); + } + + #updateExistingSync(id: string, record: { data: string, version: number }, update: (r: TRecord) => TRecord) { + const gid = guid(id); + const data = JSON.parse(record.data); + const updatedData = update(data); + const updatedJson = JSON.stringify(updatedData); + + // Version check is implemented to ensure the record isn't modified by another process. + // A conflict resolution strategy could potentially be passed as an option to this method, + // but for now, conflict resolution should happen outside this class. + const r = this.#updateByIdAndVersionQuery.run(updatedJson, gid, record.version); + if (r.changes !== 1) + throw new Error(`Record '${id}' could not be updated`); + } + + async updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + if (typeof update !== 'function') + throw new TypeError('update argument must be a Function'); + + await this.assertConnection(); + + const record = this.#getQuery.get(guid(id)); + if (record) + this.#updateExistingSync(id, record, update as (r: TRecord) => TRecord); + else + this.#createSync(id, update()); + } + + async delete(id: string): Promise { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + await this.assertConnection(); + + const r = this.#deleteQuery.run(guid(id)); + return r.changes === 1; + } +} diff --git a/src/sqlite/SqliteObjectView.ts b/src/sqlite/SqliteObjectView.ts new file mode 100644 index 0000000..ac99aed --- /dev/null +++ b/src/sqlite/SqliteObjectView.ts @@ -0,0 +1,58 @@ +import { AbstractSqliteView } from './AbstractSqliteView'; +import { IObjectStorage, IEventLocker } from '../interfaces'; +import { SqliteObjectStorage } from './SqliteObjectStorage'; +import { Database } from 'better-sqlite3'; + +export class SqliteObjectView extends AbstractSqliteView implements IObjectStorage, IEventLocker { + + #sqliteObjectStorage: SqliteObjectStorage; + + constructor(options: ConstructorParameters[0] & { + tableNamePrefix: string + }) { + if (typeof options.tableNamePrefix !== 'string' || !options.tableNamePrefix.length) + throw new TypeError('tableNamePrefix argument must be a non-empty String'); + if (typeof options.schemaVersion !== 'string' || !options.schemaVersion.length) + throw new TypeError('schemaVersion argument must be a non-empty String'); + + super(options); + + this.#sqliteObjectStorage = new SqliteObjectStorage({ + viewModelSqliteDb: options.viewModelSqliteDb, + viewModelSqliteDbFactory: options.viewModelSqliteDbFactory, + tableName: `${options.tableNamePrefix}_${options.schemaVersion}` + }); + } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + protected initialize(db: Database): Promise | void { + // No need to initialize the table here, it's done in SqliteObjectStorage + } + + async get(id: string): Promise { + if (!this.ready) + await this.once('ready'); + + return this.#sqliteObjectStorage.get(id); + } + + getSync(id: string) { + return this.#sqliteObjectStorage.getSync(id); + } + + async create(id: string, data: TRecord) { + await this.#sqliteObjectStorage.create(id, data); + } + + async update(id: string, update: (r: TRecord) => TRecord) { + await this.#sqliteObjectStorage.update(id, update); + } + + async updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + await this.#sqliteObjectStorage.updateEnforcingNew(id, update); + } + + async delete(id: string): Promise { + return this.#sqliteObjectStorage.delete(id); + } +} diff --git a/src/sqlite/SqliteProjectionDataParams.ts b/src/sqlite/SqliteProjectionDataParams.ts new file mode 100644 index 0000000..24c721e --- /dev/null +++ b/src/sqlite/SqliteProjectionDataParams.ts @@ -0,0 +1,16 @@ +export type SqliteProjectionDataParams = { + + /** + * Unique identifier for the projection, used with the schema version to distinguish data ownership. + */ + projectionName: string; + + /** + * The version of the schema used for data produced by the projection. + * When the projection's output format changes, this version should be incremented. + * A version change indicates that previously stored data is obsolete and must be rebuilt. + * + * @example "20250519", "1.0.0" + */ + schemaVersion: string; +} diff --git a/src/sqlite/SqliteViewLocker.ts b/src/sqlite/SqliteViewLocker.ts new file mode 100644 index 0000000..b5f8e49 --- /dev/null +++ b/src/sqlite/SqliteViewLocker.ts @@ -0,0 +1,171 @@ +import { Database, Statement } from 'better-sqlite3'; +import { IContainer, ILogger, IViewLocker } from '../interfaces'; +import { Deferred } from '../utils'; +import { promisify } from 'util'; +import { viewLockTableInit } from './queries'; +import { SqliteProjectionDataParams } from './SqliteProjectionDataParams'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; +const delay = promisify(setTimeout); + +export type SqliteViewLockerParams = SqliteProjectionDataParams & { + + /** + * (Optional) SQLite table name where event locks along with the latest event are stored + * + * @default "tbl_view_lock" + */ + viewLockTableName?: string; + + /** + * (Optional) Time-to-live (TTL) duration (in milliseconds) for which a view remains locked + * + * @default 120_000 + */ + viewLockTtl?: number; +}; + +export class SqliteViewLocker extends AbstractSqliteAccessor implements IViewLocker { + + #projectionName: string; + #schemaVersion: string; + + #viewLockTableName: string; + #viewLockTtl: number; + #logger: ILogger | undefined; + + #upsertTableLockQuery!: Statement<[string, string, number], void>; + #updateTableLockQuery!: Statement<[number, string, string], void>; + #removeTableLockQuery!: Statement<[string, string], void>; + + #lockMarker: Deferred | undefined; + #lockProlongationTimeout: NodeJS.Timeout | undefined; + + constructor(o: Partial> + & SqliteViewLockerParams) { + super(o); + + if (!o.projectionName) + throw new TypeError('projectionName argument required'); + if (!o.schemaVersion) + throw new TypeError('schemaVersion argument required'); + + this.#projectionName = o.projectionName; + this.#schemaVersion = o.schemaVersion; + + this.#viewLockTableName = o.viewLockTableName ?? 'tbl_view_lock'; + this.#viewLockTtl = o.viewLockTtl ?? 120_000; + this.#logger = o.logger && 'child' in o.logger ? + o.logger.child({ service: this.constructor.name }) : + o.logger; + } + + protected initialize(db: Database) { + db.exec(viewLockTableInit(this.#viewLockTableName)); + + this.#upsertTableLockQuery = db.prepare(` + INSERT INTO ${this.#viewLockTableName} (projection_name, schema_version, locked_till) + VALUES (?, ?, ?) + ON CONFLICT (projection_name, schema_version) + DO UPDATE SET + locked_till = excluded.locked_till + WHERE + locked_till IS NULL + OR locked_till < excluded.locked_till + `); + + this.#updateTableLockQuery = db.prepare(` + UPDATE ${this.#viewLockTableName} + SET + locked_till = ? + WHERE + projection_name = ? + AND schema_version = ? + AND locked_till IS NOT NULL + `); + + this.#removeTableLockQuery = db.prepare(` + UPDATE ${this.#viewLockTableName} + SET + locked_till = NULL + WHERE + projection_name = ? + AND schema_version = ? + AND locked_till IS NOT NULL + `); + } + + get ready(): boolean { + return !this.#lockMarker; + } + + async lock() { + this.#lockMarker = new Deferred(); + + await this.assertConnection(); + + let lockAcquired = false; + while (!lockAcquired) { + const lockedTill = Date.now() + this.#viewLockTtl; + const upsertResult = this.#upsertTableLockQuery.run(this.#projectionName, this.#schemaVersion, lockedTill); + + lockAcquired = upsertResult.changes === 1; + if (!lockAcquired) { + this.#logger?.debug(`"${this.#projectionName}" is locked by another process`); + await delay(this.#viewLockTtl / 2); + } + } + + this.#logger?.debug(`"${this.#projectionName}" lock obtained for ${this.#viewLockTtl}s`); + + this.scheduleLockProlongation(); + + return true; + } + + private scheduleLockProlongation() { + const ms = this.#viewLockTtl / 2; + + this.#lockProlongationTimeout = setTimeout(() => this.prolongLock(), ms); + this.#lockProlongationTimeout.unref(); + + this.#logger?.debug(`"${this.#projectionName}" lock refresh scheduled in ${ms} ms`); + } + + private cancelLockProlongation() { + clearTimeout(this.#lockProlongationTimeout); + this.#logger?.debug(`"${this.#projectionName}" lock refresh canceled`); + } + + private async prolongLock() { + await this.assertConnection(); + + const lockedTill = Date.now() + this.#viewLockTtl; + const r = this.#updateTableLockQuery.run(lockedTill, this.#projectionName, this.#schemaVersion); + if (r.changes !== 1) + throw new Error(`"${this.#projectionName}" lock could not be prolonged`); + + this.#logger?.debug(`"${this.#projectionName}" lock prolonged for ${this.#viewLockTtl}s`); + } + + async unlock() { + this.#lockMarker?.resolve(); + this.#lockMarker = undefined; + + this.cancelLockProlongation(); + + await this.assertConnection(); + + const updateResult = this.#removeTableLockQuery.run(this.#projectionName, this.#schemaVersion); + if (updateResult.changes === 1) + this.#logger?.debug(`"${this.#projectionName}" lock released`); + else + this.#logger?.warn(`"${this.#projectionName}" lock didn't exist`); + } + + once(event: 'ready'): Promise { + if (event !== 'ready') + throw new TypeError(`Unexpected event: ${event}`); + + return this.#lockMarker?.promise ?? Promise.resolve(); + } +} diff --git a/src/sqlite/index.ts b/src/sqlite/index.ts new file mode 100644 index 0000000..068463a --- /dev/null +++ b/src/sqlite/index.ts @@ -0,0 +1,8 @@ +export * from './AbstractSqliteAccessor'; +export * from './AbstractSqliteObjectProjection'; +export * from './AbstractSqliteView'; +export * from './SqliteEventLocker'; +export * from './SqliteObjectStorage'; +export * from './SqliteObjectView'; +export * from './SqliteViewLocker'; +export * from './utils'; diff --git a/src/sqlite/queries/eventLockTableInit.ts b/src/sqlite/queries/eventLockTableInit.ts new file mode 100644 index 0000000..5480654 --- /dev/null +++ b/src/sqlite/queries/eventLockTableInit.ts @@ -0,0 +1,10 @@ +export const eventLockTableInit = (eventLockTableName: string) => ` + CREATE TABLE IF NOT EXISTS ${eventLockTableName} ( + projection_name TEXT NOT NULL, + schema_version TEXT NOT NULL, + event_id BLOB NOT NULL, + processing_at INTEGER NOT NULL DEFAULT (cast(unixepoch('subsec') * 1000 as INTEGER)), + processed_at INTEGER, + PRIMARY KEY (projection_name, schema_version, event_id) + ); +`; diff --git a/src/sqlite/queries/index.ts b/src/sqlite/queries/index.ts new file mode 100644 index 0000000..7edbb02 --- /dev/null +++ b/src/sqlite/queries/index.ts @@ -0,0 +1,2 @@ +export * from './eventLockTableInit'; +export * from './viewLockTableInit'; diff --git a/src/sqlite/queries/viewLockTableInit.ts b/src/sqlite/queries/viewLockTableInit.ts new file mode 100644 index 0000000..b3e707f --- /dev/null +++ b/src/sqlite/queries/viewLockTableInit.ts @@ -0,0 +1,9 @@ +export const viewLockTableInit = (viewLockTableName: string): string => ` + CREATE TABLE IF NOT EXISTS ${viewLockTableName} ( + projection_name TEXT NOT NULL, + schema_version TEXT NOT NULL, + locked_till INTEGER, + last_event TEXT, + PRIMARY KEY (projection_name, schema_version) + ); +`; diff --git a/src/sqlite/utils/getEventId.ts b/src/sqlite/utils/getEventId.ts new file mode 100644 index 0000000..2a99f75 --- /dev/null +++ b/src/sqlite/utils/getEventId.ts @@ -0,0 +1,8 @@ +import { IEvent } from '../../interfaces'; +import { guid } from './guid'; +import md5 = require('md5'); + +/** + * Get assigned or generate new event ID from event content + */ +export const getEventId = (event: IEvent): Buffer => guid(event.id ?? md5(JSON.stringify(event))); diff --git a/src/sqlite/utils/guid.ts b/src/sqlite/utils/guid.ts new file mode 100644 index 0000000..e8ce86c --- /dev/null +++ b/src/sqlite/utils/guid.ts @@ -0,0 +1,4 @@ +/** + * Convert Guid to Buffer for storing in Sqlite BLOB + */ +export const guid = (str: string) => Buffer.from(str.replaceAll('-', ''), 'hex'); diff --git a/src/sqlite/utils/index.ts b/src/sqlite/utils/index.ts new file mode 100644 index 0000000..f27b49b --- /dev/null +++ b/src/sqlite/utils/index.ts @@ -0,0 +1,2 @@ +export * from './guid'; +export * from './getEventId'; diff --git a/src/infrastructure/utils/Deferred.ts b/src/utils/Deferred.ts similarity index 100% rename from src/infrastructure/utils/Deferred.ts rename to src/utils/Deferred.ts diff --git a/src/utils/Lock.ts b/src/utils/Lock.ts new file mode 100644 index 0000000..2db3489 --- /dev/null +++ b/src/utils/Lock.ts @@ -0,0 +1,116 @@ +import { Deferred } from './Deferred'; + +export class LockLease { + constructor( + readonly lock: Lock, + readonly name?: string + ) { } + + release() { + this.lock.release(this.name); + } + + [Symbol.dispose]() { + this.release(); + } +} + +export class Lock { + + /** + * Indicates that global lock acquiring is started, + * so all other locks should wait to ensure that named lock raised after global don't squeeze before it + */ + #globalLockAcquiringLock?: Deferred; + + /** + * Indicates that global lock is acquired, all others should wait + */ + #globalLock?: Deferred; + + /** + * Hash of named locks. Each named lock block locks with same name and the global one + */ + #namedLocks: Map> = new Map(); + + #getAnyBlockingLock(id?: string): Deferred | undefined { + return this.#globalLock ?? ( + id ? + this.#namedLocks.get(id) : + this.#namedLocks.values().next().value + ); + } + + + isLocked(name?: string): boolean { + return !!this.#getAnyBlockingLock(name); + } + + /** + * Acquire named or global lock + * + * @returns Promise that resolves once lock is acquired + */ + async acquire(name?: string): Promise { + + while (this.#globalLockAcquiringLock) + await this.#globalLockAcquiringLock.promise; + + const isGlobal = !name; + if (isGlobal) + this.#globalLockAcquiringLock = new Deferred(); + + // the below code cannot be replaced with `await this.waitForUnlock()` + // since check of `isLocked` and `this.#deferred` assignment should happen within 1 callback + // while `async waitForUnlock(..) await..` creates one extra promise callback + while (this.isLocked(name)) + await this.#getAnyBlockingLock(name)?.promise; + + if (name) + this.#namedLocks.set(name, new Deferred()); + else + this.#globalLock = new Deferred(); + + if (isGlobal) { + this.#globalLockAcquiringLock?.resolve(); + this.#globalLockAcquiringLock = undefined; + } + + return new LockLease(this, name); + } + + /** + * @returns Promise that resolves once lock is released + */ + async waitForUnlock(name?: string): Promise { + while (this.isLocked(name)) + await this.#getAnyBlockingLock(name)?.promise; + } + + /** + * Release named or global lock + */ + release(name?: string): void { + if (name) { + this.#namedLocks.get(name)?.resolve(); + this.#namedLocks.delete(name); + } + else { + this.#globalLock?.resolve(); + this.#globalLock = undefined; + } + } + + /** + * Execute callback with lock acquired, then release lock + */ + async runExclusively(name: string | undefined, callback: () => Promise | T): Promise { + try { + await this.acquire(name); + return await callback(); + } + finally { + this.release(name); + } + } +} diff --git a/src/utils/MapAssertable.ts b/src/utils/MapAssertable.ts new file mode 100644 index 0000000..546cf7a --- /dev/null +++ b/src/utils/MapAssertable.ts @@ -0,0 +1,30 @@ +export class MapAssertable extends Map { + + #usageCounter = new Map(); + + /** + * Ensures the key exists in the map, creating it with the factory if needed, and increments its usage counter. + */ + assert(key: K, factory: () => V): V { + if (!this.has(key)) + this.set(key, factory()); + + this.#usageCounter.set(key, (this.#usageCounter.get(key) ?? 0) + 1); + + return super.get(key)!; + } + + /** + * Decrements the usage counter for the key and removes it from the map if no longer used. + */ + release(key: K) { + const count = (this.#usageCounter.get(key) ?? 0) - 1; + if (count > 0) { + this.#usageCounter.set(key, count); + } + else { + this.#usageCounter.delete(key); + this.delete(key); + } + } +} diff --git a/src/utils/delay.ts b/src/utils/delay.ts new file mode 100644 index 0000000..8663de4 --- /dev/null +++ b/src/utils/delay.ts @@ -0,0 +1,8 @@ +/** + * Returns a promise that resolves after the specified number of milliseconds. + * The internal timeout is unref'd to avoid blocking Node.js process termination. + */ +export const delay = (ms: number) => new Promise(resolve => { + const t = setTimeout(resolve, ms); + t.unref(); +}); diff --git a/src/utils/extractErrorDetails.ts b/src/utils/extractErrorDetails.ts new file mode 100644 index 0000000..bbe90c6 --- /dev/null +++ b/src/utils/extractErrorDetails.ts @@ -0,0 +1,48 @@ +const extractErrorName = (err: unknown): string | undefined => { + if (err instanceof Error) + return err.name; + + if (typeof err === 'object' && err) { + if ('name' in err && typeof err.name === 'string') + return err.name; + + return Object.getPrototypeOf(err)?.constructor?.name; + } + + return undefined; +}; + +const extractErrorMessage = (err: unknown): string => { + if (err instanceof AggregateError && err.errors?.length) + return [err.message, ...err.errors.map(extractErrorMessage)].filter(m => !!m).join('; '); + + if (err instanceof Error) + return err.message; + + if (typeof err === 'object' && err && 'message' in err && typeof err.message === 'string') + return err.message; + + return String(err); +}; + +export type ErrorDetails = { + name?: string, + message: string, + code?: any, + stack?: string, + cause?: ErrorDetails +}; + +export const extractErrorDetails = (err: unknown): ErrorDetails => ({ + name: extractErrorName(err), + message: extractErrorMessage(err), + ...typeof err === 'object' && err && 'code' in err && { + code: err.code + }, + ...err instanceof Error && { + stack: err.stack + }, + ...err instanceof Error && !!err.cause && { + cause: extractErrorDetails(err.cause) + } +}); diff --git a/src/utils/getHandledMessageTypes.ts b/src/utils/getHandledMessageTypes.ts deleted file mode 100644 index 8d910ec..0000000 --- a/src/utils/getHandledMessageTypes.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getMessageHandlerNames } from './getMessageHandlerNames'; - -/** - * Get a list of message types handled by observer - */ -export function getHandledMessageTypes( - observerInstanceOrClass: (object | Function) & { handles?: string[] } -): string[] { - if (!observerInstanceOrClass) - throw new TypeError('observerInstanceOrClass argument required'); - - if (observerInstanceOrClass.handles) - return observerInstanceOrClass.handles; - - const prototype = Object.getPrototypeOf(observerInstanceOrClass); - if (prototype && prototype.constructor && prototype.constructor.handles) - return prototype.constructor.handles; - - return getMessageHandlerNames(observerInstanceOrClass); -} diff --git a/src/utils/getHandler.ts b/src/utils/getHandler.ts index 957bbaf..f2de2be 100644 --- a/src/utils/getHandler.ts +++ b/src/utils/getHandler.ts @@ -1,4 +1,4 @@ -import { IMessageHandler } from "../interfaces"; +import { IMessageHandler } from '../interfaces'; /** * Gets a handler for a specific message type, prefers a public (w\o _ prefix) method, if available @@ -10,11 +10,11 @@ export function getHandler(context: { [key: string]: any }, messageType: string) throw new TypeError('messageType argument must be a non-empty string'); if (messageType in context && typeof context[messageType] === 'function') - return context[messageType].bind(context); + return context[messageType]; const privateHandlerName = `_${messageType}`; if (privateHandlerName in context && typeof context[privateHandlerName] === 'function') - return context[privateHandlerName].bind(context); + return context[privateHandlerName]; return null; -}; +} diff --git a/src/utils/getMessageHandlerNames.ts b/src/utils/getMessageHandlerNames.ts index 1c8eb1a..a9342ce 100644 --- a/src/utils/getMessageHandlerNames.ts +++ b/src/utils/getMessageHandlerNames.ts @@ -1,7 +1,3 @@ -const KNOWN_METHOD_NAMES = new Set([ - 'subscribe' -]); - function getInheritedPropertyNames(prototype: object): string[] { const parentPrototype = prototype && Object.getPrototypeOf(prototype); if (!parentPrototype) @@ -31,14 +27,12 @@ export function getMessageHandlerNames(observerInstanceOrClass: (object | Functi if (!prototype) throw new TypeError('prototype cannot be resolved'); - const inheritedProperties = new Set(getInheritedPropertyNames(prototype)); - + const inheritedProperties = getInheritedPropertyNames(prototype); const propDescriptors = Object.getOwnPropertyDescriptors(prototype); const propNames = Object.keys(propDescriptors); return propNames.filter(key => !key.startsWith('_') && - !inheritedProperties.has(key) && - !KNOWN_METHOD_NAMES.has(key) && + !inheritedProperties.includes(key) && typeof propDescriptors[key].value === 'function'); } diff --git a/src/utils/index.ts b/src/utils/index.ts index d95765f..b987ba6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,14 @@ +export * from './Deferred'; +export * from './delay'; export * from './getClassName'; export * from './getHandler'; -export * from './validateHandlers'; export * from './getMessageHandlerNames'; -export * from './getHandledMessageTypes'; +export * from './isClass'; +export * from './iteratorToArray'; +export * from './Lock'; +export * from './MapAssertable'; +export * from './notEmpty'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; -export * from './isClass'; +export * from './validateHandlers'; +export * from './extractErrorDetails'; diff --git a/src/utils/iteratorToArray.ts b/src/utils/iteratorToArray.ts new file mode 100644 index 0000000..9530201 --- /dev/null +++ b/src/utils/iteratorToArray.ts @@ -0,0 +1,6 @@ +export async function iteratorToArray(input: AsyncIterable | Iterable): Promise { + const result: T[] = []; + for await (const item of input) + result.push(item); + return result; +} diff --git a/src/utils/notEmpty.ts b/src/utils/notEmpty.ts new file mode 100644 index 0000000..0573fdd --- /dev/null +++ b/src/utils/notEmpty.ts @@ -0,0 +1 @@ +export const notEmpty = (t: T): t is Exclude => t !== undefined && t !== null; diff --git a/src/utils/setupOneTimeEmitterSubscription.ts b/src/utils/setupOneTimeEmitterSubscription.ts index 4fe28a0..554d8bf 100644 --- a/src/utils/setupOneTimeEmitterSubscription.ts +++ b/src/utils/setupOneTimeEmitterSubscription.ts @@ -1,4 +1,4 @@ -import { IEvent, ILogger, IObservable } from "../interfaces"; +import { IEvent, ILogger, IObservable } from '../interfaces'; /** * Create one-time eventEmitter subscription for one or multiple events that match a filter @@ -34,8 +34,10 @@ export function setupOneTimeEmitterSubscription( let handled = false; function filteredHandler(event: IEvent) { - if (filter && !filter(event)) return; - if (handled) return; + if (filter && !filter(event)) + return; + if (handled) + return; handled = true; for (const messageType of messageTypes) diff --git a/src/utils/subscribe.ts b/src/utils/subscribe.ts index af09aca..3f5b157 100644 --- a/src/utils/subscribe.ts +++ b/src/utils/subscribe.ts @@ -1,9 +1,23 @@ -import { IMessageHandler, IObservable } from "../interfaces"; +import { IMessageHandler, IObservable } from '../interfaces'; import { getHandler } from './getHandler'; -import { getHandledMessageTypes } from './getHandledMessageTypes'; +import { getMessageHandlerNames } from './getMessageHandlerNames'; const unique = (arr: T[]): T[] => [...new Set(arr)]; +/** + * Get a list of message types handled by observer + */ +export function getHandledMessageTypes(observerInstanceOrClass: (object | Function)): string[] { + if (!observerInstanceOrClass) + throw new TypeError('observerInstanceOrClass argument required'); + + const prototype = Object.getPrototypeOf(observerInstanceOrClass); + if (prototype && prototype.constructor && prototype.constructor.handles) + return prototype.constructor.handles; + + return getMessageHandlerNames(observerInstanceOrClass); +} + /** * Subscribe observer to observable */ @@ -35,17 +49,17 @@ export function subscribe( for (const messageType of unique(subscribeTo)) { const handler = masterHandler || getHandler(observer, messageType); - if (!handler) + if (!handler) throw new Error(`'${messageType}' handler is not defined or not a function`); if (queueName) { - if(!observable.queue) + if (!observable.queue) throw new TypeError('Observer does not support named queues'); - observable.queue(queueName).on(messageType, handler); + observable.queue(queueName).on(messageType, (event, meta) => handler.call(observer, event, meta)); } else { - observable.on(messageType, handler); + observable.on(messageType, (event, meta) => handler.call(observer, event, meta)); } } } diff --git a/src/utils/validateHandlers.ts b/src/utils/validateHandlers.ts index e6d5c96..7061c34 100644 --- a/src/utils/validateHandlers.ts +++ b/src/utils/validateHandlers.ts @@ -4,7 +4,8 @@ import { getHandler } from './getHandler'; * Ensure instance has handlers declared for all handled message types */ export function validateHandlers(instance: object, handlesFieldName = 'handles') { - if (!instance) throw new TypeError('instance argument required'); + if (!instance) + throw new TypeError('instance argument required'); const messageTypes = Object.getPrototypeOf(instance).constructor[handlesFieldName]; if (messageTypes === undefined) diff --git a/tests/integration/rabbitmq/RabbitMqEventBus.test.ts b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts new file mode 100644 index 0000000..10c31ad --- /dev/null +++ b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts @@ -0,0 +1,190 @@ +import * as amqplib from 'amqplib'; +import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; +import { RabbitMqEventBus } from '../../../src/rabbitmq/RabbitMqEventBus'; +import { IMessage, IEvent } from '../../../src/interfaces'; + +const delay = (ms: number) => new Promise(res => { + const t = setTimeout(res, ms); + t.unref(); +}); + +describe('RabbitMqEventBus', () => { + + let gateway1: RabbitMqGateway; + let gateway2: RabbitMqGateway; + let gateway3: RabbitMqGateway; + let eventBus1: RabbitMqEventBus; + let eventBus2: RabbitMqEventBus; + let eventBus3: RabbitMqEventBus; + + const queueName = 'test-bus-queue'; + const exchangeName = 'test-bus-exchange'; + const eventType = 'test-bus-event'; + + beforeEach(async () => { + const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); + gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory }); + gateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory }); + gateway3 = new RabbitMqGateway({ rabbitMqConnectionFactory }); + eventBus1 = new RabbitMqEventBus({ rabbitMqGateway: gateway1, exchange: exchangeName }); + eventBus2 = new RabbitMqEventBus({ rabbitMqGateway: gateway2, exchange: exchangeName }); + eventBus3 = new RabbitMqEventBus({ rabbitMqGateway: gateway3, exchange: exchangeName }); + }); + + afterEach(async () => { + const ch = await gateway1.connection.createChannel(); + await ch.deleteQueue(queueName); + await ch.deleteQueue(`${queueName}.failed`); + await ch.deleteExchange(exchangeName); + await gateway1.disconnect(); + await gateway2.disconnect(); + await gateway3.disconnect(); + }); + + describe('publish()', () => { + + it('publishes without throwing', async () => { + + await eventBus1.publish({ type: eventType }); + }); + }); + + describe('on()', () => { + + it('subscribes to events so that they are delivered to every subscriber except sender', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + const received3: IMessage[] = []; + + await eventBus1.on(eventType, e => { + received1.push(e); + }); + + await eventBus2.on(eventType, e => { + received2.push(e); + }); + + await eventBus3.on(eventType, e => { + received3.push(e); + }); + + const event: IEvent = { + type: eventType, + payload: { ok: true } + }; + + await eventBus2.publish(event); + await delay(50); + + expect(received1).toEqual([event]); + expect(received2).toEqual([]); + expect(received3).toEqual([event]); + }); + + it('allows to subscribe to all events', async () => { + + const received1: IMessage[] = []; + + await eventBus1.on(RabbitMqEventBus.allEventsWildcard, e => { + received1.push(e); + }); + + const event1: IEvent = { type: `${eventType}1` }; + const event2: IEvent = { type: `${eventType}2` }; + + await eventBus2.publish(event1); + await eventBus3.publish(event2); + + await delay(50); + + expect(received1).toEqual([event1, event2]); + }); + }); + + describe('queue()', () => { + + it('creates an isolated queue where published messages delivered to only one recipient', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + await eventBus1.queue(queueName).on(eventType, msg => { + received1.push(msg); + }); + + await eventBus2.queue(queueName).on(eventType, msg => { + received2.push(msg); + }); + + const event: IEvent = { + type: eventType, + payload: { ok: true } + }; + + await eventBus1.publish(event); + await delay(50); + + expect([...received1, ...received2]).toEqual([ + event + ]); + }); + + it('allows to subscribe to all events in the queue', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + await eventBus1.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, msg => { + received1.push(msg); + }); + + await eventBus2.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, msg => { + received2.push(msg); + }); + + const event1: IEvent = { + type: `${eventType}1` + }; + + const event2: IEvent = { + type: `${eventType}2` + }; + + await eventBus1.publish(event1); + await eventBus1.publish(event2); + + await delay(50); + + expect([...received1, ...received2]).toEqual([ + event1, + event2 + ]); + }); + + }); + + describe('off()', () => { + + it('removes previously added handler', async () => { + + const received1: IMessage[] = []; + const handler1 = (msg: IMessage) => received1.push(msg); + await eventBus1.on(eventType, handler1); + + const received2: IMessage[] = []; + const handler2 = (msg: IMessage) => received2.push(msg); + await eventBus2.on(eventType, handler2); + + eventBus2.off(eventType, handler2); + + const event = { type: eventType, payload: { removed: true } }; + await eventBus3.publish(event); + + await delay(50); + + expect(received1).toEqual([event]); + expect(received2).toEqual([]); + }); + }); +}); diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts new file mode 100644 index 0000000..bc717b2 --- /dev/null +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -0,0 +1,471 @@ +import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; +import { IMessage } from '../../../src/interfaces'; +import * as amqplib from 'amqplib'; +import { delay } from '../../../src/utils'; +import { Deferred } from '../../../src/utils/Deferred'; +import { EventEmitter } from 'stream'; + +describe('RabbitMqGateway', () => { + + let gateway1: RabbitMqGateway; + let gateway2: RabbitMqGateway | undefined; + let gateway3: RabbitMqGateway | undefined; + const exchange = 'test-exchange'; + const queueName = 'test-queue'; + const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); + + let process: EventEmitter; + + beforeEach(async () => { + // const logger = console; + const logger = undefined; + + process = new EventEmitter(); + gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger, process: process as NodeJS.Process }); + gateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger, process: process as NodeJS.Process }); + gateway3 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger, process: process as NodeJS.Process }); + }); + + afterEach(async () => { + if (gateway1.connection) { + const ch = await gateway1.connection.createChannel(); + await ch.deleteQueue(queueName); + await ch.deleteQueue(`${queueName}.failed`); + await ch.deleteExchange(exchange); + await gateway1.disconnect(); + } + await gateway2?.disconnect(); + await gateway3?.disconnect(); + }); + + describe('publish()', () => { + + it('publishes without throwing', async () => { + + const message: IMessage = { + type: 'test.confirm', + payload: { msg: 'confirmed' } + }; + + await gateway1.publish(exchange, message); + }); + }); + + + describe('subscribeToFanout', () => { + + it('ignores self-published messages', async () => { + const received: IMessage[] = []; + + await gateway1.subscribeToFanout(exchange, msg => { + received.push(msg); + }); + + const message: IMessage = { + type: 'test.event', + payload: { msg: 'self-test' } + }; + + // publish from the same instance — should be ignored + await gateway1.publish(exchange, message); + + await delay(50); // wait briefly + + expect(received).toHaveLength(0); + }); + + it('receives messages sent from external source', async () => { + const received: IMessage[] = []; + + await gateway1.subscribeToFanout(exchange, msg => { + received.push(msg); + }); + + gateway3 = new RabbitMqGateway({ + rabbitMqConnectionFactory: () => amqplib.connect('amqp://localhost') + }); + + const message: IMessage = { + type: 'test.event', + payload: { from: 'external' } + }; + + gateway3.publish(exchange, message); + await delay(50); // allow time for delivery + + expect(received).toHaveLength(1); + expect(received[0].payload.from).toBe('external'); + + await gateway3.connection.close(); + }); + + it('delivers fanout messages to multiple non-queue subscribers', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + await gateway2.subscribeToFanout(exchange, msg => received1.push(msg)); + await gateway3.subscribeToFanout(exchange, msg => received2.push(msg)); + + const message: IMessage = { + type: 'test.event', + payload: { test: 'multi' } + }; + + await gateway1.publish(exchange, message); + await delay(50); + + expect(received1).toHaveLength(1); + expect(received2).toHaveLength(1); + }); + }); + + describe('subscribeToQueue', () => { + + it('delivers locally published messages to durable queue subscription', async () => { + const received: IMessage[] = []; + await gateway1.subscribeToQueue(exchange, queueName, msg => received.push(msg)); + + const message: IMessage = { + type: 'queue.event', + payload: { local: true } + }; + + await gateway1.publish(exchange, message); + await delay(50); + + expect(received).toHaveLength(1); + expect(received[0].payload.local).toBe(true); + }); + + it('delivers queue messages to one consumer only', async () => { + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + await gateway1.subscribeToQueue(exchange, queueName, msg => received1.push(msg)); + + gateway3 = new RabbitMqGateway({ + rabbitMqConnectionFactory: () => amqplib.connect('amqp://localhost') + }); + await gateway3.subscribeToQueue(exchange, queueName, msg => received2.push(msg)); + + const message: IMessage = { + type: 'queue.once', + payload: { value: 1 } + }; + + await gateway1.publish(exchange, message); + await new Promise(res => setTimeout(res, 100)); + + const totalReceived = received1.length + received2.length; + expect(totalReceived).toBe(1); + }); + + it('sends failed queue messages to DLQ', async () => { + const dlqReceived: IMessage[] = []; + + await gateway1.subscribeToQueue(exchange, queueName, _msg => { + throw new Error('intentional failure'); + }); + + const cn2 = await amqplib.connect('amqp://localhost'); + const ch2 = await cn2.createChannel(); + await ch2.consume(`${queueName}.failed`, msg => { + dlqReceived.push(JSON.parse(msg.content.toString())); + }); + + const message: IMessage = { + type: 'dlq.test', + payload: { shouldFail: true } + }; + + await gateway1.publish(exchange, message); + await delay(50); + + expect(dlqReceived).toHaveLength(1); + expect(dlqReceived[0].payload.shouldFail).toBe(true); + + await cn2.close(); + }); + }); + + describe('subscribe', () => { + + it('subscribes to specific event type broadcast when eventType is defined', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + const event1 = { type: 'event1' }; + const event2 = { type: 'event2' }; + const event3 = { type: 'event3' }; + + await gateway1.subscribe({ exchange, eventType: event1.type, handler: e => received1.push(e) }); + await gateway1.subscribe({ exchange, eventType: event2.type, handler: e => received1.push(e) }); + await gateway2.subscribe({ exchange, eventType: event2.type, handler: e => received2.push(e) }); + await gateway2.subscribe({ exchange, eventType: event3.type, handler: e => received2.push(e) }); + + await gateway3.publish(exchange, event1); + await gateway3.publish(exchange, event2); + await gateway3.publish(exchange, event3); + + await delay(50); + + expect(received1).toEqual([event1, event2]); + expect(received2).toEqual([event2, event3]); + }); + + it('subscribe queue to given event types, when specified', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + const received3: IMessage[] = []; + + const event1 = { type: 'event1' }; + const event2 = { type: 'event2' }; + const event3 = { type: 'event3' }; + + await gateway1.subscribe({ exchange, queueName, eventType: event1.type, handler: m => received1.push(m) }); + await gateway1.subscribe({ exchange, queueName, eventType: event2.type, handler: m => received2.push(m) }); + await gateway1.subscribe({ exchange, queueName, eventType: event3.type, handler: m => received3.push(m) }); + + await gateway3.publish(exchange, event1); + await gateway3.publish(exchange, event2); + await gateway3.publish(exchange, event3); + + await delay(50); + + expect(received1).toEqual([event1]); + expect(received2).toEqual([event2]); + }); + + it('allows to limit number of concurrently running message processors', async () => { + + // @ts-ignore + const { promise: blocker, resolve: releaseBlocker } = Promise.withResolvers(); + + const received1: IMessage[] = []; + const event1 = { type: 'event1' }; + + await gateway1.subscribe({ + exchange, + queueName, + eventType: event1.type, + handler: async m => { + received1.push(m); + await blocker; + }, + concurrentLimit: 2 + }); + + await gateway3.publish(exchange, event1); + await gateway3.publish(exchange, event1); + await gateway3.publish(exchange, event1); + + await delay(50); + + expect(received1).toEqual([event1, event1]); + + releaseBlocker(); + await delay(50); + + expect(received1).toEqual([event1, event1, event1]); + }); + }); + + describe('unsubscribe', () => { + + it('removes subscription so handler does not receive further events', async () => { + + const received: IMessage[] = []; + const handler = (msg: IMessage) => { + received.push(msg); + }; + const event1 = { + type: 'test.unsubscribe', + payload: { info: 'first event' }, + context: { ts: Date.now() } + }; + + // Subscribe to a durable queue + await gateway1.subscribeToQueue(exchange, queueName, handler); + + // Publish an event and verify handler is invoked + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received).toEqual([event1]); + + await gateway1.unsubscribe({ exchange, queueName, handler }); + + // Clear received messages + received.length = 0; + expect(received).toEqual([]); + + // Publish a second event; handler should not be invoked + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received).toEqual([]); + }); + + it('cancels consumer when unsubscribing the last subscription on a queue', async () => { + + await gateway1.connect(); + + const cancelledConsumerTags: string[] = []; + const connection = gateway1.connection!; + const originalCreateChannel = connection.createChannel.bind(connection); + (connection as any).createChannel = async () => { + const ch = await originalCreateChannel(); + const originalCancel = ch.cancel.bind(ch); + (ch as any).cancel = async (consumerTag: string) => { + cancelledConsumerTags.push(consumerTag); + return originalCancel(consumerTag); + }; + return ch; + }; + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + const handler1 = (msg: IMessage) => { + received1.push(msg); + }; + const handler2 = (msg: IMessage) => { + received2.push(msg); + }; + + const event1 = { + type: 'test.unsubscribe', + payload: { info: 'event for handlers' }, + context: { ts: Date.now() } + }; + + await gateway1.subscribe({ + exchange, + queueName, + eventType: event1.type, + handler: handler1 + }); + await gateway1.subscribe({ + exchange, + queueName, + eventType: event1.type, + handler: handler2 + }); + + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received1).toEqual([event1]); + expect(received2).toEqual([event1]); + + await gateway1.unsubscribe({ + exchange, + queueName, + eventType: event1.type, + handler: handler1 + }); + + expect(cancelledConsumerTags).toHaveLength(0); + + received1.length = 0; + received2.length = 0; + + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received1).toEqual([]); + expect(received2).toEqual([event1]); + + await gateway1.unsubscribe({ + exchange, + queueName, + eventType: event1.type, + handler: handler2 + }); + + expect(cancelledConsumerTags).toHaveLength(1); + + received1.length = 0; + received2.length = 0; + + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received1).toEqual([]); + expect(received2).toEqual([]); + }); + }); + + describe('connect()', () => { + + it('retains subscriptions after reconnect', async () => { + + const fanoutReceived: IMessage[] = []; + const queueReceived: IMessage[] = []; + + await gateway1.subscribeToFanout(exchange, msg => { + fanoutReceived.push(msg); + }); + + await gateway1.subscribeToQueue(exchange, queueName, msg => { + queueReceived.push(msg); + }); + + // Force disconnect to simulate dropped connection + await gateway1.disconnect(); + await gateway1.connect(); + + const message: IMessage = { + type: 'test.reconnect', + payload: { check: true } + }; + + gateway3 = new RabbitMqGateway({ + rabbitMqConnectionFactory: () => amqplib.connect('amqp://localhost') + }); + + await gateway3.publish(exchange, message); + await delay(50); + + expect(fanoutReceived).toEqual([message]); + expect(queueReceived).toEqual([message]); + }); + + it('stops receiving messages on SIGINT', async () => { + + const received: IMessage[] = []; + const handlerBlocker = new Deferred(); + const message: IMessage = { + type: 'test.sigint', + payload: { check: true } + }; + + gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory, process: process as any }); + + await gateway1.subscribeToFanout(exchange, async msg => { + await handlerBlocker.promise; + received.push(msg); + }); + + gateway3 = new RabbitMqGateway({ rabbitMqConnectionFactory }); + + await gateway3.publish(exchange, message); + await delay(50); + + expect(received).toHaveLength(0); + + process.emit('SIGINT'); + await delay(10); + + expect(received).toHaveLength(0); + + handlerBlocker.resolve(); + await delay(10); + + expect(received).toHaveLength(1); + }); + }); +}); diff --git a/tests/integration/rabbitmq/docker-compose.yml b/tests/integration/rabbitmq/docker-compose.yml new file mode 100644 index 0000000..ebc000d --- /dev/null +++ b/tests/integration/rabbitmq/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + ports: + - "5672:5672" # AMQP + - "15672:15672" # Management UI + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + volumes: + - rabbitmq_data:/var/lib/rabbitmq + +volumes: + rabbitmq_data: diff --git a/tests/integration/sqlite/SqliteView.test.ts b/tests/integration/sqlite/SqliteView.test.ts new file mode 100644 index 0000000..eaf116c --- /dev/null +++ b/tests/integration/sqlite/SqliteView.test.ts @@ -0,0 +1,119 @@ +import { existsSync, unlinkSync } from 'fs'; +import { AbstractProjection, IEvent } from '../../../src'; +import { SqliteObjectView } from '../../../src/sqlite'; +import * as createDb from 'better-sqlite3'; + +type UserPayload = { + name: string; +} + +class MyDumbProjection extends AbstractProjection> { + + async userCreated(e: IEvent) { + if (typeof e.aggregateId !== 'string') + throw new TypeError('e.aggregateId is required'); + if (!e.payload) + throw new TypeError('e.payload is required'); + + await this.view.create(e.aggregateId, e.payload); + } + + async userModified(e: IEvent) { + if (typeof e.aggregateId !== 'string') + throw new TypeError('e.aggregateId is required'); + if (!e.payload) + throw new TypeError('e.payload is required'); + + await this.view.update(e.aggregateId, _u => e.payload); + } +} + +describe('SqliteView', () => { + + let viewModelSqliteDb: import('better-sqlite3').Database; + + const fileName = './test.sqlite'; + + beforeEach(() => { + viewModelSqliteDb = createDb(fileName); + + // Write-Ahead Logging (WAL) mode allows reads and writes to happen concurrently and reduces contention + // on the database. It keeps changes in a separate log file before they are flushed to the main database file + viewModelSqliteDb.pragma('journal_mode = WAL'); + + // The synchronous pragma controls how often SQLite synchronizes writes to the filesystem. Lowering this can + // boost performance but increases the risk of data loss in the event of a crash. + viewModelSqliteDb.pragma('synchronous = NORMAL'); + + // Limit WAL journal size to 5MB to manage disk usage in high-write scenarios. + // With WAL mode and NORMAL sync, this helps prevent excessive file growth during transactions. + viewModelSqliteDb.pragma(`journal_size_limit = ${5 * 1024 * 1024}`); + }); + + afterEach(() => { + if (viewModelSqliteDb) + viewModelSqliteDb.close(); + if (existsSync(fileName)) + unlinkSync(fileName); + }); + + // project 10_000 events (5_000 create new, 5_000 read, update, put back) + // in memory - 113 ms (88_500 events/second) + // on file system - 44_396 ms (225 events/second) + // on file system with WAL and NORMAL sync - 551 ms (18_148 events/second) + + it('handles 1_000 events within 0.5 seconds', async () => { + + const p = new MyDumbProjection({ + view: new SqliteObjectView({ + schemaVersion: '1', + viewModelSqliteDb, + projectionName: 'tbl_test', + tableNamePrefix: 'tbl_test' + }) + }); + + await p.view.lock(); + await p.view.unlock(); + + const aggregateIds = Array.from({ length: 1_000 }, (v, i) => ({ + aggregateId: `${i}A`.padStart(32, '0'), + eventId: `${i}B`.padStart(32, '0') + })); + + const startTs = Date.now(); + + for (const { aggregateId, eventId } of aggregateIds) { + await p.project({ + type: 'userCreated', + id: eventId, + aggregateId, + payload: { + name: 'Jon' + } + }); + + await p.project({ + type: 'userModified', + aggregateId, + payload: { + name: 'Jon Doe' + } + }); + } + + const totalMs = Date.now() - startTs; + expect(totalMs).toBeLessThan(500); + + const user = await p.view.get('0000000000000000000000000000999A'); + expect(user).toEqual({ + name: 'Jon Doe' + }); + + // console.log({ + // tbl_view_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_view_lock LIMIT 3`).all(), + // tbl_test_1_event_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_event_lock LIMIT 3`).all(), + // tbl_test_1: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1 LIMIT 3`).all() + // }); + }); +}); diff --git a/tests/unit/AbstractAggregate.test.ts b/tests/unit/AbstractAggregate.test.ts index a74ac4e..2ccdbe1 100644 --- a/tests/unit/AbstractAggregate.test.ts +++ b/tests/unit/AbstractAggregate.test.ts @@ -81,27 +81,24 @@ describe('AbstractAggregate', function () { it('returns immutable aggregate id', () => { expect(agg.id).to.equal(1); - expect(() => (agg as any).id = 2).to.throw(TypeError); + expect(() => { + (agg as any).id = 2; + }).to.throw(TypeError); }); }); - describe('changes', () => { + describe('protected changes', () => { - it('contains an EventStream of changes happened in aggregate', () => { + it('contains an EventStream of changes happened in aggregate', async () => { - const { changes } = agg; + expect(agg).to.haveOwnProperty('changes').that.has.length(0); - expect(changes).to.be.an('Array'); - expect(changes).to.be.empty; - expect(changes).to.not.equal(agg.changes); - expect(() => (agg as any).changes = []).to.throw(TypeError); - - return agg.doSomething({}).then(() => { + await agg.handle({ type: 'doSomething' }); - expect(agg).to.have.nested.property('changes[0].type', 'somethingDone'); - expect(agg).to.have.nested.property('changes[0].aggregateId', 1); - expect(agg).to.have.nested.property('changes[0].aggregateVersion', 0); - }); + expect(agg).to.haveOwnProperty('changes').that.has.length(1); + expect(agg).to.have.nested.property('changes.[0].type', 'somethingDone'); + expect(agg).to.have.nested.property('changes.[0].aggregateId', 1); + expect(agg).to.have.nested.property('changes.[0].aggregateVersion', 0); }); }); @@ -110,7 +107,9 @@ describe('AbstractAggregate', function () { it('is a read-only auto-incrementing aggregate version, starting from 0', () => { expect(agg.version).to.equal(0); - expect(() => (agg as any).version = 1).to.throw(TypeError); + expect(() => { + (agg as any).version = 1; + }).to.throw(TypeError); }); it('restores, when aggregate is restored from event stream', () => { @@ -127,7 +126,7 @@ describe('AbstractAggregate', function () { }); }); - describe('state', () => { + describe('protected state', () => { it('is an inner aggregate state', () => { @@ -149,9 +148,9 @@ describe('AbstractAggregate', function () { it('passes command to a handler declared within aggregate, returns a Promise', async () => { - await agg.handle({ type: 'doSomething' }); + const changes = await agg.handle({ type: 'doSomething' }); - expect(agg).to.have.nested.property('changes[0].type', 'somethingDone'); + expect(changes).to.have.nested.property('[0].type', 'somethingDone'); }); it('throws error, if command handler is not defined', async () => { @@ -173,13 +172,77 @@ describe('AbstractAggregate', function () { assert(emitSpy.calledOnce, 'emit was not called once'); }); + + it('throws error if another command is being processed', async () => { + try { + const p1 = agg.handle({ type: 'doSomething' }); + const p2 = agg.handle({ type: 'doSomething' }); + + await Promise.all([p1, p2]); + + throw new AssertionError('did not fail'); + } + catch (err) { + expect(err).to.have.property('message', 'Another command is being processed'); + } + }); + + it('appends snapshot event if shouldTakeSnapshot is true', async () => { + + class AggregateWithSnapshot extends Aggregate { + protected get shouldTakeSnapshot(): boolean { + return true; + } + } + + agg = new AggregateWithSnapshot({ id: 1 }); + + const events = await agg.handle({ type: 'doSomething' }); + + expect(events).to.have.length(2); + + expect(events[0]).to.have.property('type', 'somethingDone'); + expect(events[1]).to.have.property('type', 'snapshot'); + expect(events[1]).to.have.property('payload').that.deep.equals((agg as any).state); + }); + + it('increments snapshotVersion to avoid unnecessary snapshots on following commands', async () => { + + class AggregateWithSnapshot extends Aggregate { + protected get shouldTakeSnapshot(): boolean { + return this.version - (this.snapshotVersion || 0) >= 3; + } + } + + agg = new AggregateWithSnapshot({ id: 1 }); + + const r: Array<{ events: number, version: number, snapshotVersion: number | undefined }> = []; + + for (let i = 0; i < 5; i++) { + const events = await agg.handle({ type: 'doSomething' }); + r.push({ + events: events.length, + version: agg.version, + snapshotVersion: agg.snapshotVersion + }); + } + + expect(r).to.eql([ + { events: 1, version: 1, snapshotVersion: undefined }, + { events: 1, version: 2, snapshotVersion: undefined }, + { events: 2, version: 4, snapshotVersion: 3 }, // 2 events on 3rd command: regular + snapshot + { events: 1, version: 5, snapshotVersion: 3 }, // no snapshot on 4th command + { events: 2, version: 7, snapshotVersion: 6 } // 2 events on 5th command: regular + snapshot + ]); + }); }); - describe('emit(eventType, eventPayload)', () => { + describe('protected emit(eventType, eventPayload)', () => { it('pushes new event to #changes', () => { (agg as any).emit('eventType', {}); + expect(agg).to.have.nested.property('changes[0].type', 'eventType'); }); @@ -227,7 +290,7 @@ describe('AbstractAggregate', function () { it('does not mutate state if state event handler is not defined', () => { - const state = new class AggregateState { + const state = new class AnotherAggregateState { somethingHappened() { } }(); const somethingHappenedSpy = sinon.spy(state, 'somethingHappened'); @@ -265,19 +328,23 @@ describe('AbstractAggregate', function () { }); }); - describe('takeSnapshot()', () => { + describe('protected makeSnapshot()', () => { it('exists', () => { - expect(agg).to.respondTo('takeSnapshot'); + expect(agg).to.respondTo('makeSnapshot'); }); it('adds aggregate state snapshot to the changes queue', async () => { - await agg.handle({ type: 'doSomething' }); + class AggregateWithSnapshot extends Aggregate { + protected get shouldTakeSnapshot(): boolean { + return true; + } + } - agg.takeSnapshot(); + agg = new AggregateWithSnapshot({ id: 1 }); - const { changes } = agg; + const changes = await agg.handle({ type: 'doSomething' }); expect(changes).to.have.length(2); @@ -287,7 +354,7 @@ describe('AbstractAggregate', function () { }); }); - describe('restoreSnapshot(snapshotEvent)', () => { + describe('protected restoreSnapshot(snapshotEvent)', () => { const snapshotEvent = { type: 'snapshot', payload: { somethingDone: 1 } }; @@ -303,7 +370,9 @@ describe('AbstractAggregate', function () { const keysToCopy = Object.keys(snapshotEvent).filter(k => k !== keyToMiss); const brokenEvent = JSON.parse(JSON.stringify(snapshotEvent, keysToCopy)); - expect(() => (agg as any).restoreSnapshot(brokenEvent)).to.throw(TypeError); + expect(() => { + (agg as any).restoreSnapshot(brokenEvent); + }).to.throw(TypeError); } expect(() => (agg as any).restoreSnapshot({ aggregateVersion: 1, type: 'somethingHappened', payload: {} })).to.throw('snapshot event type expected'); diff --git a/tests/unit/AbstractProjection.test.ts b/tests/unit/AbstractProjection.test.ts index f5cff80..5701e5f 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -1,18 +1,26 @@ import { expect, assert, AssertionError } from 'chai'; import * as sinon from 'sinon'; -import { AbstractProjection, InMemoryView, InMemoryEventStorage, EventStore, InMemoryMessageBus } from '../../src'; - -class MyProjection extends AbstractProjection { +import { + AbstractProjection, + InMemoryView, + InMemoryEventStorage, + EventStore, + InMemoryMessageBus, + EventDispatcher +} from '../../src'; + +class MyProjection extends AbstractProjection> { static get handles() { return ['somethingHappened']; } - async _somethingHappened({ aggregateId, payload, context }) { + async _somethingHappened({ aggregateId }) { return this.view.updateEnforcingNew(aggregateId, (v = {}) => { if (v.somethingHappenedCnt) v.somethingHappenedCnt += 1; else v.somethingHappenedCnt = 1; + return v; }); } @@ -21,20 +29,19 @@ class MyProjection extends AbstractProjection { describe('AbstractProjection', function () { - let projection; + let projection: MyProjection; + let view: InMemoryView; beforeEach(() => { - projection = new MyProjection(); + view = new InMemoryView(); + projection = new MyProjection({ view }); }); describe('view', () => { it('returns a view storage associated with projection', () => { - const view = new InMemoryView(); - const proj = new MyProjection({ view }); - - expect(proj.view).to.equal(view); + expect(projection).to.have.property('view').that.is.equal(view); }); }); @@ -44,7 +51,7 @@ describe('AbstractProjection', function () { beforeEach(() => { observable = { - getAllEvents() { + getEventsByTypes() { return []; }, on() { } @@ -54,7 +61,7 @@ describe('AbstractProjection', function () { it('subscribes to all handlers defined', () => { - class ProjectionWithoutHandles extends AbstractProjection { + class ProjectionWithoutHandles extends AbstractProjection { somethingHappened() { } somethingHappened2() { } } @@ -68,7 +75,7 @@ describe('AbstractProjection', function () { it('ignores overridden projection methods', () => { - class ProjectionWithoutHandles extends AbstractProjection { + class ProjectionWithoutHandles extends AbstractProjection { somethingHappened() { } /** overridden projection method */ @@ -85,7 +92,7 @@ describe('AbstractProjection', function () { it('subscribes projection to all events returned by "handles"', () => { - class ProjectionWithHandles extends AbstractProjection { + class ProjectionWithHandles extends AbstractProjection { static get handles() { return ['somethingHappened2']; } @@ -106,24 +113,24 @@ describe('AbstractProjection', function () { beforeEach(() => { es = { - async* getAllEvents() { + async* getEventsByTypes() { yield { type: 'somethingHappened', aggregateId: 1, aggregateVersion: 1 }; yield { type: 'somethingHappened', aggregateId: 1, aggregateVersion: 2 }; yield { type: 'somethingHappened', aggregateId: 2, aggregateVersion: 1 }; } }; - sinon.spy(es, 'getAllEvents'); + sinon.spy(es, 'getEventsByTypes'); return projection.restore(es); }); it('queries events of specific types from event store', () => { - assert(es.getAllEvents.calledOnce, 'es.getAllEvents was not called'); + assert(es.getEventsByTypes.calledOnce, 'es.getEventsByTypes was not called'); - const { args } = es.getAllEvents.lastCall; + const { args } = es.getEventsByTypes.lastCall; - expect(args).to.have.length(1); + expect(args).to.have.length(2); expect(args[0]).to.deep.eq(MyProjection.handles); }); @@ -143,7 +150,7 @@ describe('AbstractProjection', function () { it('throws, if projection error encountered', () => { es = { - async* getAllEvents() { + async* getEventsByTypes() { yield { type: 'unexpectedEvent' }; } }; @@ -162,9 +169,15 @@ describe('AbstractProjection', function () { it('waits until the restoring process is done', async () => { - const storage = new InMemoryEventStorage(); - const messageBus = new InMemoryMessageBus(); - const es = new EventStore({ storage, messageBus }); + const eventStorageReader = new InMemoryEventStorage(); + const eventBus = new InMemoryMessageBus(); + const eventDispatcher = new EventDispatcher({ eventBus }); + const es = new EventStore({ + eventStorageReader, + eventBus, + eventDispatcher, + identifierProvider: eventStorageReader + }); let restored = false; let projected = false; @@ -198,14 +211,14 @@ describe('AbstractProjection', function () { projection.view.unlock(); sinon.spy(projection, '_somethingHappened'); - const event = { type: 'somethingHappened', aggregateId: 1 }; + const event2 = { type: 'somethingHappened', aggregateId: 1 }; expect(projection._somethingHappened).to.have.property('called', false); - await projection.project(event); + await projection.project(event2); expect(projection._somethingHappened).to.have.property('calledOnce', true); - expect(projection._somethingHappened.lastCall.args).to.eql([event]); + expect(projection._somethingHappened.lastCall.args).to.eql([event2]); }); }); }); diff --git a/tests/unit/AbstractSaga.test.ts b/tests/unit/AbstractSaga.test.ts index 86a08cd..f28bd5b 100644 --- a/tests/unit/AbstractSaga.test.ts +++ b/tests/unit/AbstractSaga.test.ts @@ -5,7 +5,7 @@ class Saga extends AbstractSaga { static get startsWith() { return ['somethingHappened']; } - _somethingHappened(event) { + _somethingHappened(_event) { super.enqueue('doSomething', undefined, { foo: 'bar' }); } } diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index 28698e0..319247a 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -1,15 +1,19 @@ import { expect, assert } from 'chai'; import * as sinon from 'sinon'; -import { ICommandBus, Identifier, IEventSet, IEventStore, IMessageBus, InMemoryMessageBus } from '../../src'; - import { + EventDispatcher, + ICommandBus, + Identifier, + IEventBus, + IEventSet, + IEventStore, + InMemoryMessageBus, AggregateCommandHandler, AbstractAggregate, InMemoryEventStorage, EventStore, InMemorySnapshotStorage } from '../../src'; -import { getHandledMessageTypes } from '../../src/utils'; function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -37,6 +41,7 @@ class CommandBus { on(messageType, listener) { this.handlers[messageType] = listener; } + off() { } } describe('AggregateCommandHandler', function () { @@ -44,25 +49,37 @@ describe('AggregateCommandHandler', function () { // this.timeout(500); // this.slow(300); - let storage: InMemoryEventStorage; + let eventStorage: InMemoryEventStorage; let snapshotStorage: InMemorySnapshotStorage; let eventStore: IEventStore; let commandBus: ICommandBus; - let messageBus: IMessageBus; + let eventBus: IEventBus; let onSpy; let getNewIdSpy; let getAggregateEventsSpy; let commitSpy; beforeEach(() => { - messageBus = new InMemoryMessageBus(); - storage = new InMemoryEventStorage(); + eventBus = new InMemoryMessageBus(); + eventStorage = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); + const eventDispatcher = new EventDispatcher({ + eventDispatchPipeline: [ + eventStorage + ], + eventBus + }); - eventStore = new EventStore({ storage, snapshotStorage, messageBus }); + eventStore = new EventStore({ + eventStorageReader: eventStorage, + snapshotStorage, + eventBus, + eventDispatcher, + identifierProvider: eventStorage + }); getNewIdSpy = sinon.spy(eventStore, 'getNewId'); getAggregateEventsSpy = sinon.spy(eventStore, 'getAggregateEvents'); - commitSpy = sinon.spy(eventStore, 'commit'); + commitSpy = sinon.spy(eventStore, 'dispatch'); commandBus = new CommandBus() as any; onSpy = sinon.spy(commandBus, 'on'); @@ -123,7 +140,7 @@ describe('AggregateCommandHandler', function () { const handler = new AggregateCommandHandler({ eventStore, aggregateFactory: () => aggregate, - handles: getHandledMessageTypes(aggregate) + handles: MyAggregate.handles }); await handler.execute({ type: 'doSomething', payload: 'test' }); @@ -135,8 +152,6 @@ describe('AggregateCommandHandler', function () { it('attaches command context, sagaId, sagaVersion to produced events', async () => { - const aggregate = new MyAggregate({ id: 1 }); - const handler = new AggregateCommandHandler({ eventStore, aggregateType: MyAggregate @@ -175,7 +190,7 @@ describe('AggregateCommandHandler', function () { expect(args[0]).to.be.an('Array'); }); - it('invokes aggregate.takeSnapshot before committing event stream, when get shouldTakeSnapshot equals true', async () => { + it('invokes aggregate.makeSnapshot before committing event stream, when get shouldTakeSnapshot equals true', async () => { // setup @@ -186,77 +201,109 @@ describe('AggregateCommandHandler', function () { return this.version !== 0 && this.version % 2 === 0; } }); - sinon.spy(aggregate, 'takeSnapshot'); + sinon.spy(aggregate, 'makeSnapshot'); const handler = new AggregateCommandHandler({ eventStore, aggregateFactory: () => aggregate, - handles: getHandledMessageTypes(aggregate) + handles: MyAggregate.handles }); // test - expect(aggregate).to.have.nested.property('takeSnapshot.called', false); + expect(aggregate).to.have.nested.property('makeSnapshot.called', false); expect(aggregate).to.have.property('version', 0); await handler.execute({ type: 'doSomething', payload: 'test' }); - expect(aggregate).to.have.nested.property('takeSnapshot.called', false); + expect(aggregate).to.have.nested.property('makeSnapshot.called', false); expect(aggregate).to.have.property('version', 1); // 1st event await handler.execute({ type: 'doSomething', payload: 'test' }); - expect(aggregate).to.have.nested.property('takeSnapshot.called', true); + expect(aggregate).to.have.nested.property('makeSnapshot.called', true); expect(aggregate).to.have.property('version', 3); // 2nd event and snapshot const [eventStream] = commitSpy.lastCall.args; - expect(eventStream).to.have.length(3); - expect(eventStream[2]).to.have.property('type', 'snapshot'); - expect(eventStream[2]).to.have.property('aggregateVersion', 2); - expect(eventStream[2]).to.have.property('payload'); + expect(eventStream).to.have.length(2); + expect(eventStream[1]).to.have.property('type', 'snapshot'); + expect(eventStream[1]).to.have.property('aggregateVersion', 2); + expect(eventStream[1]).to.have.property('payload'); }); - it.skip('executes concurrent commands on same aggregate instance', async () => { + it('produces events with sequential versions for concurrent commands to the same aggregate', async () => { - // setup + const handler = new AggregateCommandHandler({ eventStore, aggregateType: MyAggregate }); + const aggregateId = 'concurrent-test-id'; - class PersistedAggregate extends MyAggregate { - get shouldTakeSnapshot() { - return this.version > 2; - } - } + // Ensure aggregate exists + await handler.execute({ type: 'createAggregate', aggregateId }); - const handler = new AggregateCommandHandler({ eventStore, aggregateType: PersistedAggregate }); + const command1 = { type: 'doSomething', aggregateId }; + const command2 = { type: 'doSomething', aggregateId }; - const getAggregateEventsSpy = sinon.spy(storage, 'getAggregateEvents'); - const commitEventsSpy = sinon.spy(storage, 'commitEvents'); + // Execute commands concurrently + await Promise.all([ + handler.execute(command1), + handler.execute(command2) + ]); - // test + // Retrieve all events for the aggregate + const eventsIterable = eventStore.getAggregateEvents(aggregateId); + const allEvents = []; + for await (const event of eventsIterable) + allEvents.push(event); + + const emittedEventVersions = allEvents.map(e => e.aggregateVersion); + expect(emittedEventVersions).to.deep.equal([0, 1, 2]); + }); + + it('uses cached aggregate instance for concurrent commands and restores for subsequent commands', async () => { + + const aggregateId = 'cache-test-id'; + let factoryCallCount = 0; + const aggregateFactory = params => { + factoryCallCount++; + return new MyAggregate(params); + }; + + const handler = new AggregateCommandHandler({ + eventStore, + aggregateFactory, + handles: MyAggregate.handles + }); + + // Ensure aggregate exists + await handler.execute({ type: 'createAggregate', aggregateId }); - const cmd0 = { type: 'createAggregate' }; + // Reset spies/counters before the main test part + getAggregateEventsSpy.resetHistory(); + factoryCallCount = 0; - const [{ aggregateId }] = await handler.execute(cmd0); + const command1 = { type: 'doSomething', aggregateId }; + const command2 = { type: 'doSomething', aggregateId }; - expect(storage).to.have.nested.property('getAggregateEvents.callCount', 0); - expect(storage).to.have.nested.property('commitEvents.callCount', 1); + // Execute commands concurrently + await Promise.all([ + handler.execute(command1), + handler.execute(command2) + ]); - const cmd1 = { aggregateId, type: 'doSomething' }; - const cmd2 = { aggregateId, type: 'doSomething' }; - const cmd3 = { aggregateId, type: 'doSomething' }; + // Check that restore and factory were called only once for the concurrent pair + assert(getAggregateEventsSpy.calledOnce, 'getAggregateEvents should be called once for concurrent commands'); + expect(factoryCallCount).to.equal(1, 'Aggregate factory should be called once for concurrent commands'); - await Promise.all([cmd1, cmd2, cmd3].map(c => handler.execute(c))); - // expect(storage).to.have.nested.property('getAggregateEvents.callCount', 1); - // expect(storage).to.have.nested.property('commitEvents.callCount', 2); + getAggregateEventsSpy.resetHistory(); + factoryCallCount = 0; - const events = await eventStore.getAggregateEvents(aggregateId as Identifier); + // Execute a third command after the first two completed + const command3 = { type: 'doSomething', aggregateId }; + await handler.execute(command3); - expect(events).to.have.length(4); - expect(events[0]).to.have.property('type', 'snapshot'); - expect(events[0]).to.have.property('aggregateVersion', 1); - expect(events[1]).to.have.property('aggregateVersion', 2); - expect(events[2]).to.have.property('aggregateVersion', 3); - expect(events[3]).to.have.property('aggregateVersion', 4); + // Check that restore and factory were called again for the subsequent command + assert(getAggregateEventsSpy.calledOnce, 'getAggregateEvents should be called again for the subsequent command'); + expect(factoryCallCount).to.equal(1, 'Aggregate factory should be called again for the subsequent command'); }); }); diff --git a/tests/unit/CommandBus.test.ts b/tests/unit/CommandBus.test.ts index d763a40..3181ec3 100644 --- a/tests/unit/CommandBus.test.ts +++ b/tests/unit/CommandBus.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { InMemoryMessageBus } from '../../src/infrastructure/InMemoryMessageBus'; -import { CommandBus } from '../../src/CommandBus'; +import { InMemoryMessageBus, CommandBus } from '../../src'; describe('CommandBus', function () { diff --git a/tests/unit/CqrsContainerBuilder.test.ts b/tests/unit/CqrsContainerBuilder.test.ts index ca3b63b..844af3f 100644 --- a/tests/unit/CqrsContainerBuilder.test.ts +++ b/tests/unit/CqrsContainerBuilder.test.ts @@ -11,12 +11,12 @@ import { describe('CqrsContainerBuilder', function () { - let builder; + let builder: ContainerBuilder; beforeEach(() => { builder = new ContainerBuilder(); - builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('messageBus'); + builder.register(InMemoryEventStorage).as('eventStorageWriter').as('eventStorageReader').as('identifierProvider'); + builder.register(InMemoryMessageBus).as('eventBus'); }); describe('registerAggregate(aggregateType) extension', () => { @@ -81,13 +81,13 @@ describe('CqrsContainerBuilder', function () { { type: 'somethingHappened', aggregateId: 1 } ]; - container.eventStore.commit(events).catch(done); + container.eventStore.dispatch(events).catch(done); }); }); describe('registerProjection(typeOrFactory, exposedViewName) extension', () => { - class MyProjection extends AbstractProjection { + class MyProjection extends AbstractProjection { static get handles() { return ['somethingHappened']; } diff --git a/tests/unit/EventDispatcher.test.ts b/tests/unit/EventDispatcher.test.ts new file mode 100644 index 0000000..a079000 --- /dev/null +++ b/tests/unit/EventDispatcher.test.ts @@ -0,0 +1,191 @@ +import { IEvent, IEventBus, IDispatchPipelineProcessor } from '../../src'; +import { EventDispatcher } from '../../src/EventDispatcher'; + +describe('EventDispatcher', () => { + let dispatcher: EventDispatcher; + let eventBus: jest.Mocked; + + beforeEach(() => { + eventBus = { publish: jest.fn() }; + dispatcher = new EventDispatcher({ eventBus }); + }); + + it('dispatches events through processors and dispatches', async () => { + + const event1: IEvent = { type: 'test-event-1' }; + const event2: IEvent = { type: 'test-event-2' }; + + const processorMock: IDispatchPipelineProcessor = { + process: jest.fn(batch => Promise.resolve(batch)) + }; + + dispatcher.addPipelineProcessor(processorMock); + const result = await dispatcher.dispatch([event1, event2]); + + expect(processorMock.process).toHaveBeenCalledTimes(1); + expect(eventBus.publish).toHaveBeenCalledTimes(2); + expect(eventBus.publish).toHaveBeenCalledWith(event1, {}); + expect(eventBus.publish).toHaveBeenCalledWith(event2, {}); + expect(result).toEqual([event1, event2]); + }); + + it('handles processor errors and invokes revert', async () => { + + const event: IEvent = { type: 'failing-event' }; + const error = new Error('processor error'); + + const processorMock: IDispatchPipelineProcessor = { + process: jest.fn().mockRejectedValue(error), + revert: jest.fn().mockResolvedValue(undefined) + }; + + dispatcher.addPipelineProcessor(processorMock); + + await expect(dispatcher.dispatch([event])).rejects.toThrow('processor error'); + + expect(processorMock.process).toHaveBeenCalledTimes(1); + expect(processorMock.revert).toHaveBeenCalledTimes(1); + expect(eventBus.publish).not.toHaveBeenCalled(); + }); + + it('throws if dispatch called with empty event array', async () => { + + await expect(dispatcher.dispatch([])).rejects.toThrow('dispatch requires a non-empty array of events'); + }); + + it('runs multiple processors sequentially while processing batches in parallel', async () => { + + const executionOrder: string[] = []; + + const processorA: IDispatchPipelineProcessor = { + process: jest.fn(async batch => { + executionOrder.push(`A-start-${batch[0].event.type}`); + await new Promise(res => setTimeout(res, 5)); + executionOrder.push(`A-end-${batch[0].event.type}`); + return batch; + }) + }; + + const processorB: IDispatchPipelineProcessor = { + process: jest.fn(async batch => { + executionOrder.push(`B-start-${batch[0].event.type}`); + await new Promise(res => setTimeout(res, 5)); + executionOrder.push(`B-end-${batch[0].event.type}`); + return batch; + }) + }; + + dispatcher.addPipelineProcessor(processorA); + dispatcher.addPipelineProcessor(processorB); + + const event1: IEvent = { type: 'event-1' }; + const event2: IEvent = { type: 'event-2' }; + + await Promise.all([ + dispatcher.dispatch([event1]), + dispatcher.dispatch([event2]) + ]); + + expect(executionOrder).toEqual([ + 'A-start-event-1', + 'A-start-event-2', + 'A-end-event-1', + 'B-start-event-1', + 'A-end-event-2', + 'B-start-event-2', + 'B-end-event-1', + 'B-end-event-2' + ]); + }); + + it('routes events to pipelines based on meta.origin', async () => { + + const internalProcessor: IDispatchPipelineProcessor = { process: jest.fn(async b => b) }; + const externalProcessor: IDispatchPipelineProcessor = { process: jest.fn(async b => b) }; + + dispatcher = new EventDispatcher({ + eventBus, + eventDispatchPipelines: { + internal: [internalProcessor], + external: [externalProcessor] + } + }); + + const internalEvent: IEvent = { type: 'int' }; + const externalEvent: IEvent = { type: 'ext' }; + + await dispatcher.dispatch([internalEvent], { origin: 'internal' }); + await dispatcher.dispatch([externalEvent], { origin: 'external' }); + + expect(internalProcessor.process).toHaveBeenCalledTimes(1); + expect(externalProcessor.process).toHaveBeenCalledTimes(1); + expect(eventBus.publish).toHaveBeenCalledWith(internalEvent, { origin: 'internal' }); + expect(eventBus.publish).toHaveBeenCalledWith(externalEvent, { origin: 'external' }); + }); + + it('routes events according to eventDispatchRouter if provided', async () => { + const p1: IDispatchPipelineProcessor = { process: jest.fn(async b => b) }; + const p2: IDispatchPipelineProcessor = { process: jest.fn(async b => b) }; + + dispatcher = new EventDispatcher({ + eventBus, + eventDispatchPipelines: { + p1: [p1], + p2: [p2] + }, + eventDispatchRouter: (_events, meta) => meta?.route + }); + + const e1: IEvent = { type: 'r1' }; + const e2: IEvent = { type: 'r2' }; + + await dispatcher.dispatch([e1], { route: 'p1' } as any); + await dispatcher.dispatch([e2], { route: 'p2' } as any); + + expect(p1.process).toHaveBeenCalledTimes(1); + expect(p2.process).toHaveBeenCalledTimes(1); + expect(eventBus.publish).toHaveBeenCalledWith(e1, { route: 'p1' }); + expect(eventBus.publish).toHaveBeenCalledWith(e2, { route: 'p2' }); + }); + + it('routes events to default pipeline when no router is defined', async () => { + const pDefault: IDispatchPipelineProcessor = { process: jest.fn(async b => b) }; + const pOther: IDispatchPipelineProcessor = { process: jest.fn(async b => b) }; + + dispatcher = new EventDispatcher({ + eventBus, + eventDispatchPipelines: { + [EventDispatcher.DEFAULT_PIPELINE]: [pDefault], + other: [pOther] + } + }); + + const e: IEvent = { type: 'go-default' }; + await dispatcher.dispatch([e]); + + expect(pDefault.process).toHaveBeenCalledTimes(1); + expect(pOther.process).not.toHaveBeenCalled(); + expect(eventBus.publish).toHaveBeenCalledWith(e, {}); + }); + + it('throws when targeted pipeline is missing (router or default)', async () => { + const e: IEvent = { type: 'missing' }; + + // Case 1: router selects a non-existent pipeline + let d = new EventDispatcher({ + eventBus, + eventDispatchPipelines: { + foo: [] + }, + eventDispatchRouter: () => 'missing-pipe' + }); + await expect(d.dispatch([e], {})).rejects.toThrow('No "missing-pipe" pipeline configured'); + + // Case 2: no router/meta, default pipeline not provided + d = new EventDispatcher({ + eventBus, + eventDispatchPipelines: { other: [] } + }); + await expect(d.dispatch([e])).rejects.toThrow('No "default" pipeline configured'); + }); +}); diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index b0a2ef5..59f65a0 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -1,352 +1,178 @@ -import { expect } from 'chai'; -import * as sinon from 'sinon'; +import { EventDispatcher } from '../../dist/EventDispatcher'; +import { IEventDispatcher, InMemoryMessageBus } from '../../src'; import { EventStore } from '../../src/EventStore'; -import { InMemoryEventStorage } from '../../src/infrastructure/InMemoryEventStorage'; -import { InMemorySnapshotStorage } from '../../src/infrastructure/InMemorySnapshotStorage'; -import { InMemoryMessageBus } from '../../src/infrastructure/InMemoryMessageBus'; -import { IAggregateSnapshotStorage, IEvent, IEventStorage, IEventStore, IEventSet, IMessageBus } from '../../src/interfaces'; - -const goodContext = { - uid: '1', - ip: '127.0.0.1', - browser: 'test', - serverTime: Date.now() -}; - -const goodEvent = { - aggregateId: '1', - aggregateVersion: 0, - type: 'somethingHappened', - context: goodContext -}; - -const goodEvent2 = { - aggregateId: '2', - aggregateVersion: 0, - type: 'somethingHappened', - context: goodContext -}; - -const snapshotEvent = { - aggregateId: '2', - aggregateVersion: 1, - type: 'snapshot', - payload: { foo: 'bar' } -}; - - -describe('EventStore', function () { - - let es: IEventStore; - let storage: IEventStorage; - let snapshotStorage: IAggregateSnapshotStorage; - let messageBus: IMessageBus; +import { + IEvent, + IEventBus, + IEventStorageReader, + IAggregateSnapshotStorage, + IIdentifierProvider +} from '../../src/interfaces'; + +describe('EventStore', () => { + + let store: EventStore; + let eventBus: IEventBus; + let eventDispatcher: IEventDispatcher; + let mockStorage: jest.Mocked; + let mockSnapshotStorage: jest.Mocked; + let mockIdentifierProvider: jest.Mocked; + const mockId = 'test-id'; beforeEach(() => { - storage = new InMemoryEventStorage(); - snapshotStorage = new InMemorySnapshotStorage(); - messageBus = new InMemoryMessageBus(); - es = new EventStore({ storage, snapshotStorage, messageBus }); - }); - - describe('validator', () => { - - it('allows to validate events before they are committed', () => { - - const events = [ - { type: 'somethingHappened', aggregateId: 1 } - ]; - - return es.commit(events).then(() => { - - es = new EventStore({ - storage, - eventValidator: event => { - throw new Error('test validation error'); - }, - messageBus - }); - - return es.commit(events).then(() => { - throw new Error('must fail'); - }, err => { - expect(err).to.have.property('message', 'test validation error'); - }); - }); + eventBus = new InMemoryMessageBus(); + eventDispatcher = new EventDispatcher({ eventBus }); + + mockStorage = { + getAggregateEvents: jest.fn().mockResolvedValue([]), + getSagaEvents: jest.fn().mockResolvedValue([]), + getEventsByTypes: jest.fn().mockResolvedValue([]) + } as any; + + mockSnapshotStorage = { + getAggregateSnapshot: jest.fn().mockResolvedValue(undefined) + } as any; + + mockIdentifierProvider = { + getNewId: jest.fn().mockResolvedValue(mockId) + } as any; + + store = new EventStore({ + eventBus, + eventDispatcher, + eventStorageReader: mockStorage, + identifierProvider: mockIdentifierProvider, + snapshotStorage: mockSnapshotStorage, + logger: undefined }); }); - describe('commit', () => { - - it('validates event format', () => { + describe('dispatch', () => { - const badEvent = { - type: 'somethingHappened', - context: goodContext - }; + it('throws error when called with non-array argument', async () => { - return es.commit([badEvent]).then(() => { - throw new Error('must fail'); - }, err => { - expect(err).exist; - expect(err).to.be.an.instanceof(TypeError); - expect(err.message).to.equal('either event.aggregateId or event.sagaId is required'); - }); + await expect(store.dispatch(null as any)).rejects.toThrow(TypeError); }); - it('commits events to storage', async () => { + it('augments saga starter events with new sagaId and version', async () => { - await es.commit([goodEvent]); + store.registerSagaStarters(['StartSaga']); + const event: IEvent = { type: 'StartSaga' } as IEvent; + const dispatchSpy = jest.spyOn(eventDispatcher, 'dispatch'); - const events: IEvent[] = []; - for await (const e of es.getAllEvents()) - events.push(e); + await store.dispatch([event]); - expect(events[0]).to.have.property('type', 'somethingHappened'); - expect(events[0]).to.have.property('context'); - expect(events[0].context).to.have.property('ip', goodContext.ip); + expect(event.sagaId).toBe(mockId); + expect(event.sagaVersion).toBe(0); + expect(dispatchSpy).toHaveBeenCalledWith([event], { origin: 'internal' }); }); - it('submits aggregate snapshot to storage.saveAggregateSnapshot, when provided', async () => { - - snapshotStorage.getAggregateSnapshot = () => snapshotEvent as IEvent; - - // storage.saveAggregateSnapshot = () => { }; - const saveAggregateSnapshotSpy = sinon.spy(snapshotStorage, 'saveAggregateSnapshot'); - const commitEventsSpy = sinon.spy(storage, 'commitEvents'); - - expect(es).to.have.property('snapshotsSupported', true); + it('does not modify non-saga starter events', async () => { - es.commit([goodEvent]); - expect(snapshotStorage).to.have.nested.property('saveAggregateSnapshot.called', false); + const event: IEvent = { type: 'RegularEvent' } as IEvent; + const dispatchSpy = jest.spyOn(eventDispatcher, 'dispatch'); - es.commit([goodEvent2, snapshotEvent]); - expect(snapshotStorage).to.have.nested.property('saveAggregateSnapshot.calledOnce', true); + await store.dispatch([event]); - { - const { args } = saveAggregateSnapshotSpy.lastCall; - expect(args).to.have.length(1); - expect(args[0]).to.eq(snapshotEvent); - } - - { - const { args } = commitEventsSpy.lastCall; - expect(args).to.have.length(1); - expect(args[0]).to.have.length(1); - expect(args[0][0]).to.have.property('type', goodEvent2.type); - } - }); - - it('returns a promise that resolves to events committed', () => es.commit([goodEvent, goodEvent2]).then(events => { - - expect(events).to.be.an('Array'); - expect(events).to.have.length(2); - expect(events).to.have.nested.property('[0].type', 'somethingHappened'); - })); - - it('returns a promise that rejects, when commit doesn\'t succeed', () => { - - const storage = Object.create(InMemoryEventStorage.prototype, { - commitEvents: { - value: () => { - throw new Error('storage commit failure'); - } - } - }); - - es = new EventStore({ storage, messageBus }); - - return es.commit([goodEvent, goodEvent2]).then(() => { - throw new Error('should fail'); - }, err => { - expect(err).to.be.an('Error'); - expect(err).to.have.property('message', 'storage commit failure'); - }); - }); - - it('emits events asynchronously after processing is done', function (done) { - - let committed = 0; - let emitted = 0; - - es.on('somethingHappened', function (event) { - - expect(committed).to.not.equal(0); - expect(emitted).to.equal(0); - emitted++; - - expect(event).to.have.property('type', 'somethingHappened'); - expect(event).to.have.property('context'); - expect(event.context).to.have.property('ip', goodContext.ip); - - done(); - }); - - es.commit([goodEvent]).then(() => committed++).catch(done); + expect(event.sagaId).toBeUndefined(); + expect(event.sagaVersion).toBeUndefined(); + expect(dispatchSpy).toHaveBeenCalledWith([event], { origin: 'internal' }); }); }); - describe('getNewId', () => { + describe('getAggregateEvents', () => { - it('retrieves a unique ID for new aggregate from storage', () => Promise.resolve(es.getNewId()).then(id => { - expect(id).to.equal(1); - })); - }); - - describe('getAggregateEvents(aggregateId)', () => { - - it('returns all events committed for a specific aggregate', async () => { - - await es.commit([goodEvent, goodEvent2]); - - const events = await es.getAggregateEvents(goodEvent.aggregateId); - - expect(events).to.be.an('Array'); - expect(events).to.have.length(1); - expect(events).to.have.nested.property('[0].type', 'somethingHappened'); - }); - - it('tries to retrieve aggregate snapshot', async () => { + it('retrieves aggregate events including snapshot if available', async () => { + const snapshotEvent = { type: 'SnapshotEvent' } as IEvent; + const storedEvents = [{ type: 'Event1' }, { type: 'Event2' }] as IEvent[]; + mockSnapshotStorage.getAggregateSnapshot.mockResolvedValueOnce(snapshotEvent); + mockStorage.getAggregateEvents.mockResolvedValueOnce(storedEvents); - snapshotStorage.getAggregateSnapshot = () => snapshotEvent as IEvent; - snapshotStorage.saveAggregateSnapshot = () => { }; - sinon.spy(snapshotStorage, 'getAggregateSnapshot'); - const getAggregateEventsSpy = sinon.spy(storage, 'getAggregateEvents'); + const result: IEvent[] = []; + for await (const event of store.getAggregateEvents('aggregate-1')) + result.push(event); - expect(es).to.have.property('snapshotsSupported', true); - const events = await es.getAggregateEvents(goodEvent2.aggregateId); - - expect(snapshotStorage).to.have.nested.property('getAggregateSnapshot.calledOnce', true); - expect(storage).to.have.nested.property('getAggregateEvents.calledOnce', true); - - const [, eventFilter] = getAggregateEventsSpy.lastCall.args; - - expect(eventFilter).to.have.property('snapshot'); - expect(eventFilter).to.have.nested.property('snapshot.type'); - expect(eventFilter).to.have.nested.property('snapshot.aggregateId'); - expect(eventFilter).to.have.nested.property('snapshot.aggregateVersion'); + expect(result).toEqual([snapshotEvent, ...storedEvents]); + expect(mockSnapshotStorage.getAggregateSnapshot).toHaveBeenCalledWith('aggregate-1'); + expect(mockStorage.getAggregateEvents).toHaveBeenCalledWith('aggregate-1', { snapshot: snapshotEvent }); }); }); - describe('getSagaEvents(sagaId, options)', () => { - - it('returns events committed by saga prior to event that triggered saga execution', () => { + describe('getSagaEvents', () => { - const events = [ - { sagaId: 1, sagaVersion: 1, type: 'somethingHappened' }, - { sagaId: 1, sagaVersion: 2, type: 'anotherHappened' }, - { sagaId: 2, sagaVersion: 1, type: 'somethingHappened' } - ]; + it('retrieves saga events with provided filter', async () => { + const sagaEvents = [{ type: 'SagaEvent1' }] as IEvent[]; + mockStorage.getSagaEvents.mockResolvedValueOnce(sagaEvents); + const filter = { beforeEvent: { sagaVersion: 1 } }; - const triggeredBy = events[1]; + const result: IEvent[] = []; + for await (const event of store.getSagaEvents('saga-1', filter)) + result.push(event); - return es.commit(events).then(() => es.getSagaEvents(1, { beforeEvent: triggeredBy }).then(events => { - expect(events).to.be.an('Array'); - expect(events).to.have.length(1); - expect(events).to.have.nested.property('[0].type', 'somethingHappened'); - })); + expect(result).toEqual(sagaEvents); + expect(mockStorage.getSagaEvents).toHaveBeenCalledWith('saga-1', filter); }); }); - describe('getAllEvents(eventTypes)', () => { - - it('returns a promise that resolves to all committed events of specific types', async () => { - await es.commit([goodEvent, goodEvent2]); - - const events: IEvent[] = []; - for await (const e of es.getAllEvents(['somethingHappened'])) - events.push(e); + describe('getNewId', () => { - expect(events).to.have.length(2); - expect(events).to.have.nested.property('[0].aggregateId', '1'); - expect(events).to.have.nested.property('[1].aggregateId', '2'); + it('delegates to the identifierProvider', async () => { + const id = await store.getNewId(); + expect(id).toBe(mockId); + expect(mockIdentifierProvider.getNewId).toHaveBeenCalled(); }); }); - describe('on(eventType, handler)', () => { - - it('exists', () => { - expect(es).to.respondTo('on'); - }); - - it('fails, when trying to set up second messageType handler within the same node and named queue (Receptors)', () => { - - es = new EventStore({ storage, messageBus }); - - expect(() => { - es.queue('namedQueue').on('somethingHappened', () => { }); - }).to.not.throw(); - - expect(() => { - es.queue('anotherNamedQueue').on('somethingHappened', () => { }); - }).to.not.throw(); - - expect(() => { - es.queue('namedQueue').on('somethingHappened', () => { }); - }).to.throw('"somethingHappened" handler is already set up on the "namedQueue" queue'); - }); - - it('sets up multiple handlers for same messageType, when queue name is not defined (Projections)', () => { + describe('on/off/queue', () => { - es = new EventStore({ storage, eventStoreConfig: { publishAsync: false }, messageBus }); + it('delegates on, off, and queue calls to eventBus', () => { + const handler = jest.fn(); + const onSpy = jest.spyOn(eventBus, 'on'); + const offSpy = jest.spyOn(eventBus, 'off'); + const queueSpy = jest.spyOn(eventBus, 'queue'); - const projection1Handler = sinon.spy(); - const projection2Handler = sinon.spy(); + store.on('testEvent', handler); + expect(onSpy).toHaveBeenCalledWith('testEvent', handler); - es.on('somethingHappened', projection1Handler); - es.on('somethingHappened', projection2Handler); + store.off('testEvent', handler); + expect(offSpy).toHaveBeenCalledWith('testEvent', handler); - return es.commit([ - { type: 'somethingHappened', aggregateId: 1, aggregateVersion: 0 } - ]).then(() => { - expect(projection1Handler).to.have.property('calledOnce', true); - expect(projection2Handler).to.have.property('calledOnce', true); - }); + const queueResult = store.queue('testQueue'); + expect(queueResult).toBeInstanceOf(InMemoryMessageBus); + expect(queueSpy).toHaveBeenCalledWith('testQueue'); }); }); - describe('once(eventType, handler, filter)', () => { - - it('executes handler only once, when event matches filter', done => { - let firstAggregateCounter = 0; - let secondAggregateCounter = 0; - - es.once('somethingHappened', - event => ++firstAggregateCounter, - event => event.aggregateId === '1'); - - es.once('somethingHappened', - event => ++secondAggregateCounter, - event => event.aggregateId === '2'); + describe('once', () => { - es.commit([goodEvent, goodEvent, goodEvent, goodEvent2]); - es.commit([goodEvent2, goodEvent2]); + it('sets up a one-time subscription and resolves with an event', async () => { + let callCount = 0; + const testEvent = { type: 'onceEvent' } as IEvent; + const promise = store.once('onceEvent', (_e: IEvent) => { + callCount++; + }); - setTimeout(() => { - try { - expect(firstAggregateCounter).to.equal(1); - expect(secondAggregateCounter).to.equal(1); + await store.dispatch([testEvent]); - done(); - } - catch (err) { - done(err); - } - }, 100); + await expect(promise).resolves.toBe(testEvent); + expect(callCount).toBe(1); }); - it('returns a promise', () => { - - setImmediate(() => { - es.commit([goodEvent]); + it('works only once', async () => { + let callCount = 0; + const testEvent = { type: 'onceEvent' } as IEvent; + const testEvent2 = { type: 'onceEvent' } as IEvent; + const promise = store.once('onceEvent', (_e: IEvent) => { + callCount++; }); - return es.once('somethingHappened').then(e => { - expect(e).to.exist; - expect(e).to.have.property('type', goodEvent.type); - }); + await store.dispatch([testEvent, testEvent2]); + await store.dispatch([testEvent2]); + + await expect(promise).resolves.toBe(testEvent); + expect(callCount).toBe(1); }); }); }); diff --git a/tests/unit/Lock.test.ts b/tests/unit/Lock.test.ts new file mode 100644 index 0000000..0efeb91 --- /dev/null +++ b/tests/unit/Lock.test.ts @@ -0,0 +1,188 @@ +import { Lock } from '../../src/utils'; +import { promisify } from 'util'; +const delay = promisify(setTimeout); + +const isResolved = async (p?: Promise) => { + const unique = Symbol('pending'); + const result = await Promise.race([p, Promise.resolve(unique)]); + return result !== unique; +}; + +describe('Lock', () => { + + let lock: Lock; + beforeEach(() => { + lock = new Lock(); + }); + + describe('acquire', () => { + + it('acquires lock if it is not taken by another process', async () => { + // Check if acquire() resolves quickly + await expect(isResolved(lock.acquire())).resolves.toBe(true); + }); + + it('waits until previously acquired lock is released', async () => { + + await lock.acquire(); + + const l2 = lock.acquire(); + const l3 = lock.acquire(); + + // Check that l2 and l3 are pending + await expect(isResolved(l3)).resolves.toBe(false); + await expect(isResolved(l2)).resolves.toBe(false); + + await lock.release(); + + // Check that l3 is still pending, but l2 is now resolved + await expect(isResolved(l3)).resolves.toBe(false); + await expect(isResolved(l2)).resolves.toBe(true); + await l2; // Wait for l2 to fully complete if it had async operations + + await lock.release(); + + // Check that l3 is now resolved + await expect(isResolved(l3)).resolves.toBe(true); + await l3; // Wait for l3 to fully complete + + // Ensure both promises associated with acquire calls are resolved + await expect(l2).resolves.not.toThrow(); + await expect(l3).resolves.not.toThrow(); + }); + }); + + describe('isLocked', () => { + + it('returns `false` when lock is not acquired', async () => { + expect(lock).toHaveProperty('isLocked'); + expect(lock.isLocked()).toBe(false); + }); + + it('returns `true` when lock is acquired', async () => { + await lock.acquire(); + expect(lock).toHaveProperty('isLocked'); + expect(lock.isLocked()).toBe(true); + }); + + it('returns `false` when lock is released', async () => { + await lock.acquire(); + await lock.release(); + expect(lock).toHaveProperty('isLocked'); + expect(lock.isLocked()).toBe(false); + }); + }); + + describe('runExclusively', () => { + + it('executes callback with lock acquired', async () => { + + let p1status = 'not-started'; + let p2status = 'not-started'; + + const p1 = lock.runExclusively(undefined, async () => { + p1status = 'started'; + await delay(10); + p1status = 'processed'; + }); + + const p2 = lock.runExclusively(undefined, async () => { + p2status = 'started'; + await delay(5); + p2status = 'processed'; + }); + + // Check initial state: p1 started, p2 not started, both promises pending + await expect(isResolved(p1)).resolves.toBe(false); + expect(p1status).toBe('started'); + await expect(isResolved(p2)).resolves.toBe(false); + expect(p2status).toBe('not-started'); + + await p1; + + // Check state after p1 finishes: p1 processed, p2 started, p1 resolved, p2 pending + await expect(isResolved(p1)).resolves.toBe(true); + expect(p1status).toBe('processed'); + await expect(isResolved(p2)).resolves.toBe(false); + expect(p2status).toBe('started'); + + + await p2; + + // Check final state: both processed and resolved + await expect(isResolved(p1)).resolves.toBe(true); + expect(p1status).toBe('processed'); + await expect(isResolved(p2)).resolves.toBe(true); + expect(p2status).toBe('processed'); + }); + }); + + describe('waitForUnlock', () => { + + it('returns Promise', () => { + expect(lock).toHaveProperty('waitForUnlock'); + expect(lock.waitForUnlock()).toBeInstanceOf(Promise); + }); + + it('returns resolved promise when lock is not acquired', async () => { + await expect(isResolved(lock.waitForUnlock())).resolves.toBe(true); + }); + + it('returns pending promise when lock is acquired', async () => { + await lock.acquire(); + await expect(isResolved(lock.waitForUnlock())).resolves.toBe(false); + }); + + it('returns resolved promise when lock is released', async () => { + await lock.acquire(); + await lock.release(); + await expect(isResolved(lock.waitForUnlock())).resolves.toBe(true); + }); + + it('can be used to suspend non-blocking processes until lock is released', async () => { + + await lock.acquire(); // blocking process (i.e. update_by_query) + + const p2 = lock.waitForUnlock(); + const p3 = lock.waitForUnlock(); + const l4 = lock.acquire(); // blocking process (i.e. update_by_query) + const p5 = lock.waitForUnlock(); + const l6 = lock.acquire(); // blocking process (i.e. update_by_query) + + // Check all are pending initially + await expect(isResolved(p2)).resolves.toBe(false); + await expect(isResolved(p3)).resolves.toBe(false); + await expect(isResolved(l4)).resolves.toBe(false); + await expect(isResolved(p5)).resolves.toBe(false); + await expect(isResolved(l6)).resolves.toBe(false); + + await lock.release(); + + // Check p2, p3 resolve immediately, l4 acquires lock, p5, l6 still pending + await expect(isResolved(p2)).resolves.toBe(true); + await expect(isResolved(p3)).resolves.toBe(true); + await expect(isResolved(l4)).resolves.toBe(true); // l4 should resolve as it acquires the lock + await l4; // Wait for l4 acquire to complete + await expect(isResolved(p5)).resolves.toBe(false); // p5 waits for l4 + await expect(isResolved(l6)).resolves.toBe(false); // l6 waits for l4 + + // Release l4's lock + await lock.release(); + + // Check p5 resolves, l6 acquires lock + await expect(isResolved(p5)).resolves.toBe(true); + await expect(isResolved(l6)).resolves.toBe(true); // l6 should resolve as it acquires the lock + await l6; // Wait for l6 acquire to complete + + // Release l6's lock + await lock.release(); + + // Ensure all original promises eventually resolve + await expect(p2).resolves.not.toThrow(); + await expect(p3).resolves.not.toThrow(); + await expect(l4).resolves.not.toThrow(); + await expect(p5).resolves.not.toThrow(); + await expect(l6).resolves.not.toThrow(); + }); + }); +}); diff --git a/tests/unit/SagaEventHandler.test.ts b/tests/unit/SagaEventHandler.test.ts index 83c0f02..ac0d064 100644 --- a/tests/unit/SagaEventHandler.test.ts +++ b/tests/unit/SagaEventHandler.test.ts @@ -7,8 +7,9 @@ import { CommandBus, AbstractSaga, InMemoryMessageBus, - Deferred + EventDispatcher } from '../../src'; +import { Deferred } from '../../src/utils'; class Saga extends AbstractSaga { static get startsWith() { @@ -17,12 +18,12 @@ class Saga extends AbstractSaga { static get handles(): string[] { return ['followingHappened']; } - somethingHappened(event) { + somethingHappened(_event) { super.enqueue('doSomething', undefined, { foo: 'bar' }); } followingHappened() { super.enqueue('complete', undefined, { foo: 'bar' }); - } + } onError(error, { command, event }) { super.enqueue('fixError', undefined, { error, command, event }); } @@ -42,9 +43,16 @@ describe('SagaEventHandler', function () { let sagaEventHandler: SagaEventHandler; beforeEach(() => { - const messageBus = new InMemoryMessageBus(); - commandBus = new CommandBus({ messageBus }); - eventStore = new EventStore({ storage: new InMemoryEventStorage(), messageBus }); + const eventBus = new InMemoryMessageBus(); + const eventDispatcher = new EventDispatcher({ eventBus }); + const eventStorageReader = new InMemoryEventStorage(); + commandBus = new CommandBus({}); + eventStore = new EventStore({ + eventStorageReader, + identifierProvider: eventStorageReader, + eventBus, + eventDispatcher + }); sagaEventHandler = new SagaEventHandler({ sagaType: Saga, eventStore, commandBus }); }); @@ -58,7 +66,7 @@ describe('SagaEventHandler', function () { commandBus.on('complete', () => { deferred.resolve(undefined); - }) + }); sinon.spy(eventStore, 'getSagaEvents'); @@ -86,7 +94,7 @@ describe('SagaEventHandler', function () { commandBus.on('fixError', command => { resolvePromise(command); }); - commandBus.on('doSomething', command => { + commandBus.on('doSomething', _command => { throw new Error('command execution failed'); }); diff --git a/tests/unit/dispatch-pipeline.test.ts b/tests/unit/dispatch-pipeline.test.ts new file mode 100644 index 0000000..05dc35b --- /dev/null +++ b/tests/unit/dispatch-pipeline.test.ts @@ -0,0 +1,60 @@ +import { InMemorySnapshotStorage } from '../../dist/in-memory/InMemorySnapshotStorage'; +import { + ContainerBuilder, + IContainer, + InMemoryEventStorage +} from '../../src'; + +describe('eventDispatchPipeline', () => { + + let container: IContainer; + + const testEvent = { + type: 'test-event', + aggregateId: '123', + payload: { data: 'test-payload' }, + id: 'test-id-123' + }; + + beforeEach(() => { + const builder = new ContainerBuilder(); + + builder.register(InMemoryEventStorage).as('eventStorageWriter'); + builder.register(InMemorySnapshotStorage).as('snapshotStorage'); + builder.register((c: IContainer) => [ + c.eventStorageWriter, + c.snapshotStorage + ]).as('eventDispatchPipeline'); + + container = builder.container() as IContainer; + }); + + it('delivers all events to eventStorageWriter', async () => { + + const { eventDispatcher, eventStorageWriter } = container; + + jest.spyOn(eventStorageWriter, 'commitEvents'); + + await eventDispatcher.dispatch([testEvent], { origin: 'internal' }); + await eventDispatcher.dispatch([testEvent], { origin: 'external' }); + + expect(eventStorageWriter.commitEvents).toHaveBeenCalledTimes(2); + expect(eventStorageWriter.commitEvents).toHaveBeenNthCalledWith(1, [testEvent]); + expect(eventStorageWriter.commitEvents).toHaveBeenNthCalledWith(2, [testEvent]); + }); + + + it('delivers all events to eventBus', async () => { + + const { eventDispatcher, eventBus } = container; + + jest.spyOn(eventBus, 'publish'); + + await eventDispatcher.dispatch([testEvent], { origin: 'internal' }); + await eventDispatcher.dispatch([testEvent], { origin: 'external' }); + + expect(eventBus.publish).toHaveBeenCalledTimes(2); + expect(eventBus.publish).toHaveBeenNthCalledWith(1, testEvent, { origin: 'internal' }); + expect(eventBus.publish).toHaveBeenNthCalledWith(2, testEvent, { origin: 'external' }); + }); +}); diff --git a/tests/unit/memory/InMemoryEventStorage.test.ts b/tests/unit/memory/InMemoryEventStorage.test.ts new file mode 100644 index 0000000..ca47e57 --- /dev/null +++ b/tests/unit/memory/InMemoryEventStorage.test.ts @@ -0,0 +1,128 @@ +import { expect } from 'chai'; +import { InMemoryEventStorage } from '../../../src'; + +describe('InMemoryEventStorage', () => { + let storage; + + beforeEach(() => { + storage = new InMemoryEventStorage(); + }); + + describe('commitEvents', () => { + it('commits events and returns them', async () => { + const events = [ + { id: '1', aggregateId: 'agg1', aggregateVersion: 1, type: 'TestEvent' } + ]; + const result = await storage.commitEvents(events); + expect(result).to.deep.equal(events); + }); + }); + + describe('getAggregateEvents', () => { + + it('yields events with matching aggregateId', async () => { + + const event1 = { id: '1', aggregateId: 'agg1', aggregateVersion: 1, type: 'TestEvent' }; + const event2 = { id: '2', aggregateId: 'agg2', aggregateVersion: 1, type: 'TestEvent' }; + await storage.commitEvents([event1, event2]); + + const results = []; + for await (const event of storage.getAggregateEvents('agg1')) + results.push(event); + + expect(results).to.deep.equal([event1]); + }); + + it('yields events with aggregateVersion greater than snapshot.aggregateVersion', async () => { + + const event1 = { id: '1', aggregateId: 'agg1', aggregateVersion: 1, type: 'TestEvent' }; + const event2 = { id: '2', aggregateId: 'agg1', aggregateVersion: 2, type: 'TestEvent' }; + await storage.commitEvents([event1, event2]); + + const snapshot = { aggregateVersion: 1 }; + const results = []; + for await (const event of storage.getAggregateEvents('agg1', { snapshot })) + results.push(event); + + expect(results).to.deep.equal([event2]); + }); + }); + + describe('getSagaEvents', () => { + + it('yields saga events with sagaVersion less than beforeEvent.sagaVersion', async () => { + + const event1 = { id: '1', sagaId: 'saga1', sagaVersion: 1, type: 'SagaEvent' }; + const event2 = { id: '2', sagaId: 'saga1', sagaVersion: 2, type: 'SagaEvent' }; + const event3 = { id: '3', sagaId: 'saga1', sagaVersion: 3, type: 'SagaEvent' }; + await storage.commitEvents([event1, event2, event3]); + + const beforeEvent = { sagaVersion: 3 }; + const results = []; + for await (const event of storage.getSagaEvents('saga1', { beforeEvent })) + results.push(event); + + expect(results).to.deep.equal([event1, event2]); + }); + }); + + describe('getEventsByTypes', () => { + + it('yields events matching the provided types', async () => { + + const event1 = { id: '1', type: 'A' }; + const event2 = { id: '2', type: 'B' }; + const event3 = { id: '3', type: 'A' }; + await storage.commitEvents([event1, event2, event3]); + + const results = []; + for await (const event of storage.getEventsByTypes(['A'])) + results.push(event); + + expect(results).to.deep.equal([event1, event3]); + }); + + it('yields events only after the given afterEvent id', async () => { + + const event1 = { id: '1', type: 'A' }; + const event2 = { id: '2', type: 'A' }; + const event3 = { id: '3', type: 'A' }; + await storage.commitEvents([event1, event2, event3]); + + const options = { afterEvent: { id: '1' } }; + const results = []; + for await (const event of storage.getEventsByTypes(['A'], options)) + results.push(event); + + expect(results).to.deep.equal([event2, event3]); + }); + + it('throws error if afterEvent is provided without id', async () => { + + const event1 = { id: '1', type: 'A' }; + await storage.commitEvents([event1]); + const options = { afterEvent: {} }; + + const gen = storage.getEventsByTypes(['A'], options); + try { + await gen.next(); + throw new Error('Expected error was not thrown'); + } + catch (err) { + expect(err).to.be.instanceOf(TypeError); + expect(err.message).to.equal('options.afterEvent.id is required'); + } + }); + }); + + describe('getNewId', () => { + + it('returns sequential string ids', () => { + + const id1 = storage.getNewId(); + const id2 = storage.getNewId(); + expect(id1).to.equal('1'); + expect(id2).to.equal('2'); + }); + }); +}); diff --git a/tests/unit/memory/InMemoryLock.test.ts b/tests/unit/memory/InMemoryLock.test.ts new file mode 100644 index 0000000..ce9cd91 --- /dev/null +++ b/tests/unit/memory/InMemoryLock.test.ts @@ -0,0 +1,91 @@ +import { expect } from 'chai'; +import { InMemoryLock } from '../../../src'; + +describe('InMemoryLock', () => { + let lock: InMemoryLock; + + beforeEach(() => { + lock = new InMemoryLock(); + }); + + it('should call each method explicitly to satisfy coverage', async () => { + await lock.lock(); + await lock.unlock(); + await lock.once('unlocked'); // Even if tested elsewhere, call it directly + }); + + it('starts unlocked', () => { + expect(lock.locked).to.be.false; + }); + + it('acquires a lock', async () => { + await lock.lock(); + expect(lock.locked).to.be.true; + }); + + it('blocks second lock() call until unlocked', async () => { + await lock.lock(); + let secondLockAcquired = false; + + // Try acquiring the lock again, but in a separate async operation + const secondLock = lock.lock().then(() => { + secondLockAcquired = true; + }); + + // Ensure second lock() is still waiting + await new Promise(resolve => setTimeout(resolve, 100)); + expect(secondLockAcquired).to.be.false; + + // Unlock and allow second lock to proceed + await lock.unlock(); + await secondLock; + expect(secondLockAcquired).to.be.true; + }); + + it('unlocks the lock', async () => { + await lock.lock(); + expect(lock.locked).to.be.true; + + await lock.unlock(); + expect(lock.locked).to.be.false; + }); + + it('resolves once() immediately if not locked', async () => { + let resolved = false; + + await lock.once('unlocked').then(() => { + resolved = true; + }); + + expect(resolved).to.be.true; + }); + + it('resolves once() only after unlocking', async () => { + await lock.lock(); + let resolved = false; + + const waitForUnlock = lock.once('unlocked').then(() => { + resolved = true; + }); + + // Ensure it's still waiting + await new Promise(resolve => setTimeout(resolve, 100)); + expect(resolved).to.be.false; + + // Unlock and verify resolution + await lock.unlock(); + await waitForUnlock; + expect(resolved).to.be.true; + }); + + it('handles multiple unlock() calls gracefully', async () => { + await lock.lock(); + await lock.unlock(); + await lock.unlock(); // Should not throw or change state + expect(lock.locked).to.be.false; + }); + + it('throws an error for unexpected event types in once()', () => { + expect(() => lock.once('invalid_event')).to.throw(TypeError); + }); +}); diff --git a/tests/unit/InMemoryMessageBus.test.ts b/tests/unit/memory/InMemoryMessageBus.test.ts similarity index 91% rename from tests/unit/InMemoryMessageBus.test.ts rename to tests/unit/memory/InMemoryMessageBus.test.ts index 511d5f5..f260a82 100644 --- a/tests/unit/InMemoryMessageBus.test.ts +++ b/tests/unit/memory/InMemoryMessageBus.test.ts @@ -1,11 +1,13 @@ -import { IMessageBus, InMemoryMessageBus } from '../..'; -import { expect, assert, AssertionError } from 'chai'; +import { IMessageBus, InMemoryMessageBus } from '../../../src'; +import { expect, AssertionError } from 'chai'; import { spy } from 'sinon'; describe('InMemoryMessageBus', function () { let bus: IMessageBus; - beforeEach(() => bus = new InMemoryMessageBus()); + beforeEach(() => { + bus = new InMemoryMessageBus(); + }); describe('send(command)', function () { @@ -13,7 +15,6 @@ describe('InMemoryMessageBus', function () { bus.on('doSomething', cmd => { try { - // console.log(cmd); expect(cmd).to.have.nested.property('payload.message', 'test'); done(); } diff --git a/tests/unit/InMemoryView.test.ts b/tests/unit/memory/InMemoryView.test.ts similarity index 91% rename from tests/unit/InMemoryView.test.ts rename to tests/unit/memory/InMemoryView.test.ts index 7bdb6e7..a233997 100644 --- a/tests/unit/InMemoryView.test.ts +++ b/tests/unit/memory/InMemoryView.test.ts @@ -1,6 +1,6 @@ -import { InMemoryView } from '../../src/infrastructure/InMemoryView'; +import { InMemoryView } from '../../../src'; import { expect, assert } from 'chai'; -import { nextCycle } from '../../src/infrastructure/utils'; +import { nextCycle } from '../../../src/in-memory/utils'; describe('InMemoryView', function () { @@ -12,6 +12,8 @@ describe('InMemoryView', function () { describe('create', () => { + beforeEach(() => v.unlock()); + it('creates a record', async () => { await v.create('foo', 'bar'); @@ -23,14 +25,31 @@ describe('InMemoryView', function () { await v.create('foo', 'bar'); - try{ + try { await v.create('foo', 'bar'); assert(false, 'did not throw'); } - catch(e: any) { + catch (e: any) { expect(e).to.have.property('message', 'Key \'foo\' already exists'); } }); + + it('creates new record, as passed in value', async () => { + + await v.create('foo', 'bar'); + expect(await v.get('foo')).to.eq('bar'); + }); + + it('fails, when trying to pass a function as a value', async () => { + try { + await v.create('foo', () => 'bar'); + assert(false, 'did not throw'); + } + catch (err) { + if (!(err instanceof TypeError)) + throw err; + } + }); }); describe('size', () => { @@ -148,28 +167,6 @@ describe('InMemoryView', function () { }); }); - describe('create', () => { - - beforeEach(() => v.unlock()); - - it('creates new record, as passed in value', async () => { - - await v.create('foo', 'bar'); - expect(await v.get('foo')).to.eq('bar'); - }); - - it('fails, when trying to pass a function as a value', async () => { - try { - await v.create('foo', () => 'bar'); - assert(false, 'did not throw'); - } - catch (err) { - if (!(err instanceof TypeError)) - throw err; - } - }); - }); - describe('update', () => { beforeEach(() => v.unlock()); @@ -180,7 +177,7 @@ describe('InMemoryView', function () { await v.update('foo', () => null); assert(false, 'did not throw'); } - catch(e: any) { + catch (e: any) { expect(e).to.have.property('message', 'Key \'foo\' does not exist'); } }); @@ -191,7 +188,7 @@ describe('InMemoryView', function () { expect(await v.get('foo')).to.eq('bar'); - await v.updateEnforcingNew('foo', v => `${v}-upd`); + await v.updateEnforcingNew('foo', val => `${val}-upd`); expect(await v.get('foo')).to.eq('bar-upd'); }); @@ -202,8 +199,8 @@ describe('InMemoryView', function () { expect(await v.get('foo')).to.deep.eq({ x: 'bar' }); - await v.updateEnforcingNew('foo', v => { - v.x += '-upd'; + await v.updateEnforcingNew('foo', val => { + val.x += '-upd'; }); expect(await v.get('foo')).to.deep.eq({ x: 'bar-upd' }); @@ -229,7 +226,7 @@ describe('InMemoryView', function () { expect(await v.get('foo')).to.eq('bar'); - await v.updateEnforcingNew('foo', v => `${v}-upd`); + await v.updateEnforcingNew('foo', val => `${val}-upd`); expect(await v.get('foo')).to.eq('bar-upd'); }); @@ -243,7 +240,7 @@ describe('InMemoryView', function () { await v.create('x', { v: 'y' }); await v.unlock(); - await v.updateAll(v => typeof v === 'string', v => `${v}-updated`); + await v.updateAll(val => typeof val === 'string', val => `${val}-updated`); expect(await v.get('foo')).to.eq('bar-updated'); expect(await v.get('x')).to.eql({ v: 'y' }); @@ -279,7 +276,7 @@ describe('InMemoryView', function () { await v.create('x', { v: 'y' }); await v.unlock(); - await v.deleteAll(v => typeof v === 'object'); + await v.deleteAll(val => typeof val === 'object'); expect(await v.get('foo')).to.eq('bar'); expect(await v.get('x')).to.eq(undefined); diff --git a/tests/unit/sqlite/SqliteEventLocker.test.ts b/tests/unit/sqlite/SqliteEventLocker.test.ts new file mode 100644 index 0000000..243e773 --- /dev/null +++ b/tests/unit/sqlite/SqliteEventLocker.test.ts @@ -0,0 +1,90 @@ +import * as createDb from 'better-sqlite3'; +import { SqliteEventLocker } from '../../../src/sqlite/SqliteEventLocker'; +import { IEvent } from '../../../src/interfaces'; +import { guid } from '../../../src/sqlite'; +import { promisify } from 'util'; +const delay = promisify(setTimeout); + +describe('SqliteEventLocker', () => { + + let db: import('better-sqlite3').Database; + let locker: SqliteEventLocker; + const testEvent: IEvent = { id: 'event1', type: 'TEST_EVENT', payload: {} }; + + beforeEach(() => { + db = createDb(':memory:'); + locker = new SqliteEventLocker({ + viewModelSqliteDb: db, + projectionName: 'test', + schemaVersion: '1.0', + eventLockTableName: 'test_event_lock', + viewLockTableName: 'test_view_lock', + eventLockTtl: 50 // ms + }); + }); + + afterEach(() => { + db.close(); + }); + + it('allows marking an event as projecting', async () => { + const result = await locker.tryMarkAsProjecting(testEvent); + expect(result).toBe(true); + }); + + it('prevents re-locking an already locked event', async () => { + await locker.tryMarkAsProjecting(testEvent); + const result = await locker.tryMarkAsProjecting(testEvent); + expect(result).toBe(false); + }); + + it('marks an event as projected', async () => { + await locker.tryMarkAsProjecting(testEvent); + await locker.markAsProjected(testEvent); // Assuming markAsProjected might become async + + // DB query remains synchronous with better-sqlite3 + const row = db.prepare('SELECT processed_at FROM test_event_lock WHERE event_id = ?') + .get(guid(testEvent.id)) as any; + + expect(row).toBeDefined(); + expect(row.processed_at).not.toBeNull(); + }); + + it('retrieves the last projected event', async () => { + await locker.tryMarkAsProjecting(testEvent); + await locker.markAsProjected(testEvent); + + const lastEvent = await locker.getLastEvent(); // Assuming getLastEvent might become async + + expect(lastEvent).toEqual(testEvent); + }); + + it('returns undefined if no event has been projected', async () => { + const lastEvent = await locker.getLastEvent(); + expect(lastEvent).toBeUndefined(); + }); + + it('fails to mark an event as projected if it was never locked', async () => { + await expect(() => locker.markAsProjected(testEvent)) + .rejects.toThrow(`Event ${testEvent.id} could not be marked as processed`); + }); + + it('allows re-locking after TTL expires', async () => { + await locker.tryMarkAsProjecting(testEvent); + + await delay(51); // Wait for TTL to expire + + const result = await locker.tryMarkAsProjecting(testEvent); + expect(result).toBe(true); + }); + + it('fails to update an event if its version is modified in DB', async () => { + await locker.tryMarkAsProjecting(testEvent); + + db.prepare('UPDATE test_event_lock SET processed_at = ? WHERE event_id = ?') + .run(Date.now(), guid(testEvent.id)); + + await expect(() => locker.markAsProjected(testEvent)) + .rejects.toThrow(`Event ${testEvent.id} could not be marked as processed`); + }); +}); diff --git a/tests/unit/sqlite/SqliteObjectStorage.test.ts b/tests/unit/sqlite/SqliteObjectStorage.test.ts new file mode 100644 index 0000000..cd4bcff --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -0,0 +1,112 @@ +import * as createDb from 'better-sqlite3'; +import { guid, SqliteObjectStorage } from '../../../src/sqlite'; + +describe('SqliteObjectStorage', function () { + let db: import('better-sqlite3').Database; + let storage: SqliteObjectStorage<{ name: string; value: number }>; + + beforeEach(async () => { + db = createDb(':memory:'); + storage = new SqliteObjectStorage<{ name: string; value: number }>({ + viewModelSqliteDb: db, + tableName: 'test_objects' + }); + await storage.assertConnection(); + }); + + afterEach(() => { + db.close(); + }); + + it('stores and retrieves an object', async function () { + + const obj = { name: 'Test Object', value: 42 }; + await storage.create('0001', obj); + + const retrieved = await storage.get('0001'); + expect(retrieved).toEqual(obj); + }); + + it('returns undefined for a non-existent object', async function () { + const retrieved = await storage.get('nonexistent'); + expect(retrieved).not.toBeDefined(); + }); + + it('updates an existing object', async function () { + + await storage.create('0002', { name: 'Old Data', value: 5 }); + + await storage.update('0002', r => ({ ...r, value: 99 })); + + const updated = await storage.get('0002'); + expect(updated).toEqual({ name: 'Old Data', value: 99 }); + }); + + it('throws an error when updating a non-existent object', async function () { + + await expect(() => storage.update('nonexistent', r => ({ ...r, value: 99 }))) + .rejects.toThrow("Record 'nonexistent' does not exist"); + }); + + it('deletes an object', async function () { + + storage.create('0003', { name: 'To be deleted', value: 10 }); + const deleted = storage.delete('0003'); + expect(deleted).toBeTruthy(); + + const retrieved = storage.get('0003'); + expect(retrieved).toBeDefined(); + }); + + it('returns false when deleting a non-existent object', async function () { + + const deleted = await storage.delete('0000'); + expect(deleted).toBeFalsy(); + }); + + it('enforces updating or creating a new object', async function () { + + await storage.updateEnforcingNew('0004', () => ({ name: 'Created', value: 1 })); + + let retrieved = await storage.get('0004'); + expect(retrieved).toEqual({ name: 'Created', value: 1 }); + + await storage.updateEnforcingNew('0004', r => ({ ...r!, value: 100 })); + + retrieved = await storage.get('0004'); + expect(retrieved).toEqual({ name: 'Created', value: 100 }); + }); + + it('fails if invalid JSON is recorded', async function () { + db.prepare('INSERT INTO test_objects (id, data) VALUES (?, ?)') + .run(guid('0005'), 'INVALID_JSON'); + + await expect(() => storage.get('0005')).rejects.toThrow(); + }); + + it('updateEnforcingNew is safe under same-process concurrency', async function () { + const id = '00000000000000000000000000000001'; + const concurrency = 50; + + const results = await Promise.allSettled( + Array.from({ length: concurrency }, () => storage.updateEnforcingNew(id, r => ({ + name: 'counter', + value: (r?.value ?? 0) + 1 + }))) + ); + + const rejected = results.filter(r => r.status === 'rejected'); + expect(rejected).toEqual([]); + + const record = await storage.get(id); + expect(record).toEqual({ name: 'counter', value: concurrency }); + + const row = db.prepare(` + SELECT version + FROM test_objects + WHERE id = ? + `).get(guid(id)) as { version: number } | undefined; + + expect(row?.version).toBe(concurrency); + }); +}); diff --git a/tests/unit/sqlite/SqliteObjectView.test.ts b/tests/unit/sqlite/SqliteObjectView.test.ts new file mode 100644 index 0000000..3aa5de4 --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectView.test.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteObjectView } from '../../../src/sqlite'; +import { promisify } from 'util'; +const delay = promisify(setTimeout); + +describe('SqliteObjectView', function () { + let viewModelSqliteDb: import('better-sqlite3').Database; + let sqliteObjectView: SqliteObjectView; + + beforeEach(() => { + viewModelSqliteDb = createDb(':memory:'); + sqliteObjectView = new SqliteObjectView({ + viewModelSqliteDb, + projectionName: 'test', + tableNamePrefix: 'tbl_test', + schemaVersion: '1' + }); + }); + + describe('get', () => { + + it('throws an error if id is not a non-empty string', async () => { + + let error; + try { + error = null; + await sqliteObjectView.get(''); + } + catch (err) { + error = err; + } + expect(error).to.exist; + expect(error).to.have.property('message', 'id argument must be a non-empty String'); + + }); + + it('waits for readiness before returning data', async () => { + + await sqliteObjectView.lock(); + + expect(sqliteObjectView).to.have.property('ready', false); + + let resultObtained = false; + const resultPromise = sqliteObjectView.get('test').then(() => { + resultObtained = true; + }); + + await delay(5); + expect(resultObtained).to.eq(false); + + sqliteObjectView.unlock(); + + + await resultPromise; + expect(resultObtained).to.eq(true); + }); + + it('returns stored record if ready', async () => { + + sqliteObjectView.create('1', { foo: 'bar' }); + + const r = await sqliteObjectView.get('1'); + expect(r).to.eql({ foo: 'bar' }); + }); + + it('returns undefined if record does not exist', async () => { + + const r = await sqliteObjectView.get('1'); + expect(r).to.eql(undefined); + }); + }); +}); diff --git a/tests/unit/sqlite/SqliteViewLocker.test.ts b/tests/unit/sqlite/SqliteViewLocker.test.ts new file mode 100644 index 0000000..fd0f8c5 --- /dev/null +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteViewLocker } from '../../../src/sqlite'; + +describe('SqliteViewLocker', function () { + + const viewLockTtl = 1_000; // 1sec + let viewModelSqliteDb: import('better-sqlite3').Database; + let firstLock: SqliteViewLocker; + let secondLock: SqliteViewLocker; + + beforeEach(() => { + viewModelSqliteDb = createDb(':memory:'); + firstLock = new SqliteViewLocker({ + viewModelSqliteDb, + projectionName: 'test', + schemaVersion: '1.0', + viewLockTtl + }); + secondLock = new SqliteViewLocker({ + viewModelSqliteDb, + projectionName: 'test', + schemaVersion: '1.0', + viewLockTtl + }); + + jest.useFakeTimers(); + }); + + afterEach(() => { + viewModelSqliteDb.close(); + }); + + it('locks a view successfully', async function () { + const result = await firstLock.lock(); + expect(result).to.be.true; + }); + + it('unlocks a view successfully', async function () { + await firstLock.lock(); + firstLock.unlock(); + + const lockResult = await secondLock.lock(); + expect(lockResult).to.be.true; + }); + + it('sets ready flag to `false` when locked', async () => { + + await firstLock.lock(); + expect(firstLock).to.have.property('ready', false); + }); + + it('sets ready flag to `true` when unlocked', async () => { + + await firstLock.lock(); + await firstLock.unlock(); + expect(firstLock).to.have.property('ready', true); + }); + + it('waits for the lock to be released if already locked', async function () { + await firstLock.lock(); + + let secondLockAcquired = false; + + // Try locking, but it should wait + const secondLockAcquiring = secondLock.lock().then(() => { + secondLockAcquired = true; + }); + + // Wait briefly to check if it resolves too soon + await jest.advanceTimersByTimeAsync(viewLockTtl); + expect(secondLockAcquired).to.be.false; + + firstLock.unlock(); + + await secondLockAcquiring; + expect(secondLockAcquired).to.be.true; + }); + + + it('prolongs the lock while active', async function () { + await firstLock.lock(); + + const initial = viewModelSqliteDb.prepare('SELECT * FROM tbl_view_lock WHERE projection_name = ? AND schema_version = ?') + .get('test', '1.0') as any; + + expect(initial).to.have.property('locked_till').that.is.gt(Date.now()); + + await jest.advanceTimersByTimeAsync(viewLockTtl); + + const updated = viewModelSqliteDb.prepare('SELECT * FROM tbl_view_lock WHERE projection_name = ? AND schema_version = ?') + .get('test', '1.0') as any; + + expect(updated).to.have.property('locked_till').that.is.gt(initial.locked_till); + }); + + it('should release the lock upon unlock()', async function () { + await firstLock.lock(); + await firstLock.unlock(); + + const row = viewModelSqliteDb.prepare('SELECT * FROM tbl_view_lock WHERE projection_name = ? AND schema_version = ?') + .get('test', '1.0') as any; + + expect(row.locked_till).to.be.null; + }); + + it('should fail to prolong the lock if already released', async function () { + await firstLock.lock(); + await firstLock.unlock(); + + let error; + try { + await (firstLock as any).prolongLock(); + } + catch (err) { + error = err; + } + + expect(error).to.exist; + expect(error).to.have.property('message', '"test" lock could not be prolonged'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 6f3a80f..2ac6347 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,11 +6,17 @@ "sourceMap": true, "alwaysStrict": false, "outDir": "./dist", - "target": "ESNext", - "declaration": false, - "strictNullChecks": true, + "target": "ES2022", + "declaration": true, + "declarationDir": "./types", "allowSyntheticDefaultImports": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "isolatedModules": true }, "include": [ "src/**/*" @@ -19,4 +25,4 @@ "node_modules", "**/*.spec.ts" ] -} +} \ No newline at end of file