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 deleted file mode 100644 index 3df82f2..0000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Audit - -on: - push: - branches: - - master - pull_request: - -jobs: - audit: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [20.x] - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - 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..0e3644e 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -4,17 +4,18 @@ on: push: branches: - master + pull_request: jobs: test: 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..95aaefb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,19 +7,47 @@ on: pull_request: jobs: + audit: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [24.x] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm audit --parseable --production --audit-level=moderate + test: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.x, 20.x, 22.x] + node-version: [16.x, 24.x, 25.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 + - run: npm ci --omit=optional env: CI: true - run: npm run test - - run: npm run test:integration + + lint: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [24.x] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci --omit=optional + env: + CI: true + - run: npm run lint diff --git a/.gitignore b/.gitignore index ea0b7b1..98badc4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ app/tests/coverage/ coverage/ .nyc_output/ dist/ +types/ *.tgz # IDE's @@ -29,6 +30,7 @@ data/ mongod *.sublime-project *.sublime-workspace +.obsidian/ # OS-specific # =========== 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/LICENSE b/LICENSE index e02a6fa..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,202 @@ -(The MIT License) - -Copyright (c) 2017 Stanislav Natalenko - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..3c2a56c --- /dev/null +++ b/NOTICE @@ -0,0 +1,15 @@ +node-cqrs + +Copyright (c) 2015-2026 Stanislav Natalenko + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index 7750413..909097d 100644 --- a/README.md +++ b/README.md @@ -7,182 +7,469 @@ node-cqrs [![Coverage Status](https://coveralls.io/repos/github/snatalenko/node-cqrs/badge.svg?branch=master)](https://coveralls.io/github/snatalenko/node-cqrs?branch=master) [![NPM Downloads](https://img.shields.io/npm/dm/node-cqrs.svg)](https://www.npmjs.com/package/node-cqrs) +Infrastructure-agnostic building blocks for CQRS/ES, inspired by Lokad.CQRS. + +CQRS/ES can be simple in a single process. Minimal code, no framework: +[examples/user-domain-own-implementation/index.ts](examples/user-domain-own-implementation/index.ts) + +This library focuses on the "boring but hard" parts often missing from plain CQRS/ES implementations, but required in distributed environments: + +- asynchronous command and event processing with safer wiring +- persistent views with restart catch-up (checkpointing, readiness, locking) +- aggregate snapshots +- extensible event dispatching pipelines (encoding, persistence, distribution) + +It is built around ES6/TypeScript classes and dependency injection, making components easy to replace or customize without patching the library. + + ## Overview -The package provides building blocks for making a CQRS-ES application. It was inspired by Lokad.CQRS, but not tied to a specific storage implementation or infrastructure. It favors ES6 classes and dependency injection, so any components can be modified or replaced with your own implementations without hacks to the package codebase. +At a high level, the command and event flow looks like this: -[Documentation at node-cqrs.org](https://www.node-cqrs.org) +![Overview](docs/images/node-cqrs-flow.svg) -Your app is expected to operate with loosely typed commands and events that match the following interface: +Commands and events are loosely typed objects implementing the [`IMessage`](src/interfaces/IMessage.ts) interface: ```ts -declare interface IMessage { - type: string, +interface IMessage { + type: string; - aggregateId?: string|number, - aggregateVersion?: number, + aggregateId?: string | number; + aggregateVersion?: number; - sagaId?: string|number, - sagaVersion?: number, + sagaId?: string | number; + sagaVersion?: number; - payload?: any, - context?: any + payload?: TPayload; + context?: any; } ``` -Domain business logic should be placed in Aggregate, Saga and Projection classes: +Domain logic is split across three core building blocks: -- [Aggregates](entities/Aggregate/README.MD) handle commands and emit events -- [Sagas](entities/Saga/README.MD) handle events and enqueue commands -- [Projections](entities/Projection/README.md) listen to events and update views +- **[Aggregates](#aggregates-write-model)** - handle commands and emit events +- **[Projections](#projections-and-views-read-model)** - consume events and update views +- **Sagas** - manage processes by reacting to events and enqueueing follow-up commands +Message delivery is handled by the following components, in order: -Message delivery is being handled by the following services (in order of appearance): +- **[Command Bus](src/CommandBus.ts)** - routes commands to handlers +- **[Aggregate Command Handler](src/AggregateCommandHandler.ts)** - restores aggregate state and executes commands +- **[Event Store](src/EventStore.ts)** — runs the event dispatch pipeline (e.g. encoding, persistence), then publishes events to the event bus for delivery to all subscribers +- **[Saga Event Handler](src/SagaEventHandler.ts)** - restores saga state and applies events -- **Command Bus** delivers commands to command handlers -- [Aggregate Command Handler](middleware/AggregateCommandHandler.md) restores an aggregate state, executes a command -- **Event Store** persists events and deliver them to event handlers (saga event handlers, projections or any other custom services) -- **Saga Event Handler** restores saga state and applies event +**Tip**: the codebase is intentionally small and readable. `src/`, `tests/`, and `examples/` are good entry points for exploring behavior. +### Examples -From a high level, this is how the command/event flow looks like: +- [examples/browser-smoke-test](examples/browser-smoke-test) - browser smoke test with in-memory storage and buses +- [examples/user-domain](examples/user-domain) - basic CJS implementation +- [examples/user-domain-own-implementation](examples/user-domain-own-implementation/index.ts) minimal, framework-free CQRS/ES example in 1 file +- [examples/user-domain-ts](examples/user-domain-ts) - basic TypeScript implementation +- [examples/worker-projection](examples/worker-projection) - projection in a worker thread -![Overview](docs/images/node-cqrs-components.png) +TS examples can be run with `node` without transpiling. -## Getting Started +## Installation -You can find sample code of a User domain in the **/examples** folder. +```bash +npm install node-cqrs +``` +### Supported environments -### Your App → Command → Aggregate +- Node.js 16+ +- Browser (via [browserify](https://browserify.org)) -Describe an aggregate that handles a command: +### Optional peer dependencies -```js -const { AbstractAggregate } = require('node-cqrs'); +Required only if you use the corresponding infrastructure modules: -class UserAggregate extends AbstractAggregate { - static get handles() { - return ['createUser']; +- SQLite: [better-sqlite3](https://www.npmjs.com/package/better-sqlite3), [md5](https://www.npmjs.com/package/md5) +- RabbitMQ: [amqplib](https://www.npmjs.com/package/amqplib) +- Worker threads: [comlink](https://www.npmjs.com/package/comlink) + + +## ContainerBuilder + +The recommended approach is to use dependency injection to wire buses, the event store, +and your aggregates, projections, and sagas: + +```ts +const builder = new ContainerBuilder(); + +// In-memory implementations for local dev/tests +builder.register(InMemoryEventStorage) + .as('identifierProvider') // EventStore dependency to generate new aggregate and saga ID's + .as('eventStorageReader') // EventStore dependency to read events from + .as('eventStorageWriter'); // eventStorageWriter, when provided, is automatically added to the dispatch pipeline + +builder.registerAggregate(UserAggregate); +builder.registerProjection(UsersProjection, 'users'); + +const container = builder.container(); +``` + +Once created, the container exposes `commandBus` for sending commands and the `users` view managed by the projection. + +If you prefer not to use the DI container, the same wiring can be done manually: + +
+Manual setup (without DI container) + +```ts +const inMemoryMessageBus = new InMemoryMessageBus(); +const eventStorage = new InMemoryEventStorage(); +const eventStore = new EventStore({ + eventStorageReader: eventStorage, + identifierProvider: eventStorage, + eventDispatchPipeline: [eventStorage], + eventBus: inMemoryMessageBus +}); + +const commandBus = new CommandBus(); + +const aggregateCommandHandler = new AggregateCommandHandler({ + eventStore, + aggregateType: UserAggregate +}); +aggregateCommandHandler.subscribe(commandBus); + +const projection = new UsersProjection(); +await projection.subscribe(eventStore); + +const users = projection.view; +``` + +
+ +## Commands + +Commands represent intent and are sent to the `CommandBus`: + +- sent to the CommandBus explicitly +- handled by [Aggregates](#aggregates-write-model) +- may be enqueued by Sagas + +Command example (raw form): + +```json +{ + "type": "signupUser", + "payload": { + "profile": { + "name": "John Doe", + "email": "john@example.com" + }, + "password": "test" + }, + "context": { + "ip": "127.0.0.1", + "ts": 1503509747154 } - - createUser(commandPayload) { - // ... +} +``` + +The `commandBus` exposed by the container is an instance of [CommandBus](src/CommandBus.ts) and provides two methods: + +- `sendRaw(command)` - sends a fully constructed command object +- `send(type, aggregateId, { payload, context })` - a shorthand helper for common cases + +Example: + +```ts +commandBus.send('signupUser', undefined, { + payload: { profile, password } +}); +``` + + +## Events + +Events represent facts that have already happened: + +- produced by [Aggregates](#aggregates-write-model) +- persisted by the Event Store +- delivered to [Projections](#projections-and-views-read-model), Sagas, and Event Receptors + +Event example: + +```json +{ + "type": "userSignedUp", + "aggregateId": 1, + "aggregateVersion": 0, + "payload": { + "profile": { + "name": "John Doe", + "email": "john@example.com" + }, + "passwordHash": "098f6bcd4621d373cade4e832627b4f6" + }, + "context": { + "ip": "127.0.0.1", + "ts": 1503509747154 } } ``` -Then register aggregate in the [DI container](middleware/DIContainer.md). -All the wiring can be done manually, without a DI container (you can find it in samples), but with container it’s just easier: -```js -const { ContainerBuilder, InMemoryEventStorage } = require('node-cqrs'); +## Aggregates (write model) -const builder = new ContainerBuilder(); -builder.register(InMemoryEventStorage).as('storage'); -builder.registerAggregate(UserAggregate); +### IAggregate -const container = builder.container(); +Aggregates handle commands and emit events. The minimal aggregate contract is [IAggregate](src/interfaces/IAggregate.ts): + +```ts +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; +} ``` -Then send a command: +### AbstractAggregate -```js -const userAggregateId = undefined; -const payload = { - username: 'john', - password: 'test' -}; +[AbstractAggregate](src/AbstractAggregate.ts) is optional but recommended base class that provides the CQRS/ES wiring +and covers common edge cases: state restoring, command routing, validation, snapshots. + +Without an internal state it can be as simple as this: + +```ts +import { AbstractAggregate } from 'node-cqrs'; -container.commandBus.send('createUser', userAggregateId, { payload }); +type CreateUserCommandPayload = { username: string }; +type UserCreatedEventPayload = { username: string }; + +class UserAggregate extends AbstractAggregate { + createUser(payload: CreateUserCommandPayload) { + this.emit('userCreated', { username: payload.username }); + } +} ``` -Behind the scene, an AggregateCommandHandler will catch the command, -try to load an aggregate event stream and project it to aggregate state, -then it will pass the command payload to the `createUser` handler we’ve defined earlier. +By default, `node-cqrs` infers handled message types from public method names (so `createUser()` handles the `createUser` command). + +### Aggregate State + +Typically, it's simplest to keep aggregate state separate from command handlers and derive it by projecting the aggregate's emitted events. -The `createUser` implementation can look like this: +User aggregate state implementation could look like this: ```js -createUser(commandPayload) { - const { username, password } = commandPayload; +class UserAggregateState { + passwordHash: string; - this.emit('userCreated', { - username, - passwordHash: md5Hash(password) - }); -} + passwordChanged(event: IEvent) { + this.passwordHash = event.payload.passwordHash; + } +} ``` -Once the above method is executed, the emitted userCreated event will be persisted and delivered to event handlers (sagas, projections or any other custom event receptors). +Each event handler is defined as a separate method, which modifies the state. Alternatively, a common `mutate(event)` handler can be defined, which will handle all aggregate events instead. + +Aggregate state **should NOT throw any exceptions**, all type and business logic validations should be performed in the Aggregate during the command processing. + +Pass the state instance as a property to the AbstractAggregate constructor, or define it as a read-only stateful property in your aggregate class. State will be restored from past events upon new command delivery and will be ready for the business logic validations: + +```js +class UserAggregate extends AbstractAggregate { + + protected readonly state = new UserAggregateState(); + + changePassword(payload: ChangePasswordCommandPayload) { + if (md5(payload.oldPassword) !== this.state.passwordHash) + throw new Error('Invalid password'); + this.emit('passwordChanged', { + passwordHash: md5(payload.newPassword) + }); + } +} +``` -### Aggregate → Event → Projection → View +### External Dependencies -Now it’s time to work on a read model. We’ll need a projection that will handle our events. Projection must implement 2 methods: `subscribe(eventStore)` and `project(event)` . -To make it easier, you can extend an `AbstractProjection`: +If you are going to use a built-in [DI container](#containerbuilder), your aggregate constructor can accept instances of the services it depends on, they will be injected automatically upon each aggregate instance creation: ```js -const { AbstractProjection } = require('node-cqrs'); +import { ContainerBuilder, AbstractAggregate } from 'node-cqrs'; -class UsersProjection extends AbstractProjection { - static get handles() { - return ['userCreated']; +class UserAggregate extends AbstractAggregate { + + constructor({ id, authService }) { + super({ id }); + + // save injected service for use in command handlers + this._authService = authService; } - - userCreated(event) { - // ... + + async signupUser(payload) { + // use the injected service + await this._authService.registerUser(payload); } } + +const builder = new ContainerBuilder(); +builder.register(AuthService).as('authService'); +builder.registerAggregate(UserAggregate); ``` -By default, projection uses async `InMemoryView` for inner view, but we’ll use `Map` to make it more familiar: +## Projections and Views (read model) -```js -class UsersProjection extends AbstractProjection { - get view() { - return this._view || (this._view = new Map()); - } +Projection is an Observer, that listens to events and updates an associated View. + +### IProjection (minimal contract) - // ... +The minimal projection contract is [IProjection](src/interfaces/IProjection.ts): + +```ts +interface IProjection extends IObserver { + readonly view: TView; + + /** Subscribe to new events */ + subscribe(eventStore: IObservable): Promise | void; + + /** Restore view state from not-yet-projected events */ + restore(eventStore: IEventStorageReader): Promise | void; + + /** Project new event */ + project(event: IEvent): Promise | void; } ``` -With `Map` view, our event handler can look this way: +### AbstractProjection -```js -class UsersProjection extends AbstractProjection { - // ... +[AbstractProjection](src/AbstractProjection.ts) is the recommended base class for implementing projections with handler methods and built-in subscribe/restore behavior: - userCreated(event) { - this.view.set(event.aggregateId, { - username: event.payload.username - }); - } +```ts +import { AbstractProjection, type IEvent } from 'node-cqrs'; + +type UsersView = Map; + +class UsersProjection extends AbstractProjection { + + constructor() { + super(); + this.view = new Map(); + } + + userCreated(event: IEvent) { + this.view.set(event.aggregateId as string, { + username: event.payload!.username + }); + } } ``` -Once the projection is ready, it can be registered in the DI container: +Same rule applies as for AbstractAggregate: `userCreated()` handles the `userCreated` event unless you override `handles`. -```js -builder.registerProjection(UsersProjection, 'users'); +### View restoring on start + +For persistent views and safe restarts, a default projection `view` can implement [IViewLocker](src/interfaces/IViewLocker.ts) and [IEventLocker](src/interfaces/IEventLocker.ts) to support catch-up and last-processed checkpoints. + +### Accessing views + +When projection is being registered in the [DI container](#containerbuilder), the default `view` can be automatically exposed with a given name: + +```ts +import { ContainerBuilder, IContainer } from 'node-cqrs'; + +interface MyDiContainer extends IContainer { + usersView: UsersView; +} + +const builder = new ContainerBuilder(); +builder.registerProjection(UsersProjection, 'usersView'); + +const container = builder.container(); +const userRecord = container.usersView.get('1'); ``` -And accessed from anywhere in your app: +In case projection manages multiple views, those views can be exposed to container instance manually: -```js -container.users -// Map { 1 => { username: 'John' } } +```ts +builder.registerProjection(UsersProjection).as('usersProjection'); +builder.register(c => c.usersProjection.users).as('usersView'); +builder.register(c => c.usersProjection.connections).as('connectionsView'); +``` + +## Infrastructure modules + +### In-memory + +In-memory implementations intended for tests and local development. + +* [InMemoryEventStorage](src/in-memory/InMemoryEventStorage.ts) +* [InMemoryMessageBus](src/in-memory/InMemoryMessageBus.ts) +* [InMemoryView](src/in-memory/InMemoryView.ts) + +### SQLite + +Persistent views + catch-up/checkpoint tooling. + +```ts +import { AbstractSqliteView, SqliteObjectView } from 'node-cqrs/sqlite'; +``` + +- [AbstractSqliteView](src/sqlite/AbstractSqliteView.ts) - Base class for SQLite-backed projection views with restore locking and last-processed-event tracking +- [SqliteObjectView]() - SQLite-backed object view with restore locking and last-processed-event tracking + +### RabbitMQ + +Cross-process event distribution. + +```ts +import { RabbitMqEventBus, RabbitMqGateway } from 'node-cqrs/rabbitmq'; +``` + +- [RabbitMqGateway](src/rabbitmq/RabbitMqGateway.ts) — RabbitMQ-based publish/subscribe gateway for commands and events, with durable and transient queue support +- [RabbitMqEventBus](src/rabbitmq/RabbitMqEventBus.ts) - RabbitMQ-backed `IEventBus` with named queues support + +### Workers + +Run projections and corresponding views in `worker_threads` to isolate CPU-heavy work and keep the main thread responsive. + +```ts +import { AbstractWorkerProjection } from 'node-cqrs/workers'; +``` + +- [AbstractWorkerProjection](src/workers/AbstractWorkerProjection.ts) - Projection base class that can run projection handlers and the associated view in a worker thread. + +## Testing and Contribution + +```bash +git clone git@github.com:snatalenko/node-cqrs.git +cd node-cqrs +npm install +npm test +npm run lint ``` -## Contribution +Code style and formatting are enforced via: -* [editorconfig](http://editorconfig.org) -* [eslint](http://eslint.org) -* `npm test -- --watch` +- [editorconfig](http://editorconfig.org) +- [eslint](http://eslint.org) ## License -* [MIT License](https://github.com/snatalenko/node-cqrs/blob/master/LICENSE) +* [Apache-2.0](LICENSE) diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 120000 index 0b731df..0000000 --- a/SUMMARY.md +++ /dev/null @@ -1 +0,0 @@ -./docs/README.md \ No newline at end of file diff --git a/book.json b/book.json deleted file mode 100644 index 0622b56..0000000 --- a/book.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "title": "node-cqrs", - "gitbook": "3.2.2", - "plugins": [ - "edit-link", - "github", - "anchorjs" - ], - "pluginsConfig": { - "edit-link": { - "base": "https://github.com/snatalenko/node-cqrs/tree/master", - "label": "Edit This Page" - }, - "github": { - "url": "https://github.com/snatalenko/node-cqrs/" - }, - "theme-default": { - "styles": { - "website": "build/gitbook.css" - } - } - } -} diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index f1bcfed..0000000 --- a/docs/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Table of Contents - -* [ReadMe](/README.md) -* [Entities](/docs/entities/README.md) - * [Messages](/docs/entities/Messages/README.md) - * [Aggregate](/docs/entities/Aggregate/README.md) - * [State](/docs/entities/Aggregate/State.md) - * [Command Handlers](/docs/entities/Aggregate/CommandHandlers.md) - * [External Dependencies](/docs/entities/Aggregate/Dependencies.md) - * [Snapshots](/docs/entities/Aggregate/Snapshots.md) - * [Projection](/docs/entities/Projection/README.md) - * [InMemoryView](/docs/entities/Projection/InMemoryView.md) - * [Saga](/docs/entities/Saga/README.md) - * [Event Receptor](/docs/entities/EventReceptor/README.md) -* [Middleware](/docs/middleware/README.md) - * [DI Container](/docs/middleware/DIContainer.md) - * [AggregateCommandHandler](/docs/middleware/AggregateCommandHandler.md) -* [Infrastructure](/docs/infrastructure/README.md) diff --git a/docs/entities/Aggregate/CommandHandlers.md b/docs/entities/Aggregate/CommandHandlers.md deleted file mode 100644 index d10db40..0000000 --- a/docs/entities/Aggregate/CommandHandlers.md +++ /dev/null @@ -1,76 +0,0 @@ -# Aggregate Command Handlers - -At minimum Aggregates are expected to implement the following interface: - -```ts -declare interface IAggregate { - /** 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: IEventStream; -} -``` - -In a such aggregate all commands will be passed to the `handle` method and emitted events will be read from the `changes` property. - -Note that the event state restoring need to be handled separately and corresponding event stream will be passed either to Aggregate constructor or Aggregate factory. - -Most of this boilerplate code is already implemented in the AbstractAggregate class: - -## AbstractAggregate - -`AbstractAggregate` class implements `IAggregate` interface and separates command handling and state mutations (see [Aggregate State](./State.md)). - -After AbstractAggregate is inherited, a separate command handler method needs to be declared for each command. Method name should match the `command.type`. Events can be produced using either `emit` or `emitRaw` methods. - - -```js -const { AbstractAggregate } = require('node-cqrs'); - -class UserAggregate extends AbstractAggregate { - - get state() { - return this._state || (this._state = new UserAggregateState()); - } - - /** - * "signupUser" command handler. - * Being invoked by the AggregateCommandHandler service. - * Should emit events. Must not modify the state directly. - * - * @param {any} payload - command payload - * @param {any} context - command context - */ - signupUser(payload, context) { - if (this.version !== 0) - throw new Error('command executed on existing aggregate'); - - const { profile, password } = payload; - - // emitted event will mutate the state and will be committed to the EventStore - this.emit('userSignedUp', { - profile, - passwordHash: hash(password) - }); - } - - /** - * "changePassword" command handler - */ - changePassword(payload, context) { - if (this.version === 0) - throw new Error('command executed on non-existing aggregate'); - - const { oldPassword, newPassword } = payload; - - // all business logic validations should happen in the command handlers - if (!compareHash(this.state.passwordHash, oldPassword)) - throw new Error('old password does not match'); - - this.emit('userPasswordChanged', { - passwordHash: hash(newPassword) - }); - } -} -``` diff --git a/docs/entities/Aggregate/Dependencies.md b/docs/entities/Aggregate/Dependencies.md deleted file mode 100644 index f66adeb..0000000 --- a/docs/entities/Aggregate/Dependencies.md +++ /dev/null @@ -1,20 +0,0 @@ -# External Dependencies - -If you are going to use a built-in [DI container](../../middleware/DIContainer.md), your aggregate constructor can accept instances of the services it depends on, they will be injected automatically upon each aggregate instance creation: - -```js -class UserAggregate extends AbstractAggregate { - - constructor({ id, events, authService }) { - super({ id, events, state: new UserAggregateState() }); - - // save injected service for use in command handlers - this._authService = authService; - } - - async signupUser(payload, context) { - // use the injected service - await this._authService.registerUser(payload); - } -} -``` diff --git a/docs/entities/Aggregate/README.md b/docs/entities/Aggregate/README.md deleted file mode 100644 index 80341cf..0000000 --- a/docs/entities/Aggregate/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Aggregate - -At minimum Aggregates are expected to implement the following interface: - -```ts -declare interface IAggregate { - /** 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: IEventStream; -} -``` - -In a such aggregate all commands will be passed to the `handle` method and emitted events will be read from the `changes` property. - -Note that the event state restoring need to be handled separately and corresponding event stream will be passed either to Aggregate constructor or Aggregate factory. - -Most of this boilerplate code is already implemented in the [AbstractAggregate class](https://github.com/snatalenko/node-cqrs/blob/master/types/classes/AbstractAggregate.d.ts). - -It separates [command handling](./CommandHandlers.md), internal [state mutation](./State.md), and handles aggregate state restoring from event stream. It also provides a boilerplate code to simplify work with [Aggregate Snapshots](Snapshots.md) diff --git a/docs/entities/Aggregate/Snapshots.md b/docs/entities/Aggregate/Snapshots.md deleted file mode 100644 index 75a545f..0000000 --- a/docs/entities/Aggregate/Snapshots.md +++ /dev/null @@ -1,35 +0,0 @@ -# Aggregate Snapshots - -Snapshotting functionality involves the following methods: - -* `get snapshotVersion(): number` - `version` of the latest snapshot -* `get shouldTakeSnapshot(): boolean` - defines whether a snapshot should be taken -* `takeSnapshot(): void` - adds state snapshot to the `changes` collection, being invoked automatically by the [AggregateCommandHandler](#aggregatecommandhandler) -* `makeSnapshot(): object` - protected method used to snapshot an aggregate state -* `restoreSnapshot(snapshotEvent): void` - protected method used to restore state from a snapshot - -If you are going to use aggregate snapshots, you either need to keep the state structure simple (it should be possible to clone it using `JSON.parse(JSON.stringify(state))`) or override `makeSnapshots` and `restoreSnapshot` methods with your own serialization mechanisms. - -In the following sample a state snapshot will be taken every 50 events and added to the aggregate `changes` queue: - -```js -class UserAggregate extends AbstractAggregate { - get shouldTakeSnapshot() { - return this.version - this.snapshotVersion > 50; - } -} -``` - -If your state is too complex and cannot be restored with `JSON.parse` or you have data stored outside of aggregate `state`, you should define your own serialization and restoring functions: - -```js -class UserAggregate extends AbstractAggregate { - makeSnapshot() { - // return a field, stored outside of this.state - return { trickyField: this.trickyField }; - } - restoreSnapshot({ payload }) { - this.trickyField = payload.trickyField; - } -} -``` diff --git a/docs/entities/Aggregate/State.md b/docs/entities/Aggregate/State.md deleted file mode 100644 index 88da3b4..0000000 --- a/docs/entities/Aggregate/State.md +++ /dev/null @@ -1,50 +0,0 @@ -# Aggregate State - -[EventStore]: ../../middleware/README.md -[AbstractAggregate.js]: https://github.com/snatalenko/node-cqrs/blob/master/src/AbstractAggregate.js - - -Aggregate state is an internal aggregate property, which is used for domain logic validations in [Aggregate Command Handlers](CommandHandlers.md). - -## Implementation - -Typically aggregate state is expected to be managed separately from the aggregate command handlers and should be a projection of events emitted by the aggregate. - -User aggregate state implementation could look like this: - -```js -class UserAggregateState { - userSignedUp({ payload }) { - this.profile = payload.profile; - this.passwordHash = payload.passwordHash; - } - - userPasswordChanged({ payload }) { - this.passwordHash = payload.passwordHash; - } -} -``` - -Each event handler is defined as a separate method, which modifies the state. Alternatively, a common `mutate(event)` handler can be defined, which will handle all aggregate events instead. - -Aggregate state **should NOT throw any exceptions**, all type and business logic validations should be performed in the [aggregate command handlers](CommandHandlers.md). - -## Using in Aggregate - -`AbstractAggregate` restores aggregate state automatically in [its constructor][AbstractAggregate.js] from events, retrieved from the [EventStore][EventStore]. - -In order to make Aggregate use your state implementation, pass its instance as a property to the AbstractAggregate constructor, or define it as a read-only stateful property in your aggregate class: - -```js -class UserAggregate extends AbstractAggregate { - // option 1 - get state() { - return this._state || (this._state = new UserAggregateState()); - } - - constructor(props) { - // option 2 - super({ state: new UserAggregateState(), ...props }); - } -} -``` diff --git a/docs/entities/EventReceptor/README.md b/docs/entities/EventReceptor/README.md deleted file mode 100644 index d502fb0..0000000 --- a/docs/entities/EventReceptor/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Event Receptor - -Event receptor is an Observer that subscribes to events and performs operations non-related to core domain logic (i.e. send welcome email to a new user upon signup). - -```js -const { subscribe } = require('node-cqrs'); - -class MyReceptor { - static get handles() { - return [ - 'userSignedUp' - ]; - } - - subscribe(observable) { - subscribe(observable, this); - } - - userSignedUp({ payload }) { - // send welcome email to payload.email - } -} -``` - -If you are creating/registering a receptor manually: - -```js -const receptor = new MyReceptor(); -receptor.subscribe(eventStore); -``` - - -To register a receptor in the [DI Container](../../middleware/DIContainer.md): - -```js -container.registerEventReceptor(MyReceptor); -container.createUnexposedInstances(); -``` diff --git a/docs/entities/Messages/README.md b/docs/entities/Messages/README.md deleted file mode 100644 index 9b698ae..0000000 --- a/docs/entities/Messages/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Messages - -[Middleware]: ../../middleware/README.md "Middleware" -[Aggregate]: ../Aggregate/README.md "Aggregate" -[Saga]: ../Saga/README.md -[Projection]: ../Projection/README.md -[Receptor]: ../EventReceptor/README.md - -All messages flowing thru the system are loosely typed objects with a minimal set of required fields: - -* `type: string` - command or event type. for commands it's recommended to name it as a call to action (i.e. "createUser"), while for events it should describe what happened in a past tense (i.e. "userCreated"). -* `payload: any` - command or event data -* `context: object` - key-value object with information on context (i.e. logged in user ID). context must be specified when a command is being triggered by a user action and then it's being copied to events, sagas and subsequent commands - -Other fields are used for message routing and their usage depends on the flow: - -* `aggregateId: string|number|undefined` - unique aggregate identifier -* `aggregateVersion: number` -* `sagaId: string|number|undefined` -* `sagaVersion: number` - - -## Commands - -* sent to [CommandBus][Middleware] manually -* being handled by [Aggregates][Aggregate] -* may be enqueued by [Sagas][Saga] - - -Command example: - -```json -{ - "type": "signupUser", - "aggregateId": null, - "payload": { - "profile": { - "name": "John Doe", - "email": "john@example.com" - }, - "password": "test" - }, - "context": { - "ip": "127.0.0.1", - "ts": 1503509747154 - } -} -``` - - -## Events - -* produced by [Aggregates][Aggregate] -* persisted to [EventStore][Middleware] -* may be handled by [Projections][Projection], [Sagas][Saga] and [Event Receptors][Receptor] - -Event example: - -```json -{ - "type": "userSignedUp", - "aggregateId": 1, - "aggregateVersion": 0, - "payload": { - "profile": { - "name": "John Doe", - "email": "john@example.com" - }, - "passwordHash": "098f6bcd4621d373cade4e832627b4f6" - }, - "context": { - "ip": "127.0.0.1", - "ts": 1503509747154 - } -} -``` diff --git a/docs/entities/Projection/InMemoryView.md b/docs/entities/Projection/InMemoryView.md deleted file mode 100644 index 86707a6..0000000 --- a/docs/entities/Projection/InMemoryView.md +++ /dev/null @@ -1,48 +0,0 @@ -InMemoryView -============ - -By default, AbstractProjection instances get created with an instance of InMemoryView associated. - -The associted view can be accessed thru the `view` property and provides a set of methods for view manipulation: - -* `get ready(): boolean` - indicates if the view state is restored -* `once('ready'): Promise` - allows to await until the view is restored -* operations with data - * `get(key: string, options?: object): Promise` - * `create(key: string, record: any)` - * `update(key: string, callback: any => any)` - * `updateEnforcingNew(key: string, callback: any => any)` - * `delete(key: string)` - - -In case you are using the [DI container](../middleware/DIContainer.md), projection view will be exposed on the container automatically: - -```js -container.registerProjection(MyProjection, 'myView'); - -// @type {InMemoryView} -const view = container.myView; - -// @type {{ profile: object, passwordHash: string }} -const aggregateRecord = await view.get('my-aggregate-id'); -``` - -Since the view keeps state in memory, upon creation it needs to be restored from the EventStore. -This is [handled by the AbstractProjection](./README.md) automatically. - -All queries to the `view.get(..)` get suspended, until the view state is restored. Alternatively, you can either check the `ready` flag or subscribe to the "ready" event manually: - -```js -// wait until the view state is restored -await view.once('ready'); - -// query data -const record = await view.get('my-key'); -``` - -In case you need to access the view from a projection event handler (which also happens during the view restoring), to prevent the deadlock, invoke the `get` method with a `nowait` flag: - -```js -// accessing view record from a projection event handler -const record = await this.view.get('my-key', { nowait: true }); -``` diff --git a/docs/entities/Projection/README.md b/docs/entities/Projection/README.md deleted file mode 100644 index 3e797c7..0000000 --- a/docs/entities/Projection/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Projection - -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*. - -## Projection Event Handlers - -All projection event types must be listed in the static `handles` getter and event type must have a handler defined: - -```js - -const { AbstractProjection } = require('node-cqrs'); - -class MyProjection extends AbstractProjection { - static get handles() { - return [ - 'userSignedUp', - 'userPasswordChanged' - ]; - } - - async userSignedUp({ aggregateId, payload }) { - const { profile, passwordHash } = payload; - - await this.view.create(aggregateId, { - profile, - passwordHash - }); - } - - async userPasswordChanged({ aggregateId, payload }) { - const { passwordHash } = payload; - await this.view.update(aggregateId, view => { - view.passwordHash = passwordHash; - }); - } -} - -``` - -## Accessing Projection View - -Associated view is exposed on a projection instance as `view` property. - -By default, AbstractProjection instances get created with an instance of [InMemoryView](./InMemoryView.md) associated. diff --git a/docs/entities/README.md b/docs/entities/README.md deleted file mode 100644 index 4fc89d3..0000000 --- a/docs/entities/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Entities - -* [Messages](Messages/README.md) -* [Aggregate](Aggregate/README.md) -* [Projection](Projection/README.md) -* [Saga](Saga/README.md) -* [Event Receptor](EventReceptor/README.md) \ No newline at end of file diff --git a/docs/entities/Saga/README.md b/docs/entities/Saga/README.md deleted file mode 100644 index 2dc3759..0000000 --- a/docs/entities/Saga/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Saga - -[AbstractSaga.d.ts]: https://github.com/snatalenko/node-cqrs/blob/master/types/classes/AbstractSaga.d.ts - - -Saga can be used to control operations where multiple aggregates are involved. - -## SagaEventReceptor - -`SagaEventReceptor` instance is needed for each Saga type, it - -1. Subscribes to event store and awaits events handled by Saga -2. Instantiates Saga with corresponding event stream -3. Passes events to saga -4. Sends enqueued commands to the CommandBus - -Saga event receptor can be created manually: - -```js -const sagaEventReceptor = new SagaEventReceptor({ - sagaType: MySaga, - eventStore, - commandBus -}); - -sagaEventReceptor.subscribe(eventStore); -``` - -or using the [DI container](../../middleware/DIContainer.md) : - -```js -builder.registerSaga(MySaga); -``` - -## Saga Interface - -At minimum Sagas should implement the following interface: - -```ts -declare interface ISaga { - /** List of event types that trigger new Saga start */ - static readonly startsWith: string[]; - - /** List of event types being handled by Saga */ - static readonly handles?: string[]; - - /** 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; -} -``` - -Also, it needs to handle saga internal state restoring from the `events` property passed either to the Saga constructor or as a Saga factory attribute. - - -## AbstractSaga - -Most of the above logic is implemented in the [AbstractSaga class][AbstractSaga.d.ts] and it can be extended with saga business logic only. - -Event handles should be defined as a separate methods, where method name correspond to `event.type`. Commands can be sent using the `enqueue` (or `enqueueRaw`) method - -```ts -const { AbstractSaga } = require('node-cqrs'); - -class SupportNotificationSaga extends AbstractSaga { - - static get startsWith() { - return ['userLockedOut']; - } - - /** - * "userLockedOut" event handler which also starts the Saga - */ - userLockedOut({ aggregateId }) { - - // We use empty aggregate ID as we target a new aggregate here - const targetAggregateId = undefined; - - const commandPayload = { - subject: 'Account locked out', - message: `User account ${aggregateId} is locked out for 15min because of multiple unsuccessful login attempts` - }; - - // Enqueue command, which will be sent to the CommandBus - // after method execution is complete - this.enqueue('createTicket', targetAggregateId, commandPayload); - } -} -``` diff --git a/docs/images/README.md b/docs/images/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/images/node-cqrs-components.png b/docs/images/node-cqrs-components.png deleted file mode 100644 index 3cb7bb8..0000000 Binary files a/docs/images/node-cqrs-components.png and /dev/null differ diff --git a/docs/images/node-cqrs-flow.svg b/docs/images/node-cqrs-flow.svg new file mode 100644 index 0000000..1c73dea --- /dev/null +++ b/docs/images/node-cqrs-flow.svg @@ -0,0 +1,4 @@ + + + +
Aggregate Command Handler
Saga Event Handler
"Edit Profile"
Command
Write
API
"Profile Modified"
Event
Aggregate
Saga
Projection
Query Results
View
Command
Bus
Read
API
Past Events
Event Store
\ No newline at end of file diff --git a/docs/infrastructure/README.md b/docs/infrastructure/README.md deleted file mode 100644 index 762deaf..0000000 --- a/docs/infrastructure/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Infrastructure - -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) - - -The following storage/bus implementations persist data in external storages and can be used in production: - -* [MongoDB Event Storage](https://github.com/snatalenko/node-cqrs-mongo) -* [RabbitMQ Message Bus](https://github.com/snatalenko/node-cqrs-rabbitmq) diff --git a/docs/middleware/AggregateCommandHandler.md b/docs/middleware/AggregateCommandHandler.md deleted file mode 100644 index ce181f8..0000000 --- a/docs/middleware/AggregateCommandHandler.md +++ /dev/null @@ -1,24 +0,0 @@ -# AggregateCommandHandler - -AggregateCommandHandler instance is needed for every aggregate type, it does the following: - -1. Subscribes to CommandBus and awaits commands handled by Aggregate -2. Upon command receiving creates an instance of Aggregate using the corresponding event stream -3. Passes the command to the created Aggregate instance -4. Commits events emitted by the Aggregate instance to the EventStore - -Aggregate command handler can be created manually: - -```js -const myAggregateCommandHandler = new AggregateCommandHandler({ - eventStore, - aggregateType: MyAggregate -}); -myAggregateCommandHandler.subscribe(commandBus); -``` - -Or using the [DI container](DIContainer.md) (preferred method): - -```js -container.registerAggregate(MyAggregate); -``` diff --git a/docs/middleware/DIContainer.md b/docs/middleware/DIContainer.md deleted file mode 100644 index a1a9a5d..0000000 --- a/docs/middleware/DIContainer.md +++ /dev/null @@ -1,109 +0,0 @@ -# DI Container - -DI Container intended to make components wiring easier. - -All named component instances are exposed on container thru getters and get created upon accessing a getter. Default `EventStore` and `CommandBus` components are registered upon container instance creation: - -```js -const { ContainerBuilder } = require('node-cqrs'); -const builder = new ContainerBuilder(); -const container = builder.container(); - -container.eventStore; // instance of EventStore -container.commandBus; // instance of CommandBus -``` - -Other components can be registered either as classes or as factories: - -```js -// class with automatic dependency injection -builder.register(SomeService).as('someService'); - -// OR factory with more precise control -builder.register(container => new SomeService(container.commandBus)).as('someService'); -``` - -Container scans class constructors (or constructor functions) for dependencies and injects them, where possible: - -```js -class SomeRepository { /* ... */ } - -class ServiceA { - // dependency definition, as a parameter object property - constructor(options) { - this._repository = options.repository; - } -} - -class ServiceB { - // dependency defined thru parameter object destructuring - constructor({ repository, a }) { /* ... */ } -} - -class ServiceC { - constructor(repository, a, b) { /* ... */ } -} - -// dependencies passed thru factory -const serviceFactory = ({ repository, a, b }) => new ServiceC(repository, a, b); - -container.register(SomeRepository, 'repository'); -container.register(ServiceA, 'a'); -container.register(ServiceB, 'b'); -container.register(serviceFactory, 'c'); -``` - -Components that aren't going to be accessed directly by name can also be registered in the builder. Their instances will be created after invoking `container()` method: - -```js -builder.register(SomeEventObserver); -// at this point the registered observer does not exist - -const container = builder.container(); -// now it exists and got all its constructor dependencies -``` - - -DI container has a set of methods for CQRS components registration: - -* __registerAggregate(AggregateType)__ - registers aggregateCommandHandler, subscribes it to commandBus and wires Aggregate dependencies -* __registerSaga(SagaType)__ - registers sagaEventHandler, subscribes it to eventStore and wires Saga dependencies -* __registerProjection(ProjectionType, exposedViewName)__ - registers projection, subscribes it to eventStore and exposes associated projection view on the container -* __registerCommandHandler(typeOrFactory)__ - registers command handler and subscribes it to commandBus -* __registerEventReceptor(typeOrFactory)__ - registers event receptor and subscribes it to eventStore - - -Altogether: - -```js -const { ContainerBuilder, InMemoryEventStorage } = require('node-cqrs'); -const builder = new ContainerBuilder(); - -builder.registerAggregate(UserAggregate); - -// we are using non-persistent in-memory event storage, -// for a permanent storage you can look at https://www.npmjs.com/package/node-cqrs-mongo -builder.register(InMemoryEventStorage) - .as('storage'); - -// as an example of UserAggregate dependency -builder.register(AuthService) - .as('authService'); - -// setup command and event handler listeners -const container = builder.container(); - -// send a command -const aggregateId = undefined; -const payload = { profile: {}, password: '...' }; -const context = {}; -container.commandBus.send('signupUser', aggregateId, { payload, context }); - -container.eventStore.once('userSignedUp', event => { - console.log(`user aggregate created with ID ${event.aggregateId}`); -}); -``` - -In the above example, the command will be passed to an aggregate command handler, which will either restore an aggregate, or create a new one, and will invoke a corresponding method on the aggregate. - -After command processing is done, produced events will be committed to the eventStore, and emitted to subscribed projections and/or event receptors. diff --git a/docs/middleware/README.md b/docs/middleware/README.md deleted file mode 100644 index 5b49487..0000000 --- a/docs/middleware/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Middleware - -for wiring components together - -* [DI Container](DIContainer.md) - -for delivering messages to corresponding domain objects - -* [AggregateCommandHandler](AggregateCommandHandler.md) -* SagaEventHandler - -Messaging API to interact with: - -* EventStore -* CommandBus diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..4f4b779 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,669 @@ +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-use-before-define": "warn", + "strict": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "vars": "local", + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "^(_|err)" + } + ], + "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" + ], + "array-callback-return": "error", + "block-scoped-var": "error", + "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-empty-function": [ + "error", + { + "allow": [ + "arrowFunctions", + "methods", + "getters" + ] + } + ], + "no-empty-pattern": "error", + "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-implied-eval": "error", + "no-iterator": "error", + "no-labels": [ + "error", + { + "allowLoop": false, + "allowSwitch": false + } + ], + "no-lone-blocks": "error", + "no-loop-func": "error", + "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-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-unused-labels": "error", + "no-useless-concat": "error", + "no-useless-escape": "error", + "no-useless-return": "error", + "no-void": [ + "warn", + { "allowAsStatement": true } + ], + "no-warning-comments": [ + "warn", + { + "terms": ["todo", "fixme", "hack"], + "location": "start" + } + ], + "no-with": "error", + "radix": "error", + "vars-on-top": "error", + "wrap-iife": [ + "error", + "outside", + { + "functionPrototypeMethods": false + } + ], + "yoda": "error", + "no-mixed-requires": "error", + "global-require": "error", + "no-new-require": "error", + "no-path-concat": "error", + "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-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-const": [ + "error", + { + "destructuring": "any", + "ignoreReadBeforeAssign": true + } + ], + "prefer-numeric-literals": "error", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "require-yield": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "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-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", + "use-isnan": "error", + "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" + ], + "eol-last": [ + "error", + "always" + ], + "func-call-spacing": [ + "error", + "never" + ], + "indent": [ + "error", + "tab", + { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "FunctionDeclaration": { + "parameters": 1, + "body": 1 + }, + "FunctionExpression": { + "parameters": 1, + "body": 1 + } + } + ], + "key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true, + "overrides": { + "return": { + "after": true + }, + "throw": { + "after": true + }, + "case": { + "after": 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-len": [ + "warn", + 120, + 4, + { + "ignoreUrls": true, + "ignoreComments": false, + "ignoreRegExpLiterals": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true + } + ], + "max-params": [ + "warn", + 5 + ], + "new-cap": [ + "error", + { + "newIsCap": true, + "newIsCapExceptions": [], + "capIsNew": false, + "capIsNewExceptions": [ + "Immutable.Map", + "Immutable.Set", + "Immutable.List" + ] + } + ], + "new-parens": "error", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 4 + } + ], + "no-array-constructor": "error", + "no-bitwise": "error", + "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-nested-ternary": "error", + "no-new-object": "error", + "no-restricted-syntax": [ + "error", + "ForInStatement", + "LabeledStatement", + "WithStatement" + ], + "no-spaced-func": "error", + "no-trailing-spaces": "error", + "no-unneeded-ternary": [ + "error", + { + "defaultAssignment": false + } + ], + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "object-property-newline": [ + "error", + { + "allowMultiplePropertiesPerLine": true + } + ], + "one-var": [ + "error", + "never" + ], + "one-var-declaration-per-line": [ + "error", + "always" + ], + "operator-assignment": [ + "error", + "always" + ], + "quote-props": [ + "error", + "as-needed", + { + "keywords": false, + "unnecessary": true, + "numbers": false + } + ], + "quotes": [ + "error", + "single", + { + "avoidEscape": true + } + ], + "semi": [ + "error", + "always" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": 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/browser-smoke-test/README.md b/examples/browser-smoke-test/README.md new file mode 100644 index 0000000..c488ff0 --- /dev/null +++ b/examples/browser-smoke-test/README.md @@ -0,0 +1,17 @@ +# Browser smoke test + +This example is meant to quickly verify that the core `node-cqrs` APIs work in a browser environment. + +## Run + +From the repo root: + +```bash +npm run build:browser +``` + +Then open `examples/browser-smoke-test/index.html` directly (e.g. double-click it). + +Notes: +- The bundle is written to `dist/browser/node-cqrs.iife.js`. +- If you don’t have Browserify installed locally, run `npm i -D browserify`. diff --git a/examples/browser-smoke-test/index.html b/examples/browser-smoke-test/index.html new file mode 100644 index 0000000..231e1cf --- /dev/null +++ b/examples/browser-smoke-test/index.html @@ -0,0 +1,40 @@ + + + + + + + node-cqrs browser smoke test + + + + +

node-cqrs browser smoke test

+

Open DevTools console for details.

+
Running…
+ + + + + diff --git a/examples/browser-smoke-test/main.js b/examples/browser-smoke-test/main.js new file mode 100644 index 0000000..9bd93c6 --- /dev/null +++ b/examples/browser-smoke-test/main.js @@ -0,0 +1,111 @@ +(function () { + const out = document.getElementById('out'); + + function write(line) { + out.textContent = `${out.textContent}\n${line}`; + } + + function setStatusOk() { + out.classList.remove('fail'); + out.classList.add('ok'); + } + + function setStatusFail() { + out.classList.remove('ok'); + out.classList.add('fail'); + } + + if (!globalThis.Cqrs) + throw new Error('Cqrs bundle is not loaded. Run `npm run build:browser` first.'); + + const { + AbstractAggregate, + AbstractProjection, + ContainerBuilder, + InMemoryEventStorage + } = globalThis.Cqrs; + + class UserAggregateState { + userCreated(event) { + this.password = event.payload.password; + } + + passwordChanged(event) { + this.password = event.payload.newPassword; + } + } + + class UserAggregate extends AbstractAggregate { + constructor(params) { + super(params); + this.state = new UserAggregateState(); + } + + createUser(payload) { + this.emit('userCreated', { + username: payload.username, + password: payload.password + }); + } + + changePassword(payload) { + if (payload.oldPassword !== this.state.password) + throw new Error('Invalid password'); + + this.emit('passwordChanged', { + newPassword: payload.newPassword + }); + } + } + + class UsersProjection extends AbstractProjection { + constructor() { + super(); + this.view = new Map(); + } + + userCreated(event) { + this.view.set(event.aggregateId, { username: event.payload.username }); + } + } + + async function main() { + out.textContent = ''; + write('Building container…'); + + const builder = new ContainerBuilder(); + builder.register(InMemoryEventStorage) + .as('eventStorageReader') + .as('eventStorageWriter'); + builder.registerAggregate(UserAggregate); + builder.registerProjection(UsersProjection, 'users'); + + const container = builder.container(); + const { users, commandBus } = container; + + write('Sending commands…'); + const [userCreated] = await commandBus.send('createUser', undefined, { + payload: { username: 'john', password: 'magic' }, + context: {} + }); + + await commandBus.send('changePassword', userCreated.aggregateId, { + payload: { oldPassword: 'magic', newPassword: 'no magic' }, + context: {} + }); + + const user = users.get(userCreated.aggregateId); + if (!user || user.username !== 'john') + throw new Error(`Unexpected user view value: ${JSON.stringify(user)}`); + + write(`OK: ${JSON.stringify(user)}`); + setStatusOk(); + } + + main().catch(err => { + console.error(err); + out.textContent = `FAILED: ${err?.message ?? String(err)}`; + setStatusFail(); + }); +}()); + diff --git a/examples/jsconfig.json b/examples/jsconfig.json new file mode 100644 index 0000000..6653e49 --- /dev/null +++ b/examples/jsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "commonjs", + "moduleResolution": "node", + "checkJs": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "lib": [ + "es2022", + "dom" + ], + "baseUrl": "..", + "paths": { + "node-cqrs": [ + "types/index.d.ts" + ], + "node-cqrs/*": [ + "types/*/index.d.ts" + ] + } + }, + "include": [ + "**/*" + ], + "exclude": [ + "../node_modules", + "../dist", + "**/bundle.js", + "browser-smoke-test/**" + ] +} diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..875f1f5 --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022" + ], + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true + } +} diff --git a/examples/user-domain-own-implementation/index.ts b/examples/user-domain-own-implementation/index.ts new file mode 100644 index 0000000..57bfe51 --- /dev/null +++ b/examples/user-domain-own-implementation/index.ts @@ -0,0 +1,198 @@ +/** + * Plain user domain implementation example without using the framework classes. + * + * - `UserAggregate` implementing write model + * - `UserProjection` implementing read model + * - In-memory `EventStore` + * - `CommandHandler` + * - Main function wiring everything together and sending test commands + */ + +import EventEmitter from 'events'; +import crypto from 'crypto'; +import type { + IAggregate, ICommand, ICommandHandler, Identifier, IEvent, IEventSet, IEventStorageReader, IEventStorageWriter, + IObservable, IProjection +} from '../../types'; + +const md5 = (v: string): string => crypto.createHash('md5').update(v).digest('hex'); + +/** + * Sample aggregate (write interface) + */ +class UserAggregate implements IAggregate { + + readonly id: any; + + /* inner aggregate state used for write operation validation */ + #state: { passwordHash?: string } = {}; + + constructor(id) { + this.id = id; + } + + /** Restore aggregate state from past events */ + mutate(event: IEvent): void { + if (event.type === 'userCreated' || event.type === 'userPasswordChanged') + this.#state.passwordHash = event.payload.passwordHash; + } + + /** Redirect command execution to a command handler */ + handle(command: ICommand): IEventSet { + return this[command.type](command.payload); + } + + createUser({ username, password }): IEventSet { + return [{ + type: 'userCreated', + aggregateId: this.id, + payload: { + username, + passwordHash: md5(password) + } + }]; + } + + changePassword({ oldPassword, newPassword }): IEventSet { + if (md5(oldPassword) !== this.#state.passwordHash) + throw new Error('Old password is incorrect'); + + return [{ + type: 'userPasswordChanged', + aggregateId: this.id, + payload: { + passwordHash: md5(newPassword) + } + }]; + } +} + +/** + * Sample projection (read model) + */ +class UserProjection implements IProjection> { + + /** View model */ + readonly view: Map = new Map(); + + /** Subscribe only to the event types that affect the read model */ + subscribe(eventStore: IObservable) { + eventStore.on('userCreated', e => this.userCreated(e.aggregateId, e.payload)); + } + + /** If the view is not persistent, restore it from past events */ + async restore(eventStore: IEventStorageReader) { + for await (const oldEvent of eventStore.getEventsByTypes(['userCreated'])) + this.project(oldEvent); + } + + /** Pass data to corresponding event handler */ + project(event: IEvent): void { + this[event.type](event.aggregateId, event.payload); + } + + userCreated(userId, { username }) { + this.view.set(userId, { username }); + } +} + +/** + * Dumb event store that keeps all events in memory + * and re-distributes them to all subscribers + */ +class EventStore extends EventEmitter implements IObservable, IEventStorageReader, IEventStorageWriter { + + #events: IEvent[] = []; + + async commitEvents(events: Readonly) { + this.#events.push(...events); + + for (const e of events) + this.emit(e.type, e); + + return events; + } + + async* getEventsByTypes(eventTypes: string[]) { + yield* this.#events.filter(e => eventTypes.includes(e.type)); + } + + async* getAggregateEvents(aggregateId: Identifier) { + yield* this.#events.filter(e => e.aggregateId === aggregateId); + } + + async* getSagaEvents(sagaId: Identifier) { + yield* this.#events.filter(e => e.sagaId === sagaId); + } +} + +/** + * Sample command handler that routes commands to corresponding aggregates + */ +class CommandHandler implements ICommandHandler { + + #eventStore: IEventStorageReader & IEventStorageWriter; + + constructor(eventStore) { + this.#eventStore = eventStore; + } + + subscribe(commandBus: IObservable): void { + commandBus.on('createUser', cmd => this.passCommandToAggregate(cmd)); + commandBus.on('changePassword', cmd => this.passCommandToAggregate(cmd)); + } + + async passCommandToAggregate(cmd) { + const userAggregate = new UserAggregate(cmd.aggregateId); + + // restore aggregate state from past events + const oldEvents = this.#eventStore.getAggregateEvents(cmd.aggregateId); + for await (const event of oldEvents) + userAggregate.mutate(event); + + // store new events + const newEvents = userAggregate.handle(cmd); + this.#eventStore.commitEvents(newEvents); + } +} + +/** + * Run the test + */ +(async function main() { + + // create and wire all instances together + + const commandBus = new EventEmitter(); + const eventStore = new EventStore(); + + const commandHandler = new CommandHandler(eventStore); + commandHandler.subscribe(commandBus); + + const projection = new UserProjection(); + projection.subscribe(eventStore); + projection.restore(eventStore); + + // send test commands + + commandBus.emit('createUser', { + aggregateId: '1', + type: 'createUser', + payload: { username: 'John', password: 'magic' } + }); + + commandBus.emit('changeUserPassword', { + aggregateId: '1', + type: 'changeUserPassword', + payload: { oldPassword: 'magic', newPassword: 'no magic' } + }); + + // wait for the command bus to finish processing + await new Promise(setImmediate); + + const userRecord = projection.view.get('1'); + + // eslint-disable-next-line no-console + console.log(userRecord); // { username: 'John' } + +}()); diff --git a/examples/user-domain-own-implementation/package.json b/examples/user-domain-own-implementation/package.json new file mode 100644 index 0000000..f938c85 --- /dev/null +++ b/examples/user-domain-own-implementation/package.json @@ -0,0 +1,7 @@ +{ + "type": "module", + "scripts": { + "start": "node index.ts", + "test": "node index.ts" + } +} \ No newline at end of file diff --git a/examples/user-domain-tests/.eslintrc.json b/examples/user-domain-tests/.eslintrc.json deleted file mode 100644 index bbd5fb1..0000000 --- a/examples/user-domain-tests/.eslintrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "env": { - "mocha": true - }, - "rules": { - "no-unused-expressions": "off" - } -} diff --git a/examples/user-domain-ts/UserAggregate.ts b/examples/user-domain-ts/UserAggregate.ts new file mode 100644 index 0000000..b0b9f9d --- /dev/null +++ b/examples/user-domain-ts/UserAggregate.ts @@ -0,0 +1,41 @@ +import { AbstractAggregate } from 'node-cqrs'; +import { md5 } from './utils.ts'; +import type { + ChangePasswordCommandPayload, + CreateUserCommandPayload, + PasswordChangedEvent, + UserCreatedEvent +} from './messages.ts'; + +class UserAggregateState { + passwordHash!: string; + + userCreated(event: UserCreatedEvent) { + this.passwordHash = event.payload!.passwordHash; + } + + passwordChanged(event: PasswordChangedEvent) { + this.passwordHash = event.payload!.passwordHash; + } +} + +export class UserAggregate extends AbstractAggregate { + + protected readonly state = new UserAggregateState(); + + createUser(payload: CreateUserCommandPayload) { + this.emit('userCreated', { + username: payload.username, + passwordHash: md5(payload.password) + }); + } + + changePassword(payload: ChangePasswordCommandPayload) { + if (md5(payload.oldPassword) !== this.state.passwordHash) + throw new Error('Invalid password'); + + this.emit('passwordChanged', { + passwordHash: md5(payload.newPassword) + }); + } +} diff --git a/examples/user-domain-ts/UsersProjection.ts b/examples/user-domain-ts/UsersProjection.ts new file mode 100644 index 0000000..4f9755a --- /dev/null +++ b/examples/user-domain-ts/UsersProjection.ts @@ -0,0 +1,18 @@ +import { AbstractProjection } from 'node-cqrs'; +import type { UserCreatedEvent } from './messages.ts'; + +export type UsersView = Map; + +export class UsersProjection extends AbstractProjection { + + constructor() { + super(); + this.view = new Map(); + } + + userCreated(event: UserCreatedEvent) { + this.view.set(event.aggregateId as string, { + username: event.payload!.username + }); + } +} diff --git a/examples/user-domain-ts/index.ts b/examples/user-domain-ts/index.ts new file mode 100644 index 0000000..492471a --- /dev/null +++ b/examples/user-domain-ts/index.ts @@ -0,0 +1,95 @@ +import { + type IContainer, + AggregateCommandHandler, + CommandBus, + ContainerBuilder, + EventStore, + InMemoryEventStorage, + InMemoryMessageBus +} from 'node-cqrs'; +import type { ChangePasswordCommandPayload, CreateUserCommandPayload } from './messages.ts'; +import { UserAggregate } from './UserAggregate.ts'; +import { UsersProjection, type UsersView } from './UsersProjection.ts'; + +// Test with DI container +{ + interface MyDiContainer extends IContainer { + users: UsersView; + } + + const builder = new ContainerBuilder(); + + builder.register(InMemoryEventStorage) // In-memory implementations for local dev/tests + .as('identifierProvider') // EventStore dependency to generate new aggregate and saga ID's + .as('eventStorageReader') // EventStore dependency to read events from + .as('eventStorageWriter'); // eventStorageWriter, when provided, is automatically added to the dispatch pipeline + + builder.registerAggregate(UserAggregate); + builder.registerProjection(UsersProjection, 'users'); + + const container = builder.container(); + const { users, commandBus } = container; + + const [userCreated] = await commandBus.send('createUser', undefined, { + payload: { + username: 'john', + password: 'magic' + } satisfies CreateUserCommandPayload + }); + + await commandBus.send('changePassword', userCreated.aggregateId as string, { + payload: { + oldPassword: 'magic', + newPassword: 'no magic' + } satisfies ChangePasswordCommandPayload + }); + + const user = users.get(userCreated.aggregateId as string); + + // eslint-disable-next-line no-console + console.log(user); // { username: 'john' } +} + + +// Same test without DI container +{ + const inMemoryMessageBus = new InMemoryMessageBus(); + const eventStorage = new InMemoryEventStorage(); + const eventStore = new EventStore({ + eventStorageReader: eventStorage, + identifierProvider: eventStorage, + eventDispatchPipeline: [eventStorage], + eventBus: inMemoryMessageBus + }); + + const commandBus = new CommandBus(); + const aggregateCommandHandler = new AggregateCommandHandler({ + eventStore, + aggregateType: UserAggregate + }); + aggregateCommandHandler.subscribe(commandBus); + + const projection = new UsersProjection(); + await projection.subscribe(eventStore); + const users = projection.view; + + + const [userCreatedEvent] = await commandBus.send('createUser', undefined, { + payload: { + username: 'john', + password: 'magic' + } satisfies CreateUserCommandPayload + }); + + await commandBus.send('changePassword', userCreatedEvent.aggregateId as string, { + payload: { + oldPassword: 'magic', + newPassword: 'no magic' + } satisfies ChangePasswordCommandPayload + }); + + const user = await users.get(userCreatedEvent.aggregateId as string); + + // eslint-disable-next-line no-console + console.log(user); +} diff --git a/examples/user-domain-ts/messages.ts b/examples/user-domain-ts/messages.ts new file mode 100644 index 0000000..f721677 --- /dev/null +++ b/examples/user-domain-ts/messages.ts @@ -0,0 +1,9 @@ +import type { IEvent } from 'node-cqrs'; + +export type CreateUserCommandPayload = { username: string, password: string }; +export type UserCreatedEventPayload = { username: string, passwordHash: string }; +export type UserCreatedEvent = IEvent; + +export type ChangePasswordCommandPayload = { oldPassword: string, newPassword: string }; +export type PasswordChangedEventPayload = { passwordHash: string }; +export type PasswordChangedEvent = IEvent; diff --git a/examples/user-domain-ts/utils.ts b/examples/user-domain-ts/utils.ts new file mode 100644 index 0000000..7f60531 --- /dev/null +++ b/examples/user-domain-ts/utils.ts @@ -0,0 +1,3 @@ +import * as crypto from 'crypto'; + +export const md5 = (v: string): string => crypto.createHash('md5').update(v).digest('hex'); diff --git a/examples/user-domain/UserAggregate.js b/examples/user-domain/UserAggregate.cjs similarity index 98% rename from examples/user-domain/UserAggregate.js rename to examples/user-domain/UserAggregate.cjs index 13d2299..abe0bb1 100644 --- a/examples/user-domain/UserAggregate.js +++ b/examples/user-domain/UserAggregate.cjs @@ -1,7 +1,7 @@ // @ts-check 'use strict'; -const { AbstractAggregate } = require('../..'); // node-cqrs +const { AbstractAggregate } = require('node-cqrs'); const crypto = require('crypto'); diff --git a/examples/user-domain/UsersProjection.js b/examples/user-domain/UsersProjection.cjs similarity index 87% rename from examples/user-domain/UsersProjection.js rename to examples/user-domain/UsersProjection.cjs index 6e21124..3c1cf1b 100644 --- a/examples/user-domain/UsersProjection.js +++ b/examples/user-domain/UsersProjection.cjs @@ -1,7 +1,7 @@ // @ts-check 'use strict'; -const { AbstractProjection } = require('../..'); // node-cqrs +const { AbstractProjection } = require('node-cqrs'); /** * Users projection listens to events and updates associated view (read model) @@ -29,7 +29,7 @@ class UsersProjection extends AbstractProjection { * userCreated event handler * * @param {object} event - * @param {import('../../src').Identifier} event.aggregateId + * @param {import('node-cqrs').Identifier} event.aggregateId * @param {object} event.payload * @param {string} event.payload.username * @param {string} event.payload.passwordHash diff --git a/examples/user-domain/index.cjs b/examples/user-domain/index.cjs new file mode 100644 index 0000000..2e9f4f4 --- /dev/null +++ b/examples/user-domain/index.cjs @@ -0,0 +1,66 @@ +'use strict'; + +const { + ContainerBuilder, + InMemoryEventStorage, + CommandBus, + EventStore, + AggregateCommandHandler, + InMemoryMessageBus, + EventDispatcher, + InMemorySnapshotStorage +} = require('node-cqrs'); +const UserAggregate = require('./UserAggregate.cjs'); +const UsersProjection = require('./UsersProjection.cjs'); + +/** + * DI container factory + */ +exports.createContainer = () => { + const builder = new ContainerBuilder(); + + // 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); + builder.registerProjection(UsersProjection, 'users'); + + // create instances of command/event handlers and related subscriptions + return builder.container(); +}; + +/** + * Same as above, but without the DI container + */ +exports.createBaseInstances = () => { + // create infrastructure services + const eventBus = new InMemoryMessageBus(); + const storage = new InMemoryEventStorage(); + const eventDispatcher = new EventDispatcher({ eventBus }) + eventDispatcher.addPipelineProcessor(storage); + + const eventStore = new EventStore({ eventStorageReader: storage, eventBus, eventDispatcher }); + const commandBus = new CommandBus(); + + /** @type {import('node-cqrs').IAggregateConstructor} */ + const aggregateType = UserAggregate; + + /** @type {import('node-cqrs').ICommandHandler} */ + const userCommandHandler = new AggregateCommandHandler({ eventStore, aggregateType }); + userCommandHandler.subscribe(commandBus); + + /** @type {import('node-cqrs').IProjection} */ + const usersProjection = new UsersProjection(); + usersProjection.subscribe(eventStore); + + return { + eventStore, + commandBus, + users: usersProjection.view + }; +}; diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js deleted file mode 100644 index 11e2bee..0000000 --- a/examples/user-domain/index.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -const { - ContainerBuilder, - InMemoryEventStorage, - CommandBus, - EventStore, - AggregateCommandHandler, - InMemoryMessageBus -} = require('../..'); // node-cqrs -const UserAggregate = require('./UserAggregate'); -const UsersProjection = require('./UsersProjection'); - -/** - * DI container factory - */ -exports.createContainer = () => { - const builder = new ContainerBuilder(); - - // register infrastructure services - builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('messageBus'); - - // register domain entities - builder.registerAggregate(UserAggregate); - builder.registerProjection(UsersProjection, 'users'); - - // create instances of command/event handlers and related subscriptions - return builder.container(); -}; - -/** - * Same as above, but without the DI container - */ -exports.createBaseInstances = () => { - // create infrastructure services - const messageBus = new InMemoryMessageBus(); - const storage = new InMemoryEventStorage(); - const eventStore = new EventStore({ storage, messageBus }); - const commandBus = new CommandBus({ messageBus }); - - /** @type {IAggregateConstructor} */ - const aggregateType = UserAggregate; - - /** @type {ICommandHandler} */ - const userCommandHandler = new AggregateCommandHandler({ eventStore, aggregateType }); - userCommandHandler.subscribe(commandBus); - - /** @type {IProjection} */ - const usersProjection = new UsersProjection(); - usersProjection.subscribe(eventStore); - - return { - eventStore, - commandBus, - users: usersProjection.view - }; -}; diff --git a/examples/user-domain-tests/index.test.js b/examples/user-domain/tests/index.test.cjs similarity index 86% rename from examples/user-domain-tests/index.test.js rename to examples/user-domain/tests/index.test.cjs index 4dcb988..482e6e7 100644 --- a/examples/user-domain-tests/index.test.js +++ b/examples/user-domain/tests/index.test.cjs @@ -1,8 +1,7 @@ 'use strict'; const { expect } = require('chai'); -const { createContainer, createBaseInstances } = require('../user-domain'); -const { nextCycle } = require('../../src/infrastructure/utils'); +const { createContainer, createBaseInstances } = require('../index.cjs'); 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/worker-projection/CounterProjection.cjs b/examples/worker-projection/CounterProjection.cjs new file mode 100644 index 0000000..3247bcf --- /dev/null +++ b/examples/worker-projection/CounterProjection.cjs @@ -0,0 +1,30 @@ +const { AbstractWorkerProjection } = require('node-cqrs/workers'); + +class CounterView { + counter = 0; + + increment() { + this.counter += 1; + } + + getCounter() { + return this.counter; + } +} + +class CounterProjection extends AbstractWorkerProjection { + constructor() { + super({ + workerModulePath: __filename, + view: new CounterView() + }); + } + + somethingHappened() { + this.view.increment(); + } +} + +CounterProjection.createInstanceIfWorkerThread(); + +module.exports = CounterProjection; diff --git a/examples/worker-projection/index.cjs b/examples/worker-projection/index.cjs new file mode 100644 index 0000000..230b185 --- /dev/null +++ b/examples/worker-projection/index.cjs @@ -0,0 +1,18 @@ +const CounterProjection = require('./CounterProjection.cjs'); + +async function main() { + const projection = new CounterProjection(); + + await projection.project({ id: '1', type: 'somethingHappened' }); + await projection.project({ id: '2', type: 'somethingHappened' }); + + const counter = await projection.view.getCounter(); + console.log('counter =', counter); + + projection.dispose(); +} + +main().catch(err => { + console.error(err); + process.exitCode = 1; +}); 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/jsconfig.json b/jsconfig.json index 1d876e5..b15664f 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,15 +1,29 @@ { "compilerOptions": { - "target": "es6", + "target": "es2022", "module": "commonjs", + "moduleResolution": "node", + "baseUrl": ".", + "paths": { + "node-cqrs": [ + "./types/index.d.ts" + ], + "node-cqrs/*": [ + "./types/*/index.d.ts" + ] + }, "allowSyntheticDefaultImports": true, "checkJs": true, "resolveJsonModule": true, "lib": [ - "es2018" + "es2022", + "dom" ] }, "exclude": [ - "node_modules" + "node_modules", + "dist", + "dist/**", + "examples/**/bundle.js" ] } diff --git a/package-lock.json b/package-lock.json index 264c148..78fe4a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,85 +1,111 @@ { "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", - "license": "MIT", + "version": "1.0.0-rc.33", + "license": "Apache-2.0", "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", + "@types/amqplib": "^0.10.8", + "@types/better-sqlite3": "^7.6.13", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.14", + "@types/md5": "^2.3.6", + "@types/node": "^20.19.30", + "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", + "amqplib": "^0.10.9", + "better-sqlite3": "^11.10.0", "chai": "^4.5.0", + "comlink": "^4.4.2", "conventional-changelog": "^3.1.25", - "coveralls": "^3.1.1", + "eslint": "^9.39.2", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.5.0", "jest": "^29.7.0", - "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "md5": "^2.3.0", + "sinon": "^19.0.5", + "ts-jest": "^29.4.6", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.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": ">=16.0.0" }, - "engines": { - "node": ">=6.0.0" + "peerDependencies": { + "amqplib": "^0.10.9", + "better-sqlite3": "^11.10.0", + "comlink": "^4.4.2", + "md5": "^2.3.0" + }, + "peerDependenciesMeta": { + "amqplib": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "comlink": { + "optional": true + }, + "md5": { + "optional": true + } } }, "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.28.5", + "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.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "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.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -94,30 +120,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.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "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.6", + "@babel/types": "^7.28.6", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "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.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -130,39 +169,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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "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.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "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.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -172,160 +234,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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "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.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "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.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "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.28.6", + "@babel/types": "^7.28.6" }, "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.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -339,6 +308,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 +321,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 +334,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 +342,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.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "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 +392,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 +401,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.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -414,6 +421,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 +434,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 +447,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 +460,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 +473,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 +486,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 +494,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 +527,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.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -512,46 +543,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.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "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.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "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.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "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.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "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.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "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.28.5" }, "engines": { "node": ">=6.9.0" @@ -561,13 +594,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,81 +615,444 @@ "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==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=6.9.0" + "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/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/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": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, + "license": "Apache-2.0", "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/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/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": ">=8" + "node": "*" } }, - "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/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, + "license": "Apache-2.0", "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" + "@eslint/core": "^0.17.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "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", + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "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.1", + "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.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "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.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.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", @@ -688,6 +1086,7 @@ "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 +1102,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 +1116,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 +1129,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 +1147,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 +1163,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 +1207,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 +1220,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 +1235,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 +1251,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 +1267,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 +1294,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 +1308,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,30 +1334,24 @@ "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" @@ -949,13 +1361,15 @@ "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 +1379,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 +1389,73 @@ "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" - } - }, - "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==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=4" + "@sinonjs/commons": "^3.0.1", + "type-detect": "^4.1.0" } }, "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 + "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, + "license": "(Unlicense OR Apache-2.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 + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "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.8", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-vtDp8Pk1wsE/AuQ8/Rgtm6KUZYqcnTgNvEHwzCkX8rL7AGsC6zqAfKAAJhUZXFhM/Pp++tbnUHiam/8vVpPztA==", + "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 +1465,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 +1479,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 +1533,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,67 +1551,91 @@ "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.6", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.6.tgz", + "integrity": "sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA==", + "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.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", "dev": true, + "license": "MIT", + "peer": true, "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": "*" } }, "node_modules/@types/sinonjs__fake-timers": { - "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 + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "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.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -1182,13 +1644,238 @@ "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", + "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/type-utils": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.53.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", + "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3" + }, + "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", + "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.53.1", + "@typescript-eslint/types": "^8.53.1", + "debug": "^4.4.3" + }, + "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", + "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", + "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", + "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", + "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", + "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.53.1", + "@typescript-eslint/tsconfig-utils": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", + "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.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.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", + "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.53.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", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1196,11 +1883,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 +1910,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 +1930,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==", + "dev": true, + "license": "MIT", + "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 +1965,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 +1975,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 +1991,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 +2004,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 +2080,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 +2097,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 +2109,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,67 +2136,129 @@ } }, "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 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "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/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "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==", + "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" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", + "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==", "dev": true, + "hasInstallScript": 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" + "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==", + "dev": true, + "license": "MIT", + "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==", + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, + "license": "MIT", "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 +2266,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 +2275,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.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1529,11 +2293,14 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", + "peer": true, "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1547,6 +2314,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 +2327,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==", + "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": { + "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==", + "dev": true, + "license": "MIT" }, "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 +2386,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 +2396,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 +2410,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.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { @@ -1622,19 +2427,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 +2454,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 +2471,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==", + "dev": true, + "license": "BSD-3-Clause", + "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 +2499,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==", + "dev": true, + "license": "ISC" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1696,21 +2517,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,22 +2546,25 @@ "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" } }, "node_modules/collect-v8-coverage": { - "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 + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "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 +2576,22 @@ "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 + "dev": true, + "license": "MIT" }, - "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==", + "node_modules/comlink": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.2.tgz", + "integrity": "sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g==", "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "license": "Apache-2.0" }, "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 +2601,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 +2632,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 +2646,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 +2659,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 +2672,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 +2687,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 +2713,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 +2726,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 +2739,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 +2752,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 +2765,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 +2779,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 +2789,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 +2808,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 +2837,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 +2857,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 +2893,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 +2911,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==", + "dev": true, + "license": "BSD-3-Clause", + "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 +2964,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 +2974,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 +2991,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==", + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "dev": true, + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -2175,6 +3032,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 +3040,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==", + "dev": true, + "license": "MIT", + "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==", + "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==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, "node_modules/detect-newline": { @@ -2205,20 +3082,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 +3108,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 +3118,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 +3126,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.278", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz", + "integrity": "sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==", + "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==", + "dev": true, + "license": "MIT", + "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": { + "is-arrayish": "^0.2.1" + } + }, + "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.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "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": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": 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": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "@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/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "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": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "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/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "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": ">=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/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 +3370,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 +3379,58 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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 +3458,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==", + "dev": true, + "license": "(MIT OR WTFPL)", + "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 +3485,63 @@ "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-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/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, + "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==", "dev": true, + "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" } }, - "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==", - "dev": true, - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "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==", + "dev": true, + "license": "MIT" + }, "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 +3550,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==", + "dev": true, + "license": "MIT" + }, "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 +3607,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2520,6 +3621,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 +3631,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 +3641,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 +3651,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 +3661,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 +3671,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 +3690,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 +3705,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 +3723,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 +3734,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 +3742,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 +3767,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 +3781,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 +3793,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==", + "dev": true, + "license": "MIT" + }, "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 +3842,69 @@ "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.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "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/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 +3921,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 +3936,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 +3946,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 +3959,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 +3971,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==", + "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": "BSD-3-Clause" + }, + "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 +4057,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 +4067,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 +4078,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 +4088,36 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "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 + "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==", + "dev": true, + "license": "MIT" }, "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 +4128,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 +4153,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 +4186,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 +4196,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 +4206,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 +4219,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 +4227,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 +4256,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 +4268,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 +4288,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 +4299,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 +4312,13 @@ "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", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -3149,6 +4345,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 +4360,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 +4392,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 +4426,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 +4441,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 +4460,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 +4470,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 +4516,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 +4532,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 +4545,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 +4562,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 +4580,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 +4590,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 +4616,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 +4630,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 +4646,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 +4667,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 +4682,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 +4700,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 +4710,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 +4731,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 +4745,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 +4778,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 +4812,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 +4839,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 +4862,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 +4880,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3682,6 +4893,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 +4913,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 +4929,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 +4944,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 +5035,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 +5055,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 +5087,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 +5144,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 +5158,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 +5168,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 + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "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 +5232,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 +5245,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 +5256,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 +5278,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 +5286,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==", + "dev": true, + "license": "BSD-3-Clause", + "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 +5324,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 +5408,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 +5426,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 +5436,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 +5449,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 +5459,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 +5469,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 +5481,21 @@ "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" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">= 0.6" - } + "dev": true, + "license": "MIT" }, - "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,29 +5503,48 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "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": { @@ -4248,6 +5552,7 @@ "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 +5562,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 +5572,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==", + "dev": true, + "license": "MIT" + }, "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==", + "dev": true, + "license": "MIT" }, "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.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "dev": true, + "license": "MIT", + "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.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "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 +5684,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 +5699,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 +5707,12 @@ "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 +5722,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 +5733,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 +5768,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 +5788,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 +5811,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 +5830,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 +5840,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 +5850,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 +5859,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 +5891,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 +5901,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 +5931,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 +5951,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 +5959,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==", + "dev": true, + "license": "MIT", + "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 +6072,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 +6084,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 +6101,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==", + "dev": true, + "license": "MIT", + "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 +6136,8 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ] + ], + "license": "MIT" }, "node_modules/q": { "version": "1.5.1", @@ -4682,40 +6145,68 @@ "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==", "dev": true, - "engines": { - "node": ">=0.6" - } + "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==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "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==", + "dev": true, + "license": "MIT", + "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 +6221,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 +6235,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 +6248,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 +6262,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 +6275,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 +6288,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 +6298,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 +6307,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 +6328,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" } @@ -4837,6 +6338,7 @@ "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 +6353,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 +6362,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==", + "dev": true, + "license": "MIT" + }, "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.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.1", "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 +6405,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,20 +6413,32 @@ "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" } @@ -4965,21 +6461,20 @@ "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==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/shebang-command": { @@ -4987,6 +6482,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 +6495,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 +6504,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==", + "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" + }, + "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==", + "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": { + "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 +6573,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 +6605,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 +6615,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 +6626,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 +6636,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 +6675,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 +6684,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 +6700,22 @@ "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 +6725,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 +6739,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 +6754,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 +6767,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 +6777,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 +6787,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 +6800,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 +6813,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 +6826,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 +6834,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==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "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 +6879,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,37 +6917,81 @@ "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" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmpl": { "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 +6999,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.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "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.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "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 +7047,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 +7069,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 +7093,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 +7103,8 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5472,10 +7144,11 @@ } }, "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -5485,6 +7158,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -5492,17 +7166,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 +7194,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 +7203,12 @@ } }, "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", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5532,11 +7217,36 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", + "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.53.1", + "@typescript-eslint/parser": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.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 +7256,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.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -5570,9 +7281,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 +7298,42 @@ "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==", + "dev": true, + "license": "MIT", + "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 +7348,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 +7369,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 +7380,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 +7419,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "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 +7441,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 +7451,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 +7460,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 +7487,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 +7497,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 +7507,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..a850bda 100644 --- a/package.json +++ b/package.json @@ -1,62 +1,130 @@ { "name": "node-cqrs", - "version": "0.17.0", + "version": "1.0.0-rc.33", "description": "Basic ES6 backbone for CQRS app development", + "type": "module", + "keywords": [ + "cqrs", + "eventsourcing" + ], "repository": { "type": "git", "url": "https://github.com/snatalenko/node-cqrs.git" }, + "main": "./dist/cjs/index.js", + "types": "./types/index.d.ts", + "typesVersions": { + "*": { + "workers": [ + "types/workers/index.d.ts" + ], + "rabbitmq": [ + "types/rabbitmq/index.d.ts" + ], + "sqlite": [ + "types/sqlite/index.d.ts" + ] + } + }, + "exports": { + ".": { + "require": "./dist/cjs/index.js", + "import": "./dist/esm/index.js", + "types": "./types/index.d.ts" + }, + "./workers": { + "require": "./dist/cjs/workers/index.js", + "import": "./dist/esm/workers/index.js", + "types": "./types/workers/index.d.ts" + }, + "./rabbitmq": { + "require": "./dist/cjs/rabbitmq/index.js", + "import": "./dist/esm/rabbitmq/index.js", + "types": "./types/rabbitmq/index.d.ts" + }, + "./sqlite": { + "require": "./dist/cjs/sqlite/index.js", + "import": "./dist/esm/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": ">=16.0.0" }, "scripts": { + "cleanup": "rm -rf ./dist ./types ./coverage", "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", - "build": "tsc --build", + "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:cjs": "tsc -p ./tsconfig.cjs.json && cp ./scripts/etc/cjs-package.json ./dist/cjs/package.json", + "build:esm": "tsc -p ./tsconfig.esm.json", + "build:browser": "tsc -p ./tsconfig.browser.json && npx browserify dist/browser/cjs/index.js --standalone Cqrs --outfile ./dist/browser/bundle.iife.js", + "build": "npm run build:esm && npm run build:cjs", "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", + "license": "Apache-2.0", "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", + "@types/amqplib": "^0.10.8", + "@types/better-sqlite3": "^7.6.13", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.14", + "@types/md5": "^2.3.6", + "@types/node": "^20.19.30", + "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", + "amqplib": "^0.10.9", + "better-sqlite3": "^11.10.0", "chai": "^4.5.0", + "comlink": "^4.4.2", "conventional-changelog": "^3.1.25", - "coveralls": "^3.1.1", + "eslint": "^9.39.2", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.5.0", "jest": "^29.7.0", - "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "md5": "^2.3.0", + "sinon": "^19.0.5", + "ts-jest": "^29.4.6", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.9.3", + "typescript-eslint": "^8.53.1" + }, + "peerDependencies": { + "amqplib": "^0.10.9", + "better-sqlite3": "^11.10.0", + "comlink": "^4.4.2", + "md5": "^2.3.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "comlink": { + "optional": true + }, + "md5": { + "optional": true + } } } 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/scripts/etc/cjs-package.json b/scripts/etc/cjs-package.json new file mode 100644 index 0000000..5bbefff --- /dev/null +++ b/scripts/etc/cjs-package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index a7da6ae..6c950bc 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -1,49 +1,43 @@ import { - IAggregate, - IMutableAggregateState, - ICommand, - Identifier, - IEvent, - IEventSet, - IAggregateConstructorParams -} 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'; + type IAggregate, + type IMutableAggregateState, + type ICommand, + type Identifier, + type IEvent, + type IEventSet, + type IAggregateConstructorParams, + SNAPSHOT_EVENT_TYPE +} from './interfaces/index.ts'; + +import { getClassName, validateHandlers, getHandler, getMessageHandlerNames, clone } from './utils/index.ts'; /** * Base class for Aggregate definition */ -export abstract class AbstractAggregate implements IAggregate { +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 +57,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 +90,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,18 +110,73 @@ 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): IEvent; + protected emit(type: string, payload: TPayload): IEvent; + 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); + const event = this.makeEvent(type, payload as TPayload, this.command); this.emitRaw(event); + + return event; } /** Format event based on a current aggregate state and a command being executed */ - protected makeEvent(type: string, payload?: TPayload, sourceCommand?: ICommand): IEvent { + protected makeEvent(type: string, payload: TPayload, sourceCommand?: ICommand): IEvent { const event: IEvent = { aggregateId: this.id, aggregateVersion: this.version, @@ -181,24 +211,23 @@ export abstract class AbstractAggregate) { if (!snapshotEvent) diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index 54b52b4..d6c98ec 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -1,99 +1,131 @@ -import { InMemoryView } from './infrastructure/InMemoryView'; - +import { describe } from './Event.ts'; +import { InMemoryView } from './in-memory/InMemoryView.ts'; import { - IProjectionView, - IEvent, - IPersistentView, - IEventStore, - IExtendableLogger, - ILogger, - IProjection, - IViewFactory -} from "./interfaces"; + type IViewLocker, + type IEventLocker, + type IProjection, + type ILogger, + type IExtendableLogger, + type IEvent, + type IObservable, + type IEventStorageReader, + isViewLocker, + isEventLocker +} from './interfaces/index.ts'; import { getClassName, validateHandlers, getHandler, - getHandledMessageTypes, - subscribe -} from './utils'; + subscribe, + getMessageHandlerNames +} from './utils/index.ts'; + +export type AbstractProjectionParams = { + + /** + * The default view associated with the projection. + * Can optionally implement IViewLocker and/or IEventLocker. + */ + view?: T, -const isProjectionView = (view: IProjectionView): view is IProjectionView => - 'ready' in view && - 'lock' in view && - 'unlock' in view && - 'once' in view; + /** + * 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 | null; + #eventLocker?: IEventLocker | null; + 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(); + public get view(): TView { + return this.#view ?? (this.#view = new InMemoryView() as TView); + } - return this.#view; + protected set view(value: TView) { + this.#view = value; } - #viewFactory: IViewFactory; - #view?: TView; + /** + * Manages view restoration state to prevent early access to an inconsistent view + * or conflicts from concurrent restoration by other processes. + */ + protected get _viewLocker(): IViewLocker | null { + if (this.#viewLocker === undefined) + this.#viewLocker = isViewLocker(this.view) ? this.view : null; - protected _logger?: ILogger; + return this.#viewLocker; + } - get collectionName(): string { - return getClassName(this); + protected set _viewLocker(value: IViewLocker | undefined | null) { + 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 | null { + if (this.#eventLocker === undefined) + this.#eventLocker = isEventLocker(this.view) ? this.view : null; + + return this.#eventLocker; + } + + protected set _eventLocker(value: IEventLocker | undefined | null) { + 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) }) : logger; } - /** Subscribe to event store */ - async subscribe(eventStore: IEventStore): Promise { + /** + * Subscribe to event store + * and restore view state from not yet projected events + */ + async subscribe(eventStore: IObservable & IEventStorageReader): Promise { subscribe(eventStore, this, { - masterHandler: (e: IEvent) => this.project(e) + masterHandler: this.project }); await this.restore(eventStore); @@ -101,9 +133,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 +148,53 @@ 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(); + await handler.call(this, event); - const shouldRestore = await this.shouldRestoreView; - if (shouldRestore) - await this._restore(eventStore); + if (this._eventLocker) + await this._eventLocker.markAsProjected(event); + } - if (concurrentView) - concurrentView.unlock(); + /** + * Restore view state from not-yet-projected events. + * + * Lock the view to ensure same restoring procedure + * won't be performed by another projection instance. + * */ + async restore(eventStore: IEventStorageReader): Promise { + if (this._viewLocker) + await this._viewLocker.lock(); + + await this._restore(eventStore); + + if (this._viewLocker) + this._viewLocker.unlock(); } - /** Restore projection view from event store */ - protected async _restore(eventStore: IEventStore): Promise { + /** Restore view state from not-yet-projected events */ + protected async _restore(eventStore: IEventStorageReader): 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 events and restoring projection...'); + this._logger?.debug(`retrieving ${lastEvent ? `events after ${describe(lastEvent)}` : 'all events'}...`); + + 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,21 +203,27 @@ 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..d6c49d6 100644 --- a/src/CommandBus.ts +++ b/src/CommandBus.ts @@ -1,5 +1,5 @@ -import { InMemoryMessageBus } from "./infrastructure/InMemoryMessageBus"; -import { +import { InMemoryMessageBus } from './in-memory/index.ts'; +import type { ICommand, ICommandBus, IEventSet, @@ -7,25 +7,22 @@ import { ILogger, IMessageBus, IMessageHandler -} from "./interfaces"; +} from './interfaces/index.ts'; 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,23 +52,52 @@ 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 + } + ): Promise; + + /** + * Format and send a command for execution (obsolete signature) + * + * @deprecated Use `send(type, aggregateId, { context, payload })` + */ + send( + type: string, + aggregateId?: string, + context?: object, + payload?: TPayload + ): Promise; + + send( + type: string, + aggregateId?: string, + options?: { + payload?: TPayload, + context?: object + } | object, + payload?: TPayload + ): Promise { if (typeof type !== 'string' || !type.length) throw new TypeError('type argument must be a non-empty String'); - if (options && typeof options !== 'object') + if (options !== undefined && (options === null || typeof options !== 'object')) throw new TypeError('options argument, when defined, must be an Object'); - if (otherArgs.length > 1) - throw new TypeError('more than expected arguments supplied'); // obsolete. left for backward compatibility - const optionsContainContext = options && !('context' in options) && !('payload' in options); - if (otherArgs.length || optionsContainContext) { + const isOptionsObject = !!options && ('context' in options || 'payload' in options); + if (!isOptionsObject) { const context = options; - const payload = otherArgs.length ? otherArgs[0] : undefined; return this.sendRaw({ type, aggregateId, context, payload }); } - return this.sendRaw({ type, aggregateId, ...options }); + if (payload !== undefined) + throw new TypeError('more than expected arguments supplied'); + + return this.sendRaw({ type, aggregateId, ...options } as ICommand); } /** diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index e2c8e0d..158ba88 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -1,47 +1,47 @@ -import { ContainerBuilder, Container, 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 { ContainerBuilder, type TypeConfig, type TClassOrFactory } from 'di0'; +import { AggregateCommandHandler } from './AggregateCommandHandler.ts'; +import { CommandBus } from './CommandBus.ts'; +import { EventStore } from './EventStore.ts'; +import { SagaEventHandler } from './SagaEventHandler.ts'; +import { EventDispatcher } from './EventDispatcher.ts'; +import { InMemoryMessageBus } from './in-memory/index.ts'; +import { isClass } from './utils/isClass.ts'; import { - IAggregateConstructor, - ICommandBus, - ICommandHandler, - IEventReceptor, - IEventStore, - IProjection, - IProjectionConstructor, - ISagaConstructor -} from './interfaces'; - -interface CqrsContainer extends Container { - eventStore: IEventStore; - commandBus: ICommandBus; -} - -export class CqrsContainerBuilder extends ContainerBuilder { + type IAggregateConstructor, + type ICommandHandler, + type IContainer, + type IEventReceptor, + type IProjection, + type IProjectionConstructor, + type ISagaConstructor, + isDispatchPipelineProcessor +} from './interfaces/index.ts'; + +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..86318e7 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 type { IEvent } from './interfaces/IEvent.ts'; /** * 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..7f6cc68 --- /dev/null +++ b/src/EventDispatchPipeline.ts @@ -0,0 +1,109 @@ +import { + type DispatchPipelineBatch, + type IEvent, + type IDispatchPipelineProcessor, + type IEventBus, + isDispatchPipelineProcessor, + isSnapshotEvent +} from './interfaces/index.ts'; + +import { parallelPipe } from 'async-parallel-pipe'; +import { AsyncIterableBuffer } from 'async-iterable-buffer'; +import { getClassName } from './utils/index.ts'; + +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; + + readonly #eventBus; + readonly #concurrentLimit: number; + + constructor(eventBus: IEventBus, concurrentLimit: number) { + this.#eventBus = eventBus; + this.#concurrentLimit = concurrentLimit; + } + + 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; + + void 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..f8de8a1 --- /dev/null +++ b/src/EventDispatcher.ts @@ -0,0 +1,126 @@ +import { + type IEventDispatcher, + type IDispatchPipelineProcessor, + type IEventSet, + type IEventBus, + type IContainer, + isEventSet +} from './interfaces/index.ts'; +import { InMemoryMessageBus } from './in-memory/index.ts'; +import { type EventBatchEnvelope, EventDispatchPipeline } from './EventDispatchPipeline.ts'; + +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..c6d7cf3 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -1,124 +1,108 @@ import { - IAggregateSnapshotStorage, - Identifier, - IEvent, - IEventQueryFilter, - IEventStorage, - 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'; + type IAggregateSnapshotStorage, + type IEvent, + type IEventStorageReader, + type IEventSet, + type ILogger, + type IMessageHandler, + type IObservable, + type IEventStream, + type IEventStore, + type EventQueryAfter, + type EventQueryBefore, + type Identifier, + type IIdentifierProvider, + type IEventDispatcher, + type IEventBus, + type IContainer, + isIdentifierProvider, + isIEventBus, + isIEventStorageReader, + isEventSet, + isIObservableQueueProvider +} from './interfaces/index.ts'; +import { + getClassName, + setupOneTimeEmitterSubscription +} from './utils/index.ts'; +import { EventDispatcher } from './EventDispatcher.ts'; 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, + eventDispatchPipeline, + eventDispatchPipelines, 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'); + if (eventBus && !isIEventBus(eventBus)) + throw new TypeError('eventBus does not implement IMessageBus interface'); - const defaultEventEmitter = isIObservable(storage) ? storage : messageBus; - if (!defaultEventEmitter) - throw new TypeError('storage must implement IObservable if messageBus is not injected'); - - this.#publishAsync = eventStoreConfig?.publishAsync ?? true; - this.#validator = eventValidator; + this.#eventStorageReader = eventStorageReader; + this.#identifierProvider = identifierProvider; + this.#snapshotStorage = snapshotStorage; + this.#eventDispatcher = eventDispatcher ?? new EventDispatcher({ + eventBus, + eventDispatchPipeline, + eventDispatchPipelines + }); + 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 +112,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 +135,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 +147,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'); - - const containsSagaStarters = this.#sagaStarters.length && events.some(e => this.#sagaStarters.includes(e.type)); - const augmentedEvents = containsSagaStarters ? - await this.#attachSagaIdToSagaStarterEvents(events) : - events; - - const eventStreamWithoutSnapshots = await this.save(augmentedEvents); + async dispatch(events: IEventSet): Promise { + if (!isEventSet(events) || events.length === 0) + throw new TypeError('dispatch requires a non-empty array of events'); - // after events are saved to the persistent storage, - // publish them to the event bus (i.e. RabbitMq) - if (this.#messageBus) - await this.#publish(eventStreamWithoutSnapshots); + const augmentedEvents = await this.#attachSagaIdToSagaStarterEvents(events); - 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 +191,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 (!isIObservableQueueProvider(this.eventBus)) + 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..b758191 100644 --- a/src/SagaEventHandler.ts +++ b/src/SagaEventHandler.ts @@ -1,21 +1,22 @@ -import * as Event from './Event'; -import { +import * as Event from './Event.ts'; +import type { ICommandBus, + IContainer, IEvent, IEventReceptor, IEventStore, - IExtendableLogger, ILogger, IObservable, ISaga, ISagaConstructor, ISagaFactory -} from './interfaces'; +} from './interfaces/index.ts'; import { subscribe, - getClassName -} from './utils'; + getClassName, + iteratorToArray +} from './utils/index.ts'; /** * Listens to Saga events, @@ -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..d3071d1 --- /dev/null +++ b/src/in-memory/InMemoryEventStorage.ts @@ -0,0 +1,108 @@ +import type { + IIdentifierProvider, + IEvent, + IEventSet, + EventQueryAfter, + IEventStorageReader, + IEventStream, + IEventStorageWriter, + Identifier, + IDispatchPipelineProcessor, + DispatchPipelineBatch +} from '../interfaces/index.ts'; +import { nextCycle } from './utils/index.ts'; + +/** + * 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..ce6ac25 --- /dev/null +++ b/src/in-memory/InMemoryLock.ts @@ -0,0 +1,43 @@ +import { Deferred } from '../utils/index.ts'; + +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 61% rename from src/infrastructure/InMemoryMessageBus.ts rename to src/in-memory/InMemoryMessageBus.ts index c8f43f3..73b02dd 100644 --- a/src/infrastructure/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -1,28 +1,29 @@ -import { +import type { ICommand, IEvent, IMessageBus, IMessageHandler, - IObservable -} from "../interfaces"; + IObservable, + IObservableQueueProvider +} from '../interfaces/index.ts'; /** * Default implementation of the message bus. * Keeps all subscriptions and messages in memory. */ -export class InMemoryMessageBus implements IMessageBus { +export class InMemoryMessageBus implements IMessageBus, IObservableQueueProvider { - #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 +39,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 +73,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 +88,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 +96,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..51ebaaa --- /dev/null +++ b/src/in-memory/InMemorySnapshotStorage.ts @@ -0,0 +1,86 @@ +import { + type DispatchPipelineBatch, + type IAggregateSnapshotStorage, + type IContainer, + type Identifier, + type IDispatchPipelineProcessor, + type IEvent, + type ILogger, + isSnapshotEvent +} from '../interfaces/index.ts'; +import * as Event from '../Event.ts'; + +/** + * 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 88% rename from src/infrastructure/InMemoryView.ts rename to src/in-memory/InMemoryView.ts index 20f528a..b76e49f 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 { nextCycle } from './utils'; +import { InMemoryLock } from './InMemoryLock.ts'; +import type { IViewLocker, Identifier, IObjectStorage } from '../interfaces/index.ts'; +import { nextCycle } from './utils/index.ts'; /** * 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..74b00c7 --- /dev/null +++ b/src/in-memory/index.ts @@ -0,0 +1,5 @@ +export * from './InMemoryEventStorage.ts'; +export * from './InMemoryLock.ts'; +export * from './InMemoryMessageBus.ts'; +export * from './InMemorySnapshotStorage.ts'; +export * from './InMemoryView.ts'; diff --git a/src/in-memory/utils/index.ts b/src/in-memory/utils/index.ts new file mode 100644 index 0000000..408a24b --- /dev/null +++ b/src/in-memory/utils/index.ts @@ -0,0 +1 @@ +export * from './nextCycle.ts'; diff --git a/src/infrastructure/utils/nextCycle.ts b/src/in-memory/utils/nextCycle.ts similarity index 86% rename from src/infrastructure/utils/nextCycle.ts rename to src/in-memory/utils/nextCycle.ts index 69c01a4..346ac96 100644 --- a/src/infrastructure/utils/nextCycle.ts +++ b/src/in-memory/utils/nextCycle.ts @@ -1,4 +1,4 @@ /** * @returns Promise that resolves on next event loop cycle */ -export const nextCycle = (): Promise => new Promise(rs => setImmediate(rs)); +export const nextCycle = (): Promise => new Promise(rs => setTimeout(rs, 0)); diff --git a/src/index.ts b/src/index.ts index 94b96da..a802aff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,21 @@ -export { CqrsContainerBuilder as ContainerBuilder } from './CqrsContainerBuilder'; +export { CqrsContainerBuilder as ContainerBuilder } from './CqrsContainerBuilder.ts'; -export * from './CommandBus'; -export * from './EventStore'; +export * from './CommandBus.ts'; +export * from './EventStore.ts'; -export * from './AbstractAggregate'; -export * from './AggregateCommandHandler'; -export * from './AbstractSaga'; -export * from './SagaEventHandler'; -export * from './AbstractProjection'; +export * from './AbstractAggregate.ts'; +export * from './AggregateCommandHandler.ts'; +export * from './AbstractSaga.ts'; +export * from './SagaEventHandler.ts'; +export * from './AbstractProjection.ts'; +export * from './EventDispatcher.ts'; -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/index.ts'; -export * as Event from './Event'; +export * as Event from './Event.ts'; export { getMessageHandlerNames, - getHandledMessageTypes, subscribe -} from './utils'; +} from './utils/index.ts'; -export * from './interfaces'; +export * from './interfaces/index.ts'; 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/infrastructure/utils/index.ts b/src/infrastructure/utils/index.ts deleted file mode 100644 index 3622022..0000000 --- a/src/infrastructure/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Deferred'; -export * from './nextCycle'; 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..528ee9c --- /dev/null +++ b/src/interfaces/IAggregate.ts @@ -0,0 +1,68 @@ +import type { ICommand } from './ICommand.ts'; +import type { Identifier } from './Identifier.ts'; +import type { IEvent } from './IEvent.ts'; +import type { IEventSet } from './IEventSet.ts'; + +/** + * 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..9dae771 --- /dev/null +++ b/src/interfaces/IAggregateSnapshotStorage.ts @@ -0,0 +1,11 @@ +import type { Identifier } from './Identifier.ts'; +import type { IEvent } from './IEvent.ts'; + +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..e674513 --- /dev/null +++ b/src/interfaces/ICommand.ts @@ -0,0 +1,3 @@ +import type { IMessage } from './IMessage.ts'; + +export type ICommand = Omit, 'aggregateVersion'>; diff --git a/src/interfaces/ICommandBus.ts b/src/interfaces/ICommandBus.ts new file mode 100644 index 0000000..edea416 --- /dev/null +++ b/src/interfaces/ICommandBus.ts @@ -0,0 +1,16 @@ +import type { ICommand } from './ICommand.ts'; +import type { IEventSet } from './IEventSet.ts'; +import type { IObservable } from './IObservable.ts'; +import type { IObserver } from './IObserver.ts'; + +export interface ICommandBus extends IObservable { + send(commandType: string, aggregateId?: string, 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..27642d7 --- /dev/null +++ b/src/interfaces/IContainer.ts @@ -0,0 +1,33 @@ +import { Container } from 'di0'; +import type { ICommandBus } from './ICommandBus.ts'; +import type { IEventDispatcher } from './IEventDispatcher.ts'; +import type { IEventStore } from './IEventStore.ts'; +import type { IEventBus } from './IEventBus.ts'; +import type { IDispatchPipelineProcessor } from './IDispatchPipelineProcessor.ts'; +import type { IEventStorageReader } from './IEventStorageReader.ts'; +import type { IAggregateSnapshotStorage } from './IAggregateSnapshotStorage.ts'; +import type { IIdentifierProvider } from './IIdentifierProvider.ts'; +import type { IExtendableLogger, ILogger } from './ILogger.ts'; +import type { IEventStorageWriter } from './IEventStorageWriter.ts'; + +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..924313c --- /dev/null +++ b/src/interfaces/IDispatchPipelineProcessor.ts @@ -0,0 +1,35 @@ +import type { IEvent } from './IEvent.ts'; +import { isObject } from './isObject.ts'; + +/** + * 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..eabc728 --- /dev/null +++ b/src/interfaces/IEvent.ts @@ -0,0 +1,14 @@ +import type { IMessage } from './IMessage.ts'; +import { isObject } from './isObject.ts'; + +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..10bd236 --- /dev/null +++ b/src/interfaces/IEventBus.ts @@ -0,0 +1,11 @@ +import type { IEvent } from './IEvent.ts'; +import { type IObservable, isIObservable } from './IObservable.ts'; + +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..a8c3d1d --- /dev/null +++ b/src/interfaces/IEventDispatcher.ts @@ -0,0 +1,7 @@ +import type { IEventSet } from './IEventSet.ts'; +import type { IEventBus } from './IEventBus.ts'; + +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..0731d7a --- /dev/null +++ b/src/interfaces/IEventLocker.ts @@ -0,0 +1,41 @@ +import type { IEvent } from './IEvent.ts'; +import { isObject } from './isObject.ts'; + +/** + * 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 + ) || ( + typeof view === 'function' + && typeof (view as any).getLastEvent === 'function' + && typeof (view as any).tryMarkAsProjecting === 'function' + && typeof (view as any).markAsProjected === 'function' + ); diff --git a/src/interfaces/IEventReceptor.ts b/src/interfaces/IEventReceptor.ts new file mode 100644 index 0000000..af34913 --- /dev/null +++ b/src/interfaces/IEventReceptor.ts @@ -0,0 +1,6 @@ +import type { IEventStore } from './IEventStore.ts'; +import type { IObserver } from './IObserver.ts'; + +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..3d95808 --- /dev/null +++ b/src/interfaces/IEventSet.ts @@ -0,0 +1,7 @@ +import { type IEvent, isEvent } from './IEvent.ts'; + +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..9329ae7 --- /dev/null +++ b/src/interfaces/IEventStorageReader.ts @@ -0,0 +1,44 @@ +import type { Identifier } from './Identifier.ts'; +import type { IEvent } from './IEvent.ts'; +import type { IEventStream } from './IEventStream.ts'; +import { isObject } from './isObject.ts'; + +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..f5330b5 --- /dev/null +++ b/src/interfaces/IEventStorageWriter.ts @@ -0,0 +1,10 @@ +import type { IEventSet } from './IEventSet.ts'; + +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..07a7b2a --- /dev/null +++ b/src/interfaces/IEventStore.ts @@ -0,0 +1,14 @@ +import type { IEventDispatcher } from './IEventDispatcher.ts'; +import type { IEvent } from './IEvent.ts'; +import type { IEventStorageReader } from './IEventStorageReader.ts'; +import type { IIdentifierProvider } from './IIdentifierProvider.ts'; +import type { IMessageHandler, IObservable } from './IObservable.ts'; +import type { IObservableQueueProvider } from './IObservableQueueProvider.ts'; + +export interface IEventStore + extends IObservable, IObservableQueueProvider, 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..0739242 --- /dev/null +++ b/src/interfaces/IEventStream.ts @@ -0,0 +1,3 @@ +import type { IEvent } from './IEvent.ts'; + +export type IEventStream = AsyncIterableIterator>; diff --git a/src/interfaces/IIdentifierProvider.ts b/src/interfaces/IIdentifierProvider.ts new file mode 100644 index 0000000..b168580 --- /dev/null +++ b/src/interfaces/IIdentifierProvider.ts @@ -0,0 +1,17 @@ +import type { Identifier } from './Identifier.ts'; +import { isObject } from './isObject.ts'; + +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..4ebaa13 --- /dev/null +++ b/src/interfaces/IMessage.ts @@ -0,0 +1,37 @@ +import type { Identifier } from './Identifier.ts'; +import { isObject } from './isObject.ts'; + +export interface IMessage { + + /** Event or command type */ + type: string; + + /** + * Target aggregate identifier for commands, + * originating aggregate identifier for events + */ + aggregateId?: Identifier; + + /** Aggregate version at the time of the message */ + aggregateVersion?: number; + + /** Saga identifier (used when a saga coordinates multiple steps/commands) */ + sagaId?: Identifier; + + /** Saga version for ordering saga events */ + sagaVersion?: number; + + /** Business data */ + payload: TPayload; + + /** + * Optional metadata/context (e.g. auth info, request id); + * Commonly set on commands, then copied to emitted events + */ + 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..a42e343 --- /dev/null +++ b/src/interfaces/IMessageBus.ts @@ -0,0 +1,8 @@ +import type { ICommand } from './ICommand.ts'; +import type { IEvent } from './IEvent.ts'; +import type { IObservable } from './IObservable.ts'; + +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..3b28bca --- /dev/null +++ b/src/interfaces/IObjectStorage.ts @@ -0,0 +1,13 @@ +import type { Identifier } from './Identifier.ts'; + +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..4c62d00 --- /dev/null +++ b/src/interfaces/IObservable.ts @@ -0,0 +1,26 @@ +import type { IMessage } from './IMessage.ts'; +import { isObject } from './isObject.ts'; + +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; +} + +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/IObservableQueueProvider.ts b/src/interfaces/IObservableQueueProvider.ts new file mode 100644 index 0000000..437d301 --- /dev/null +++ b/src/interfaces/IObservableQueueProvider.ts @@ -0,0 +1,15 @@ +import type { IObservable } from './IObservable.ts'; +import { isObject } from './isObject.ts'; + +export interface IObservableQueueProvider { + + /** + * Get or create a named queue, which delivers events to a single handler only + */ + queue(name: string): IObservable; +} + +export const isIObservableQueueProvider = (obj: unknown): obj is IObservableQueueProvider => + isObject(obj) + && 'queue' in obj + && typeof obj.queue === 'function'; diff --git a/src/interfaces/IObserver.ts b/src/interfaces/IObserver.ts new file mode 100644 index 0000000..f94bc5e --- /dev/null +++ b/src/interfaces/IObserver.ts @@ -0,0 +1,5 @@ +import type { IObservable } from './IObservable.ts'; + +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..b6a749d --- /dev/null +++ b/src/interfaces/IProjection.ts @@ -0,0 +1,22 @@ +import type { IObserver } from './IObserver.ts'; +import type { IObservable } from './IObservable.ts'; +import type { IEventStorageReader } from './IEventStorageReader.ts'; +import type { IEvent } from './IEvent.ts'; + +export interface IProjection extends IObserver { + readonly view: TView; + + /** Subscribe to new events */ + subscribe(eventStore: IObservable): Promise | void; + + /** Restore view state from not-yet-projected events */ + restore(eventStore: IEventStorageReader): Promise | void; + + /** Project new event */ + project(event: IEvent): Promise | void; +} + +export interface IProjectionConstructor { + new(c?: any): IProjection; + readonly handles?: string[]; +} diff --git a/src/interfaces/ISaga.ts b/src/interfaces/ISaga.ts new file mode 100644 index 0000000..104f7b3 --- /dev/null +++ b/src/interfaces/ISaga.ts @@ -0,0 +1,38 @@ +import type { ICommand } from './ICommand.ts'; +import type { Identifier } from './Identifier.ts'; +import type { IEvent } from './IEvent.ts'; +import type { IEventSet } from './IEventSet.ts'; + +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..8ca2afe --- /dev/null +++ b/src/interfaces/ISnapshotEvent.ts @@ -0,0 +1,10 @@ +import { type IEvent, isEvent } from './IEvent.ts'; + +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..90b3e1b --- /dev/null +++ b/src/interfaces/IViewLocker.ts @@ -0,0 +1,54 @@ +import { isObject } from './isObject.ts'; + +/** + * 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 + ) || ( + typeof view === 'function' + && typeof (view as any).lock === 'function' + && typeof (view as any).unlock === 'function' + && typeof (view as any).once === 'function' + && (view as any).ready !== undefined + ); 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..05b5e7a --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,29 @@ +export * from './IAggregate.ts'; +export * from './IAggregateSnapshotStorage.ts'; +export * from './ICommand.ts'; +export * from './ICommandBus.ts'; +export * from './IContainer.ts'; +export * from './Identifier.ts'; +export * from './IDispatchPipelineProcessor.ts'; +export * from './IEvent.ts'; +export * from './IEventBus.ts'; +export * from './IEventDispatcher.ts'; +export * from './IEventLocker.ts'; +export * from './IEventReceptor.ts'; +export * from './IEventSet.ts'; +export * from './IEventStorageReader.ts'; +export * from './IEventStorageWriter.ts'; +export * from './IEventStore.ts'; +export * from './IEventStream.ts'; +export * from './IIdentifierProvider.ts'; +export * from './ILogger.ts'; +export * from './IMessage.ts'; +export * from './IMessageBus.ts'; +export * from './IObjectStorage.ts'; +export * from './IObservable.ts'; +export * from './IObservableQueueProvider.ts'; +export * from './IObserver.ts'; +export * from './IProjection.ts'; +export * from './ISaga.ts'; +export * from './ISnapshotEvent.ts'; +export * from './IViewLocker.ts'; 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..936850d --- /dev/null +++ b/src/rabbitmq/IContainer.ts @@ -0,0 +1,7 @@ +import { RabbitMqGateway } from './RabbitMqGateway.ts'; + +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..7a40ce1 --- /dev/null +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -0,0 +1,86 @@ +import type { IEvent, IEventBus, IMessageHandler, IObservable, IObservableQueueProvider } from '../interfaces/index.ts'; +import { RabbitMqGateway } from './RabbitMqGateway.ts'; + +/** + * RabbitMQ-backed `IEventBus` with named queues support + */ +export class RabbitMqEventBus implements IEventBus, IObservableQueueProvider { + + 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..4fbe805 --- /dev/null +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -0,0 +1,683 @@ +import type { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; +import { type IContainer, type ILogger, type IMessage, isMessage } from '../interfaces/index.ts'; +import * as Event from '../Event.ts'; +import { extractErrorDetails, Lock } from '../utils/index.ts'; +import { registerExitCleanup } from './utils/index.ts'; +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 provides RabbitMQ-based publish/subscribe for ICommand/IEvent-style messages. + * + * 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..20d12c6 --- /dev/null +++ b/src/rabbitmq/index.ts @@ -0,0 +1,2 @@ +export * from './RabbitMqEventBus.ts'; +export * from './RabbitMqGateway.ts'; diff --git a/src/rabbitmq/utils/index.ts b/src/rabbitmq/utils/index.ts new file mode 100644 index 0000000..61dbe19 --- /dev/null +++ b/src/rabbitmq/utils/index.ts @@ -0,0 +1 @@ +export * from './registerExitCleanup.ts'; 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..8bb0b1f --- /dev/null +++ b/src/sqlite/AbstractSqliteAccessor.ts @@ -0,0 +1,58 @@ +import type { IContainer } from '../interfaces/index.ts'; +import { Lock } from '../utils/index.ts'; +import type { 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..12ef42a --- /dev/null +++ b/src/sqlite/AbstractSqliteObjectProjection.ts @@ -0,0 +1,31 @@ +import { AbstractProjection } from '../AbstractProjection.ts'; +import { IContainer } from '../interfaces/index.ts'; +import { SqliteObjectView } from './SqliteObjectView.ts'; + +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..350abe3 --- /dev/null +++ b/src/sqlite/AbstractSqliteView.ts @@ -0,0 +1,56 @@ +import { IContainer, IEvent, IEventLocker, ILogger, IViewLocker } from '../interfaces/index.ts'; +import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker.ts'; +import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker.ts'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor.ts'; + +/** + * Base class for SQLite-backed projection views with restore locking and last-processed-event tracking + */ +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..a3d8145 --- /dev/null +++ b/src/sqlite/IContainer.ts @@ -0,0 +1,8 @@ +import type { 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..8df600e --- /dev/null +++ b/src/sqlite/SqliteEventLocker.ts @@ -0,0 +1,137 @@ +import type { Database, Statement } from 'better-sqlite3'; +import type { IContainer, IEvent, IEventLocker } from '../interfaces/index.ts'; +import { getEventId } from './utils/index.ts'; +import { viewLockTableInit, eventLockTableInit } from './queries/index.ts'; +import type { SqliteViewLockerParams } from './SqliteViewLocker.ts'; +import type { SqliteProjectionDataParams } from './SqliteProjectionDataParams.ts'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor.ts'; + +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..262aa97 --- /dev/null +++ b/src/sqlite/SqliteObjectStorage.ts @@ -0,0 +1,153 @@ +import type { Statement, Database } from 'better-sqlite3'; +import { guid } from './utils/index.ts'; +import type { IContainer, IObjectStorage } from '../interfaces/index.ts'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor.ts'; + +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..47f0c93 --- /dev/null +++ b/src/sqlite/SqliteObjectView.ts @@ -0,0 +1,61 @@ +import { AbstractSqliteView } from './AbstractSqliteView.ts'; +import type { IObjectStorage, IEventLocker } from '../interfaces/index.ts'; +import { SqliteObjectStorage } from './SqliteObjectStorage.ts'; +import type { Database } from 'better-sqlite3'; + +/** + * SQLite-backed object view with restore locking and last-processed-event tracking + */ +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..ad3dee7 --- /dev/null +++ b/src/sqlite/SqliteViewLocker.ts @@ -0,0 +1,171 @@ +import type { Database, Statement } from 'better-sqlite3'; +import type { IContainer, ILogger, IViewLocker } from '../interfaces/index.ts'; +import { Deferred } from '../utils/index.ts'; +import { promisify } from 'util'; +import { viewLockTableInit } from './queries/index.ts'; +import type { SqliteProjectionDataParams } from './SqliteProjectionDataParams.ts'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor.ts'; +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..d90c4c8 --- /dev/null +++ b/src/sqlite/index.ts @@ -0,0 +1,8 @@ +export * from './AbstractSqliteAccessor.ts'; +export * from './AbstractSqliteObjectProjection.ts'; +export * from './AbstractSqliteView.ts'; +export * from './SqliteEventLocker.ts'; +export * from './SqliteObjectStorage.ts'; +export * from './SqliteObjectView.ts'; +export * from './SqliteViewLocker.ts'; +export * from './utils/index.ts'; 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..93f0de6 --- /dev/null +++ b/src/sqlite/queries/index.ts @@ -0,0 +1,2 @@ +export * from './eventLockTableInit.ts'; +export * from './viewLockTableInit.ts'; 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..2860279 --- /dev/null +++ b/src/sqlite/utils/getEventId.ts @@ -0,0 +1,8 @@ +import { IEvent } from '../../interfaces/index.ts'; +import { guid } from './guid.ts'; +import md5 from '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..989235a --- /dev/null +++ b/src/sqlite/utils/index.ts @@ -0,0 +1,2 @@ +export * from './guid.ts'; +export * from './getEventId.ts'; 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..5fdf105 --- /dev/null +++ b/src/utils/Lock.ts @@ -0,0 +1,120 @@ +import { Deferred } from './Deferred.ts'; + +export class LockLease { + + readonly lock: Lock; + readonly name?: string; + + constructor(lock: Lock, name?: string) { + this.lock = lock; + this.name = name; + } + + 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/clone.ts b/src/utils/clone.ts new file mode 100644 index 0000000..09a0e04 --- /dev/null +++ b/src/utils/clone.ts @@ -0,0 +1,11 @@ +export function clone(value: T): T { + const sc = (globalThis as any).structuredClone as undefined | ((v: U) => U); + if (typeof sc === 'function') + return sc(value); + + const json = JSON.stringify(value); + if (json === undefined) + throw new TypeError('Object payload must be JSON-serializable'); + + return JSON.parse(json) as T; +} 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..c46b7f0 100644 --- a/src/utils/getHandler.ts +++ b/src/utils/getHandler.ts @@ -1,4 +1,4 @@ -import { IMessageHandler } from "../interfaces"; +import type { IMessageHandler } from '../interfaces/index.ts'; /** * 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..03bc5e9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,14 @@ -export * from './getClassName'; -export * from './getHandler'; -export * from './validateHandlers'; -export * from './getMessageHandlerNames'; -export * from './getHandledMessageTypes'; -export * from './setupOneTimeEmitterSubscription'; -export * from './subscribe'; -export * from './isClass'; +export * from './clone.ts'; +export * from './Deferred.ts'; +export * from './extractErrorDetails.ts'; +export * from './getClassName.ts'; +export * from './getHandler.ts'; +export * from './getMessageHandlerNames.ts'; +export * from './isClass.ts'; +export * from './iteratorToArray.ts'; +export * from './Lock.ts'; +export * from './MapAssertable.ts'; +export * from './notEmpty.ts'; +export * from './setupOneTimeEmitterSubscription.ts'; +export * from './subscribe.ts'; +export * from './validateHandlers.ts'; 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..e39566e 100644 --- a/src/utils/setupOneTimeEmitterSubscription.ts +++ b/src/utils/setupOneTimeEmitterSubscription.ts @@ -1,4 +1,4 @@ -import { IEvent, ILogger, IObservable } from "../interfaces"; +import type { IEvent, ILogger, IObservable } from '../interfaces/index.ts'; /** * 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..331b752 100644 --- a/src/utils/subscribe.ts +++ b/src/utils/subscribe.ts @@ -1,9 +1,23 @@ -import { IMessageHandler, IObservable } from "../interfaces"; -import { getHandler } from './getHandler'; -import { getHandledMessageTypes } from './getHandledMessageTypes'; +import { type IMessageHandler, type IObservable, isIObservableQueueProvider } from '../interfaces/index.ts'; +import { getHandler } from './getHandler.ts'; +import { getMessageHandlerNames } from './getMessageHandlerNames.ts'; 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 */ @@ -26,8 +40,6 @@ export function subscribe( const { masterHandler, messageTypes, queueName } = options; if (masterHandler && typeof masterHandler !== 'function') throw new TypeError('masterHandler parameter, when provided, must be a Function'); - if (queueName && typeof observable.queue !== 'function') - throw new TypeError('observable.queue, when queueName is specified, must be a Function'); const subscribeTo = messageTypes || getHandledMessageTypes(observer); if (!Array.isArray(subscribeTo)) @@ -35,17 +47,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 (!isIObservableQueueProvider(observable)) 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..96ec344 100644 --- a/src/utils/validateHandlers.ts +++ b/src/utils/validateHandlers.ts @@ -1,10 +1,11 @@ -import { getHandler } from './getHandler'; +import { getHandler } from './getHandler.ts'; /** * 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/src/workers/AbstractWorkerProjection.ts b/src/workers/AbstractWorkerProjection.ts new file mode 100644 index 0000000..3a4cfaa --- /dev/null +++ b/src/workers/AbstractWorkerProjection.ts @@ -0,0 +1,246 @@ +import { isMainThread, Worker, MessageChannel, parentPort, workerData } from 'node:worker_threads'; +import { AbstractProjection, type AbstractProjectionParams } from '../AbstractProjection.ts'; +import type { IEvent } from '../interfaces/index.ts'; +import * as Comlink from 'comlink'; +import { nodeEndpoint, createWorker } from './utils/index.ts'; +import { extractErrorDetails } from '../utils/index.ts'; +import { isWorkerData, type IWorkerData, type WorkerInitMessage } from './protocol.ts'; + +export type AbstractWorkerProjectionParams = AbstractProjectionParams & { + + /** + * Required in the main thread to spawn a worker (derived projection module path). + * Not used in the worker thread. + */ + workerModulePath?: string; + + /** + * When `false`, runs projection + view in the current thread (no Worker, no RPC). + * Intended for tests and environments where worker threads aren't desired. + */ + useWorkerThreads?: boolean; +}; + +interface IRemoteProjectionApi { + project(event: IEvent): Promise | void; + _project(event: IEvent): Promise | void; + ping(): true; +} + +interface IMainThreadProjection { + get remoteProjection(): Comlink.Remote; + get remoteView(): Comlink.Remote; +} + +/** + * Projection base class that can run projection handlers and the associated view in a worker thread + * to isolate CPU-heavy work and keep the main thread responsive + */ +export abstract class AbstractWorkerProjection extends AbstractProjection { + + #worker?: Worker; + readonly #workerInit?: Promise; + readonly #remoteProjection?: Comlink.Remote; + readonly #remoteView?: Comlink.Remote; + readonly #useWorkerThreads: boolean; + + /** + * Creates an instance of a class derived from AbstractWorkerProjection in a Worker thread + * + * @param factory - Optional factory function to create the projection instance + */ + static createWorkerInstance>( + this: new () => T, + factory?: () => T + ): T { + if (!parentPort) + throw new Error('createWorkerInstance can only be called from a Worker thread'); + if (!isWorkerData(workerData)) + throw new Error('workerData does not contain projectionPort and viewPort'); + + const workerProjectionInstance = factory?.() ?? new this(); + const workerProjectionInstanceApi: IRemoteProjectionApi = { + project: event => workerProjectionInstance.project(event), + _project: event => workerProjectionInstance._project(event), + ping: () => workerProjectionInstance._pong() + }; + + Comlink.expose(workerProjectionInstanceApi, nodeEndpoint(workerData.projectionPort)); + Comlink.expose(workerProjectionInstance.view, nodeEndpoint(workerData.viewPort)); + + parentPort.postMessage({ type: 'ready' } satisfies WorkerInitMessage); + + return workerProjectionInstance; + } + + /** + * Convenience wrapper for module-level bootstrapping. + * + * In the main thread, does nothing. + * In a worker thread, creates and exposes the projection singleton (same as createWorkerInstance). + */ + static createInstanceIfWorkerThread>( + this: (new () => T) & { createWorkerInstance: (factory?: () => T) => T }, + factory?: () => T + ): T | undefined { + if (isMainThread) + return undefined; + + return this.createWorkerInstance(factory); + } + + async project(event: IEvent): Promise { + if (this.#useWorkerThreads && isMainThread) { + if (!this.#worker) + await this.#workerInit; + + return this.remoteProjection.project(event); + } + + return super.project(event); + } + + /** + * Proxy to the projection instance in the worker thread + */ + get remoteProjection(): Comlink.Remote { + this.assertMainThread(); + return this.#remoteProjection!; + } + + /** + * Proxy to the projection instance in the worker thread (awaits worker init) + */ + get remoteProjectionInitializer(): Promise> { + this.assertMainThread(); + return this.ensureWorkerReady().then(() => this.remoteProjection); + } + + /** + * Proxy to the view instance in the worker thread + */ + get remoteView(): Comlink.Remote { + this.assertMainThread(); + return this.#remoteView!; + } + + get view(): TView { + if (this.#useWorkerThreads && isMainThread) + return this.remoteView as unknown as TView; + + return super.view; + } + + /** + * Proxy to the view instance in the worker thread (awaits worker init) + */ + get remoteViewInitializer(): Promise> { + this.assertMainThread(); + return this.ensureWorkerReady().then(() => this.remoteView); + } + + constructor({ + workerModulePath, + useWorkerThreads = true, + view, + viewLocker, + eventLocker, + logger + }: AbstractWorkerProjectionParams = {}) { + super({ + view, + viewLocker, + eventLocker, + logger + }); + + this.#useWorkerThreads = useWorkerThreads; + + if (this.#useWorkerThreads && isMainThread) { + if (!workerModulePath) + throw new TypeError('workerModulePath parameter is required in the main thread when useWorkerThreads=true'); + + const { port1: projectionPortMain, port2: projectionPort } = new MessageChannel(); + const { port1: viewPortMain, port2: viewPort } = new MessageChannel(); + + this.#workerInit = this._createWorker(workerModulePath, { + projectionPort, + viewPort + }).then(worker => { + this.#worker = worker; + worker.once('error', this._onWorkerError); + worker.once('exit', this._onWorkerExit); + return worker; + }); + + this.#workerInit.catch(() => { }); + + this.#remoteProjection = Comlink.wrap(nodeEndpoint(projectionPortMain)); + this.#remoteView = Comlink.wrap(nodeEndpoint(viewPortMain)); + } + } + + // eslint-disable-next-line class-methods-use-this + protected async _createWorker(workerModulePath: string, data: IWorkerData): Promise { + return createWorker(workerModulePath, data); + } + + protected _onWorkerError = (error: unknown) => { + this._logger?.error('worker error', { + error: extractErrorDetails(error) + }); + }; + + protected _onWorkerExit = (exitCode: number) => { + if (exitCode !== 0) + this._logger?.error(`worker exited with code ${exitCode}`); + }; + + protected _pong(): true { + this.assertWorkerThread(); + return true; + } + + protected assertMainThread(): asserts this is this & IMainThreadProjection { + if (!isMainThread) + throw new Error('This method can only be called from the main thread'); + if (!this.#useWorkerThreads) + throw new Error('Worker threads are disabled for this projection instance'); + if (!this.#workerInit) + throw new Error('Worker instance is not initialized'); + if (!this.#remoteProjection) + throw new Error('Remote projection instance is not initialized'); + if (!this.#remoteView) + throw new Error('Remote view instance is not initialized'); + } + + // eslint-disable-next-line class-methods-use-this + protected assertWorkerThread() { + if (!parentPort) + throw new Error('This method can only be called from a Worker thread'); + } + + async ensureWorkerReady(): Promise { + if (this.#useWorkerThreads && isMainThread) + await this.#workerInit; + } + + protected async _project(event: IEvent): Promise { + if (this.#useWorkerThreads && isMainThread) { + if (!this.#worker) + await this.#workerInit; + + return this.remoteProjection._project(event); + } + + return super._project(event); + } + + dispose() { + if (this.#useWorkerThreads && isMainThread) { + this.#remoteProjection?.[Comlink.releaseProxy](); + this.#remoteView?.[Comlink.releaseProxy](); + this.#worker?.terminate(); + } + } +} diff --git a/src/workers/index.ts b/src/workers/index.ts new file mode 100644 index 0000000..e646338 --- /dev/null +++ b/src/workers/index.ts @@ -0,0 +1 @@ +export * from './AbstractWorkerProjection.ts'; diff --git a/src/workers/protocol.ts b/src/workers/protocol.ts new file mode 100644 index 0000000..5c9cd14 --- /dev/null +++ b/src/workers/protocol.ts @@ -0,0 +1,23 @@ +import type { MessagePort } from 'node:worker_threads'; + +export interface IWorkerData { + projectionPort: MessagePort, + viewPort: MessagePort +} + +export const isWorkerData = (obj: unknown): obj is IWorkerData => + typeof obj === 'object' + && obj !== null + && 'projectionPort' in obj + && !!obj.projectionPort + && 'viewPort' in obj + && !!obj.viewPort; + +export type WorkerInitMessage = { type: 'ready' }; + +export const isWorkerInitMessage = (msg: unknown): msg is WorkerInitMessage => + typeof msg === 'object' + && msg !== null + && 'type' in msg + && msg.type === 'ready'; + diff --git a/src/workers/readme.md b/src/workers/readme.md new file mode 100644 index 0000000..efa0942 --- /dev/null +++ b/src/workers/readme.md @@ -0,0 +1,90 @@ +# Workers (Worker Projections) + +This module provides `AbstractWorkerProjection`, which lets you run projection handlers and view +computations inside a Node.js `worker_threads` Worker while keeping an `AbstractProjection`-like +API in the main thread. + +## Import + +CommonJS: + +```js +const { AbstractWorkerProjection } = require('node-cqrs/workers'); +``` + +ESM: + +```js +import { AbstractWorkerProjection } from 'node-cqrs/workers'; +``` + +## Defining a worker projection + +Key points: + +- The same projection module is used as the **worker entry point**. +- The module should bootstrap the worker-side singleton via + `YourProjection.createInstanceIfWorkerThread()`. +- In the main thread, `project()` automatically waits for worker startup (so `ensureWorkerReady()` is optional). + +Example (CommonJS): + +```js +const { AbstractWorkerProjection } = require('node-cqrs/workers'); + +class CounterView { + counter = 0; + increment() { this.counter += 1; } + getCounter() { return this.counter; } +} + +class CounterProjection extends AbstractWorkerProjection { + constructor() { + super({ + workerModulePath: __filename, + view: new CounterView() + }); + } + + somethingHappened() { + this.view.increment(); + } +} + +CounterProjection.createInstanceIfWorkerThread(); + +module.exports = CounterProjection; +``` + +## Using it (main thread) + +```js +const CounterProjection = require('./CounterProjection.cjs'); + +const projection = new CounterProjection(); +await projection.project({ id: '1', type: 'somethingHappened' }); + +// `projection.view` is a remote proxy (methods-only) +const counter = await projection.view.getCounter(); + +projection.dispose(); +``` + +## `workerModulePath` patterns + +- **CommonJS**: `__filename` (inside the projection module). +- **ESM**: `fileURLToPath(import.meta.url)` inside the projection module. + +Note: `workerModulePath` must point to the **JavaScript file that Node can execute** +(e.g. `dist/...` in TS projects), not a TypeScript source file. + +Tip: call `await projection.ensureWorkerReady()` if you want to fail fast on worker startup +before processing events. + +## Disabling workers (tests) + +To run everything in-thread (no Worker, no RPC): + +```js +const projection = new CounterProjection({ useWorkerThreads: false }); +``` diff --git a/src/workers/utils/createWorker.ts b/src/workers/utils/createWorker.ts new file mode 100644 index 0000000..7e90eda --- /dev/null +++ b/src/workers/utils/createWorker.ts @@ -0,0 +1,61 @@ +import { Worker } from 'node:worker_threads'; +import * as path from 'node:path'; +import { isWorkerInitMessage, type IWorkerData } from '../protocol.ts'; + +/** + * Create a worker instance, await a handshake or a failure + * + * @param workerModulePath - Path to worker module + * @param ports - Container with MessagePorts for communication with worker projection and view instances + * @returns Worker instance + */ +export async function createWorker(workerModulePath: string, ports: IWorkerData) { + + const workerEntrypoint = path.isAbsolute(workerModulePath) ? + workerModulePath : + path.resolve(process.cwd(), workerModulePath); + + const worker = new Worker(workerEntrypoint, { + workerData: ports, + transferList: [ + ports.projectionPort, + ports.viewPort + ] + }); + + await new Promise((resolve, reject) => { + + const cleanup = () => { + // eslint-disable-next-line no-use-before-define + worker.off('error', onError); + // eslint-disable-next-line no-use-before-define + worker.off('message', onMessage); + // eslint-disable-next-line no-use-before-define + worker.off('exit', onExit); + }; + + const onMessage = (msg: unknown) => { + if (!isWorkerInitMessage(msg)) + return; + + cleanup(); + resolve(undefined); + }; + + const onError = (err: unknown) => { + cleanup(); + reject(err); + }; + + const onExit = (exitCode: number) => { + cleanup(); + reject(new Error(`Worker exited prematurely with exit code ${exitCode}`)); + }; + + worker.on('message', onMessage); + worker.once('error', onError); + worker.once('exit', onExit); + }); + + return worker; +} diff --git a/src/workers/utils/index.ts b/src/workers/utils/index.ts new file mode 100644 index 0000000..f4cec7a --- /dev/null +++ b/src/workers/utils/index.ts @@ -0,0 +1,2 @@ +export * from './createWorker.ts'; +export * from './nodeEndpoint.ts'; diff --git a/src/workers/utils/nodeEndpoint.ts b/src/workers/utils/nodeEndpoint.ts new file mode 100644 index 0000000..13869bd --- /dev/null +++ b/src/workers/utils/nodeEndpoint.ts @@ -0,0 +1,7 @@ +import * as Comlink from 'comlink'; + +// Jest (CJS) cannot import the ESM adapter; +// the UMD build is CJS/UMD but the default export shape varies by loader +const nodeEndpointModule = require('comlink/dist/umd/node-adapter'); +export const nodeEndpoint: (arg: any) => Comlink.Endpoint = + (nodeEndpointModule?.default ?? nodeEndpointModule) as any; diff --git a/tests/integration/rabbitmq/RabbitMqEventBus.test.ts b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts new file mode 100644 index 0000000..0c262ba --- /dev/null +++ b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts @@ -0,0 +1,186 @@ +import * as amqplib from 'amqplib'; +import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; +import { RabbitMqEventBus } from '../../../src/rabbitmq/RabbitMqEventBus'; +import { IMessage, IEvent } from '../../../src/interfaces'; +import { delay } from './utils'; + +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..31fa52a --- /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 './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/rabbitmq/utils/delay.ts b/tests/integration/rabbitmq/utils/delay.ts new file mode 100644 index 0000000..8663de4 --- /dev/null +++ b/tests/integration/rabbitmq/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/tests/integration/rabbitmq/utils/index.ts b/tests/integration/rabbitmq/utils/index.ts new file mode 100644 index 0000000..1e0db20 --- /dev/null +++ b/tests/integration/rabbitmq/utils/index.ts @@ -0,0 +1 @@ +export * from './delay'; diff --git a/tests/integration/sqlite/SqliteView.test.ts b/tests/integration/sqlite/SqliteView.test.ts new file mode 100644 index 0000000..841b709 --- /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 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..2d20f21 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 () { @@ -71,7 +70,6 @@ describe('CommandBus', function () { it('validates parameters', () => { expect(() => bus.send(undefined)).to.throw('type argument must be a non-empty String'); - expect(() => bus.send('test', 1, {}, {}, {})).to.throw('more than expected arguments supplied'); }); it('formats a command and passes it to sendRaw', async () => { 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..2f60713 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/cjs/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..faa821e --- /dev/null +++ b/tests/unit/dispatch-pipeline.test.ts @@ -0,0 +1,60 @@ +import { InMemorySnapshotStorage } from '../../dist/cjs/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/memory/InMemorySnapshotStorage.test.ts b/tests/unit/memory/InMemorySnapshotStorage.test.ts new file mode 100644 index 0000000..a9a422a --- /dev/null +++ b/tests/unit/memory/InMemorySnapshotStorage.test.ts @@ -0,0 +1,59 @@ +import { InMemorySnapshotStorage } from '../../../src/in-memory/InMemorySnapshotStorage.ts'; + +describe('InMemorySnapshotStorage', () => { + + it('saves and retrieves snapshots', async () => { + const storage = new InMemorySnapshotStorage(); + await storage.saveAggregateSnapshot({ + type: 'snapshot', + aggregateId: 'a1', + aggregateVersion: 1 + }); + + const snapshot = await storage.getAggregateSnapshot('a1'); + expect(snapshot).toMatchObject({ type: 'snapshot', aggregateId: 'a1', aggregateVersion: 1 }); + }); + + it('throws on saving snapshot without aggregateId', async () => { + const storage = new InMemorySnapshotStorage(); + await expect(storage.saveAggregateSnapshot({ type: 'snapshot' } as any)).rejects.toThrow('event.aggregateId is required'); + }); + + it('throws on deleting snapshot without aggregateId', async () => { + const storage = new InMemorySnapshotStorage(); + expect(() => storage.deleteAggregateSnapshot({ type: 'snapshot' } as any)).toThrow('snapshotEvent.aggregateId argument required'); + }); + + it('process() persists snapshot events and filters them out from the batch', async () => { + const storage = new InMemorySnapshotStorage(); + + const snapshot1 = { type: 'snapshot', aggregateId: 'a1', aggregateVersion: 1 }; + const event = { type: 'somethingHappened', aggregateId: 'a1', aggregateVersion: 2 }; + const snapshot2 = { type: 'snapshot', aggregateId: 'a2', aggregateVersion: 1 }; + + const batch = [ + { event: snapshot1, origin: 'internal' }, + { event, origin: 'internal' }, + { event: snapshot2, origin: 'internal' } + ]; + + const result = await storage.process(batch as any); + + expect(result).toHaveLength(1); + expect(result[0].event).toEqual(event); + + expect(await storage.getAggregateSnapshot('a1')).toEqual(snapshot1); + expect(await storage.getAggregateSnapshot('a2')).toEqual(snapshot2); + }); + + it('revert() removes snapshots for snapshot events', async () => { + const storage = new InMemorySnapshotStorage(); + + const snapshot = { type: 'snapshot', aggregateId: 'a1', aggregateVersion: 1 }; + await storage.saveAggregateSnapshot(snapshot as any); + expect(await storage.getAggregateSnapshot('a1')).toBeDefined(); + + await storage.revert([{ event: snapshot }] as any); + expect(await storage.getAggregateSnapshot('a1')).toBeUndefined(); + }); +}); 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..7ee7f47 --- /dev/null +++ b/tests/unit/sqlite/SqliteEventLocker.test.ts @@ -0,0 +1,90 @@ +import 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..2f6691b --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -0,0 +1,112 @@ +import 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..685b69f --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectView.test.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import 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..107c4bd --- /dev/null +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import 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/tests/unit/utils/Deferred.test.ts b/tests/unit/utils/Deferred.test.ts new file mode 100644 index 0000000..438dacc --- /dev/null +++ b/tests/unit/utils/Deferred.test.ts @@ -0,0 +1,30 @@ +import { Deferred } from '../../../src/utils/Deferred.ts'; + +describe('Deferred', () => { + + it('tracks resolve state', async () => { + const d = new Deferred(); + expect(d.settled).toBe(false); + expect(d.resolved).toBe(false); + expect(d.rejected).toBe(false); + + d.resolve(42); + + expect(d.resolved).toBe(true); + expect(d.rejected).toBe(false); + expect(d.settled).toBe(true); + await expect(d.promise).resolves.toBe(42); + }); + + it('tracks reject state', async () => { + const d = new Deferred(); + + d.reject(new Error('nope')); + + expect(d.resolved).toBe(false); + expect(d.rejected).toBe(true); + expect(d.settled).toBe(true); + await expect(d.promise).rejects.toThrow('nope'); + }); +}); + diff --git a/tests/unit/utils/clone.test.ts b/tests/unit/utils/clone.test.ts new file mode 100644 index 0000000..7274589 --- /dev/null +++ b/tests/unit/utils/clone.test.ts @@ -0,0 +1,54 @@ +import { clone } from '../../../src/utils/clone.ts'; + +describe('clone', () => { + + it('uses structuredClone when available', () => { + const originalStructuredClone = (globalThis as any).structuredClone; + + const structuredCloneMock = jest.fn((v: unknown) => ({ wrapped: v })); + (globalThis as any).structuredClone = structuredCloneMock; + + try { + const value = { a: 1 }; + const result = clone(value); + + expect(structuredCloneMock).toHaveBeenCalledTimes(1); + expect(structuredCloneMock).toHaveBeenCalledWith(value); + expect(result).toEqual({ wrapped: value }); + } + finally { + (globalThis as any).structuredClone = originalStructuredClone; + } + }); + + it('falls back to JSON clone when structuredClone is not available', () => { + const originalStructuredClone = (globalThis as any).structuredClone; + (globalThis as any).structuredClone = undefined; + + try { + const value = { a: 1, nested: { b: 2 } }; + const result = clone(value); + + expect(result).toEqual(value); + expect(result).not.toBe(value); + expect(result.nested).not.toBe(value.nested); + } + finally { + (globalThis as any).structuredClone = originalStructuredClone; + } + }); + + it('throws when JSON serialization fails', () => { + const originalStructuredClone = (globalThis as any).structuredClone; + (globalThis as any).structuredClone = undefined; + + try { + expect(() => clone(() => undefined as any)).toThrow(TypeError); + expect(() => clone(() => undefined as any)).toThrow('Object payload must be JSON-serializable'); + } + finally { + (globalThis as any).structuredClone = originalStructuredClone; + } + }); +}); + diff --git a/tests/unit/utils/extractErrorDetails.test.ts b/tests/unit/utils/extractErrorDetails.test.ts new file mode 100644 index 0000000..727ce56 --- /dev/null +++ b/tests/unit/utils/extractErrorDetails.test.ts @@ -0,0 +1,49 @@ +import { extractErrorDetails } from '../../../src/utils/extractErrorDetails.ts'; + +describe('extractErrorDetails', () => { + + it('extracts name/message/stack for Error instances', () => { + const err = new Error('boom'); + err.name = 'CustomError'; + + const details = extractErrorDetails(err); + expect(details).toMatchObject({ + name: 'CustomError', + message: 'boom' + }); + expect(typeof details.stack === 'string' || details.stack === undefined).toBe(true); + }); + + it('extracts message/name/code from plain objects', () => { + const err = { name: 'PlainError', message: 'bad', code: 'E_BAD' }; + + expect(extractErrorDetails(err)).toEqual({ + name: 'PlainError', + message: 'bad', + code: 'E_BAD' + }); + }); + + it('extracts cause recursively', () => { + const cause = new Error('root'); + const err = new Error('top', { cause }); + + const details = extractErrorDetails(err); + expect(details.message).toBe('top'); + expect(details.cause).toBeDefined(); + expect(details.cause?.message).toBe('root'); + }); + + it('flattens AggregateError messages', () => { + const aggregate = new AggregateError([new Error('a'), { message: 'b' }], 'top'); + const details = extractErrorDetails(aggregate); + + expect(details.message).toBe('top; a; b'); + }); + + it('stringifies non-objects', () => { + expect(extractErrorDetails('x')).toEqual({ name: undefined, message: 'x' }); + expect(extractErrorDetails(123)).toEqual({ name: undefined, message: '123' }); + }); +}); + diff --git a/tests/unit/workers/AbstractWorkerProjection.test.ts b/tests/unit/workers/AbstractWorkerProjection.test.ts new file mode 100644 index 0000000..3570c69 --- /dev/null +++ b/tests/unit/workers/AbstractWorkerProjection.test.ts @@ -0,0 +1,222 @@ +import { expect } from 'chai'; +import * as path from 'node:path'; +import type { IEvent } from '../../../src/interfaces'; + +type ProjectionFixtureCtor = typeof import('./fixtures/ProjectionFixture.cjs'); +// eslint-disable-next-line global-require +const ProjectionFixture = require('./fixtures/ProjectionFixture.cjs') as ProjectionFixtureCtor; + +function createEventStore(events: IEvent[]) { + return { + getEventsByTypes: (types: string[], options?: { afterEvent?: IEvent }) => (async function* () { + const afterId = options?.afterEvent?.id; + const startIndex = afterId ? Math.max(0, events.findIndex(e => e.id === afterId) + 1) : 0; + for (const event of events.slice(startIndex)) { + if (types.includes(event.type)) + yield event; + } + }()) + } as any; +} + +describe('AbstractWorkerProjection', () => { + + it('handles missing worker module error', async () => { + const workerModulePath = path.resolve(process.cwd(), 'tests/unit/workers/fixtures/DOES_NOT_EXIST.cjs'); + const projection = new ProjectionFixture({ workerModulePath }); + try { + let error: any; + try { + await projection.ensureWorkerReady(); + } + catch (err) { + error = err; + } + + expect(error).to.be.ok; + expect(error).to.have.property('message').that.includes('DOES_NOT_EXIST'); + } + finally { + projection.dispose(); + } + }); + + it('runs in-thread when workers are disabled', async () => { + const projection = new ProjectionFixture({ + useWorkerThreads: false, + workerModulePath: 'tests/unit/workers/fixtures/DOES_NOT_EXIST.cjs' + }); + try { + await projection.ensureWorkerReady(); + + await projection.project({ id: '1', type: 'somethingHappened' }); + expect(await projection.view.getCounter()).to.equal(1); + + let error: any; + try { + // accessing remote API should fail when workers are disabled + projection.remoteProjection; + } + catch (err) { + error = err; + } + + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Worker threads are disabled'); + } + finally { + projection.dispose(); + } + }); + + it('awaits view method calls while restoring', async () => { + const projection = new ProjectionFixture(); + try { + const eventStore = createEventStore([ + { id: '1', type: 'slowHappened' } + ]); + + const restorePromise = projection.restore(eventStore); + + await new Promise(resolve => setTimeout(resolve, 10)); + + const counterPromise = projection.view.getCounter(); + + const resolvedEarly = await Promise.race([ + counterPromise.then(() => true), + new Promise(resolve => setTimeout(() => resolve(false), 20)) + ]); + + expect(resolvedEarly).to.equal(false); + + await restorePromise; + + expect(await counterPromise).to.equal(1); + } + finally { + projection.dispose(); + } + }); + + it('spawns worker with an instance of projection', async () => { + const projection = new ProjectionFixture(); + try { + await projection.ensureWorkerReady(); + const pong = await projection.remoteProjection.ping(); + expect(pong).to.be.ok; + } + finally { + projection.dispose(); + } + }); + + it('exposes remote view', async () => { + const projection = new ProjectionFixture(); + try { + await projection.ensureWorkerReady(); + const counter = await projection.view.getCounter(); + expect(counter).to.eq(0); + } + finally { + projection.dispose(); + } + }); + + it('locks view during restore and unlocks on success', async () => { + const projection = new ProjectionFixture(); + try { + const eventStore = createEventStore([ + { id: '1', type: 'somethingHappened' }, + { id: '2', type: 'somethingHappened' } + ]); + + await projection.restore(eventStore); + + await projection.ensureWorkerReady(); + expect(await projection.view.getCalls()).to.deep.equal({ lock: 1, unlock: 1 }); + expect(await projection.view.isReady()).to.equal(true); + expect((await projection.view.getLastEvent())?.id).to.equal('2'); + expect(await projection.view.getCounter()).to.equal(2); + } + finally { + projection.dispose(); + } + }); + + it('restores only events after getLastEvent', async () => { + const projection = new ProjectionFixture(); + try { + await projection.restore(createEventStore([ + { id: '1', type: 'somethingHappened' }, + { id: '2', type: 'somethingHappened' } + ])); + + await projection.restore(createEventStore([ + { id: '1', type: 'somethingBadHappened' }, + { id: '2', type: 'somethingBadHappened' }, + { id: '3', type: 'somethingHappened' } + ])); + + await projection.ensureWorkerReady(); + expect(await projection.view.getCalls()).to.deep.equal({ lock: 2, unlock: 2 }); + expect((await projection.view.getLastEvent())?.id).to.equal('3'); + expect(await projection.view.getCounter()).to.equal(3); + } + finally { + projection.dispose(); + } + }); + + it('halts restore on handler error and keeps view locked', async () => { + const projection = new ProjectionFixture(); + try { + const eventStore = createEventStore([ + { id: '1', type: 'somethingHappened' }, + { id: '2', type: 'somethingBadHappened' }, + { id: '3', type: 'somethingHappened' } + ]); + + let error: any; + try { + await projection.restore(eventStore); + } + catch (err) { + error = err; + } + + expect(error).to.be.instanceOf(Error); + expect(error).to.have.property('message', 'boom'); + + await projection.ensureWorkerReady(); + expect(await projection.view.getCalls()).to.deep.equal({ lock: 1, unlock: 0 }); + expect(await projection.view.isReady()).to.equal(false); + expect((await projection.view.getLastEvent())?.id).to.equal('1'); + expect(await projection.view.getCounterNowait()).to.equal(1); + } + finally { + projection.dispose(); + } + }); + + it('does not project events when event lock is not obtained', async () => { + const projection = new ProjectionFixture(); + try { + await projection.ensureWorkerReady(); + await projection.view.setSkipIds(['1']); + + const eventStore = createEventStore([ + { id: '1', type: 'somethingHappened' }, + { id: '2', type: 'somethingHappened' } + ]); + + await projection.restore(eventStore); + + await projection.ensureWorkerReady(); + expect((await projection.view.getLastEvent())?.id).to.equal('2'); + expect(await projection.view.getCounter()).to.equal(1); + } + finally { + projection.dispose(); + } + }); +}); diff --git a/tests/unit/workers/fixtures/ProjectionFixture.cjs b/tests/unit/workers/fixtures/ProjectionFixture.cjs new file mode 100644 index 0000000..0d64e53 --- /dev/null +++ b/tests/unit/workers/fixtures/ProjectionFixture.cjs @@ -0,0 +1,125 @@ +const { isMainThread } = require('node:worker_threads'); + +// In Jest (main thread), import from src/ so coverage is collected from instrumented sources. +// In worker threads, use the built CJS entrypoint because Node can't execute TS without a loader. +const workers = isMainThread ? + require('../../../../src/workers/index.ts') : + require('node-cqrs/workers'); + +const { AbstractWorkerProjection } = workers; + +class ViewFixture { + counter = 0; + ready = true; + + #calls = { + lock: 0, + unlock: 0 + }; + #lastEvent = null; + #skipIds = new Set(); + #readyPromise = Promise.resolve(); + #resolveReady = null; + + increment() { + this.counter += 1; + } + + async getCounter() { + if (!this.ready) + await this.once('ready'); + return this.counter; + } + + getCounterNowait() { + return this.counter; + } + + setSkipIds(ids = []) { + this.#skipIds = new Set(ids); + } + + getCalls() { + return { ...this.#calls }; + } + + isReady() { + return this.ready; + } + + async lock() { + this.#calls.lock += 1; + this.ready = false; + this.#readyPromise = new Promise(resolve => { + this.#resolveReady = resolve; + }); + return true; + } + + async unlock() { + this.#calls.unlock += 1; + this.ready = true; + if (this.#resolveReady) + this.#resolveReady(); + this.#resolveReady = null; + } + + once(event) { + if (event !== 'ready') + throw new Error(`Unexpected event: ${event}`); + return this.#readyPromise; + } + + getLastEvent() { + return this.#lastEvent; + } + + tryMarkAsProjecting(event) { + if (event?.id && this.#skipIds.has(event.id)) + return false; + return true; + } + + markAsProjected(event) { + this.#lastEvent = event; + } +} + +/** + * @extends {AbstractWorkerProjection} + */ +class ProjectionFixture extends AbstractWorkerProjection { + + /** + * @param {any} container + */ + constructor({ + workerModulePath = __filename, + useWorkerThreads, + logger + } = {}) { + super({ + workerModulePath, + useWorkerThreads, + view: new ViewFixture(), + logger + }); + } + + async somethingHappened() { + this.view.increment(); + } + + async slowHappened() { + await new Promise(resolve => setTimeout(resolve, 50)); + this.view.increment(); + } + + async somethingBadHappened() { + throw new Error('boom'); + } +} + +ProjectionFixture.createInstanceIfWorkerThread(); + +module.exports = ProjectionFixture; diff --git a/tests/unit/workers/fixtures/jsconfig.json b/tests/unit/workers/fixtures/jsconfig.json new file mode 100644 index 0000000..6653e49 --- /dev/null +++ b/tests/unit/workers/fixtures/jsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "commonjs", + "moduleResolution": "node", + "checkJs": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "lib": [ + "es2022", + "dom" + ], + "baseUrl": "..", + "paths": { + "node-cqrs": [ + "types/index.d.ts" + ], + "node-cqrs/*": [ + "types/*/index.d.ts" + ] + } + }, + "include": [ + "**/*" + ], + "exclude": [ + "../node_modules", + "../dist", + "**/bundle.js", + "browser-smoke-test/**" + ] +} diff --git a/tsconfig.browser.json b/tsconfig.browser.json new file mode 100644 index 0000000..b36159d --- /dev/null +++ b/tsconfig.browser.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "target": "ES2019", + "outDir": "./dist/browser/cjs", + "noEmit": false, + "declaration": false + } +} + diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..62ca15a --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "target": "ES2022", + "outDir": "./dist/cjs", + "noEmit": false, + "declaration": false + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..23392f1 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ES2022", + "target": "ES2022", + "declaration": true, + "declarationDir": "./types", + "declarationMap": false, + "outDir": "./dist/esm", + "noEmit": false, + "noEmitOnError": true + } +} diff --git a/tsconfig.json b/tsconfig.json index 6f3a80f..b7330f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,33 @@ { - "compileOnSave": true, - "compilerOptions": { - "module": "CommonJS", - "removeComments": false, - "sourceMap": true, - "alwaysStrict": false, - "outDir": "./dist", - "target": "ESNext", - "declaration": false, - "strictNullChecks": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true - }, - "include": [ - "src/**/*" + "compilerOptions": { + "module": "CommonJS", + "target": "ES2022", + "lib": [ + "ES2022", + "WebWorker" ], - "exclude": [ - "node_modules", - "**/*.spec.ts" - ] + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "isolatedModules": true, + "removeComments": false, + "sourceMap": true, + "alwaysStrict": false, + "noEmit": true, + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] }