From 34836c899c791bf9540e4719c26bec4b8ba9d81e Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 1 Oct 2024 20:27:09 +0100 Subject: [PATCH 001/135] WIP on SQLite infrastructure --- examples/user-domain-tests/index.test.js | 2 +- package-lock.json | 1445 ++++++++++++++--- package.json | 16 +- src/AbstractAggregate.ts | 5 +- src/AbstractProjection.ts | 64 +- src/AbstractSaga.ts | 8 +- src/AggregateCommandHandler.ts | 3 +- src/CqrsContainerBuilder.ts | 2 +- src/EventStore.ts | 19 +- src/index.ts | 12 +- .../{ => memory}/InMemoryEventStorage.ts | 35 +- .../{ => memory}/InMemoryLock.ts | 25 +- .../{ => memory}/InMemoryMessageBus.ts | 2 +- .../{ => memory}/InMemorySnapshotStorage.ts | 7 +- .../{ => memory}/InMemoryView.ts | 27 +- src/infrastructure/memory/index.ts | 5 + .../{ => memory}/utils/Deferred.ts | 0 .../{ => memory}/utils/index.ts | 0 .../{ => memory}/utils/nextCycle.ts | 0 .../sqlite/AbstractSqliteView.ts | 202 +++ src/infrastructure/sqlite/ObjectSqliteView.ts | 124 ++ src/infrastructure/sqlite/index.ts | 2 + src/interfaces.ts | 328 ---- src/interfaces/IAggregate.ts | 55 + src/interfaces/IAggregateSnapshotStorage.ts | 7 + src/interfaces/ICommand.ts | 3 + src/interfaces/ICommandBus.ts | 18 + src/interfaces/IEvent.ts | 6 + src/interfaces/IEventReceptor.ts | 6 + src/interfaces/IEventSet.ts | 6 + src/interfaces/IEventStorage.ts | 31 + src/interfaces/IEventStore.ts | 27 + src/interfaces/IEventStream.ts | 3 + src/interfaces/ILogger.ts | 11 + src/interfaces/IMessage.ts | 13 + src/interfaces/IMessageBus.ts | 8 + src/interfaces/IObjectView.ts | 11 + src/interfaces/IObservable.ts | 11 + src/interfaces/IObserver.ts | 5 + src/interfaces/IPersistentView.ts | 24 + src/interfaces/IProjection.ts | 20 + src/interfaces/IProjectionView.ts | 22 + src/interfaces/ISaga.ts | 36 + src/interfaces/index.ts | 20 + tests/integration/SqliteView.test.ts | 134 ++ tests/unit/AbstractProjection.test.ts | 19 +- tests/unit/CommandBus.test.ts | 3 +- tests/unit/EventStore.test.ts | 8 +- tests/unit/InMemoryView.test.ts | 4 +- 49 files changed, 2152 insertions(+), 692 deletions(-) rename src/infrastructure/{ => memory}/InMemoryEventStorage.ts (59%) rename src/infrastructure/{ => memory}/InMemoryLock.ts (61%) rename src/infrastructure/{ => memory}/InMemoryMessageBus.ts (99%) rename src/infrastructure/{ => memory}/InMemorySnapshotStorage.ts (66%) rename src/infrastructure/{ => memory}/InMemoryView.ts (85%) create mode 100644 src/infrastructure/memory/index.ts rename src/infrastructure/{ => memory}/utils/Deferred.ts (100%) rename src/infrastructure/{ => memory}/utils/index.ts (100%) rename src/infrastructure/{ => memory}/utils/nextCycle.ts (100%) create mode 100644 src/infrastructure/sqlite/AbstractSqliteView.ts create mode 100644 src/infrastructure/sqlite/ObjectSqliteView.ts create mode 100644 src/infrastructure/sqlite/index.ts delete mode 100644 src/interfaces.ts create mode 100644 src/interfaces/IAggregate.ts create mode 100644 src/interfaces/IAggregateSnapshotStorage.ts create mode 100644 src/interfaces/ICommand.ts create mode 100644 src/interfaces/ICommandBus.ts create mode 100644 src/interfaces/IEvent.ts create mode 100644 src/interfaces/IEventReceptor.ts create mode 100644 src/interfaces/IEventSet.ts create mode 100644 src/interfaces/IEventStorage.ts create mode 100644 src/interfaces/IEventStore.ts create mode 100644 src/interfaces/IEventStream.ts create mode 100644 src/interfaces/ILogger.ts create mode 100644 src/interfaces/IMessage.ts create mode 100644 src/interfaces/IMessageBus.ts create mode 100644 src/interfaces/IObjectView.ts create mode 100644 src/interfaces/IObservable.ts create mode 100644 src/interfaces/IObserver.ts create mode 100644 src/interfaces/IPersistentView.ts create mode 100644 src/interfaces/IProjection.ts create mode 100644 src/interfaces/IProjectionView.ts create mode 100644 src/interfaces/ISaga.ts create mode 100644 src/interfaces/index.ts create mode 100644 tests/integration/SqliteView.test.ts diff --git a/examples/user-domain-tests/index.test.js b/examples/user-domain-tests/index.test.js index 4dcb988..6d9bb19 100644 --- a/examples/user-domain-tests/index.test.js +++ b/examples/user-domain-tests/index.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const { createContainer, createBaseInstances } = require('../user-domain'); -const { nextCycle } = require('../../src/infrastructure/utils'); +const { nextCycle } = require('../../src/infrastructure/memory/utils'); describe('user-domain example', () => { diff --git a/package-lock.json b/package-lock.json index 1a03e5f..510d5fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,21 +12,27 @@ "di0": "^1.0.0" }, "devDependencies": { - "@types/chai": "^4.3.17", - "@types/jest": "^29.5.12", - "@types/node": "^20.14.14", + "@types/better-sqlite3": "^7.6.11", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.13", + "@types/node": "^20.16.9", "@types/sinon": "^10.0.20", + "@types/uuid": "^10.0.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "coveralls": "^3.1.1", "jest": "^29.7.0", "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.6.2", + "uuid": "^10.0.0" }, "engines": { "node": ">=10.3.0" + }, + "peerDependencies": { + "better-sqlite3": "^11.3.0" } }, "node_modules/@ampproject/remapping": { @@ -34,6 +40,7 @@ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -47,6 +54,7 @@ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" @@ -56,10 +64,11 @@ } }, "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.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -69,6 +78,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -95,12 +105,13 @@ } }, "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.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.0", + "@babel/types": "^7.25.6", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -114,6 +125,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.25.2", "@babel/helper-validator-option": "^7.24.8", @@ -130,6 +142,7 @@ "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" } @@ -138,13 +151,15 @@ "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-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==", "dev": true, + "license": "MIT", "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -158,6 +173,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", @@ -176,6 +192,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -185,6 +202,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==", "dev": true, + "license": "MIT", "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -198,6 +216,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -207,6 +226,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -216,18 +236,20 @@ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "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==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/types": "^7.25.6" }, "engines": { "node": ">=6.9.0" @@ -238,6 +260,7 @@ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", @@ -253,6 +276,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -265,6 +289,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -279,6 +304,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -287,13 +313,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -303,6 +331,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -312,6 +341,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -320,12 +350,13 @@ } }, "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.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.25.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -339,6 +370,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 +383,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 +396,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 +404,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.25.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.6.tgz", + "integrity": "sha512-sXaDXaJN9SNLymBdlWFA+bjzBhFD617ZaFiY13dGt7TVslVvVgA6fkZOP7Ki3IGElC45lwHdOTrCtKZGVAWeLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "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 +454,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" }, @@ -399,6 +467,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==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -414,6 +483,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 +496,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 +509,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 +522,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 +535,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 +548,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 +556,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 +589,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.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.8" }, "engines": { "node": ">=6.9.0" @@ -516,6 +609,7 @@ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/parser": "^7.25.0", @@ -526,16 +620,17 @@ } }, "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.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", + "@babel/types": "^7.25.6", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -544,10 +639,11 @@ } }, "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.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.24.8", "@babel/helper-validator-identifier": "^7.24.7", @@ -561,13 +657,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,6 +678,7 @@ "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" @@ -590,6 +689,7 @@ "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" } @@ -599,6 +699,7 @@ "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", @@ -615,6 +716,7 @@ "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" } @@ -624,6 +726,7 @@ "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": "*", @@ -641,6 +744,7 @@ "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", @@ -688,6 +792,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 +808,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 +822,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 +835,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 +853,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 +869,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 +913,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 +926,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 +941,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 +957,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 +973,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 +1000,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", @@ -902,6 +1018,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -916,6 +1033,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -925,6 +1043,7 @@ "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" } @@ -933,13 +1052,15 @@ "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 + "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==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -949,13 +1070,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 +1088,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 +1098,64 @@ "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.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^2.0.0", + "@sinonjs/commons": "^3.0.1", "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" + "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 + "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/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", @@ -1055,6 +1169,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -1064,6 +1179,7 @@ "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" @@ -1074,21 +1190,34 @@ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.11", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.11.tgz", + "integrity": "sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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/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 +1226,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,15 +1244,17 @@ "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.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -1131,28 +1264,32 @@ "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.16.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.9.tgz", + "integrity": "sha512-rkvIVJxsOfBejxK7I0FO5sa2WxFmJCzoDwcd88+fq/CUfynNywTo/1/T6hyFz22CyztsnLS9nVlHOnTI36RH5w==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "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==", "dev": true, + "license": "MIT", "dependencies": { "@types/sinonjs__fake-timers": "*" } @@ -1161,19 +1298,29 @@ "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -1182,13 +1329,15 @@ "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/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1197,10 +1346,11 @@ } }, "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 +1362,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", @@ -1235,6 +1387,7 @@ "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 +1403,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 +1413,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 +1429,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,13 +1442,15 @@ "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==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -1301,13 +1459,15 @@ "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" } @@ -1317,6 +1477,7 @@ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } @@ -1326,6 +1487,7 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } @@ -1335,42 +1497,48 @@ "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 + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" }, "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 + "dev": true, + "license": "MIT" }, "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, + "license": "Apache-2.0", "engines": { "node": "*" } }, "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 + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "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 +1560,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 +1577,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", @@ -1424,6 +1594,7 @@ "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,23 +1606,27 @@ } }, "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.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", "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" @@ -1462,6 +1637,7 @@ "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" @@ -1477,22 +1653,80 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } }, + "node_modules/better-sqlite3": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.3.0.tgz", + "integrity": "sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "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==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1503,6 +1737,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 +1746,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.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -1529,9 +1764,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", "node-releases": "^2.0.18", "update-browserslist-db": "^1.1.0" }, @@ -1547,6 +1783,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 +1796,49 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/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 +1848,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 +1858,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 +1872,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.30001664", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz", + "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==", "dev": true, "funding": [ { @@ -1622,19 +1889,22 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true + "dev": true, + "license": "Apache-2.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 +1923,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,6 +1940,7 @@ "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" } @@ -1678,6 +1950,7 @@ "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 +1958,13 @@ "node": "*" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "peer": true + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1696,21 +1976,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.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "dev": true, + "license": "MIT" }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -1722,6 +2005,7 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, + "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -1731,13 +2015,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1749,13 +2035,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "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==", "dev": true, + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -1768,6 +2056,7 @@ "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 +2066,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 +2097,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 +2111,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 +2124,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 +2137,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 +2152,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 +2178,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 +2191,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 +2204,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 +2217,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 +2230,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 +2244,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 +2254,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", @@ -1975,6 +2278,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 +2292,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,19 +2312,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 + "dev": true, + "license": "MIT" }, "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==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "js-yaml": "^3.13.1", "lcov-parse": "^1.0.0", @@ -2039,6 +2347,7 @@ "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 +2368,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==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2080,6 +2391,7 @@ "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" } @@ -2089,6 +2401,7 @@ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" }, @@ -2101,17 +2414,19 @@ "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.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2127,6 +2442,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 +2452,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 +2469,33 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "dev": true, + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -2175,6 +2510,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,11 +2518,22 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/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" } @@ -2196,15 +2543,27 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "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" } @@ -2212,13 +2571,15 @@ "node_modules/di0": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/di0/-/di0-1.0.0.tgz", - "integrity": "sha512-RRZsfbOmxiB0ZI+4ABfw/O7GUOnqmgFJGEPFzj7IX+mpm73Hkd38akjaTagaFmwzzRAqIIVR3uB3zSzwnt8ZFw==" + "integrity": "sha512-RRZsfbOmxiB0ZI+4ABfw/O7GUOnqmgFJGEPFzj7IX+mpm73Hkd38akjaTagaFmwzzRAqIIVR3uB3zSzwnt8ZFw==", + "license": "MIT" }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -2228,6 +2589,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 +2599,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" }, @@ -2249,6 +2612,7 @@ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, + "license": "MIT", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -2259,6 +2623,7 @@ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -2270,16 +2635,18 @@ } }, "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 + "version": "1.5.29", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.29.tgz", + "integrity": "sha512-PF8n2AlIhCKXQ+gTpiJi0VhcHDb69kYX4MtCiivctc2QD3XuNZ/XIOlbGzt7WAjjEev0TtaH6Cu3arZExm5DOw==", + "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" }, @@ -2291,22 +2658,35 @@ "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 + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "once": "^1.4.0" + } }, "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==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "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" } @@ -2316,6 +2696,7 @@ "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" } @@ -2325,6 +2706,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" @@ -2338,6 +2720,7 @@ "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 +2748,22 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -2385,7 +2779,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/extsprintf": { "version": "1.3.0", @@ -2394,34 +2789,46 @@ "dev": true, "engines": [ "node >=0.6.0" - ] + ], + "license": "MIT" }, "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/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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "peer": true + }, "node_modules/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, + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } @@ -2431,6 +2838,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2440,6 +2848,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2452,6 +2861,7 @@ "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" }, @@ -2464,6 +2874,7 @@ "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" @@ -2477,6 +2888,7 @@ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "*" } @@ -2486,6 +2898,7 @@ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -2495,11 +2908,19 @@ "node": ">= 0.12" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "peer": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2507,6 +2928,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2520,6 +2942,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 +2952,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 +2962,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 +2972,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 +2982,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 +2992,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 +3011,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 +3026,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 +3044,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 +3055,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" }, @@ -2635,6 +3068,7 @@ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" } @@ -2644,6 +3078,7 @@ "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 +3098,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 +3112,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" @@ -2692,16 +3129,25 @@ "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", "dev": true, + "license": "BSD", "dependencies": { "ini": "^1.3.2" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "peer": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2722,6 +3168,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -2730,13 +3177,15 @@ "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", @@ -2758,6 +3207,7 @@ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", "dev": true, + "license": "ISC", "engines": { "node": ">=4" } @@ -2768,6 +3218,7 @@ "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "deprecated": "this library is no longer supported", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -2781,6 +3232,7 @@ "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 +3242,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 +3252,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 +3265,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,13 +3277,15 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -2844,15 +3301,38 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/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 +3352,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 +3362,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 +3373,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 +3383,27 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "license": "ISC" }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-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.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2934,6 +3419,7 @@ "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,6 +3429,7 @@ "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" } @@ -2952,6 +3439,7 @@ "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 +3449,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 +3459,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 +3469,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 +3482,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" }, @@ -3002,31 +3494,36 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true + "dev": true, + "license": "MIT" }, "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 + "dev": true, + "license": "ISC" }, "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": "MIT" }, "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 +3533,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", @@ -3052,6 +3550,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3064,6 +3563,7 @@ "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 +3578,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", @@ -3092,6 +3593,7 @@ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -3105,6 +3607,7 @@ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -3123,6 +3626,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -3149,6 +3653,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 +3668,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 +3700,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 +3734,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 +3749,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 +3768,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 +3778,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 +3824,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 +3840,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 +3853,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 +3870,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 +3888,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 +3898,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 +3924,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 +3938,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 +3954,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 +3975,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 +3990,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 +4008,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 +4018,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 +4039,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 +4053,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 +4086,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 +4120,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", @@ -3624,6 +4152,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3636,6 +4165,7 @@ "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 +4183,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 +4201,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3682,6 +4214,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 +4234,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 +4250,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,13 +4265,15 @@ "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==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3749,13 +4286,15 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -3767,37 +4306,43 @@ "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 + "dev": true, + "license": "MIT" }, "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": "(AFL-2.1 OR BSD-3-Clause)" }, "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-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 +4357,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" @@ -3835,6 +4382,7 @@ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -3849,13 +4397,15 @@ "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/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,6 +4415,7 @@ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3874,6 +4425,7 @@ "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { "lcov-parse": "bin/cli.js" } @@ -3883,6 +4435,7 @@ "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" } @@ -3891,13 +4444,15 @@ "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 +4468,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 +4482,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,6 +4492,7 @@ "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" } @@ -3944,6 +4502,7 @@ "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" }, @@ -3955,31 +4514,36 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-driver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", "dev": true, + "license": "ISC", "engines": { "node": ">=0.8.6" } @@ -3989,6 +4553,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 +4563,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 +4576,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" }, @@ -4025,6 +4592,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4036,13 +4604,15 @@ "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 +4622,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" }, @@ -4064,6 +4635,7 @@ "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", @@ -4088,13 +4660,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/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 +4684,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 +4702,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 +4712,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 +4725,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 +4735,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 +4745,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,13 +4757,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "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": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4197,6 +4779,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4206,6 +4789,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -4218,15 +4802,30 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=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" } @@ -4236,6 +4835,7 @@ "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" }, @@ -4247,7 +4847,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4257,6 +4857,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,38 +4867,57 @@ "node": ">= 6" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "peer": true + }, "node_modules/modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "license": "MIT", + "peer": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nise": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -4307,31 +4927,61 @@ } }, "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": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/node-abi": { + "version": "3.68.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", + "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", + "license": "MIT", + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "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 + "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", @@ -4347,6 +4997,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4359,6 +5010,7 @@ "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 +5020,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" }, @@ -4380,6 +5033,7 @@ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "*" } @@ -4388,7 +5042,7 @@ "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 +5052,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" }, @@ -4413,6 +5068,7 @@ "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" }, @@ -4428,6 +5084,7 @@ "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" }, @@ -4440,6 +5097,7 @@ "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" }, @@ -4455,6 +5113,7 @@ "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" } @@ -4464,6 +5123,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 +5142,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 +5152,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 +5162,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 +5171,22 @@ "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": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" }, "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 +5199,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,6 +5209,7 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -4550,19 +5218,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true + "dev": true, + "license": "MIT" }, "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.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "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,6 +5246,7 @@ "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" } @@ -4584,6 +5256,7 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -4593,6 +5266,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 +5274,39 @@ "node": ">=8" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "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/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 +5321,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 +5333,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" @@ -4649,13 +5354,26 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -4674,7 +5392,8 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ] + ], + "license": "MIT" }, "node_modules/q": { "version": "1.5.1", @@ -4682,6 +5401,7 @@ "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" @@ -4692,6 +5412,7 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.6" } @@ -4701,21 +5422,50 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "peer": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, + "license": "MIT", "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", @@ -4730,6 +5480,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 +5494,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 +5507,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 +5521,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 +5534,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 +5547,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 +5557,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 +5566,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 +5587,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -4836,7 +5596,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4851,6 +5611,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" @@ -4865,6 +5626,7 @@ "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "dev": true, + "license": "Apache-2.0", "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -4891,11 +5653,23 @@ "node": ">= 6" } }, + "node_modules/request/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, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "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" } @@ -4905,6 +5679,7 @@ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4922,6 +5697,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" }, @@ -4934,6 +5710,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4943,6 +5720,7 @@ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -4951,7 +5729,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -4965,19 +5742,22 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "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 + "dev": true, + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -4987,6 +5767,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 +5780,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,7 +5789,55 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "node_modules/sinon": { "version": "15.2.0", @@ -5015,6 +5845,7 @@ "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", "deprecated": "16.1.1", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^10.3.0", @@ -5032,13 +5863,15 @@ "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 +5881,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 +5891,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 +5902,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 +5912,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.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "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 +5951,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,13 +5960,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "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, + "license": "MIT", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -5151,6 +5994,7 @@ "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" }, @@ -5162,7 +6006,7 @@ "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 +6016,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 +6030,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 +6045,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 +6058,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 +6068,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 +6078,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 +6091,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 +6104,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 +6117,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 +6125,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "license": "MIT", + "peer": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -5291,6 +6175,7 @@ "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10" } @@ -5299,13 +6184,15 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "3" } @@ -5314,13 +6201,15 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "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, + "license": "MIT", "engines": { "node": ">=4" } @@ -5330,6 +6219,7 @@ "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" }, @@ -5342,6 +6232,7 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -5355,25 +6246,27 @@ "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-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.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, + "license": "MIT", "dependencies": { - "bs-logger": "0.x", + "bs-logger": "^0.2.6", "ejs": "^3.1.10", - "fast-json-stable-stringify": "2.x", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "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.6.3", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" @@ -5412,6 +6305,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5424,6 +6318,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 +6328,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -5476,6 +6372,7 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -5484,7 +6381,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -5496,13 +6393,15 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + "dev": true, + "license": "Unlicense" }, "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 +6411,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 +6420,11 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5533,10 +6434,11 @@ } }, "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,10 +6448,11 @@ } }, "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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.1.0", @@ -5570,6 +6473,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.1.2", "picocolors": "^1.0.1" @@ -5586,6 +6490,7 @@ "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" } @@ -5594,29 +6499,35 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "license": "MIT" }, "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.", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", "bin": { - "uuid": "bin/uuid" + "uuid": "dist/bin/uuid" } }, "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,6 +6542,7 @@ "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" @@ -5644,6 +6556,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -5655,6 +6568,7 @@ "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 +6578,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" }, @@ -5678,13 +6593,15 @@ "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 +6618,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -5721,6 +6639,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 +6649,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 +6658,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 +6685,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 +6695,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 +6705,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 d975a05..c741803 100644 --- a/package.json +++ b/package.json @@ -46,17 +46,23 @@ "di0": "^1.0.0" }, "devDependencies": { - "@types/chai": "^4.3.17", - "@types/jest": "^29.5.12", - "@types/node": "^20.14.14", + "@types/better-sqlite3": "^7.6.11", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.13", + "@types/node": "^20.16.9", "@types/sinon": "^10.0.20", + "@types/uuid": "^10.0.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "coveralls": "^3.1.1", "jest": "^29.7.0", "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.6.2", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "better-sqlite3": "^11.3.0" } } diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index a7da6ae..8587a77 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -2,7 +2,6 @@ import { IAggregate, IMutableAggregateState, ICommand, - Identifier, IEvent, IEventSet, IAggregateConstructorParams @@ -37,7 +36,7 @@ export abstract class AbstractAggregate const asProjectionView = (view: any): IProjectionView | undefined => (isProjectionView(view) ? view : undefined); +const isPersistentView = (view: any): view is IPersistentView => + 'getLastEvent' in view && + 'tryMarkAsProjecting' in view && + 'markAsProjected' in view; + /** * Base class for Projection definition */ @@ -42,12 +47,14 @@ export abstract class AbstractProjection { - return (this.view instanceof Map) - || (this.view instanceof InMemoryView); + throw new Error('shouldRestoreView is deprecated'); } constructor({ @@ -81,9 +89,8 @@ export abstract class AbstractProjection view : - viewFactory; + this.#view = view; + this.#viewFactory = viewFactory; this._logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : @@ -114,7 +121,17 @@ export abstract class AbstractProjection { + async #restoreAggregate(id: string): Promise { if (!id) throw new TypeError('id argument required'); diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 4886c43..5ff5740 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -4,7 +4,7 @@ import { AggregateCommandHandler } from './AggregateCommandHandler'; import { CommandBus } from './CommandBus'; import { EventStore } from './EventStore'; import { SagaEventHandler } from './SagaEventHandler'; -import { InMemoryMessageBus } from './infrastructure/InMemoryMessageBus'; +import { InMemoryMessageBus } from './infrastructure/memory/InMemoryMessageBus'; import { getHandledMessageTypes, diff --git a/src/EventStore.ts b/src/EventStore.ts index e69de0f..ff448d0 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -1,8 +1,6 @@ import { IAggregateSnapshotStorage, - Identifier, IEvent, - IEventQueryFilter, IEventStorage, IEventSet, IExtendableLogger, @@ -11,7 +9,9 @@ import { IMessageHandler, IObservable, IEventStream, - IEventStore + IEventStore, + EventQueryAfter, + EventQueryBefore } from "./interfaces"; import { getClassName, setupOneTimeEmitterSubscription } from "./utils"; import * as Event from './Event'; @@ -83,8 +83,9 @@ export class EventStore implements IEventStore { this.#messageBus = messageBus; } + /** Retrieve new ID from the storage */ - async getNewId(): Promise { + async getNewId(): Promise { return this.#storage.getNewId(); } @@ -102,8 +103,14 @@ export class EventStore implements IEventStore { this.#logger?.debug(`${eventTypes ? eventTypes.join(', ') : 'all'} events retrieved`); } + async* getEventsByTypes(eventTypes: Readonly, options: EventQueryAfter): IEventStream { + const eventsIterable = await this.#storage.getEventsByTypes(eventTypes, options); + + yield* eventsIterable; + } + /** Retrieve all events of specific Aggregate */ - async getAggregateEvents(aggregateId: Identifier): Promise { + async getAggregateEvents(aggregateId: string): Promise { if (!aggregateId) throw new TypeError('aggregateId argument required'); @@ -127,7 +134,7 @@ export class EventStore implements IEventStore { } /** Retrieve events of specific Saga */ - async getSagaEvents(sagaId: Identifier, filter: Pick) { + async getSagaEvents(sagaId: string, filter: EventQueryBefore) { if (!sagaId) throw new TypeError('sagaId argument required'); if (!filter) diff --git a/src/index.ts b/src/index.ts index 94b96da..9bbea12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,12 @@ export * from './AbstractSaga'; export * from './SagaEventHandler'; export * from './AbstractProjection'; -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 './infrastructure/memory/InMemoryMessageBus'; +export * from './infrastructure/memory/InMemoryEventStorage'; +export * from './infrastructure/memory/InMemorySnapshotStorage'; +export * from './infrastructure/memory/InMemoryView'; +export * from './infrastructure/memory/InMemoryLock'; +export * from './infrastructure/memory/utils/Deferred'; export * as Event from './Event'; export { diff --git a/src/infrastructure/InMemoryEventStorage.ts b/src/infrastructure/memory/InMemoryEventStorage.ts similarity index 59% rename from src/infrastructure/InMemoryEventStorage.ts rename to src/infrastructure/memory/InMemoryEventStorage.ts index 3fa6d95..4ddce9b 100644 --- a/src/infrastructure/InMemoryEventStorage.ts +++ b/src/infrastructure/memory/InMemoryEventStorage.ts @@ -1,4 +1,7 @@ -import { IEvent, IEventStorage, IEventSet, IEventStream } from "../interfaces"; +import { IEvent } from "../../interfaces/IEvent"; +import { IEventSet } from "../../interfaces/IEventSet"; +import { EventQueryAfter, IEventStorage } from "../../interfaces/IEventStorage"; +import { IEventStream } from "../../interfaces/IEventStream"; import { nextCycle } from "./utils"; /** @@ -9,7 +12,6 @@ import { nextCycle } from "./utils"; * @implements {IEventStorage} */ export class InMemoryEventStorage implements IEventStorage { - #nextId: number = 0; #events: IEventSet = []; @@ -61,8 +63,33 @@ export class InMemoryEventStorage implements IEventStorage { } } - getNewId(): number { + async* getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream { + await nextCycle(); + + let newEvents: IEventSet; + if (options?.afterEvent) { + const lastEventId = options.afterEvent.id; + if (!lastEventId) + throw new TypeError('options.afterEvent.id is required'); + + const lastEventIndex = this.#events.findIndex(e => e.id === lastEventId); + if (!lastEventIndex) + throw new TypeError(`Event "${lastEventId}" could not be found`); + + newEvents = this.#events.slice(lastEventIndex + 1); + } + else { + newEvents = this.#events; + } + + for await (const event of newEvents) { + if (!eventTypes || eventTypes.includes(event.type)) + yield event; + } + } + + getNewId(): string { this.#nextId += 1; - return this.#nextId; + return String(this.#nextId); } } diff --git a/src/infrastructure/InMemoryLock.ts b/src/infrastructure/memory/InMemoryLock.ts similarity index 61% rename from src/infrastructure/InMemoryLock.ts rename to src/infrastructure/memory/InMemoryLock.ts index dee6195..7203884 100644 --- a/src/infrastructure/InMemoryLock.ts +++ b/src/infrastructure/memory/InMemoryLock.ts @@ -1,10 +1,8 @@ -import { ILockable, ILockableWithIndication } from "../interfaces"; import { Deferred } from "./utils"; -export class InMemoryLock implements ILockableWithIndication { +export class InMemoryLock { #lockMarker: Deferred | undefined; - #innerLock: ILockable | undefined; /** * Indicates if lock is acquired @@ -13,15 +11,6 @@ export class InMemoryLock implements ILockableWithIndication { 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 @@ -32,8 +21,6 @@ export class InMemoryLock implements ILockableWithIndication { try { this.#lockMarker = new Deferred(); - if (this.#innerLock) - await this.#innerLock.lock(); } catch (err: any) { try { @@ -50,14 +37,8 @@ export class InMemoryLock implements ILockableWithIndication { * Release the lock acquired earlier */ async unlock(): Promise { - try { - if (this.#innerLock) - await this.#innerLock.unlock(); - } - finally { - this.#lockMarker?.resolve(); - this.#lockMarker = undefined; - } + this.#lockMarker?.resolve(); + this.#lockMarker = undefined; } /** diff --git a/src/infrastructure/InMemoryMessageBus.ts b/src/infrastructure/memory/InMemoryMessageBus.ts similarity index 99% rename from src/infrastructure/InMemoryMessageBus.ts rename to src/infrastructure/memory/InMemoryMessageBus.ts index c8f43f3..b14548e 100644 --- a/src/infrastructure/InMemoryMessageBus.ts +++ b/src/infrastructure/memory/InMemoryMessageBus.ts @@ -4,7 +4,7 @@ import { IMessageBus, IMessageHandler, IObservable -} from "../interfaces"; +} from "../../interfaces"; /** * Default implementation of the message bus. diff --git a/src/infrastructure/InMemorySnapshotStorage.ts b/src/infrastructure/memory/InMemorySnapshotStorage.ts similarity index 66% rename from src/infrastructure/InMemorySnapshotStorage.ts rename to src/infrastructure/memory/InMemorySnapshotStorage.ts index d3fd377..a306535 100644 --- a/src/infrastructure/InMemorySnapshotStorage.ts +++ b/src/infrastructure/memory/InMemorySnapshotStorage.ts @@ -1,4 +1,5 @@ -import { IAggregateSnapshotStorage, Identifier, IEvent } from "../interfaces"; +import { IAggregateSnapshotStorage } from "../../interfaces/IAggregateSnapshotStorage"; +import { IEvent } from "../../interfaces/IEvent"; /** * In-memory storage for aggregate snapshots. @@ -6,12 +7,12 @@ import { IAggregateSnapshotStorage, Identifier, IEvent } from "../interfaces"; */ export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { - #snapshots: Map = new Map(); + #snapshots: Map = new Map(); /** * Get latest aggregate snapshot */ - async getAggregateSnapshot(aggregateId: Identifier): Promise { + async getAggregateSnapshot(aggregateId: string): Promise { return this.#snapshots.get(aggregateId); } diff --git a/src/infrastructure/InMemoryView.ts b/src/infrastructure/memory/InMemoryView.ts similarity index 85% rename from src/infrastructure/InMemoryView.ts rename to src/infrastructure/memory/InMemoryView.ts index bd678bb..a58cba7 100644 --- a/src/infrastructure/InMemoryView.ts +++ b/src/infrastructure/memory/InMemoryView.ts @@ -1,6 +1,7 @@ import { InMemoryLock } from './InMemoryLock'; -import { IProjectionView, Identifier } from "../interfaces"; import { nextCycle } from './utils'; +import { IObjectView } from '../../interfaces/IObjectView'; +import { IProjectionView } from '../../interfaces/IProjectionView'; /** * Update given value with an update Cb and return updated value. @@ -16,13 +17,13 @@ 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 IProjectionView, IObjectView { static factory(): TView { return (new InMemoryView() as unknown) as TView; } - protected _map: Map = new Map(); + protected _map: Map = new Map(); #lock: InMemoryLock; @@ -77,12 +78,12 @@ export class InMemoryView implements IProjectionView { * * @deprecated Use `async get()` instead */ - has(key: Identifier): boolean { + has(key: string): boolean { return this._map.has(key); } /** Get record with a given key; await until the view is restored */ - async get(key: Identifier, options?: { nowait?: boolean }): Promise { + async get(key: string, options?: { nowait?: boolean }): Promise { if (!key) throw new TypeError('key argument required'); @@ -95,8 +96,8 @@ export class InMemoryView implements IProjectionView { } /** Get all records matching an optional filter */ - async getAll(filter?: (r: TRecord | undefined, i: Identifier) => boolean): - Promise> { + async getAll(filter?: (r: TRecord | undefined, i: string) => boolean): + Promise> { if (filter && typeof filter !== 'function') throw new TypeError('filter argument, when defined, must be a Function'); @@ -105,7 +106,7 @@ export class InMemoryView implements IProjectionView { await nextCycle(); - const r: Array<[Identifier, TRecord | undefined]> = []; + const r: Array<[string, TRecord | undefined]> = []; for (const entry of this._map.entries()) { if (!filter || filter(entry[1], entry[0])) r.push(entry); @@ -115,7 +116,7 @@ export class InMemoryView implements IProjectionView { } /** Create record with a given key and value */ - async create(key: Identifier, value: TRecord = {} as TRecord) { + async create(key: string, value: TRecord = {} as TRecord) { if (!key) throw new TypeError('key argument required'); if (typeof value === 'function') @@ -131,7 +132,7 @@ export class InMemoryView implements IProjectionView { } /** Update existing view record */ - async update(key: Identifier, update: (r?: TRecord) => TRecord) { + async update(key: string, update: (r: TRecord) => TRecord) { if (!key) throw new TypeError('key argument required'); if (typeof update !== 'function') @@ -144,7 +145,7 @@ export class InMemoryView implements IProjectionView { } /** Update existing view record or create new */ - async updateEnforcingNew(key: Identifier, update: (r?: TRecord) => TRecord) { + async updateEnforcingNew(key: string, update: (r?: TRecord) => TRecord) { if (!key) throw new TypeError('key argument required'); if (typeof update !== 'function') @@ -170,7 +171,7 @@ export class InMemoryView implements IProjectionView { } /** Update existing record */ - private async _update(key: Identifier, update: (r?: TRecord) => TRecord) { + private async _update(key: string, update: (r?: TRecord) => TRecord) { const value = this._map.get(key); const updatedValue = applyUpdate(value, update); @@ -181,7 +182,7 @@ export class InMemoryView implements IProjectionView { } /** Delete record */ - async delete(key: Identifier) { + async delete(key: string) { if (!key) throw new TypeError('key argument required'); diff --git a/src/infrastructure/memory/index.ts b/src/infrastructure/memory/index.ts new file mode 100644 index 0000000..3ef457a --- /dev/null +++ b/src/infrastructure/memory/index.ts @@ -0,0 +1,5 @@ +export * from './InMemoryEventStorage'; +export * from './InMemoryLock'; +export * from './InMemoryMessageBus'; +export * from './InMemorySnapshotStorage'; +export * from './InMemoryView'; diff --git a/src/infrastructure/utils/Deferred.ts b/src/infrastructure/memory/utils/Deferred.ts similarity index 100% rename from src/infrastructure/utils/Deferred.ts rename to src/infrastructure/memory/utils/Deferred.ts diff --git a/src/infrastructure/utils/index.ts b/src/infrastructure/memory/utils/index.ts similarity index 100% rename from src/infrastructure/utils/index.ts rename to src/infrastructure/memory/utils/index.ts diff --git a/src/infrastructure/utils/nextCycle.ts b/src/infrastructure/memory/utils/nextCycle.ts similarity index 100% rename from src/infrastructure/utils/nextCycle.ts rename to src/infrastructure/memory/utils/nextCycle.ts diff --git a/src/infrastructure/sqlite/AbstractSqliteView.ts b/src/infrastructure/sqlite/AbstractSqliteView.ts new file mode 100644 index 0000000..8d62c79 --- /dev/null +++ b/src/infrastructure/sqlite/AbstractSqliteView.ts @@ -0,0 +1,202 @@ +import { IEvent } from '../../interfaces/IEvent'; +import { IExtendableLogger, ILogger } from '../../interfaces/ILogger'; +import { IPersistentView } from '../../interfaces/IPersistentView'; + +const guid = (str: string) => Buffer.from(str.replaceAll('-', ''), 'hex'); + +const EVENT_PROCESSING_LOCK_TTL = 15; // sec + +export type AbstractSqliteViewOptions = { + schemaVersion: string; + sqliteDb: import('better-sqlite3').Database; + viewLockTableName?: string; + logger?: IExtendableLogger | ILogger; +} + +export abstract class AbstractSqliteView implements IPersistentView { + + /** + * Version of the the schema representing the structure of the data stored in the view + */ + readonly schemaVersion: string; + + /** + * Shared table where view locks and last projected events are tracked + */ + readonly viewLockTableName: string; + + /** + * Main table where the view data is stored + * + * @example `tbl_users_${this.schemaVersion}` + */ + abstract get tableName(): string; + + /** + * Table where events are being tracked as projecting/projected + * + * @example `tbl_users_${this.schemaVersion}_event_lock` + */ + abstract get eventLockTableName(): string; + + protected db: import('better-sqlite3').Database; + protected logger: ILogger | undefined; + + #getLastEventQuery: import('better-sqlite3').Statement; + #lockEventQuery: import('better-sqlite3').Statement<[Buffer], void>; + #finalizeEventLockQuery: import('better-sqlite3').Statement<[Buffer], void>; + #recordLastEventQuery: import('better-sqlite3').Statement<[string, string, string], void>; + #upsertTableLockQuery: import('better-sqlite3').Statement<[string, string], void>; + #removeTableLockQuery: import('better-sqlite3').Statement<[string, string], void>; + + + constructor(options: AbstractSqliteViewOptions) { + this.schemaVersion = options.schemaVersion; + this.viewLockTableName = options.viewLockTableName ?? 'tbl_view_lock'; + this.db = options.sqliteDb; + this.logger = options.logger && 'child' in options.logger ? + options.logger.child({ service: this.constructor.name }) : + options.logger; + } + + /** + * SQLite tables initialization. + * Must be called in the derived class before getting to work. + */ + protected initialize(): void { + this.db.exec(` + CREATE TABLE IF NOT EXISTS ${this.viewLockTableName} ( + table_name TEXT, + schema_version TEXT, + locked_at DATETIME DEFAULT (strftime('%s', 'now')), + last_event TEXT, + PRIMARY KEY (table_name, schema_version) + ); + + CREATE TABLE IF NOT EXISTS ${this.eventLockTableName} ( + event_id BLOB PRIMARY KEY, + processing_at DATETIME DEFAULT (strftime('%s', 'now')), + processed_at DATETIME + ); + `); + + this.#getLastEventQuery = this.db.prepare(` + SELECT + last_event + FROM ${this.viewLockTableName} + WHERE + table_name = ? + AND schema_version =? + `); + + this.#lockEventQuery = this.db.prepare(` + INSERT INTO ${this.eventLockTableName} (event_id) + VALUES (?) + ON CONFLICT (event_id) + DO UPDATE SET + processing_at = strftime('%s', 'now') + WHERE + processed_at IS NULL + AND processing_at <= strftime('%s', 'now') - ${EVENT_PROCESSING_LOCK_TTL} + `); + + this.#finalizeEventLockQuery = this.db.prepare(` + UPDATE ${this.eventLockTableName} + SET + processed_at = strftime('%s', 'now') + WHERE + event_id = ? + AND processed_at IS NULL + `); + + this.#recordLastEventQuery = this.db.prepare(` + UPDATE ${this.viewLockTableName} + SET + last_event = ? + WHERE + table_name = ? + AND schema_version = ? + `); + + this.#upsertTableLockQuery = this.db.prepare(` + INSERT INTO ${this.viewLockTableName} (table_name, schema_version, locked_at) + VALUES (?, ?, strftime('%s', 'now')) + ON CONFLICT (table_name, schema_version) + DO UPDATE SET + locked_at = excluded.locked_at + WHERE + locked_at IS NULL + `); + + this.#removeTableLockQuery = this.db.prepare(` + UPDATE ${this.viewLockTableName} + SET + locked_at = NULL + WHERE + table_name = ? + AND schema_version = ? + AND locked_at IS NOT NULL + `); + } + + getLastEvent() { + const tableInfoRecord = this.#getLastEventQuery.get(this.tableName, this.schemaVersion); + if (!tableInfoRecord?.last_event) + return undefined; + + return JSON.parse(tableInfoRecord.last_event); + } + + tryMarkAsProjecting(event: IEvent) { + if (!event.id) + throw new TypeError('event.id is required'); + + const r = this.#lockEventQuery.run(guid(event.id)); + + return r.changes !== 0; + } + + markAsProjected(event: IEvent) { + if (!event.id) + throw new TypeError('event.id is required'); + + const updateResult = this.#finalizeEventLockQuery.run(guid(event.id)); + if (updateResult.changes === 0) + throw new Error(`Event ${event.id} could not be marked as processed`); + + this.#recordLastEventQuery.run(JSON.stringify(event), this.tableName, this.schemaVersion); + } + + ready: boolean = false; + + lock() { + this.ready = false; + + const upsertResult = this.#upsertTableLockQuery.run(this.tableName, this.schemaVersion); + if (upsertResult.changes === 1) + this.logger?.debug(`Table "${this.tableName}" lock obtained`); + else + this.logger?.debug(`Table "${this.tableName}" is already locked`); + + // TODO: automatic lock prolongation + + return upsertResult.changes === 1; + } + + async unlock(): Promise { + const updateResult = this.#removeTableLockQuery.run(this.tableName, this.schemaVersion); + if (updateResult.changes === 1) + this.logger?.debug(`Table "${this.tableName}" lock released`); + else + this.logger?.debug(`Table "${this.tableName}" lock didn't exist`); + + this.ready = true; + } + + async once(eventType: 'ready'): Promise { + + // TODO: periodically check until unlocked + + throw new Error('Method not implemented'); + } +} diff --git a/src/infrastructure/sqlite/ObjectSqliteView.ts b/src/infrastructure/sqlite/ObjectSqliteView.ts new file mode 100644 index 0000000..bf244e6 --- /dev/null +++ b/src/infrastructure/sqlite/ObjectSqliteView.ts @@ -0,0 +1,124 @@ +import * as BetterSqlite3 from 'better-sqlite3'; +import { AbstractSqliteView, AbstractSqliteViewOptions } from "./AbstractSqliteView"; +import { IObjectView, IPersistentView } from '../../interfaces'; + +const guid = (str: string) => Buffer.from(str.replaceAll('-', ''), 'hex'); + +export class ObjectSqliteView extends AbstractSqliteView implements IObjectView, IPersistentView { + + #tableNamePrefix: string; + #getQuery: BetterSqlite3.Statement<[Buffer], { data: string, version: number }>; + #insertQuery: BetterSqlite3.Statement<[Buffer, string], void>; + #updateByIdAndVersionQuery: BetterSqlite3.Statement<[string, Buffer, number], void>; + #deleteQuery: BetterSqlite3.Statement<[Buffer], void>; + + get tableName(): string { + return `${this.#tableNamePrefix}_${this.schemaVersion}`; + } + + get eventLockTableName(): string { + return `${this.#tableNamePrefix}_${this.schemaVersion}_event_lock`; + } + + constructor(options: AbstractSqliteViewOptions & { tableNamePrefix: string }) { + if (typeof options.tableNamePrefix !== 'string' || !options.tableNamePrefix.length) + throw new TypeError('options.tableNamePrefix argument must be a non-empty String'); + + super(options); + + this.#tableNamePrefix = options.tableNamePrefix; + + this.initialize(); + } + + protected initialize(): void { + super.initialize(); + + this.db.exec(`CREATE TABLE IF NOT EXISTS ${this.tableName} ( + id BLOB PRIMARY KEY, + version INTEGER DEFAULT 1, + data TEXT NOT NULL + );`); + + this.#getQuery = this.db.prepare(` + SELECT data, version + FROM ${this.tableName} + WHERE id = ? + `); + + this.#insertQuery = this.db.prepare(` + INSERT INTO ${this.tableName} (id, data) + VALUES (?, ?) + `); + + this.#updateByIdAndVersionQuery = this.db.prepare(` + UPDATE ${this.tableName} + SET + data = ?, + version = version + 1 + WHERE + id = ? + AND version = ? + `); + + this.#deleteQuery = this.db.prepare(` + DELETE FROM ${this.tableName} + WHERE id = ? + `); + } + + get(id: string): TRecord | undefined { + const r = this.#getQuery.get(guid(id)); + if (!r) + return undefined; + + return JSON.parse(r.data); + } + + create(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`); + } + + update(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`); + + const data = JSON.parse(record.data); + const updatedData = update(data); + const updatedJson = JSON.stringify(updatedData); + + const r = this.#updateByIdAndVersionQuery.run(updatedJson, gid, record.version); + if (r.changes !== 1) + throw new Error(`Record '${id}' could not be updated`); + } + + updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + const gid = guid(id); + const record = this.#getQuery.get(gid); + if (record) { + const data = JSON.parse(record.data); + const updatedData = update(data); + const updatedJson = JSON.stringify(updatedData); + + const r = this.#updateByIdAndVersionQuery.run(updatedJson, gid, record.version); + if (r.changes !== 1) + throw new Error(`Record '${id}' could not be updated`); + } + else { + const newData = update(); + + const r = this.#insertQuery.run(guid(id), JSON.stringify(newData)); + if (r.changes !== 1) + throw new Error(`Record '${id}' could not be created`); + } + } + + delete(id: string): boolean { + const r = this.#deleteQuery.run(guid(id)); + return r.changes === 1; + } +} diff --git a/src/infrastructure/sqlite/index.ts b/src/infrastructure/sqlite/index.ts new file mode 100644 index 0000000..e6c6429 --- /dev/null +++ b/src/infrastructure/sqlite/index.ts @@ -0,0 +1,2 @@ +export * from './AbstractSqliteView'; +export * from './ObjectSqliteView'; 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..50f20dd --- /dev/null +++ b/src/interfaces/IAggregate.ts @@ -0,0 +1,55 @@ +import { ICommand } from "./ICommand"; +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; + +/** + * Minimum aggregate interface, as it's used by default `AggregateCommandHandler` + */ +export interface IAggregate { + + /** Unique aggregate identifier */ + readonly id: string; + + /** 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: string, + + /** 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; + diff --git a/src/interfaces/IAggregateSnapshotStorage.ts b/src/interfaces/IAggregateSnapshotStorage.ts new file mode 100644 index 0000000..10064e0 --- /dev/null +++ b/src/interfaces/IAggregateSnapshotStorage.ts @@ -0,0 +1,7 @@ +import { IEvent } from "./IEvent"; + +export interface IAggregateSnapshotStorage { + getAggregateSnapshot(aggregateId: string): Promise | undefined> | IEvent | undefined; + + saveAggregateSnapshot(snapshotEvent: IEvent): Promise | void; +} diff --git a/src/interfaces/ICommand.ts b/src/interfaces/ICommand.ts new file mode 100644 index 0000000..95b5a2b --- /dev/null +++ b/src/interfaces/ICommand.ts @@ -0,0 +1,3 @@ +import { IMessage } from "./IMessage"; + +export type ICommand = IMessage; diff --git a/src/interfaces/ICommandBus.ts b/src/interfaces/ICommandBus.ts new file mode 100644 index 0000000..87e2f59 --- /dev/null +++ b/src/interfaces/ICommandBus.ts @@ -0,0 +1,18 @@ +import { ICommand } from "./ICommand"; +import { IEventSet } from "./IEventSet"; +import { IMessageHandler, IObservable } from "./IObservable"; +import { IObserver } from "./IObserver"; + +export interface ICommandBus extends IObservable { + send(commandType: string, aggregateId: string, 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; +} diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts new file mode 100644 index 0000000..4d54f07 --- /dev/null +++ b/src/interfaces/IEvent.ts @@ -0,0 +1,6 @@ +import { IMessage } from "./IMessage"; + +export type IEvent = IMessage & { + /** Unique event identifier */ + id?: string; +}; diff --git a/src/interfaces/IEventReceptor.ts b/src/interfaces/IEventReceptor.ts new file mode 100644 index 0000000..6059e78 --- /dev/null +++ b/src/interfaces/IEventReceptor.ts @@ -0,0 +1,6 @@ +import { IEventStore } from "./IEventStore"; +import { IObserver } from "./IObserver"; + +export interface IEventReceptor extends IObserver { + subscribe(eventStore: IEventStore): void; +} diff --git a/src/interfaces/IEventSet.ts b/src/interfaces/IEventSet.ts new file mode 100644 index 0000000..fcde354 --- /dev/null +++ b/src/interfaces/IEventSet.ts @@ -0,0 +1,6 @@ +import { IEvent } from "./IEvent"; + +/** + * @deprecated Try to use `IEventStream` instead + */ +export type IEventSet = ReadonlyArray>; diff --git a/src/interfaces/IEventStorage.ts b/src/interfaces/IEventStorage.ts new file mode 100644 index 0000000..d99d113 --- /dev/null +++ b/src/interfaces/IEventStorage.ts @@ -0,0 +1,31 @@ +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; +import { IEventStream } from "./IEventStream"; + +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 IEventStorage { + /** + * Create unique identifier + */ + getNewId(): string | Promise; + + commitEvents(events: IEventSet): Promise; + + getEvents(eventTypes?: Readonly): IEventStream; + + getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): + IEventStream; + + getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): Promise; + + getSagaEvents(sagaId: string, options: EventQueryBefore): Promise; +} diff --git a/src/interfaces/IEventStore.ts b/src/interfaces/IEventStore.ts new file mode 100644 index 0000000..6ca763d --- /dev/null +++ b/src/interfaces/IEventStore.ts @@ -0,0 +1,27 @@ +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; +import { EventQueryAfter, EventQueryBefore } from "./IEventStorage"; +import { IEventStream } from "./IEventStream"; +import { IMessageHandler, IObservable } from "./IObservable"; + +export interface IEventStore extends IObservable { + readonly snapshotsSupported?: boolean; + + getNewId(): string | Promise; + + commit(events: IEventSet): Promise; + + getAllEvents(eventTypes?: Readonly): IEventStream; + + getEventsByTypes(eventTypes: Readonly, options: EventQueryAfter): IEventStream; + + getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): Promise; + + getSagaEvents(sagaId: string, options: EventQueryBefore): Promise; + + once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise; + + queue(name: string): IObservable; + + registerSagaStarters(startsWith: string[] | undefined): void; +} diff --git a/src/interfaces/IEventStream.ts b/src/interfaces/IEventStream.ts new file mode 100644 index 0000000..f8c9337 --- /dev/null +++ b/src/interfaces/IEventStream.ts @@ -0,0 +1,3 @@ +import { IEvent } from "./IEvent"; + +export type IEventStream = AsyncIterableIterator>; 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..448e8aa --- /dev/null +++ b/src/interfaces/IMessage.ts @@ -0,0 +1,13 @@ +export interface IMessage { + /** Event or command type */ + type: string; + + aggregateId?: string; + aggregateVersion?: number; + + sagaId?: string; + sagaVersion?: number; + + payload?: TPayload; + context?: any; +} diff --git a/src/interfaces/IMessageBus.ts b/src/interfaces/IMessageBus.ts new file mode 100644 index 0000000..c20af13 --- /dev/null +++ b/src/interfaces/IMessageBus.ts @@ -0,0 +1,8 @@ +import { ICommand } from "./ICommand"; +import { IEvent } from "./IEvent"; +import { IObservable } from "./IObservable"; + +export interface IMessageBus extends IObservable { + send(command: ICommand): Promise; + publish(event: IEvent): Promise; +} diff --git a/src/interfaces/IObjectView.ts b/src/interfaces/IObjectView.ts new file mode 100644 index 0000000..de324c8 --- /dev/null +++ b/src/interfaces/IObjectView.ts @@ -0,0 +1,11 @@ +export interface IObjectView { + get(id: string): Promise | TRecord | undefined; + + create(id: string, r: TRecord): Promise | any; + + update(id: string, cb: (r: TRecord) => TRecord): Promise | any; + + updateEnforcingNew(id: string, cb: (r?: TRecord) => TRecord): Promise | any; + + delete(id: string): Promise | any; +} diff --git a/src/interfaces/IObservable.ts b/src/interfaces/IObservable.ts new file mode 100644 index 0000000..0fc311d --- /dev/null +++ b/src/interfaces/IObservable.ts @@ -0,0 +1,11 @@ +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; +} diff --git a/src/interfaces/IObserver.ts b/src/interfaces/IObserver.ts new file mode 100644 index 0000000..6f1365f --- /dev/null +++ b/src/interfaces/IObserver.ts @@ -0,0 +1,5 @@ +import { IObservable } from "./IObservable"; + +export interface IObserver { + subscribe(observable: IObservable): void; +} diff --git a/src/interfaces/IPersistentView.ts b/src/interfaces/IPersistentView.ts new file mode 100644 index 0000000..2832c0f --- /dev/null +++ b/src/interfaces/IPersistentView.ts @@ -0,0 +1,24 @@ +import { IEvent } from "./IEvent"; +import { IProjectionView } from "./IProjectionView"; + +export interface IPersistentView extends IProjectionView { + + /** + * Get last projected event, + * so that projection state can be restored from following events + */ + getLastEvent(): Promise | IEvent | undefined; + + /** + * 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 | boolean; + + /** + * Mark event as projected + */ + markAsProjected(event: IEvent): Promise | void; +} diff --git a/src/interfaces/IProjection.ts b/src/interfaces/IProjection.ts new file mode 100644 index 0000000..52819eb --- /dev/null +++ b/src/interfaces/IProjection.ts @@ -0,0 +1,20 @@ +import { IEvent } from "./IEvent"; +import { IEventStore } from "./IEventStore"; +import { IObserver } from "./IObserver"; + +export interface IProjection extends IObserver { + readonly view: TView; + + subscribe(eventStore: IEventStore): Promise; + + project(event: IEvent): Promise; +} + +export interface IProjectionConstructor { + new(c?: any): IProjection; + readonly handles?: string[]; +} + +export interface IViewFactory { + (options: { schemaVersion: string }): TView; +} diff --git a/src/interfaces/IProjectionView.ts b/src/interfaces/IProjectionView.ts new file mode 100644 index 0000000..469666a --- /dev/null +++ b/src/interfaces/IProjectionView.ts @@ -0,0 +1,22 @@ +export interface IProjectionView { + + /** + * Indicates if view is ready for new events projecting + */ + ready: boolean; + + /** + * Lock the view for external reads/writes + */ + lock(): Promise | boolean; + + /** + * Unlock external read/write operations + */ + unlock(): Promise | void; + + /** + * Wait till the view is ready to accept new events + */ + once(eventType: "ready"): Promise; +} diff --git a/src/interfaces/ISaga.ts b/src/interfaces/ISaga.ts new file mode 100644 index 0000000..a4e585c --- /dev/null +++ b/src/interfaces/ISaga.ts @@ -0,0 +1,36 @@ +import { ICommand } from "./ICommand"; +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; + +export interface ISaga { + /** Unique Saga ID */ + readonly id: 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; + + onError?(error: Error, options: { event: IEvent, command: ICommand }): void; +} + +export type ISagaConstructorParams = { + id: string, + 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/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..67b8da1 --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,20 @@ +export * from './IAggregate'; +export * from './IAggregateSnapshotStorage'; +export * from './ICommand'; +export * from './ICommandBus'; +export * from './IEvent'; +export * from './IEventReceptor'; +export * from './IEventSet'; +export * from './IEventStorage'; +export * from './IEventStore'; +export * from './IEventStream'; +export * from './ILogger'; +export * from './IMessage'; +export * from './IMessageBus'; +export * from './IObjectView'; +export * from './IObservable'; +export * from './IObserver'; +export * from './IPersistentView'; +export * from './IProjection'; +export * from './IProjectionView'; +export * from './ISaga'; diff --git a/tests/integration/SqliteView.test.ts b/tests/integration/SqliteView.test.ts new file mode 100644 index 0000000..b2ee415 --- /dev/null +++ b/tests/integration/SqliteView.test.ts @@ -0,0 +1,134 @@ + +import { existsSync, unlinkSync } from 'fs'; +import { AbstractProjection, IEvent } from '../../src'; +import { ObjectSqliteView } from '../../src/infrastructure/sqlite/ObjectSqliteView'; +import * as createDb from 'better-sqlite3'; +import { v7 } from 'uuid'; + +type UserPayload = { + name: string; +} + +class MyDumbProjection extends AbstractProjection> { + + get schemaVersion() { + return '1'; + } + + constructor({ myDumbViewFactory }) { + super({ viewFactory: myDumbViewFactory }); + } + + 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.only('SqliteView', () => { + + let sqliteInMemoryDb: import('better-sqlite3').Database; + + const logState = () => { + console.log({ + tbl_view_lock: sqliteInMemoryDb.prepare(`SELECT * FROM tbl_view_lock`).all(), + tbl_test_1_event_lock: sqliteInMemoryDb.prepare(`SELECT * FROM tbl_test_1_event_lock`).all(), + tbl_test_1: sqliteInMemoryDb.prepare(`SELECT * FROM tbl_test_1`).all() + }); + } + + const fileName = './test.sqlite'; + + beforeEach(() => { + sqliteInMemoryDb = 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 + sqliteInMemoryDb.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. + sqliteInMemoryDb.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. + sqliteInMemoryDb.pragma(`journal_size_limit = ${5 * 1024 * 1024}`); + }); + + afterEach(() => { + sqliteInMemoryDb.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 10_000 events within 0.5 seconds', async () => { + + const p = new MyDumbProjection({ + myDumbViewFactory: ({ schemaVersion }) => new ObjectSqliteView({ + schemaVersion, + sqliteDb: sqliteInMemoryDb, + tableNamePrefix: 'tbl_test' + }) + }); + + await p.view.lock(); + await p.view.unlock(); + + const aggregateIds = Array.from({ length: 5_000 }, () => ({ + id1: v7(), + id2: v7(), + id3: v7() + })); + + console.time(); + + for (const { id1: aggregateId, id2, id3 } of aggregateIds) { + await p.project({ + type: 'userCreated', + id: id2, + aggregateId, + payload: { + name: 'Jon' + } + }); + + await p.project({ + type: 'userModified', + id: id3, + aggregateId, + payload: { + name: 'Jon Doe' + } + }); + } + + console.timeEnd(); + + // logState(); + + // const user = await p.view.get(aggregateId); + + // expect(user).toEqual({ + // name: 'Jon Doe' + // }); + }); +}); diff --git a/tests/unit/AbstractProjection.test.ts b/tests/unit/AbstractProjection.test.ts index f5cff80..04ef51d 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -2,11 +2,15 @@ import { expect, assert, AssertionError } from 'chai'; import * as sinon from 'sinon'; import { AbstractProjection, InMemoryView, InMemoryEventStorage, EventStore, InMemoryMessageBus } from '../../src'; -class MyProjection extends AbstractProjection { +class MyProjection extends AbstractProjection> { static get handles() { return ['somethingHappened']; } + get schemaVersion(): string { + return 'v1'; + } + async _somethingHappened({ aggregateId, payload, context }) { return this.view.updateEnforcingNew(aggregateId, (v = {}) => { if (v.somethingHappenedCnt) @@ -31,7 +35,7 @@ describe('AbstractProjection', function () { it('returns a view storage associated with projection', () => { - const view = new InMemoryView(); + const view = new InMemoryView(); const proj = new MyProjection({ view }); expect(proj.view).to.equal(view); @@ -54,7 +58,10 @@ describe('AbstractProjection', function () { it('subscribes to all handlers defined', () => { - class ProjectionWithoutHandles extends AbstractProjection { + class ProjectionWithoutHandles extends AbstractProjection { + get schemaVersion(): string { + return 'v1'; + } somethingHappened() { } somethingHappened2() { } } @@ -68,7 +75,11 @@ describe('AbstractProjection', function () { it('ignores overridden projection methods', () => { - class ProjectionWithoutHandles extends AbstractProjection { + class ProjectionWithoutHandles extends AbstractProjection { + get schemaVersion(): string { + return 'v1'; + } + somethingHappened() { } /** overridden projection method */ diff --git a/tests/unit/CommandBus.test.ts b/tests/unit/CommandBus.test.ts index d763a40..3181ec3 100644 --- a/tests/unit/CommandBus.test.ts +++ b/tests/unit/CommandBus.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { InMemoryMessageBus } from '../../src/infrastructure/InMemoryMessageBus'; -import { CommandBus } from '../../src/CommandBus'; +import { InMemoryMessageBus, CommandBus } from '../../src'; describe('CommandBus', function () { diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index b0a2ef5..1983d57 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -1,10 +1,8 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; 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'; +import { InMemoryEventStorage, InMemorySnapshotStorage, InMemoryMessageBus } from '../../src'; +import { IAggregateSnapshotStorage, IEvent, IEventStorage, IEventStore, IMessageBus } from '../../src/interfaces'; const goodContext = { uid: '1', @@ -189,7 +187,7 @@ describe('EventStore', function () { describe('getNewId', () => { it('retrieves a unique ID for new aggregate from storage', () => Promise.resolve(es.getNewId()).then(id => { - expect(id).to.equal(1); + expect(id).to.equal('1'); })); }); diff --git a/tests/unit/InMemoryView.test.ts b/tests/unit/InMemoryView.test.ts index 7bdb6e7..d2b6e26 100644 --- a/tests/unit/InMemoryView.test.ts +++ b/tests/unit/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/infrastructure/memory/utils'; describe('InMemoryView', function () { From bb489d99085da04398d630f864aa94757247e130 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Oct 2024 17:05:13 +0100 Subject: [PATCH 002/135] Fix merge --- src/CommandBus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommandBus.ts b/src/CommandBus.ts index 56b08f2..e7cfbbf 100644 --- a/src/CommandBus.ts +++ b/src/CommandBus.ts @@ -1,4 +1,4 @@ -import { InMemoryMessageBus } from "./infrastructure/InMemoryMessageBus"; +import { InMemoryMessageBus } from "./infrastructure/memory"; import { ICommand, ICommandBus, From 79257e59d322df5dd8e41bedf5273c97ae77b609 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Oct 2024 18:14:56 +0100 Subject: [PATCH 003/135] Change: Remove `publishAsync` setting, simplify publishing sequence --- src/EventStore.ts | 94 +++++++++++------------------------ src/interfaces/IObservable.ts | 9 ++++ src/utils/CompoundEmitter.ts | 45 +++++++++++++++++ src/utils/index.ts | 8 +-- src/utils/isIObservable.ts | 7 +++ tests/unit/EventStore.test.ts | 23 +-------- 6 files changed, 95 insertions(+), 91 deletions(-) create mode 100644 src/utils/CompoundEmitter.ts create mode 100644 src/utils/isIObservable.ts diff --git a/src/EventStore.ts b/src/EventStore.ts index 4840523..80d4965 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -13,7 +13,12 @@ import { EventQueryAfter, EventQueryBefore } from "./interfaces"; -import { getClassName, setupOneTimeEmitterSubscription } from "./utils"; +import { + getClassName, + setupOneTimeEmitterSubscription, + isIObservable, + CompoundEmitter +} from "./utils"; import * as Event from './Event'; const isIEventStorage = (storage: IEventStorage): storage is IEventStorage => @@ -24,13 +29,6 @@ const isIEventStorage = (storage: IEventStorage): storage is IEventStorage => && 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) @@ -43,14 +41,13 @@ const SNAPSHOT_EVENT_TYPE = 'snapshot'; export class EventStore implements IEventStore { - #publishAsync: boolean; #validator: (event: IEvent) => void; #logger?: ILogger; #storage: IEventStorage; #messageBus?: IMessageBus; #snapshotStorage: IAggregateSnapshotStorage | undefined; #sagaStarters: string[] = []; - #defaultEventEmitter: IObservable; + #compoundEmitter: CompoundEmitter; /** Whether storage supports aggregate snapshots */ get snapshotsSupported(): boolean { @@ -62,16 +59,12 @@ export class EventStore implements IEventStore { messageBus, snapshotStorage, eventValidator = Event.validate, - eventStoreConfig, logger }: { storage: IEventStorage, messageBus?: IMessageBus, snapshotStorage?: IAggregateSnapshotStorage, eventValidator?: IMessageHandler, - eventStoreConfig?: { - publishAsync?: boolean - }, logger?: ILogger | IExtendableLogger }) { if (!storage) @@ -80,14 +73,7 @@ export class EventStore implements IEventStore { throw new TypeError('storage does not implement IEventStorage interface'); if (messageBus && !isIMessageBus(messageBus)) throw new TypeError('messageBus does not implement IMessageBus interface'); - if (messageBus && isIObservable(storage)) - throw new TypeError('both storage and messageBus implement IObservable interface, it is not yet supported'); - const defaultEventEmitter = isIObservable(storage) ? storage : messageBus; - if (!defaultEventEmitter) - throw new TypeError('storage must implement IObservable if messageBus is not injected'); - - this.#publishAsync = eventStoreConfig?.publishAsync ?? true; this.#validator = eventValidator; this.#logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : @@ -95,7 +81,7 @@ export class EventStore implements IEventStore { this.#storage = storage; this.#snapshotStorage = snapshotStorage; this.#messageBus = messageBus; - this.#defaultEventEmitter = defaultEventEmitter; + this.#compoundEmitter = new CompoundEmitter(messageBus, storage); } @@ -183,10 +169,10 @@ export class EventStore implements IEventStore { /** * 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) { + async commit(events: IEventSet): Promise { if (!Array.isArray(events)) throw new TypeError('events argument must be an Array'); @@ -195,12 +181,12 @@ export class EventStore implements IEventStore { await this.#attachSagaIdToSagaStarterEvents(events) : events; - const eventStreamWithoutSnapshots = await this.save(augmentedEvents); + const eventStreamWithoutSnapshots = await this.persistEventsAndSnapshots(augmentedEvents); // after events are saved to the persistent storage, // publish them to the event bus (i.e. RabbitMq) if (this.#messageBus) - await this.#publish(eventStreamWithoutSnapshots); + await this.publishEvents(eventStreamWithoutSnapshots); return eventStreamWithoutSnapshots; } @@ -225,8 +211,12 @@ export class EventStore implements IEventStore { return augmentedEvents; } - /** Save events to the persistent storage(s) */ - async save(events: IEventSet): Promise { + /** + * Save events and snapshots to the persistent storages + * + * @returns Event set without "snapshot" events + */ + protected async persistEventsAndSnapshots(events: IEventSet): Promise { if (!Array.isArray(events)) throw new TypeError('events argument must be an Array'); @@ -253,20 +243,11 @@ export class EventStore implements IEventStore { 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) { + protected async publishEvents(events: IEventSet) { if (!this.#messageBus) - return; + throw new Error('No messageBus injected, events cannot be published'); + + this.#logger?.debug(`publishing ${Event.describeMultiple(events)}...`); try { await Promise.all(events.map(event => @@ -282,41 +263,22 @@ export class EventStore implements IEventStore { } } - /** 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.#compoundEmitter.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.#compoundEmitter.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'); - - return this.#defaultEventEmitter.queue(name); + return this.#compoundEmitter.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.#compoundEmitter, subscribeTo, filter, handler, this.#logger); } } diff --git a/src/interfaces/IObservable.ts b/src/interfaces/IObservable.ts index 0fc311d..dc824fd 100644 --- a/src/interfaces/IObservable.ts +++ b/src/interfaces/IObservable.ts @@ -3,9 +3,18 @@ export interface IMessageHandler { }; export interface IObservable { + /** + * Setup a listener for a specific event type + */ on(type: string, handler: IMessageHandler): void; + /** + * Remove previously installed listener + */ off(type: string, handler: IMessageHandler): void; + /** + * Get or create a named queue, which delivers events to a single handler only + */ queue?(name: string): IObservable; } diff --git a/src/utils/CompoundEmitter.ts b/src/utils/CompoundEmitter.ts new file mode 100644 index 0000000..3e2c2a5 --- /dev/null +++ b/src/utils/CompoundEmitter.ts @@ -0,0 +1,45 @@ +import { IObservable, IMessageHandler } from "../interfaces"; +import { isIObservable } from "./isIObservable"; + +interface IObservableQueueProvider extends Required> { } + +const isObservableQueueProvider = (obj: any): obj is IObservableQueueProvider => + obj + && 'queue' in obj + && typeof obj.queue === 'function'; + +export class CompoundEmitter implements IObservable { + + #emitters: IObservable[]; + #queueProvider?: IObservableQueueProvider; + + constructor(...emitters: any[]) { + const observableEmitters = emitters.filter(isIObservable); + if (!observableEmitters.length) + throw new TypeError('none of the arguments implement IObservable interface'); + + const queueProviders = emitters.filter(isObservableQueueProvider); + if (queueProviders.length > 1) + throw new TypeError('more than one argument implements IObservable `queue` method'); + + this.#emitters = observableEmitters; + this.#queueProvider = queueProviders[0]; + } + + on(type: string, handler: IMessageHandler): void { + for (const emitter of this.#emitters) + emitter.on(type, handler); + } + + off(type: string, handler: IMessageHandler): void { + for (const emitter of this.#emitters) + emitter.off(type, handler); + } + + queue(name: string): IObservable { + if (!this.#queueProvider) + throw new Error('none of the emitters support named queues'); + + return this.#queueProvider.queue(name); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index d95765f..ccd8c88 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,10 @@ +export * from './CompoundEmitter'; export * from './getClassName'; +export * from './getHandledMessageTypes'; export * from './getHandler'; -export * from './validateHandlers'; export * from './getMessageHandlerNames'; -export * from './getHandledMessageTypes'; +export * from './isClass'; +export * from './isIObservable'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; -export * from './isClass'; +export * from './validateHandlers'; diff --git a/src/utils/isIObservable.ts b/src/utils/isIObservable.ts new file mode 100644 index 0000000..191627c --- /dev/null +++ b/src/utils/isIObservable.ts @@ -0,0 +1,7 @@ +import { IObservable } from "../interfaces"; + +export const isIObservable = (obj: IObservable | any): obj is IObservable => obj + && 'on' in obj + && typeof obj.on === 'function' + && 'off' in obj + && typeof obj.off === 'function'; diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index 1983d57..9816fe3 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -161,27 +161,6 @@ describe('EventStore', function () { 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); - }); }); describe('getNewId', () => { @@ -288,7 +267,7 @@ describe('EventStore', function () { it('sets up multiple handlers for same messageType, when queue name is not defined (Projections)', () => { - es = new EventStore({ storage, eventStoreConfig: { publishAsync: false }, messageBus }); + es = new EventStore({ storage, messageBus }); const projection1Handler = sinon.spy(); const projection2Handler = sinon.spy(); From 0a3bd6fdb090a427896d1d607ec314915e93c188 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Oct 2024 18:34:44 +0100 Subject: [PATCH 004/135] Fix tests --- examples/user-domain-tests/index.test.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/user-domain-tests/index.test.js b/examples/user-domain-tests/index.test.js index 6d9bb19..2b87755 100644 --- a/examples/user-domain-tests/index.test.js +++ b/examples/user-domain-tests/index.test.js @@ -55,8 +55,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 +64,7 @@ describe('user-domain example', () => { } }); - const userCreated = await eventStore.once('userCreated'); + const userCreated = await userCreatedPromise; const viewRecord = await users.get(userCreated.aggregateId); From c7a0c090f802a7e066766f0257619537eac68c53 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 17 Oct 2024 21:51:53 +0100 Subject: [PATCH 005/135] Make local events publishing sequential --- src/EventStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EventStore.ts b/src/EventStore.ts index 80d4965..4c30ca8 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -250,8 +250,8 @@ export class EventStore implements IEventStore { this.#logger?.debug(`publishing ${Event.describeMultiple(events)}...`); try { - await Promise.all(events.map(event => - this.#messageBus?.publish(event))); + for (const event of events) + this.#messageBus.publish(event); this.#logger?.debug(`${Event.describeMultiple(events)} published`); } From 412fd4775c624da2f2ba282d06d8bed732757c45 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 17 Oct 2024 21:52:43 +0100 Subject: [PATCH 006/135] Include tests/integration in "test:integration" npm script --- .editorconfig | 1 + package.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) 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/package.json b/package.json index add9116..eb6178f 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "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", + "test:integration": "jest --verbose tests/integration examples/user-domain-tests", "pretest:coveralls": "npm run test:coverage", "test:coveralls": "cat ./coverage/lcov.info | coveralls", "posttest:coveralls": "rm -rf ./coverage", @@ -65,4 +65,4 @@ "peerDependencies": { "better-sqlite3": "^11.3.0" } -} +} \ No newline at end of file From 80e9bd5007225db0149b89dceea59e7ba70ee3c9 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 24 Feb 2025 20:14:44 +0000 Subject: [PATCH 007/135] Fix merge --- src/infrastructure/memory/InMemoryView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/memory/InMemoryView.ts b/src/infrastructure/memory/InMemoryView.ts index f851c46..8ed5a91 100644 --- a/src/infrastructure/memory/InMemoryView.ts +++ b/src/infrastructure/memory/InMemoryView.ts @@ -23,7 +23,7 @@ export class InMemoryView implements IProjectionView, IObjectView = new Map(); + protected _map: Map = new Map(); #lock: InMemoryLock; @@ -98,7 +98,7 @@ export class InMemoryView implements IProjectionView, IObjectView Date: Mon, 24 Feb 2025 21:27:06 +0000 Subject: [PATCH 008/135] Fix failed test error reporting --- tests/integration/SqliteView.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/SqliteView.test.ts b/tests/integration/SqliteView.test.ts index b2ee415..1c73b05 100644 --- a/tests/integration/SqliteView.test.ts +++ b/tests/integration/SqliteView.test.ts @@ -70,7 +70,8 @@ describe.only('SqliteView', () => { }); afterEach(() => { - sqliteInMemoryDb.close(); + if (sqliteInMemoryDb) + sqliteInMemoryDb.close(); if (existsSync(fileName)) unlinkSync(fileName); }); From 5976ca309dbd1693af2d4dbcc2041c2e8f2e4749 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 6 Mar 2025 01:53:03 +0000 Subject: [PATCH 009/135] Refactoring --- src/AbstractProjection.ts | 8 +- src/AggregateCommandHandler.ts | 5 +- src/EventStore.ts | 84 ++++-------- src/SagaEventHandler.ts | 6 +- .../memory/InMemoryEventStorage.ts | 47 ++----- src/infrastructure/memory/InMemoryView.ts | 18 +-- src/interfaces/IEventStorage.ts | 9 +- src/interfaces/IEventStore.ts | 8 +- src/utils/index.ts | 3 + src/utils/isIEventStorage.ts | 8 ++ src/utils/isIMessageBus.ts | 9 ++ src/utils/iteratorToArray.ts | 6 + tests/unit/AbstractProjection.test.ts | 20 +-- tests/unit/AggregateCommandHandler.test.ts | 6 +- tests/unit/CqrsContainerBuilder.test.ts | 4 +- tests/unit/EventStore.test.ts | 58 ++++---- tests/unit/InMemoryEventStorage.test.ts | 127 ++++++++++++++++++ tests/unit/SagaEventHandler.test.ts | 8 +- 18 files changed, 262 insertions(+), 172 deletions(-) create mode 100644 src/utils/isIEventStorage.ts create mode 100644 src/utils/isIMessageBus.ts create mode 100644 src/utils/iteratorToArray.ts create mode 100644 tests/unit/InMemoryEventStorage.test.ts diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index de88f77..a05d3f5 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -152,8 +152,8 @@ export abstract class AbstractProjection { 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'); this._logger?.debug('retrieving last event projected'); @@ -164,9 +164,7 @@ export abstract class AbstractProjection - storage - && typeof storage.getNewId === 'function' - && typeof storage.commitEvents === 'function' - && typeof storage.getEvents === 'function' - && typeof storage.getAggregateEvents === 'function' - && typeof storage.getSagaEvents === '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'; export class EventStore implements IEventStore { @@ -44,7 +29,7 @@ export class EventStore implements IEventStore { #validator: (event: IEvent) => void; #logger?: ILogger; #storage: IEventStorage; - #messageBus?: IMessageBus; + #supplementaryEventBus?: IMessageBus; #snapshotStorage: IAggregateSnapshotStorage | undefined; #sagaStarters: string[] = []; #compoundEmitter: CompoundEmitter; @@ -56,13 +41,15 @@ export class EventStore implements IEventStore { constructor({ storage, - messageBus, + supplementaryEventBus, snapshotStorage, eventValidator = Event.validate, logger }: { storage: IEventStorage, - messageBus?: IMessageBus, + + /** Optional event dispatcher for publishing persisted events externally */ + supplementaryEventBus?: IMessageBus, snapshotStorage?: IAggregateSnapshotStorage, eventValidator?: IMessageHandler, logger?: ILogger | IExtendableLogger @@ -71,8 +58,8 @@ export class EventStore implements IEventStore { throw new TypeError('storage argument required'); if (!isIEventStorage(storage)) throw new TypeError('storage does not implement IEventStorage interface'); - if (messageBus && !isIMessageBus(messageBus)) - throw new TypeError('messageBus does not implement IMessageBus interface'); + if (supplementaryEventBus && !isIMessageBus(supplementaryEventBus)) + throw new TypeError('supplementaryEventBus does not implement IMessageBus interface'); this.#validator = eventValidator; this.#logger = logger && 'child' in logger ? @@ -80,8 +67,8 @@ export class EventStore implements IEventStore { logger; this.#storage = storage; this.#snapshotStorage = snapshotStorage; - this.#messageBus = messageBus; - this.#compoundEmitter = new CompoundEmitter(messageBus, storage); + this.#supplementaryEventBus = supplementaryEventBus; + this.#compoundEmitter = new CompoundEmitter(supplementaryEventBus, storage); } @@ -90,28 +77,21 @@ export class EventStore implements IEventStore { return this.#storage.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'); - - this.#logger?.debug(`retrieving ${eventTypes ? eventTypes.join(', ') : 'all'} events...`); - - const eventsIterable = await this.#storage.getEvents(eventTypes); - - yield* eventsIterable; + async* getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream { + if (!Array.isArray(eventTypes)) + throw new TypeError('eventTypes argument must be an Array'); - this.#logger?.debug(`${eventTypes ? eventTypes.join(', ') : 'all'} events retrieved`); - } + this.#logger?.debug(`retrieving ${eventTypes.join(', ')} events...`); - async* getEventsByTypes(eventTypes: Readonly, options: EventQueryAfter): IEventStream { const eventsIterable = await this.#storage.getEventsByTypes(eventTypes, options); yield* eventsIterable; + + this.#logger?.debug(`${eventTypes.join(', ')} events retrieved`); } /** Retrieve all events of specific Aggregate */ - async getAggregateEvents(aggregateId: string): Promise { + async* getAggregateEvents(aggregateId: string): IEventStream { if (!aggregateId) throw new TypeError('aggregateId argument required'); @@ -121,21 +101,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); - 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: string, filter: EventQueryBefore) { + async* getSagaEvents(sagaId: string, filter: EventQueryBefore) { if (!sagaId) throw new TypeError('sagaId argument required'); if (!filter) @@ -147,14 +124,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); - this.#logger?.debug(`${Event.describeMultiple(events)} retrieved`); + yield* eventsIterable; - return events; + this.#logger?.debug(`all events for saga ${sagaId} retrieved`); } /** @@ -185,7 +159,7 @@ export class EventStore implements IEventStore { // after events are saved to the persistent storage, // publish them to the event bus (i.e. RabbitMq) - if (this.#messageBus) + if (this.#supplementaryEventBus) await this.publishEvents(eventStreamWithoutSnapshots); return eventStreamWithoutSnapshots; @@ -244,14 +218,14 @@ export class EventStore implements IEventStore { } protected async publishEvents(events: IEventSet) { - if (!this.#messageBus) - throw new Error('No messageBus injected, events cannot be published'); + if (!this.#supplementaryEventBus) + throw new Error('No supplementaryEventBus injected, events cannot be published'); this.#logger?.debug(`publishing ${Event.describeMultiple(events)}...`); try { for (const event of events) - this.#messageBus.publish(event); + this.#supplementaryEventBus.publish(event); this.#logger?.debug(`${Event.describeMultiple(events)} published`); } diff --git a/src/SagaEventHandler.ts b/src/SagaEventHandler.ts index b66c639..6b538b3 100644 --- a/src/SagaEventHandler.ts +++ b/src/SagaEventHandler.ts @@ -14,7 +14,8 @@ import { import { subscribe, - getClassName + getClassName, + iteratorToArray } from './utils'; /** @@ -151,7 +152,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/infrastructure/memory/InMemoryEventStorage.ts b/src/infrastructure/memory/InMemoryEventStorage.ts index 4ddce9b..c78dc36 100644 --- a/src/infrastructure/memory/InMemoryEventStorage.ts +++ b/src/infrastructure/memory/InMemoryEventStorage.ts @@ -7,9 +7,6 @@ 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; @@ -25,11 +22,11 @@ export class InMemoryEventStorage implements IEventStorage { return events; } - async getAggregateEvents(aggregateId, options?: { snapshot: IEvent }): Promise { + async *getAggregateEvents(aggregateId, options?: { snapshot: IEvent }): IEventStream { await nextCycle(); const afterVersion = options?.snapshot?.aggregateVersion; - const result = !afterVersion ? + const results = !afterVersion ? this.#events.filter(e => e.aggregateId == aggregateId) : this.#events.filter(e => e.aggregateId == aggregateId && @@ -38,10 +35,10 @@ export class InMemoryEventStorage implements IEventStorage { await nextCycle(); - return result; + yield* results; } - async getSagaEvents(sagaId, { beforeEvent }): Promise { + async *getSagaEvents(sagaId, { beforeEvent }): IEventStream { await nextCycle(); const results = this.#events.filter(e => @@ -51,39 +48,21 @@ export class InMemoryEventStorage implements IEventStorage { 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; - } + yield* results; } async* getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream { await nextCycle(); - let newEvents: IEventSet; - if (options?.afterEvent) { - const lastEventId = options.afterEvent.id; - if (!lastEventId) - throw new TypeError('options.afterEvent.id is required'); - - const lastEventIndex = this.#events.findIndex(e => e.id === lastEventId); - if (!lastEventIndex) - throw new TypeError(`Event "${lastEventId}" could not be found`); - - newEvents = this.#events.slice(lastEventIndex + 1); - } - else { - newEvents = this.#events; - } + const lastEventId = options?.afterEvent?.id; + if (options?.afterEvent && !lastEventId) + throw new TypeError('options.afterEvent.id is required'); - for await (const event of newEvents) { - if (!eventTypes || eventTypes.includes(event.type)) + let offsetFound = !lastEventId; + for (const event of this.#events) { + if (!offsetFound) + offsetFound = event.id === lastEventId; + else if (!eventTypes || eventTypes.includes(event.type)) yield event; } } diff --git a/src/infrastructure/memory/InMemoryView.ts b/src/infrastructure/memory/InMemoryView.ts index 8ed5a91..47ede46 100644 --- a/src/infrastructure/memory/InMemoryView.ts +++ b/src/infrastructure/memory/InMemoryView.ts @@ -27,8 +27,6 @@ export class InMemoryView implements IProjectionView, IObjectView implements IProjectionView, IObjectView implements IProjectionView, IObjectView implements IProjectionView, IObjectView implements IProjectionView, IObjectView; - getEvents(eventTypes?: Readonly): IEventStream; + getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; - getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): - IEventStream; + getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): Promise | IEventStream; - getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): Promise; - - getSagaEvents(sagaId: string, options: EventQueryBefore): Promise; + getSagaEvents(sagaId: string, options: EventQueryBefore): Promise | IEventStream; } diff --git a/src/interfaces/IEventStore.ts b/src/interfaces/IEventStore.ts index 6ca763d..bba8c38 100644 --- a/src/interfaces/IEventStore.ts +++ b/src/interfaces/IEventStore.ts @@ -11,13 +11,11 @@ export interface IEventStore extends IObservable { commit(events: IEventSet): Promise; - getAllEvents(eventTypes?: Readonly): IEventStream; + getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; - getEventsByTypes(eventTypes: Readonly, options: EventQueryAfter): IEventStream; + getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): IEventStream; - getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): Promise; - - getSagaEvents(sagaId: string, options: EventQueryBefore): Promise; + getSagaEvents(sagaId: string, options: EventQueryBefore): IEventStream; once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise; diff --git a/src/utils/index.ts b/src/utils/index.ts index ccd8c88..848eb58 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,7 +4,10 @@ export * from './getHandledMessageTypes'; export * from './getHandler'; export * from './getMessageHandlerNames'; export * from './isClass'; +export * from './isIEventStorage'; +export * from './isIMessageBus'; export * from './isIObservable'; +export * from './iteratorToArray'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; export * from './validateHandlers'; diff --git a/src/utils/isIEventStorage.ts b/src/utils/isIEventStorage.ts new file mode 100644 index 0000000..bbe7cf2 --- /dev/null +++ b/src/utils/isIEventStorage.ts @@ -0,0 +1,8 @@ +import { IEventStorage } from "../interfaces"; + +export const isIEventStorage = (storage: IEventStorage): storage is IEventStorage => storage + && typeof storage.getNewId === 'function' + && typeof storage.commitEvents === 'function' + && typeof storage.getEventsByTypes === 'function' + && typeof storage.getAggregateEvents === 'function' + && typeof storage.getSagaEvents === 'function'; diff --git a/src/utils/isIMessageBus.ts b/src/utils/isIMessageBus.ts new file mode 100644 index 0000000..4a47228 --- /dev/null +++ b/src/utils/isIMessageBus.ts @@ -0,0 +1,9 @@ +import { IMessageBus } from "../interfaces"; +import { isIObservable } from "."; + +export 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'; 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/tests/unit/AbstractProjection.test.ts b/tests/unit/AbstractProjection.test.ts index 04ef51d..a4945c5 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -48,7 +48,7 @@ describe('AbstractProjection', function () { beforeEach(() => { observable = { - getAllEvents() { + getEventsByTypes() { return []; }, on() { } @@ -96,7 +96,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']; } @@ -117,24 +117,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); }); @@ -154,7 +154,7 @@ describe('AbstractProjection', function () { it('throws, if projection error encountered', () => { es = { - async* getAllEvents() { + async* getEventsByTypes() { yield { type: 'unexpectedEvent' }; } }; @@ -174,8 +174,8 @@ 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 supplementaryEventBus = new InMemoryMessageBus(); + const es = new EventStore({ storage, supplementaryEventBus }); let restored = false; let projected = false; diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index 28698e0..e057c44 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -48,18 +48,18 @@ describe('AggregateCommandHandler', function () { let snapshotStorage: InMemorySnapshotStorage; let eventStore: IEventStore; let commandBus: ICommandBus; - let messageBus: IMessageBus; + let supplementaryEventBus: IMessageBus; let onSpy; let getNewIdSpy; let getAggregateEventsSpy; let commitSpy; beforeEach(() => { - messageBus = new InMemoryMessageBus(); + supplementaryEventBus = new InMemoryMessageBus(); storage = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); - eventStore = new EventStore({ storage, snapshotStorage, messageBus }); + eventStore = new EventStore({ storage, snapshotStorage, supplementaryEventBus }); getNewIdSpy = sinon.spy(eventStore, 'getNewId'); getAggregateEventsSpy = sinon.spy(eventStore, 'getAggregateEvents'); commitSpy = sinon.spy(eventStore, 'commit'); diff --git a/tests/unit/CqrsContainerBuilder.test.ts b/tests/unit/CqrsContainerBuilder.test.ts index ca3b63b..02d2047 100644 --- a/tests/unit/CqrsContainerBuilder.test.ts +++ b/tests/unit/CqrsContainerBuilder.test.ts @@ -16,7 +16,7 @@ describe('CqrsContainerBuilder', function () { beforeEach(() => { builder = new ContainerBuilder(); builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('messageBus'); + builder.register(InMemoryMessageBus).as('supplementaryEventBus'); }); describe('registerAggregate(aggregateType) extension', () => { @@ -87,7 +87,7 @@ describe('CqrsContainerBuilder', function () { describe('registerProjection(typeOrFactory, exposedViewName) extension', () => { - class MyProjection extends AbstractProjection { + class MyProjection extends AbstractProjection { static get handles() { return ['somethingHappened']; } diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index 9816fe3..5113df0 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -3,6 +3,7 @@ import * as sinon from 'sinon'; import { EventStore } from '../../src/EventStore'; import { InMemoryEventStorage, InMemorySnapshotStorage, InMemoryMessageBus } from '../../src'; import { IAggregateSnapshotStorage, IEvent, IEventStorage, IEventStore, IMessageBus } from '../../src/interfaces'; +import { iteratorToArray } from '../../src/utils'; const goodContext = { uid: '1', @@ -38,13 +39,13 @@ describe('EventStore', function () { let es: IEventStore; let storage: IEventStorage; let snapshotStorage: IAggregateSnapshotStorage; - let messageBus: IMessageBus; + let supplementaryEventBus: IMessageBus; beforeEach(() => { storage = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); - messageBus = new InMemoryMessageBus(); - es = new EventStore({ storage, snapshotStorage, messageBus }); + supplementaryEventBus = new InMemoryMessageBus(); + es = new EventStore({ storage, snapshotStorage, supplementaryEventBus }); }); describe('validator', () => { @@ -52,7 +53,7 @@ describe('EventStore', function () { it('allows to validate events before they are committed', () => { const events = [ - { type: 'somethingHappened', aggregateId: 1 } + { type: 'somethingHappened', aggregateId: '1' } ]; return es.commit(events).then(() => { @@ -62,7 +63,7 @@ describe('EventStore', function () { eventValidator: event => { throw new Error('test validation error'); }, - messageBus + supplementaryEventBus }); return es.commit(events).then(() => { @@ -97,7 +98,7 @@ describe('EventStore', function () { await es.commit([goodEvent]); const events: IEvent[] = []; - for await (const e of es.getAllEvents()) + for await (const e of es.getEventsByTypes(['somethingHappened'], {})) events.push(e); expect(events[0]).to.have.property('type', 'somethingHappened'); @@ -152,7 +153,7 @@ describe('EventStore', function () { } }); - es = new EventStore({ storage, messageBus }); + es = new EventStore({ storage, supplementaryEventBus }); return es.commit([goodEvent, goodEvent2]).then(() => { throw new Error('should fail'); @@ -176,11 +177,12 @@ describe('EventStore', function () { await es.commit([goodEvent, goodEvent2]); - const events = await es.getAggregateEvents(goodEvent.aggregateId); + const events = 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'); + expect(events).to.be.have.property(Symbol.asyncIterator); + + const event = (await events.next()).value; + expect(event).to.have.nested.property('type', 'somethingHappened'); }); it('tries to retrieve aggregate snapshot', async () => { @@ -192,7 +194,7 @@ describe('EventStore', function () { expect(es).to.have.property('snapshotsSupported', true); - const events = await es.getAggregateEvents(goodEvent2.aggregateId); + const events = await iteratorToArray(es.getAggregateEvents(goodEvent2.aggregateId)); expect(snapshotStorage).to.have.nested.property('getAggregateSnapshot.calledOnce', true); expect(storage).to.have.nested.property('getAggregateEvents.calledOnce', true); @@ -208,33 +210,33 @@ describe('EventStore', function () { describe('getSagaEvents(sagaId, options)', () => { - it('returns events committed by saga prior to event that triggered saga execution', () => { + it('returns events committed by saga prior to event that triggered saga execution', async () => { const events = [ - { sagaId: 1, sagaVersion: 1, type: 'somethingHappened' }, - { sagaId: 1, sagaVersion: 2, type: 'anotherHappened' }, - { sagaId: 2, sagaVersion: 1, type: 'somethingHappened' } + { sagaId: '1', sagaVersion: 1, type: 'somethingHappened' }, + { sagaId: '1', sagaVersion: 2, type: 'anotherHappened' }, + { sagaId: '2', sagaVersion: 1, type: 'somethingHappened' } ]; const triggeredBy = events[1]; - return es.commit(events).then(() => es.getSagaEvents(1, { beforeEvent: triggeredBy }).then(events => { + await es.commit(events); - expect(events).to.be.an('Array'); - expect(events).to.have.length(1); - expect(events).to.have.nested.property('[0].type', 'somethingHappened'); - })); + const ii = es.getSagaEvents('1', { beforeEvent: triggeredBy }); + const retrievedEvents = await iteratorToArray(ii); + + expect(retrievedEvents).to.be.an('Array'); + expect(retrievedEvents).to.have.length(1); + expect(retrievedEvents).to.have.nested.property('[0].type', 'somethingHappened'); }); }); - describe('getAllEvents(eventTypes)', () => { + describe('getEventsByTypes(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); + const events = await iteratorToArray(es.getEventsByTypes(['somethingHappened'], {})); expect(events).to.have.length(2); expect(events).to.have.nested.property('[0].aggregateId', '1'); @@ -250,7 +252,7 @@ describe('EventStore', function () { it('fails, when trying to set up second messageType handler within the same node and named queue (Receptors)', () => { - es = new EventStore({ storage, messageBus }); + es = new EventStore({ storage, supplementaryEventBus }); expect(() => { es.queue('namedQueue').on('somethingHappened', () => { }); @@ -267,7 +269,7 @@ describe('EventStore', function () { it('sets up multiple handlers for same messageType, when queue name is not defined (Projections)', () => { - es = new EventStore({ storage, messageBus }); + es = new EventStore({ storage, supplementaryEventBus }); const projection1Handler = sinon.spy(); const projection2Handler = sinon.spy(); @@ -276,7 +278,7 @@ describe('EventStore', function () { es.on('somethingHappened', projection2Handler); return es.commit([ - { type: 'somethingHappened', aggregateId: 1, aggregateVersion: 0 } + { type: 'somethingHappened', aggregateId: '1', aggregateVersion: 0 } ]).then(() => { expect(projection1Handler).to.have.property('calledOnce', true); expect(projection2Handler).to.have.property('calledOnce', true); diff --git a/tests/unit/InMemoryEventStorage.test.ts b/tests/unit/InMemoryEventStorage.test.ts new file mode 100644 index 0000000..fe25589 --- /dev/null +++ b/tests/unit/InMemoryEventStorage.test.ts @@ -0,0 +1,127 @@ +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/SagaEventHandler.test.ts b/tests/unit/SagaEventHandler.test.ts index 83c0f02..5e13ece 100644 --- a/tests/unit/SagaEventHandler.test.ts +++ b/tests/unit/SagaEventHandler.test.ts @@ -22,7 +22,7 @@ class Saga extends AbstractSaga { } followingHappened() { super.enqueue('complete', undefined, { foo: 'bar' }); - } + } onError(error, { command, event }) { super.enqueue('fixError', undefined, { error, command, event }); } @@ -42,9 +42,9 @@ describe('SagaEventHandler', function () { let sagaEventHandler: SagaEventHandler; beforeEach(() => { - const messageBus = new InMemoryMessageBus(); - commandBus = new CommandBus({ messageBus }); - eventStore = new EventStore({ storage: new InMemoryEventStorage(), messageBus }); + const supplementaryEventBus = new InMemoryMessageBus(); + commandBus = new CommandBus({}); + eventStore = new EventStore({ storage: new InMemoryEventStorage(), supplementaryEventBus }); sagaEventHandler = new SagaEventHandler({ sagaType: Saga, eventStore, commandBus }); }); From e45f6ff588e467fb3c37b9fac1579a873a951209 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 14 Mar 2025 00:16:12 +0000 Subject: [PATCH 010/135] Minor refactoring --- src/AbstractProjection.ts | 9 +++++---- src/CqrsContainerBuilder.ts | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index a05d3f5..157dad8 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -155,11 +155,12 @@ export abstract class AbstractProjection Date: Fri, 14 Mar 2025 01:08:06 +0000 Subject: [PATCH 011/135] Revert removal of Identifier type --- src/AbstractAggregate.ts | 5 +++-- src/AbstractSaga.ts | 8 ++++---- src/AggregateCommandHandler.ts | 3 ++- src/EventStore.ts | 9 +++++---- .../memory/InMemorySnapshotStorage.ts | 5 ++--- src/infrastructure/memory/InMemoryView.ts | 18 +++++++++--------- src/interfaces/IAggregate.ts | 5 +++-- src/interfaces/IAggregateSnapshotStorage.ts | 3 ++- src/interfaces/IEventStorage.ts | 7 ++++--- src/interfaces/IEventStore.ts | 7 ++++--- src/interfaces/IMessage.ts | 6 ++++-- src/interfaces/ISaga.ts | 5 +++-- src/interfaces/Identifier.ts | 1 + src/interfaces/index.ts | 1 + 14 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 src/interfaces/Identifier.ts diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index 8587a77..a7da6ae 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -2,6 +2,7 @@ import { IAggregate, IMutableAggregateState, ICommand, + Identifier, IEvent, IEventSet, IAggregateConstructorParams @@ -36,7 +37,7 @@ export abstract class AbstractAggregate { + async #restoreAggregate(id: Identifier): Promise { if (!id) throw new TypeError('id argument required'); diff --git a/src/EventStore.ts b/src/EventStore.ts index f8c2b98..99d596a 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -11,7 +11,8 @@ import { IEventStream, IEventStore, EventQueryAfter, - EventQueryBefore + EventQueryBefore, + Identifier } from "./interfaces"; import { getClassName, @@ -73,7 +74,7 @@ export class EventStore implements IEventStore { /** Retrieve new ID from the storage */ - async getNewId(): Promise { + async getNewId(): Promise { return this.#storage.getNewId(); } @@ -91,7 +92,7 @@ export class EventStore implements IEventStore { } /** Retrieve all events of specific Aggregate */ - async* getAggregateEvents(aggregateId: string): IEventStream { + async* getAggregateEvents(aggregateId: Identifier): IEventStream { if (!aggregateId) throw new TypeError('aggregateId argument required'); @@ -112,7 +113,7 @@ export class EventStore implements IEventStore { } /** Retrieve events of specific Saga */ - async* getSagaEvents(sagaId: string, filter: EventQueryBefore) { + async* getSagaEvents(sagaId: Identifier, filter: EventQueryBefore) { if (!sagaId) throw new TypeError('sagaId argument required'); if (!filter) diff --git a/src/infrastructure/memory/InMemorySnapshotStorage.ts b/src/infrastructure/memory/InMemorySnapshotStorage.ts index a306535..7943217 100644 --- a/src/infrastructure/memory/InMemorySnapshotStorage.ts +++ b/src/infrastructure/memory/InMemorySnapshotStorage.ts @@ -1,5 +1,4 @@ -import { IAggregateSnapshotStorage } from "../../interfaces/IAggregateSnapshotStorage"; -import { IEvent } from "../../interfaces/IEvent"; +import { IAggregateSnapshotStorage, Identifier, IEvent } from "../../interfaces"; /** * In-memory storage for aggregate snapshots. @@ -7,7 +6,7 @@ import { IEvent } from "../../interfaces/IEvent"; */ export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { - #snapshots: Map = new Map(); + #snapshots: Map = new Map(); /** * Get latest aggregate snapshot diff --git a/src/infrastructure/memory/InMemoryView.ts b/src/infrastructure/memory/InMemoryView.ts index 47ede46..42a8ff5 100644 --- a/src/infrastructure/memory/InMemoryView.ts +++ b/src/infrastructure/memory/InMemoryView.ts @@ -1,7 +1,7 @@ import { InMemoryLock } from './InMemoryLock'; +import { IProjectionView, Identifier } from "../../interfaces"; import { nextCycle } from './utils'; import { IObjectView } from '../../interfaces/IObjectView'; -import { IProjectionView } from '../../interfaces/IProjectionView'; /** * Update given value with an update Cb and return updated value. @@ -23,7 +23,7 @@ export class InMemoryView implements IProjectionView, IObjectView = new Map(); + protected _map: Map = new Map(); #lock: InMemoryLock; @@ -76,7 +76,7 @@ export class InMemoryView implements IProjectionView, IObjectView { + async get(key: Identifier, options?: { nowait?: boolean }): Promise { if (!key) throw new TypeError('key argument required'); @@ -91,7 +91,7 @@ export class InMemoryView implements IProjectionView, IObjectView implements IProjectionView, IObjectView boolean): - Promise> { + async getAll(filter?: (r: TRecord | undefined, i: Identifier) => boolean): + Promise> { if (filter && typeof filter !== 'function') throw new TypeError('filter argument, when defined, must be a Function'); @@ -109,7 +109,7 @@ export class InMemoryView implements IProjectionView, IObjectView = []; + const r: Array<[Identifier, TRecord | undefined]> = []; for (const entry of this._map.entries()) { if (!filter || filter(entry[1], entry[0])) r.push(entry); @@ -171,7 +171,7 @@ export class InMemoryView implements IProjectionView, IObjectView TRecord) { + private async _update(key: Identifier, update: (r?: TRecord) => TRecord) { const value = this._map.get(key); const updatedValue = applyUpdate(value, update); if (updatedValue === undefined) @@ -181,7 +181,7 @@ export class InMemoryView implements IProjectionView, IObjectView; @@ -36,7 +37,7 @@ export interface IMutableAggregateState { export type IAggregateConstructorParams = { /** Unique aggregate identifier */ - id: string, + id: Identifier, /** Aggregate events, logged after latest snapshot */ events?: IEventSet, diff --git a/src/interfaces/IAggregateSnapshotStorage.ts b/src/interfaces/IAggregateSnapshotStorage.ts index 10064e0..41c293d 100644 --- a/src/interfaces/IAggregateSnapshotStorage.ts +++ b/src/interfaces/IAggregateSnapshotStorage.ts @@ -1,7 +1,8 @@ +import { Identifier } from "./Identifier"; import { IEvent } from "./IEvent"; export interface IAggregateSnapshotStorage { - getAggregateSnapshot(aggregateId: string): Promise | undefined> | IEvent | undefined; + getAggregateSnapshot(aggregateId: Identifier): Promise | undefined> | IEvent | undefined; saveAggregateSnapshot(snapshotEvent: IEvent): Promise | void; } diff --git a/src/interfaces/IEventStorage.ts b/src/interfaces/IEventStorage.ts index 5bafb34..cbd1cfe 100644 --- a/src/interfaces/IEventStorage.ts +++ b/src/interfaces/IEventStorage.ts @@ -1,3 +1,4 @@ +import { Identifier } from "./Identifier"; import { IEvent } from "./IEvent"; import { IEventSet } from "./IEventSet"; import { IEventStream } from "./IEventStream"; @@ -16,13 +17,13 @@ export interface IEventStorage { /** * Create unique identifier */ - getNewId(): string | Promise; + getNewId(): Identifier | Promise; commitEvents(events: IEventSet): Promise; getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; - getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): Promise | IEventStream; + getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): Promise | IEventStream; - getSagaEvents(sagaId: string, options: EventQueryBefore): Promise | IEventStream; + getSagaEvents(sagaId: Identifier, options: EventQueryBefore): Promise | IEventStream; } diff --git a/src/interfaces/IEventStore.ts b/src/interfaces/IEventStore.ts index bba8c38..21954f6 100644 --- a/src/interfaces/IEventStore.ts +++ b/src/interfaces/IEventStore.ts @@ -1,3 +1,4 @@ +import { Identifier } from "./Identifier"; import { IEvent } from "./IEvent"; import { IEventSet } from "./IEventSet"; import { EventQueryAfter, EventQueryBefore } from "./IEventStorage"; @@ -7,15 +8,15 @@ import { IMessageHandler, IObservable } from "./IObservable"; export interface IEventStore extends IObservable { readonly snapshotsSupported?: boolean; - getNewId(): string | Promise; + getNewId(): Identifier | Promise; commit(events: IEventSet): Promise; getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; - getAggregateEvents(aggregateId: string, options?: { snapshot?: IEvent }): IEventStream; + getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): IEventStream; - getSagaEvents(sagaId: string, options: EventQueryBefore): IEventStream; + getSagaEvents(sagaId: Identifier, options: EventQueryBefore): IEventStream; once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise; diff --git a/src/interfaces/IMessage.ts b/src/interfaces/IMessage.ts index 448e8aa..c40dc95 100644 --- a/src/interfaces/IMessage.ts +++ b/src/interfaces/IMessage.ts @@ -1,11 +1,13 @@ +import { Identifier } from "./Identifier"; + export interface IMessage { /** Event or command type */ type: string; - aggregateId?: string; + aggregateId?: Identifier; aggregateVersion?: number; - sagaId?: string; + sagaId?: Identifier; sagaVersion?: number; payload?: TPayload; diff --git a/src/interfaces/ISaga.ts b/src/interfaces/ISaga.ts index a4e585c..8507ac1 100644 --- a/src/interfaces/ISaga.ts +++ b/src/interfaces/ISaga.ts @@ -1,10 +1,11 @@ import { ICommand } from "./ICommand"; +import { Identifier } from "./Identifier"; import { IEvent } from "./IEvent"; import { IEventSet } from "./IEventSet"; export interface ISaga { /** Unique Saga ID */ - readonly id: string; + readonly id: Identifier; /** List of commands emitted by Saga */ readonly uncommittedMessages: ICommand[]; @@ -19,7 +20,7 @@ export interface ISaga { } export type ISagaConstructorParams = { - id: string, + id: Identifier, events?: IEventSet }; 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 index 67b8da1..ab97393 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -2,6 +2,7 @@ export * from './IAggregate'; export * from './IAggregateSnapshotStorage'; export * from './ICommand'; export * from './ICommandBus'; +export * from './Identifier'; export * from './IEvent'; export * from './IEventReceptor'; export * from './IEventSet'; From 72b6304dbb50909e385dede7d91307d76f725486 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 14 Mar 2025 01:11:24 +0000 Subject: [PATCH 012/135] Fix vulnerabilities in dev dependencies --- package-lock.json | 664 ++++------------------------------------------ package.json | 4 - 2 files changed, 53 insertions(+), 615 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3371379..603dbe4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@types/sinon": "^10.0.20", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "coveralls": "^3.1.1", "jest": "^29.7.0", "sinon": "^15.2.0", "ts-jest": "^29.2.4", @@ -43,12 +42,14 @@ } }, "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.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -194,19 +195,21 @@ } }, "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.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "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.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -221,111 +224,27 @@ } }, "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==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "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==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "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.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -512,14 +431,15 @@ } }, "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.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "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.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -544,14 +464,14 @@ } }, "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.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "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.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1214,22 +1134,6 @@ "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", "dev": true }, - "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, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -1312,24 +1216,6 @@ "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", @@ -1345,27 +1231,6 @@ "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/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/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1479,15 +1344,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1624,12 +1480,6 @@ } ] }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true - }, "node_modules/chai": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", @@ -1751,18 +1601,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -2015,25 +1853,6 @@ "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==", - "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" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2062,10 +1881,11 @@ "dev": true }, "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", @@ -2084,18 +1904,6 @@ "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", @@ -2191,15 +1999,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2244,16 +2043,6 @@ "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==", - "dev": true, - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2381,27 +2170,6 @@ "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 - }, "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", @@ -2472,29 +2240,6 @@ "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_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==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2630,15 +2375,6 @@ "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", @@ -2753,29 +2489,6 @@ "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", @@ -2824,21 +2537,6 @@ "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" - } - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -2998,12 +2696,6 @@ "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", @@ -3016,12 +2708,6 @@ "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 - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -3730,7 +3416,8 @@ "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", @@ -3745,12 +3432,6 @@ "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", @@ -3775,18 +3456,6 @@ "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 - }, - "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 - }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3830,21 +3499,6 @@ "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", @@ -3869,15 +3523,6 @@ "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", @@ -3975,15 +3620,6 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, - "node_modules/log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", - "dev": true, - "engines": { - "node": ">=0.8.6" - } - }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -4180,10 +3816,11 @@ "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==", + "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": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4192,27 +3829,6 @@ "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" - } - }, - "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==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4375,15 +3991,6 @@ "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", @@ -4511,10 +4118,11 @@ "dev": true }, "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": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", @@ -4546,12 +4154,6 @@ "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", @@ -4645,21 +4247,6 @@ "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/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -4687,15 +4274,6 @@ "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==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -4859,38 +4437,6 @@ "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", @@ -4967,12 +4513,6 @@ } ] }, - "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 - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5121,31 +4661,6 @@ "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" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -5316,15 +4831,6 @@ "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" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5337,19 +4843,6 @@ "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", @@ -5480,24 +4973,6 @@ "node": ">=0.3.1" } }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "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-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", @@ -5581,31 +5056,12 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.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" - } - }, "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", @@ -5636,20 +5092,6 @@ "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", diff --git a/package.json b/package.json index fa40998..3247729 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,6 @@ "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", @@ -52,7 +49,6 @@ "@types/sinon": "^10.0.20", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "coveralls": "^3.1.1", "jest": "^29.7.0", "sinon": "^15.2.0", "ts-jest": "^29.2.4", From 7babd038f068b280e674d42a3f54bed29b056613 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 14 Mar 2025 01:12:18 +0000 Subject: [PATCH 013/135] Update github actions --- .github/workflows/audit.yml | 6 +++--- .github/workflows/coveralls.yml | 6 +++--- .github/workflows/publish.yml | 6 +++--- .github/workflows/tests.yml | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 3df82f2..9ad2354 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm audit --parseable --production --audit-level=moderate diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml index f092473..413b288 100644 --- a/.github/workflows/coveralls.yml +++ b/.github/workflows/coveralls.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20.x] + node-version: [22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci --no-optional diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 058a839..2cee9cd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,12 +11,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' registry-url: 'https://registry.npmjs.org' - name: Install dependencies diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b35bb3..2fe7111 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,9 +13,9 @@ jobs: matrix: node-version: [18.x, 20.x, 22.x] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci --no-optional From 9878c3acd2484c34d43b831ed40954559eec4971 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 16 Mar 2025 02:24:13 +0000 Subject: [PATCH 014/135] Refactoring --- package-lock.json | 42 +++++++- package.json | 5 +- src/index.ts | 8 +- src/infrastructure/memory/index.ts | 1 + .../sqlite/AbstractSqliteView.ts | 101 +++++++++++------- src/infrastructure/sqlite/ObjectSqliteView.ts | 10 +- src/infrastructure/sqlite/index.ts | 1 + src/infrastructure/sqlite/utils/getEventId.ts | 8 ++ src/infrastructure/sqlite/utils/guid.ts | 4 + src/infrastructure/sqlite/utils/index.ts | 2 + 10 files changed, 129 insertions(+), 53 deletions(-) create mode 100644 src/infrastructure/sqlite/utils/getEventId.ts create mode 100644 src/infrastructure/sqlite/utils/guid.ts create mode 100644 src/infrastructure/sqlite/utils/index.ts diff --git a/package-lock.json b/package-lock.json index ce0c394..b445f61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,7 +32,8 @@ "node": ">=10.3.0" }, "peerDependencies": { - "better-sqlite3": "^11.3.0" + "better-sqlite3": "^11.3.0", + "md5": "^2.3.0" } }, "node_modules/@ampproject/remapping": { @@ -1945,6 +1946,16 @@ "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", @@ -2386,6 +2397,16 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", @@ -3398,6 +3419,13 @@ "dev": true, "license": "MIT" }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT", + "peer": true + }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -4630,6 +4658,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", diff --git a/package.json b/package.json index 5f8d411..e190ace 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "uuid": "^10.0.0" }, "peerDependencies": { - "better-sqlite3": "^11.3.0" + "better-sqlite3": "^11.3.0", + "md5": "^2.3.0" } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 9bbea12..31360d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,8 @@ export * from './AbstractSaga'; export * from './SagaEventHandler'; export * from './AbstractProjection'; -export * from './infrastructure/memory/InMemoryMessageBus'; -export * from './infrastructure/memory/InMemoryEventStorage'; -export * from './infrastructure/memory/InMemorySnapshotStorage'; -export * from './infrastructure/memory/InMemoryView'; -export * from './infrastructure/memory/InMemoryLock'; -export * from './infrastructure/memory/utils/Deferred'; +export * from './infrastructure/memory'; +export * as SQLite from './infrastructure/sqlite'; export * as Event from './Event'; export { diff --git a/src/infrastructure/memory/index.ts b/src/infrastructure/memory/index.ts index 3ef457a..3f6f779 100644 --- a/src/infrastructure/memory/index.ts +++ b/src/infrastructure/memory/index.ts @@ -3,3 +3,4 @@ export * from './InMemoryLock'; export * from './InMemoryMessageBus'; export * from './InMemorySnapshotStorage'; export * from './InMemoryView'; +export * from './utils/Deferred'; diff --git a/src/infrastructure/sqlite/AbstractSqliteView.ts b/src/infrastructure/sqlite/AbstractSqliteView.ts index 8d62c79..ff2f022 100644 --- a/src/infrastructure/sqlite/AbstractSqliteView.ts +++ b/src/infrastructure/sqlite/AbstractSqliteView.ts @@ -1,58 +1,73 @@ import { IEvent } from '../../interfaces/IEvent'; import { IExtendableLogger, ILogger } from '../../interfaces/ILogger'; import { IPersistentView } from '../../interfaces/IPersistentView'; - -const guid = (str: string) => Buffer.from(str.replaceAll('-', ''), 'hex'); - -const EVENT_PROCESSING_LOCK_TTL = 15; // sec +import { getEventId } from './utils'; +import { Database, Statement } from 'better-sqlite3'; export type AbstractSqliteViewOptions = { - schemaVersion: string; - sqliteDb: import('better-sqlite3').Database; + schemaVersion?: string; + sqliteDb: Database; viewLockTableName?: string; logger?: IExtendableLogger | ILogger; + eventProcessingLockTtl?: number; + viewRestoringLockTtl?: number; } export abstract class AbstractSqliteView implements IPersistentView { - /** - * Version of the the schema representing the structure of the data stored in the view - */ - readonly schemaVersion: string; + #schemaVersion: string | undefined; + #viewLockTableName: string | undefined; + #getLastEventQuery: Statement; + #lockEventQuery: Statement<[Buffer], void>; + #finalizeEventLockQuery: Statement<[Buffer], void>; + #recordLastEventQuery: Statement<[string, string, string], void>; + #upsertTableLockQuery: Statement<[string, string], void>; + #removeTableLockQuery: Statement<[string, string], void>; + #eventProcessingLockTtl: number; + #viewRestoringLockTtl: number; + + protected db: Database; + protected logger: ILogger | undefined; /** - * Shared table where view locks and last projected events are tracked + * Shared table tracking view locks and last projected events. + * Defaults to "tbl_view_lock" if not provided or overridden. */ - readonly viewLockTableName: string; + get viewLockTableName(): string { + return this.#viewLockTableName ?? 'tbl_view_lock'; + } /** - * Main table where the view data is stored - * - * @example `tbl_users_${this.schemaVersion}` + * Version of the schema representing the structure of data stored in the view */ - abstract get tableName(): string; + get schemaVersion(): string { + if (!this.#schemaVersion) + throw new Error(`schemaVersion is not defined. Either pass it to constructor, or override the getter`); + + return this.#schemaVersion; + } /** * Table where events are being tracked as projecting/projected * * @example `tbl_users_${this.schemaVersion}_event_lock` */ - abstract get eventLockTableName(): string; - - protected db: import('better-sqlite3').Database; - protected logger: ILogger | undefined; - - #getLastEventQuery: import('better-sqlite3').Statement; - #lockEventQuery: import('better-sqlite3').Statement<[Buffer], void>; - #finalizeEventLockQuery: import('better-sqlite3').Statement<[Buffer], void>; - #recordLastEventQuery: import('better-sqlite3').Statement<[string, string, string], void>; - #upsertTableLockQuery: import('better-sqlite3').Statement<[string, string], void>; - #removeTableLockQuery: import('better-sqlite3').Statement<[string, string], void>; + get eventLockTableName(): string { + return `${this.tableName}_event_lock`; + } + /** + * Main table where the view data is stored + * + * @example `tbl_users_${this.schemaVersion}` + */ + abstract get tableName(): string; constructor(options: AbstractSqliteViewOptions) { - this.schemaVersion = options.schemaVersion; - this.viewLockTableName = options.viewLockTableName ?? 'tbl_view_lock'; + this.#schemaVersion = options.schemaVersion; + this.#viewLockTableName = options.viewLockTableName; + this.#eventProcessingLockTtl = options.eventProcessingLockTtl ?? 15; + this.#viewRestoringLockTtl = options.viewRestoringLockTtl ?? 120; this.db = options.sqliteDb; this.logger = options.logger && 'child' in options.logger ? options.logger.child({ service: this.constructor.name }) : @@ -97,7 +112,7 @@ export abstract class AbstractSqliteView implements IPersistentView { processing_at = strftime('%s', 'now') WHERE processed_at IS NULL - AND processing_at <= strftime('%s', 'now') - ${EVENT_PROCESSING_LOCK_TTL} + AND processing_at <= strftime('%s', 'now') - ${this.#eventProcessingLockTtl} `); this.#finalizeEventLockQuery = this.db.prepare(` @@ -137,6 +152,8 @@ export abstract class AbstractSqliteView implements IPersistentView { AND schema_version = ? AND locked_at IS NOT NULL `); + + this.logger?.info(`View "${this.constructor.name}" lock tables initialized`); } getLastEvent() { @@ -148,19 +165,17 @@ export abstract class AbstractSqliteView implements IPersistentView { } tryMarkAsProjecting(event: IEvent) { - if (!event.id) - throw new TypeError('event.id is required'); + const eventId = getEventId(event); - const r = this.#lockEventQuery.run(guid(event.id)); + const r = this.#lockEventQuery.run(eventId); return r.changes !== 0; } markAsProjected(event: IEvent) { - if (!event.id) - throw new TypeError('event.id is required'); + const eventId = getEventId(event); - const updateResult = this.#finalizeEventLockQuery.run(guid(event.id)); + const updateResult = this.#finalizeEventLockQuery.run(eventId); if (updateResult.changes === 0) throw new Error(`Event ${event.id} could not be marked as processed`); @@ -173,14 +188,18 @@ export abstract class AbstractSqliteView implements IPersistentView { this.ready = false; const upsertResult = this.#upsertTableLockQuery.run(this.tableName, this.schemaVersion); - if (upsertResult.changes === 1) + + if (upsertResult.changes === 1) { this.logger?.debug(`Table "${this.tableName}" lock obtained`); - else - this.logger?.debug(`Table "${this.tableName}" is already locked`); - // TODO: automatic lock prolongation + // TODO: automatic lock prolongation - return upsertResult.changes === 1; + return true; + } + else { + this.logger?.debug(`Table "${this.tableName}" is already locked`); + return false; + } } async unlock(): Promise { diff --git a/src/infrastructure/sqlite/ObjectSqliteView.ts b/src/infrastructure/sqlite/ObjectSqliteView.ts index bf244e6..c919e05 100644 --- a/src/infrastructure/sqlite/ObjectSqliteView.ts +++ b/src/infrastructure/sqlite/ObjectSqliteView.ts @@ -1,8 +1,7 @@ import * as BetterSqlite3 from 'better-sqlite3'; import { AbstractSqliteView, AbstractSqliteViewOptions } from "./AbstractSqliteView"; import { IObjectView, IPersistentView } from '../../interfaces'; - -const guid = (str: string) => Buffer.from(str.replaceAll('-', ''), 'hex'); +import { guid } from './utils'; export class ObjectSqliteView extends AbstractSqliteView implements IObjectView, IPersistentView { @@ -20,7 +19,10 @@ export class ObjectSqliteView extends AbstractSqliteView implements IOb return `${this.#tableNamePrefix}_${this.schemaVersion}_event_lock`; } - constructor(options: AbstractSqliteViewOptions & { tableNamePrefix: string }) { + constructor(options: AbstractSqliteViewOptions & { + tableNamePrefix: string, + schemaVersion: string + }) { if (typeof options.tableNamePrefix !== 'string' || !options.tableNamePrefix.length) throw new TypeError('options.tableNamePrefix argument must be a non-empty String'); @@ -65,6 +67,8 @@ export class ObjectSqliteView extends AbstractSqliteView implements IOb DELETE FROM ${this.tableName} WHERE id = ? `); + + this.logger?.info(`Table "${this.tableName}" initialized`); } get(id: string): TRecord | undefined { diff --git a/src/infrastructure/sqlite/index.ts b/src/infrastructure/sqlite/index.ts index e6c6429..113bcfa 100644 --- a/src/infrastructure/sqlite/index.ts +++ b/src/infrastructure/sqlite/index.ts @@ -1,2 +1,3 @@ export * from './AbstractSqliteView'; export * from './ObjectSqliteView'; +export * from './utils'; diff --git a/src/infrastructure/sqlite/utils/getEventId.ts b/src/infrastructure/sqlite/utils/getEventId.ts new file mode 100644 index 0000000..62fc2ac --- /dev/null +++ b/src/infrastructure/sqlite/utils/getEventId.ts @@ -0,0 +1,8 @@ +import { IEvent } from "../../../interfaces"; +import * as md5 from 'md5'; +import { guid } from './guid'; + +/** + * 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/infrastructure/sqlite/utils/guid.ts b/src/infrastructure/sqlite/utils/guid.ts new file mode 100644 index 0000000..e8ce86c --- /dev/null +++ b/src/infrastructure/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/infrastructure/sqlite/utils/index.ts b/src/infrastructure/sqlite/utils/index.ts new file mode 100644 index 0000000..f27b49b --- /dev/null +++ b/src/infrastructure/sqlite/utils/index.ts @@ -0,0 +1,2 @@ +export * from './guid'; +export * from './getEventId'; From 8c5ec26b69f9f98bc3f3ebfc568da4793d8f9e76 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 20 Mar 2025 01:24:59 +0000 Subject: [PATCH 015/135] Refactoring and tests --- examples/user-domain/index.js | 4 +- package-lock.json | 75 +++--- package.json | 4 +- src/AbstractProjection.ts | 119 +++++---- src/infrastructure/memory/InMemoryView.ts | 5 +- .../sqlite/AbstractSqliteObjectProjection.ts | 27 ++ .../sqlite/AbstractSqliteView.ts | 231 +++--------------- src/infrastructure/sqlite/ObjectSqliteView.ts | 128 ---------- .../sqlite/SqliteEventLocker.ts | 131 ++++++++++ .../sqlite/SqliteObjectStorage.ts | 110 +++++++++ src/infrastructure/sqlite/SqliteObjectView.ts | 44 ++++ src/infrastructure/sqlite/SqliteViewLocker.ts | 173 +++++++++++++ src/infrastructure/sqlite/commonParams.ts | 22 ++ src/infrastructure/sqlite/index.ts | 5 +- .../sqlite/queries/eventLockTableInit.ts | 10 + src/infrastructure/sqlite/queries/index.ts | 2 + .../sqlite/queries/viewLockTableInit.ts | 9 + src/interfaces/IEventLocker.ts | 34 +++ .../{IObjectView.ts => IObjectStorage.ts} | 2 +- src/interfaces/IPersistentView.ts | 24 -- src/interfaces/IProjection.ts | 2 +- src/interfaces/IProjectionView.ts | 22 -- src/interfaces/IViewLocker.ts | 46 ++++ src/interfaces/index.ts | 6 +- src/interfaces/isObject.ts | 5 + tests/integration/SqliteView.test.ts | 39 ++- tests/unit/AbstractProjection.test.ts | 22 +- .../{ => memory}/InMemoryEventStorage.test.ts | 2 +- .../{ => memory}/InMemoryMessageBus.test.ts | 4 +- tests/unit/{ => memory}/InMemoryView.test.ts | 4 +- tests/unit/sqlite/SqliteEventLocker.test.ts | 97 ++++++++ tests/unit/sqlite/SqliteObjectStorage.test.ts | 86 +++++++ tests/unit/sqlite/SqliteViewLocker.test.ts | 109 +++++++++ 33 files changed, 1078 insertions(+), 525 deletions(-) create mode 100644 src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts delete mode 100644 src/infrastructure/sqlite/ObjectSqliteView.ts create mode 100644 src/infrastructure/sqlite/SqliteEventLocker.ts create mode 100644 src/infrastructure/sqlite/SqliteObjectStorage.ts create mode 100644 src/infrastructure/sqlite/SqliteObjectView.ts create mode 100644 src/infrastructure/sqlite/SqliteViewLocker.ts create mode 100644 src/infrastructure/sqlite/commonParams.ts create mode 100644 src/infrastructure/sqlite/queries/eventLockTableInit.ts create mode 100644 src/infrastructure/sqlite/queries/index.ts create mode 100644 src/infrastructure/sqlite/queries/viewLockTableInit.ts create mode 100644 src/interfaces/IEventLocker.ts rename src/interfaces/{IObjectView.ts => IObjectStorage.ts} (88%) delete mode 100644 src/interfaces/IPersistentView.ts delete mode 100644 src/interfaces/IProjectionView.ts create mode 100644 src/interfaces/IViewLocker.ts create mode 100644 src/interfaces/isObject.ts rename tests/unit/{ => memory}/InMemoryEventStorage.test.ts (98%) rename tests/unit/{ => memory}/InMemoryMessageBus.test.ts (95%) rename tests/unit/{ => memory}/InMemoryView.test.ts (98%) create mode 100644 tests/unit/sqlite/SqliteEventLocker.test.ts create mode 100644 tests/unit/sqlite/SqliteObjectStorage.test.ts create mode 100644 tests/unit/sqlite/SqliteViewLocker.test.ts diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index 11e2bee..75d239c 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -19,7 +19,7 @@ exports.createContainer = () => { // register infrastructure services builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('messageBus'); + builder.register(InMemoryMessageBus).as('supplementaryEventBus'); // register domain entities builder.registerAggregate(UserAggregate); @@ -36,7 +36,7 @@ exports.createBaseInstances = () => { // create infrastructure services const messageBus = new InMemoryMessageBus(); const storage = new InMemoryEventStorage(); - const eventStore = new EventStore({ storage, messageBus }); + const eventStore = new EventStore({ storage, supplementaryEventBus: messageBus }); const commandBus = new CommandBus({ messageBus }); /** @type {IAggregateConstructor} */ diff --git a/package-lock.json b/package-lock.json index b445f61..68b2cc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,13 @@ "@types/chai": "^4.3.20", "@types/jest": "^29.5.13", "@types/node": "^20.16.9", - "@types/sinon": "^10.0.20", + "@types/sinon": "^17.0.4", "@types/uuid": "^10.0.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "coveralls": "^3.1.1", "jest": "^29.7.0", - "sinon": "^15.2.0", + "sinon": "^19.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.6.2", @@ -1286,9 +1286,9 @@ "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": { @@ -2596,9 +2596,9 @@ "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": { @@ -4549,6 +4549,7 @@ "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==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", "dev": true, "license": "MIT" }, @@ -4953,23 +4954,23 @@ "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.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", - "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "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": { @@ -5215,11 +5216,14 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=16" + } }, "node_modules/path-type": { "version": "3.0.0", @@ -5880,18 +5884,17 @@ } }, "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.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", "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.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", "supports-color": "^7.2.0" }, "funding": { @@ -5899,6 +5902,16 @@ "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", diff --git a/package.json b/package.json index e190ace..19141af 100644 --- a/package.json +++ b/package.json @@ -50,13 +50,13 @@ "@types/chai": "^4.3.20", "@types/jest": "^29.5.13", "@types/node": "^20.16.9", - "@types/sinon": "^10.0.20", + "@types/sinon": "^17.0.4", "@types/uuid": "^10.0.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "coveralls": "^3.1.1", "jest": "^29.7.0", - "sinon": "^15.2.0", + "sinon": "^19.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.6.2", diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index 157dad8..87aaef7 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -1,14 +1,16 @@ import { describe } from './Event'; import { InMemoryView } from './infrastructure/memory/InMemoryView'; import { - IProjectionView, - IPersistentView, + IViewLocker, + IEventLocker, IProjection, - IViewFactory, ILogger, IExtendableLogger, IEventStore, - IEvent + IEvent, + isViewLocker, + isEventLocker, + IViewFactory } from './interfaces'; import { @@ -19,24 +21,30 @@ import { subscribe } from './utils'; -const isProjectionView = (view: IProjectionView): view is IProjectionView => - 'ready' in view && - 'lock' in view && - 'unlock' in view && - 'once' in view; +export type AbstractProjectionParams = { + /** + * (Optional) Default view associated with the projection + */ + view?: T, -const asProjectionView = (view: any): IProjectionView | undefined => - (isProjectionView(view) ? view : undefined); + /** + * Instance for managing view restoration state to prevent early access to an inconsistent view + * or conflicts caused by concurrent restoration by another process. + */ + viewLocker?: IViewLocker, -const isPersistentView = (view: any): view is IPersistentView => - 'getLastEvent' in view && - 'tryMarkAsProjecting' in view && - 'markAsProjected' in view; + /** + * Instance for tracking event processing state to prevent concurrent processing 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. @@ -47,50 +55,38 @@ export abstract class AbstractProjection; - #view?: TView; - - protected _logger?: ILogger; + protected set view(value: TView) { + this.#view = value; + } - get collectionName(): string { - return getClassName(this); + protected get _viewLocker(): IViewLocker | undefined { + return this.#viewLocker ?? (isViewLocker(this.view) ? this.view : undefined); } - /** - * Indicates if view should be restored from EventStore on start. - * Override for custom behavior. - * - * @deprecated View must implement `getLastEvent()` instead - */ - get shouldRestoreView(): boolean | Promise { - throw new Error('shouldRestoreView is deprecated'); + protected get _eventLocker(): IEventLocker | undefined { + return this.#eventLocker ?? (isEventLocker(this.view) ? this.view : undefined); } constructor({ view, - viewFactory = InMemoryView.factory, + viewLocker, + eventLocker, logger - }: { - view?: TView, - viewFactory?: IViewFactory, - logger?: ILogger | IExtendableLogger - } = {}) { + }: AbstractProjectionParams = {}) { validateHandlers(this); - this.#view = view; - this.#viewFactory = viewFactory; + this.#view = view ?? new InMemoryView() as any; + this.#viewLocker = viewLocker; + this.#eventLocker = eventLocker; this._logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : @@ -108,9 +104,10 @@ 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'); + await this._viewLocker.once('ready'); + } return this._project(event); } @@ -121,31 +118,29 @@ export abstract class AbstractProjection { // lock the view to ensure same restoring procedure // won't be performed by another projection instance - const concurrentView = asProjectionView(this.view); - if (concurrentView) - await concurrentView.lock(); + if (this._viewLocker) + await this._viewLocker.lock(); await this._restore(eventStore); - if (concurrentView) - concurrentView.unlock(); + if (this._viewLocker) + this._viewLocker.unlock(); } /** Restore projection view from event store */ @@ -157,9 +152,9 @@ export abstract class AbstractProjection(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, IObjectView { +export class InMemoryView implements IViewLocker, IObjectStorage { static factory(): TView { return (new InMemoryView() as unknown) as TView; diff --git a/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts b/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts new file mode 100644 index 0000000..5c13f9a --- /dev/null +++ b/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts @@ -0,0 +1,27 @@ +import { AbstractProjection } from "../../AbstractProjection"; +import { IExtendableLogger } from "../../interfaces"; +import { SqliteDbParams } from "./commonParams"; +import { SqliteObjectView } from "./SqliteObjectView"; + +export abstract class AbstractSqliteObjectProjection extends AbstractProjection> { + + static get tableName(): string { + throw new Error('tableName is not defined'); + } + + static get schemaVersion(): string { + throw new Error('schemaVersion is not defined'); + } + + constructor({ viewModelSqliteDb, logger }: SqliteDbParams & { logger?: IExtendableLogger }) { + super({ logger }); + + this.view = new SqliteObjectView({ + schemaVersion: new.target.schemaVersion, + projectionName: new.target.name, + viewModelSqliteDb, + tableNamePrefix: new.target.tableName, + logger + }); + } +} diff --git a/src/infrastructure/sqlite/AbstractSqliteView.ts b/src/infrastructure/sqlite/AbstractSqliteView.ts index ff2f022..224aa97 100644 --- a/src/infrastructure/sqlite/AbstractSqliteView.ts +++ b/src/infrastructure/sqlite/AbstractSqliteView.ts @@ -1,221 +1,52 @@ -import { IEvent } from '../../interfaces/IEvent'; -import { IExtendableLogger, ILogger } from '../../interfaces/ILogger'; -import { IPersistentView } from '../../interfaces/IPersistentView'; -import { getEventId } from './utils'; -import { Database, Statement } from 'better-sqlite3'; - -export type AbstractSqliteViewOptions = { - schemaVersion?: string; - sqliteDb: Database; - viewLockTableName?: string; - logger?: IExtendableLogger | ILogger; - eventProcessingLockTtl?: number; - viewRestoringLockTtl?: number; -} - -export abstract class AbstractSqliteView implements IPersistentView { - - #schemaVersion: string | undefined; - #viewLockTableName: string | undefined; - #getLastEventQuery: Statement; - #lockEventQuery: Statement<[Buffer], void>; - #finalizeEventLockQuery: Statement<[Buffer], void>; - #recordLastEventQuery: Statement<[string, string, string], void>; - #upsertTableLockQuery: Statement<[string, string], void>; - #removeTableLockQuery: Statement<[string, string], void>; - #eventProcessingLockTtl: number; - #viewRestoringLockTtl: number; - - protected db: Database; +import { IEvent, IEventLocker, ILogger } from '../../interfaces'; +import { Database } from 'better-sqlite3'; +import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker'; +import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker'; +import { IViewLocker } from '../../interfaces'; + +export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { + + protected readonly db: Database; + protected readonly schemaVersion: string; + protected readonly viewLocker: SqliteViewLocker; + protected readonly eventLocker: SqliteEventLocker; protected logger: ILogger | undefined; - /** - * Shared table tracking view locks and last projected events. - * Defaults to "tbl_view_lock" if not provided or overridden. - */ - get viewLockTableName(): string { - return this.#viewLockTableName ?? 'tbl_view_lock'; + get ready(): boolean { + return this.viewLocker.ready; } - /** - * Version of the schema representing the structure of data stored in the view - */ - get schemaVersion(): string { - if (!this.#schemaVersion) - throw new Error(`schemaVersion is not defined. Either pass it to constructor, or override the getter`); - - return this.#schemaVersion; + constructor(options: SqliteEventLockerParams & SqliteViewLockerParams) { + this.db = options.viewModelSqliteDb; + 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; } - /** - * Table where events are being tracked as projecting/projected - * - * @example `tbl_users_${this.schemaVersion}_event_lock` - */ - get eventLockTableName(): string { - return `${this.tableName}_event_lock`; + async lock() { + return this.viewLocker.lock(); } - /** - * Main table where the view data is stored - * - * @example `tbl_users_${this.schemaVersion}` - */ - abstract get tableName(): string; - - constructor(options: AbstractSqliteViewOptions) { - this.#schemaVersion = options.schemaVersion; - this.#viewLockTableName = options.viewLockTableName; - this.#eventProcessingLockTtl = options.eventProcessingLockTtl ?? 15; - this.#viewRestoringLockTtl = options.viewRestoringLockTtl ?? 120; - this.db = options.sqliteDb; - this.logger = options.logger && 'child' in options.logger ? - options.logger.child({ service: this.constructor.name }) : - options.logger; + unlock(): void { + this.viewLocker.unlock(); } - /** - * SQLite tables initialization. - * Must be called in the derived class before getting to work. - */ - protected initialize(): void { - this.db.exec(` - CREATE TABLE IF NOT EXISTS ${this.viewLockTableName} ( - table_name TEXT, - schema_version TEXT, - locked_at DATETIME DEFAULT (strftime('%s', 'now')), - last_event TEXT, - PRIMARY KEY (table_name, schema_version) - ); - - CREATE TABLE IF NOT EXISTS ${this.eventLockTableName} ( - event_id BLOB PRIMARY KEY, - processing_at DATETIME DEFAULT (strftime('%s', 'now')), - processed_at DATETIME - ); - `); - - this.#getLastEventQuery = this.db.prepare(` - SELECT - last_event - FROM ${this.viewLockTableName} - WHERE - table_name = ? - AND schema_version =? - `); - - this.#lockEventQuery = this.db.prepare(` - INSERT INTO ${this.eventLockTableName} (event_id) - VALUES (?) - ON CONFLICT (event_id) - DO UPDATE SET - processing_at = strftime('%s', 'now') - WHERE - processed_at IS NULL - AND processing_at <= strftime('%s', 'now') - ${this.#eventProcessingLockTtl} - `); - - this.#finalizeEventLockQuery = this.db.prepare(` - UPDATE ${this.eventLockTableName} - SET - processed_at = strftime('%s', 'now') - WHERE - event_id = ? - AND processed_at IS NULL - `); - - this.#recordLastEventQuery = this.db.prepare(` - UPDATE ${this.viewLockTableName} - SET - last_event = ? - WHERE - table_name = ? - AND schema_version = ? - `); - - this.#upsertTableLockQuery = this.db.prepare(` - INSERT INTO ${this.viewLockTableName} (table_name, schema_version, locked_at) - VALUES (?, ?, strftime('%s', 'now')) - ON CONFLICT (table_name, schema_version) - DO UPDATE SET - locked_at = excluded.locked_at - WHERE - locked_at IS NULL - `); - - this.#removeTableLockQuery = this.db.prepare(` - UPDATE ${this.viewLockTableName} - SET - locked_at = NULL - WHERE - table_name = ? - AND schema_version = ? - AND locked_at IS NOT NULL - `); - - this.logger?.info(`View "${this.constructor.name}" lock tables initialized`); + once(event: 'ready') { + return this.viewLocker.once(event); } getLastEvent() { - const tableInfoRecord = this.#getLastEventQuery.get(this.tableName, this.schemaVersion); - if (!tableInfoRecord?.last_event) - return undefined; - - return JSON.parse(tableInfoRecord.last_event); + return this.eventLocker.getLastEvent(); } tryMarkAsProjecting(event: IEvent) { - const eventId = getEventId(event); - - const r = this.#lockEventQuery.run(eventId); - - return r.changes !== 0; + return this.eventLocker.tryMarkAsProjecting(event); } markAsProjected(event: IEvent) { - const eventId = getEventId(event); - - const updateResult = this.#finalizeEventLockQuery.run(eventId); - if (updateResult.changes === 0) - throw new Error(`Event ${event.id} could not be marked as processed`); - - this.#recordLastEventQuery.run(JSON.stringify(event), this.tableName, this.schemaVersion); - } - - ready: boolean = false; - - lock() { - this.ready = false; - - const upsertResult = this.#upsertTableLockQuery.run(this.tableName, this.schemaVersion); - - if (upsertResult.changes === 1) { - this.logger?.debug(`Table "${this.tableName}" lock obtained`); - - // TODO: automatic lock prolongation - - return true; - } - else { - this.logger?.debug(`Table "${this.tableName}" is already locked`); - return false; - } - } - - async unlock(): Promise { - const updateResult = this.#removeTableLockQuery.run(this.tableName, this.schemaVersion); - if (updateResult.changes === 1) - this.logger?.debug(`Table "${this.tableName}" lock released`); - else - this.logger?.debug(`Table "${this.tableName}" lock didn't exist`); - - this.ready = true; - } - - async once(eventType: 'ready'): Promise { - - // TODO: periodically check until unlocked - - throw new Error('Method not implemented'); + return this.eventLocker.markAsProjected(event); } } diff --git a/src/infrastructure/sqlite/ObjectSqliteView.ts b/src/infrastructure/sqlite/ObjectSqliteView.ts deleted file mode 100644 index c919e05..0000000 --- a/src/infrastructure/sqlite/ObjectSqliteView.ts +++ /dev/null @@ -1,128 +0,0 @@ -import * as BetterSqlite3 from 'better-sqlite3'; -import { AbstractSqliteView, AbstractSqliteViewOptions } from "./AbstractSqliteView"; -import { IObjectView, IPersistentView } from '../../interfaces'; -import { guid } from './utils'; - -export class ObjectSqliteView extends AbstractSqliteView implements IObjectView, IPersistentView { - - #tableNamePrefix: string; - #getQuery: BetterSqlite3.Statement<[Buffer], { data: string, version: number }>; - #insertQuery: BetterSqlite3.Statement<[Buffer, string], void>; - #updateByIdAndVersionQuery: BetterSqlite3.Statement<[string, Buffer, number], void>; - #deleteQuery: BetterSqlite3.Statement<[Buffer], void>; - - get tableName(): string { - return `${this.#tableNamePrefix}_${this.schemaVersion}`; - } - - get eventLockTableName(): string { - return `${this.#tableNamePrefix}_${this.schemaVersion}_event_lock`; - } - - constructor(options: AbstractSqliteViewOptions & { - tableNamePrefix: string, - schemaVersion: string - }) { - if (typeof options.tableNamePrefix !== 'string' || !options.tableNamePrefix.length) - throw new TypeError('options.tableNamePrefix argument must be a non-empty String'); - - super(options); - - this.#tableNamePrefix = options.tableNamePrefix; - - this.initialize(); - } - - protected initialize(): void { - super.initialize(); - - this.db.exec(`CREATE TABLE IF NOT EXISTS ${this.tableName} ( - id BLOB PRIMARY KEY, - version INTEGER DEFAULT 1, - data TEXT NOT NULL - );`); - - this.#getQuery = this.db.prepare(` - SELECT data, version - FROM ${this.tableName} - WHERE id = ? - `); - - this.#insertQuery = this.db.prepare(` - INSERT INTO ${this.tableName} (id, data) - VALUES (?, ?) - `); - - this.#updateByIdAndVersionQuery = this.db.prepare(` - UPDATE ${this.tableName} - SET - data = ?, - version = version + 1 - WHERE - id = ? - AND version = ? - `); - - this.#deleteQuery = this.db.prepare(` - DELETE FROM ${this.tableName} - WHERE id = ? - `); - - this.logger?.info(`Table "${this.tableName}" initialized`); - } - - get(id: string): TRecord | undefined { - const r = this.#getQuery.get(guid(id)); - if (!r) - return undefined; - - return JSON.parse(r.data); - } - - create(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`); - } - - update(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`); - - const data = JSON.parse(record.data); - const updatedData = update(data); - const updatedJson = JSON.stringify(updatedData); - - const r = this.#updateByIdAndVersionQuery.run(updatedJson, gid, record.version); - if (r.changes !== 1) - throw new Error(`Record '${id}' could not be updated`); - } - - updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { - const gid = guid(id); - const record = this.#getQuery.get(gid); - if (record) { - const data = JSON.parse(record.data); - const updatedData = update(data); - const updatedJson = JSON.stringify(updatedData); - - const r = this.#updateByIdAndVersionQuery.run(updatedJson, gid, record.version); - if (r.changes !== 1) - throw new Error(`Record '${id}' could not be updated`); - } - else { - const newData = update(); - - const r = this.#insertQuery.run(guid(id), JSON.stringify(newData)); - if (r.changes !== 1) - throw new Error(`Record '${id}' could not be created`); - } - } - - delete(id: string): boolean { - const r = this.#deleteQuery.run(guid(id)); - return r.changes === 1; - } -} diff --git a/src/infrastructure/sqlite/SqliteEventLocker.ts b/src/infrastructure/sqlite/SqliteEventLocker.ts new file mode 100644 index 0000000..f99edcc --- /dev/null +++ b/src/infrastructure/sqlite/SqliteEventLocker.ts @@ -0,0 +1,131 @@ +import { Database, Statement } from 'better-sqlite3'; +import { IEvent, IEventLocker } from '../../interfaces'; +import { getEventId } from './utils'; +import { viewLockTableInit, eventLockTableInit } from './queries'; +import { SqliteViewLockerParams } from './SqliteViewLocker'; +import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; + +export type SqliteEventLockerParams = SqliteDbParams & SqliteProjectionDataParams & { + /** + * (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; +} + & Pick; + +export class SqliteEventLocker implements IEventLocker { + + #db: Database; + #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: SqliteEventLockerParams) { + if (!o.viewModelSqliteDb) + throw new TypeError('viewModelSqliteDb argument required'); + if (!o.projectionName) + throw new TypeError('projectionName argument required'); + if (!o.schemaVersion) + throw new TypeError('schemaVersion argument required'); + + this.#db = o.viewModelSqliteDb; + 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; + + this.#initialize(); + } + + #initialize() { + this.#db.exec(viewLockTableInit(this.#viewLockTableName)); + this.#db.exec(eventLockTableInit(this.#eventLockTableName)); + + this.#upsertLastEventQuery = this.#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 = this.#db.prepare(` + SELECT + last_event + FROM ${this.#viewLockTableName} + WHERE + projection_name = ? + AND schema_version =? + `); + + this.#lockEventQuery = this.#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(strftime('%f', 'now') * 1000 as INTEGER) + WHERE + processed_at IS NULL + AND processing_at <= cast(strftime('%f', 'now') * 1000 as INTEGER) - ${this.#eventLockTtl} + `); + + this.#finalizeEventLockQuery = this.#db.prepare(` + UPDATE ${this.#eventLockTableName} + SET + processed_at = (cast(strftime('%f', 'now') * 1000 as INTEGER)) + WHERE + projection_name = ? + AND schema_version = ? + AND event_id = ? + AND processed_at IS NULL + `); + } + + tryMarkAsProjecting(event: IEvent) { + const eventId = getEventId(event); + + const r = this.#lockEventQuery.run(this.#projectionName, this.#schemaVersion, eventId); + + return r.changes !== 0; + } + + markAsProjected(event: IEvent) { + 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(); + } + + getLastEvent(): IEvent | undefined { + 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/infrastructure/sqlite/SqliteObjectStorage.ts b/src/infrastructure/sqlite/SqliteObjectStorage.ts new file mode 100644 index 0000000..83a7347 --- /dev/null +++ b/src/infrastructure/sqlite/SqliteObjectStorage.ts @@ -0,0 +1,110 @@ +import { Statement, Database } from 'better-sqlite3'; +import { guid } from './utils'; +import { IObjectStorage } from '../../interfaces'; + +export class SqliteObjectStorage implements IObjectStorage { + + #db: Database; + #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: { + viewModelSqliteDb: Database, + tableName: string + }) { + if (!o.viewModelSqliteDb) + throw new TypeError('viewModelSqliteDb argument required'); + if (!o.tableName) + throw new TypeError('tableName argument required'); + + this.#db = o.viewModelSqliteDb; + this.#tableName = o.tableName; + + this.#initialize(); + } + + #initialize(): void { + this.#db.exec(`CREATE TABLE IF NOT EXISTS ${this.#tableName} ( + id BLOB PRIMARY KEY, + version INTEGER DEFAULT 1, + data TEXT NOT NULL + );`); + + this.#getQuery = this.#db.prepare(` + SELECT data, version + FROM ${this.#tableName} + WHERE id = ? + `); + + this.#insertQuery = this.#db.prepare(` + INSERT INTO ${this.#tableName} (id, data) + VALUES (?, ?) + `); + + this.#updateByIdAndVersionQuery = this.#db.prepare(` + UPDATE ${this.#tableName} + SET + data = ?, + version = version + 1 + WHERE + id = ? + AND version = ? + `); + + this.#deleteQuery = this.#db.prepare(` + DELETE FROM ${this.#tableName} + WHERE id = ? + `); + } + + get(id: string): TRecord | undefined { + const r = this.#getQuery.get(guid(id)); + if (!r) + return undefined; + + return JSON.parse(r.data); + } + + create(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`); + + } + + update(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`); + + 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`); + } + + updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + // Due to better-sqlite3 sync nature, + // it's safe to get then modify within this process + const record = this.#getQuery.get(guid(id)); + if (record) + this.update(id, update); + else + this.create(id, update()); + } + + delete(id: string): boolean { + const r = this.#deleteQuery.run(guid(id)); + return r.changes === 1; + } +} diff --git a/src/infrastructure/sqlite/SqliteObjectView.ts b/src/infrastructure/sqlite/SqliteObjectView.ts new file mode 100644 index 0000000..8a6d005 --- /dev/null +++ b/src/infrastructure/sqlite/SqliteObjectView.ts @@ -0,0 +1,44 @@ +import { AbstractSqliteView } from "./AbstractSqliteView"; +import { IObjectStorage, IEventLocker } from '../../interfaces'; +import { SqliteObjectStorage } from './SqliteObjectStorage'; + +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, + tableName: `${options.tableNamePrefix}_${options.schemaVersion}` + }); + } + + get(id: string): TRecord | undefined { + return this.#sqliteObjectStorage.get(id); + } + + create(id: string, data: TRecord) { + this.#sqliteObjectStorage.create(id, data); + } + + update(id: string, update: (r: TRecord) => TRecord) { + this.#sqliteObjectStorage.update(id, update); + } + + updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + this.#sqliteObjectStorage.updateEnforcingNew(id, update); + } + + delete(id: string): boolean { + return this.#sqliteObjectStorage.delete(id); + } +} diff --git a/src/infrastructure/sqlite/SqliteViewLocker.ts b/src/infrastructure/sqlite/SqliteViewLocker.ts new file mode 100644 index 0000000..b29b5b1 --- /dev/null +++ b/src/infrastructure/sqlite/SqliteViewLocker.ts @@ -0,0 +1,173 @@ +import { Database, Statement } from 'better-sqlite3'; +import { IExtendableLogger, ILogger, IViewLocker } from '../../interfaces'; +import { Deferred } from '../memory'; +import { promisify } from 'util'; +import { viewLockTableInit } from './queries'; +import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; +const delay = promisify(setTimeout); + +export type SqliteViewLockerParams = SqliteDbParams & 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; + + /** + * (Optional) Logger instance for logging operations, + * can be an IExtendableLogger (Winston) + * or ILogger (Console) + */ + logger?: IExtendableLogger | ILogger; +}; + +export class SqliteViewLocker implements IViewLocker { + + #db: Database; + #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: SqliteViewLockerParams) { + if (!o.viewModelSqliteDb) + throw new TypeError('viewModelSqliteDb argument required'); + if (!o.projectionName) + throw new TypeError('projectionName argument required'); + if (!o.schemaVersion) + throw new TypeError('schemaVersion argument required'); + + this.#db = o.viewModelSqliteDb; + 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; + + this.#initialize(); + } + + #initialize() { + this.#db.exec(viewLockTableInit(this.#viewLockTableName)); + + this.#upsertTableLockQuery = this.#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 = this.#db.prepare(` + UPDATE ${this.#viewLockTableName} + SET + locked_till = ? + WHERE + projection_name = ? + AND schema_version = ? + AND locked_till IS NOT NULL + `); + + this.#removeTableLockQuery = this.#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(); + + 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 prolongLock() { + 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`); + } + + unlock() { + this.#lockMarker?.resolve(); + this.#lockMarker = undefined; + + this.cancelLockProlongation(); + + 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/infrastructure/sqlite/commonParams.ts b/src/infrastructure/sqlite/commonParams.ts new file mode 100644 index 0000000..9b51c7a --- /dev/null +++ b/src/infrastructure/sqlite/commonParams.ts @@ -0,0 +1,22 @@ +import { Database } from 'better-sqlite3'; + +export type SqliteDbParams = { + /** Configured instance of better-sqlite3.Database */ + viewModelSqliteDb: Database; +}; + +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/infrastructure/sqlite/index.ts b/src/infrastructure/sqlite/index.ts index 113bcfa..3eaf404 100644 --- a/src/infrastructure/sqlite/index.ts +++ b/src/infrastructure/sqlite/index.ts @@ -1,3 +1,6 @@ export * from './AbstractSqliteView'; -export * from './ObjectSqliteView'; +export * from './SqliteEventLocker'; +export * from './SqliteObjectStorage'; +export * from './SqliteObjectView'; +export * from './SqliteViewLocker'; export * from './utils'; diff --git a/src/infrastructure/sqlite/queries/eventLockTableInit.ts b/src/infrastructure/sqlite/queries/eventLockTableInit.ts new file mode 100644 index 0000000..31a6b95 --- /dev/null +++ b/src/infrastructure/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(strftime('%f', 'now') * 1000 as INTEGER)), + processed_at INTEGER, + PRIMARY KEY (projection_name, schema_version, event_id) + ); +`; diff --git a/src/infrastructure/sqlite/queries/index.ts b/src/infrastructure/sqlite/queries/index.ts new file mode 100644 index 0000000..7edbb02 --- /dev/null +++ b/src/infrastructure/sqlite/queries/index.ts @@ -0,0 +1,2 @@ +export * from './eventLockTableInit'; +export * from './viewLockTableInit'; diff --git a/src/infrastructure/sqlite/queries/viewLockTableInit.ts b/src/infrastructure/sqlite/queries/viewLockTableInit.ts new file mode 100644 index 0000000..b3e707f --- /dev/null +++ b/src/infrastructure/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/interfaces/IEventLocker.ts b/src/interfaces/IEventLocker.ts new file mode 100644 index 0000000..0d6c5a4 --- /dev/null +++ b/src/interfaces/IEventLocker.ts @@ -0,0 +1,34 @@ +import { IEvent } from "./IEvent"; +import { isObject } from "./isObject"; + +/** + * Interface for tracking event processing state to prevent concurrent processing + * by multiple processes. + */ +export interface IEventLocker { + + /** + * Retrieves the last projected event, + * allowing the projection state to be restored from subsequent events. + */ + getLastEvent(): Promise | IEvent | undefined; + + /** + * Marks an event as projecting to prevent it from being processed + * by another projection instance using the same storage. + * + * @returns `false` if the event is already being processed or has been processed. + */ + tryMarkAsProjecting(event: IEvent): Promise | boolean; + + /** + * Marks an event as projected. + */ + markAsProjected(event: IEvent): Promise | void; +} + +export const isEventLocker = (view: unknown): view is IEventLocker => + isObject(view) + && 'getLastEvent' in view + && 'tryMarkAsProjecting' in view + && 'markAsProjected' in view; diff --git a/src/interfaces/IObjectView.ts b/src/interfaces/IObjectStorage.ts similarity index 88% rename from src/interfaces/IObjectView.ts rename to src/interfaces/IObjectStorage.ts index de324c8..53c4a76 100644 --- a/src/interfaces/IObjectView.ts +++ b/src/interfaces/IObjectStorage.ts @@ -1,4 +1,4 @@ -export interface IObjectView { +export interface IObjectStorage { get(id: string): Promise | TRecord | undefined; create(id: string, r: TRecord): Promise | any; diff --git a/src/interfaces/IPersistentView.ts b/src/interfaces/IPersistentView.ts deleted file mode 100644 index 2832c0f..0000000 --- a/src/interfaces/IPersistentView.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IEvent } from "./IEvent"; -import { IProjectionView } from "./IProjectionView"; - -export interface IPersistentView extends IProjectionView { - - /** - * Get last projected event, - * so that projection state can be restored from following events - */ - getLastEvent(): Promise | IEvent | undefined; - - /** - * 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 | boolean; - - /** - * Mark event as projected - */ - markAsProjected(event: IEvent): Promise | void; -} diff --git a/src/interfaces/IProjection.ts b/src/interfaces/IProjection.ts index 52819eb..aaa6721 100644 --- a/src/interfaces/IProjection.ts +++ b/src/interfaces/IProjection.ts @@ -2,7 +2,7 @@ import { IEvent } from "./IEvent"; import { IEventStore } from "./IEventStore"; import { IObserver } from "./IObserver"; -export interface IProjection extends IObserver { +export interface IProjection extends IObserver { readonly view: TView; subscribe(eventStore: IEventStore): Promise; diff --git a/src/interfaces/IProjectionView.ts b/src/interfaces/IProjectionView.ts deleted file mode 100644 index 469666a..0000000 --- a/src/interfaces/IProjectionView.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface IProjectionView { - - /** - * Indicates if view is ready for new events projecting - */ - ready: boolean; - - /** - * Lock the view for external reads/writes - */ - lock(): Promise | boolean; - - /** - * Unlock external read/write operations - */ - unlock(): Promise | void; - - /** - * Wait till the view is ready to accept new events - */ - once(eventType: "ready"): Promise; -} diff --git a/src/interfaces/IViewLocker.ts b/src/interfaces/IViewLocker.ts new file mode 100644 index 0000000..238479d --- /dev/null +++ b/src/interfaces/IViewLocker.ts @@ -0,0 +1,46 @@ +import { isObject } from "./isObject"; + +/** + * Interface for managing view restoration state to prevent early access to an inconsistent view + * or concurrent restoration by another process. + */ +export interface IViewLocker { + + /** + * Indicates whether the view is fully restored and ready to accept new event projections. + */ + ready: boolean; + + /** + * Locks the view to prevent external read/write operations. + * + * @returns `true` if the lock is successfully acquired, `false` otherwise. + */ + lock(): Promise | boolean; + + /** + * Unlocks the view, allowing external read/write operations to resume. + */ + unlock(): Promise | void; + + /** + * Waits until the view is fully restored and ready to accept new events. + * + * @param eventType The event type to listen for (`"ready"`). + * @returns A promise that resolves when the view is ready. + */ + once(eventType: "ready"): Promise; +} + +/** + * Checks if a given object conforms to the `IViewLocker` interface. + * + * @param view The object to check. + * @returns `true` if the object implements `IViewLocker`, `false` otherwise. + */ +export const isViewLocker = (view: unknown): view is IViewLocker => + isObject(view) + && 'ready' in view + && 'lock' in view + && 'unlock' in view + && 'once' in view; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index ab97393..b81c515 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -4,6 +4,7 @@ export * from './ICommand'; export * from './ICommandBus'; export * from './Identifier'; export * from './IEvent'; +export * from './IEventLocker'; export * from './IEventReceptor'; export * from './IEventSet'; export * from './IEventStorage'; @@ -12,10 +13,9 @@ export * from './IEventStream'; export * from './ILogger'; export * from './IMessage'; export * from './IMessageBus'; -export * from './IObjectView'; +export * from './IObjectStorage'; export * from './IObservable'; export * from './IObserver'; -export * from './IPersistentView'; export * from './IProjection'; -export * from './IProjectionView'; export * from './ISaga'; +export * from './IViewLocker'; diff --git a/src/interfaces/isObject.ts b/src/interfaces/isObject.ts new file mode 100644 index 0000000..8c26dac --- /dev/null +++ b/src/interfaces/isObject.ts @@ -0,0 +1,5 @@ +export const isObject = (obj: unknown): obj is {} => + typeof obj === 'object' + && obj !== null + && !(obj instanceof Date) + && !Array.isArray(obj); diff --git a/tests/integration/SqliteView.test.ts b/tests/integration/SqliteView.test.ts index 1c73b05..b09e72d 100644 --- a/tests/integration/SqliteView.test.ts +++ b/tests/integration/SqliteView.test.ts @@ -1,7 +1,7 @@ import { existsSync, unlinkSync } from 'fs'; import { AbstractProjection, IEvent } from '../../src'; -import { ObjectSqliteView } from '../../src/infrastructure/sqlite/ObjectSqliteView'; +import { SqliteObjectView } from '../../src/infrastructure/sqlite'; import * as createDb from 'better-sqlite3'; import { v7 } from 'uuid'; @@ -9,15 +9,7 @@ type UserPayload = { name: string; } -class MyDumbProjection extends AbstractProjection> { - - get schemaVersion() { - return '1'; - } - - constructor({ myDumbViewFactory }) { - super({ viewFactory: myDumbViewFactory }); - } +class MyDumbProjection extends AbstractProjection> { async userCreated(e: IEvent) { if (typeof e.aggregateId !== 'string') @@ -41,37 +33,37 @@ class MyDumbProjection extends AbstractProjection> { describe.only('SqliteView', () => { - let sqliteInMemoryDb: import('better-sqlite3').Database; + let viewModelSqliteDb: import('better-sqlite3').Database; const logState = () => { console.log({ - tbl_view_lock: sqliteInMemoryDb.prepare(`SELECT * FROM tbl_view_lock`).all(), - tbl_test_1_event_lock: sqliteInMemoryDb.prepare(`SELECT * FROM tbl_test_1_event_lock`).all(), - tbl_test_1: sqliteInMemoryDb.prepare(`SELECT * FROM tbl_test_1`).all() + tbl_view_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_view_lock`).all(), + tbl_test_1_event_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1_event_lock`).all(), + tbl_test_1: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1`).all() }); } const fileName = './test.sqlite'; beforeEach(() => { - sqliteInMemoryDb = createDb(fileName); + 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 - sqliteInMemoryDb.pragma('journal_mode = WAL'); + 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. - sqliteInMemoryDb.pragma('synchronous = NORMAL'); + 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. - sqliteInMemoryDb.pragma(`journal_size_limit = ${5 * 1024 * 1024}`); + viewModelSqliteDb.pragma(`journal_size_limit = ${5 * 1024 * 1024}`); }); afterEach(() => { - if (sqliteInMemoryDb) - sqliteInMemoryDb.close(); + if (viewModelSqliteDb) + viewModelSqliteDb.close(); if (existsSync(fileName)) unlinkSync(fileName); }); @@ -84,9 +76,10 @@ describe.only('SqliteView', () => { it('handles 10_000 events within 0.5 seconds', async () => { const p = new MyDumbProjection({ - myDumbViewFactory: ({ schemaVersion }) => new ObjectSqliteView({ - schemaVersion, - sqliteDb: sqliteInMemoryDb, + view: new SqliteObjectView({ + schemaVersion: '1', + viewModelSqliteDb, + projectionName: 'tbl_test', tableNamePrefix: 'tbl_test' }) }); diff --git a/tests/unit/AbstractProjection.test.ts b/tests/unit/AbstractProjection.test.ts index a4945c5..4abada0 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -7,10 +7,6 @@ class MyProjection extends AbstractProjection { if (v.somethingHappenedCnt) @@ -25,20 +21,19 @@ class MyProjection extends AbstractProjection; 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); }); }); @@ -59,9 +54,6 @@ describe('AbstractProjection', function () { it('subscribes to all handlers defined', () => { class ProjectionWithoutHandles extends AbstractProjection { - get schemaVersion(): string { - return 'v1'; - } somethingHappened() { } somethingHappened2() { } } @@ -76,10 +68,6 @@ describe('AbstractProjection', function () { it('ignores overridden projection methods', () => { class ProjectionWithoutHandles extends AbstractProjection { - get schemaVersion(): string { - return 'v1'; - } - somethingHappened() { } /** overridden projection method */ diff --git a/tests/unit/InMemoryEventStorage.test.ts b/tests/unit/memory/InMemoryEventStorage.test.ts similarity index 98% rename from tests/unit/InMemoryEventStorage.test.ts rename to tests/unit/memory/InMemoryEventStorage.test.ts index fe25589..bd8079c 100644 --- a/tests/unit/InMemoryEventStorage.test.ts +++ b/tests/unit/memory/InMemoryEventStorage.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { InMemoryEventStorage } from '../../src'; +import { InMemoryEventStorage } from '../../../src'; describe('InMemoryEventStorage', () => { let storage; diff --git a/tests/unit/InMemoryMessageBus.test.ts b/tests/unit/memory/InMemoryMessageBus.test.ts similarity index 95% rename from tests/unit/InMemoryMessageBus.test.ts rename to tests/unit/memory/InMemoryMessageBus.test.ts index 511d5f5..fef5b23 100644 --- a/tests/unit/InMemoryMessageBus.test.ts +++ b/tests/unit/memory/InMemoryMessageBus.test.ts @@ -1,5 +1,5 @@ -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 () { diff --git a/tests/unit/InMemoryView.test.ts b/tests/unit/memory/InMemoryView.test.ts similarity index 98% rename from tests/unit/InMemoryView.test.ts rename to tests/unit/memory/InMemoryView.test.ts index d2b6e26..617b931 100644 --- a/tests/unit/InMemoryView.test.ts +++ b/tests/unit/memory/InMemoryView.test.ts @@ -1,6 +1,6 @@ -import { InMemoryView } from '../../src'; +import { InMemoryView } from '../../../src'; import { expect, assert } from 'chai'; -import { nextCycle } from '../../src/infrastructure/memory/utils'; +import { nextCycle } from '../../../src/infrastructure/memory/utils'; describe('InMemoryView', function () { diff --git a/tests/unit/sqlite/SqliteEventLocker.test.ts b/tests/unit/sqlite/SqliteEventLocker.test.ts new file mode 100644 index 0000000..ee26dd4 --- /dev/null +++ b/tests/unit/sqlite/SqliteEventLocker.test.ts @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteEventLocker } from '../../../src/infrastructure/sqlite/SqliteEventLocker'; +import { IEvent } from '../../../src/interfaces'; +import { guid } from '../../../src/infrastructure/sqlite'; +import { promisify } from 'util'; +const delay = promisify(setTimeout); + +describe('SqliteEventLocker', function () { + + 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 + }); + jest.useFakeTimers(); + }); + + afterEach(() => { + db.close(); + jest.useRealTimers(); + }); + + it('allows marking an event as projecting', function () { + const result = locker.tryMarkAsProjecting(testEvent); + expect(result).to.be.true; + }); + + it('prevents re-locking an already locked event', function () { + locker.tryMarkAsProjecting(testEvent); + const result = locker.tryMarkAsProjecting(testEvent); + expect(result).to.be.false; + }); + + it('marks an event as projected', function () { + locker.tryMarkAsProjecting(testEvent); + locker.markAsProjected(testEvent); + + const row = db.prepare(`SELECT processed_at FROM test_event_lock WHERE event_id = ?`) + .get(guid(testEvent.id)) as any; + + expect(row).to.exist; + expect(row.processed_at).to.not.be.null; + }); + + it('retrieves the last projected event', function () { + + locker.tryMarkAsProjecting(testEvent); + locker.markAsProjected(testEvent); + + const lastEvent = locker.getLastEvent(); + + expect(lastEvent).to.deep.equal(testEvent); + }); + + it('returns undefined if no event has been projected', function () { + const lastEvent = locker.getLastEvent(); + expect(lastEvent).to.be.undefined; + }); + + it('fails to mark an event as projected if it was never locked', function () { + expect(() => locker.markAsProjected(testEvent)) + .to.throw(Error, `Event ${testEvent.id} could not be marked as processed`); + }); + + it('allows re-locking after TTL expires', async function () { + + locker.tryMarkAsProjecting(testEvent); + + await delay(51); + + const result = locker.tryMarkAsProjecting(testEvent); + expect(result).to.be.true; + }); + + it('fails to update an event if its version is modified in DB', function () { + + locker.tryMarkAsProjecting(testEvent); + + // Modify the event in DB to simulate an external change + db.prepare('UPDATE test_event_lock SET processed_at = ? WHERE event_id = ?') + .run(Date.now(), guid(testEvent.id)); + + // Attempt to finalize the event processing + expect(() => locker.markAsProjected(testEvent)) + .to.throw(Error, `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..ce2aca7 --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { guid, SqliteObjectStorage } from '../../../src/infrastructure/sqlite'; + +describe('SqliteObjectStorage', function () { + let db: import('better-sqlite3').Database; + let storage: SqliteObjectStorage<{ name: string; value: number }>; + + beforeEach(() => { + db = createDb(':memory:'); + storage = new SqliteObjectStorage<{ name: string; value: number }>({ + viewModelSqliteDb: db, + tableName: 'test_objects', + }); + }); + + afterEach(() => { + db.close(); + }); + + it('stores and retrieves an object', async function () { + + const obj = { name: 'Test Object', value: 42 }; + storage.create('0001', obj); + + const retrieved = storage.get('0001'); + expect(retrieved).to.deep.equal(obj); + }); + + it('returns undefined for a non-existent object', async function () { + const retrieved = storage.get('nonexistent'); + expect(retrieved).to.be.undefined; + }); + + it('updates an existing object', async function () { + + storage.create('0002', { name: 'Old Data', value: 5 }); + + storage.update('0002', (r) => ({ ...r, value: 99 })); + + const updated = storage.get('0002'); + expect(updated).to.deep.equal({ name: 'Old Data', value: 99 }); + }); + + it('throws an error when updating a non-existent object', async function () { + + expect(() => storage.update('nonexistent', (r) => ({ ...r, value: 99 }))) + .to.throw(Error, "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).to.be.true; + + const retrieved = storage.get('0003'); + expect(retrieved).to.be.undefined; + }); + + it('returns false when deleting a non-existent object', async function () { + + const deleted = storage.delete('0000'); + expect(deleted).to.be.false; + }); + + it('enforces updating or creating a new object', async function () { + + storage.updateEnforcingNew('0004', () => ({ name: 'Created', value: 1 })); + + let retrieved = storage.get('0004'); + expect(retrieved).to.deep.equal({ name: 'Created', value: 1 }); + + storage.updateEnforcingNew('0004', (r) => ({ ...r!, value: 100 })); + + retrieved = storage.get('0004'); + expect(retrieved).to.deep.equal({ 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'); + + expect(() => storage.get('0005')).to.throw(); + }); +}); diff --git a/tests/unit/sqlite/SqliteViewLocker.test.ts b/tests/unit/sqlite/SqliteViewLocker.test.ts new file mode 100644 index 0000000..43573a2 --- /dev/null +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -0,0 +1,109 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteViewLocker } from '../../../src/infrastructure/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('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(); + 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(); + 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'); + }); +}); From 5d816bc45eada5d79d54bc16951f0f37fe67b0bb Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 20 Mar 2025 12:41:28 +0000 Subject: [PATCH 016/135] Make SqliteObjectView.get to wait until view is unlocked --- src/infrastructure/sqlite/SqliteObjectView.ts | 31 +++++++- src/infrastructure/sqlite/SqliteViewLocker.ts | 2 +- src/interfaces/IObjectStorage.ts | 12 +-- tests/unit/sqlite/SqliteObjectView.test.ts | 73 +++++++++++++++++++ tests/unit/sqlite/SqliteViewLocker.test.ts | 13 ++++ 5 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 tests/unit/sqlite/SqliteObjectView.test.ts diff --git a/src/infrastructure/sqlite/SqliteObjectView.ts b/src/infrastructure/sqlite/SqliteObjectView.ts index 8a6d005..e7956a3 100644 --- a/src/infrastructure/sqlite/SqliteObjectView.ts +++ b/src/infrastructure/sqlite/SqliteObjectView.ts @@ -22,23 +22,52 @@ export class SqliteObjectView extends AbstractSqliteView implements IOb }); } - get(id: string): TRecord | undefined { + async get(id: string): Promise { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + if (!this.ready) + await this.once('ready'); + + return this.#sqliteObjectStorage.get(id); + } + + getSync(id: string) { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + return this.#sqliteObjectStorage.get(id); } create(id: string, data: TRecord) { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + this.#sqliteObjectStorage.create(id, data); } 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'); + this.#sqliteObjectStorage.update(id, update); } 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'); + this.#sqliteObjectStorage.updateEnforcingNew(id, update); } delete(id: string): boolean { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + return this.#sqliteObjectStorage.delete(id); } } diff --git a/src/infrastructure/sqlite/SqliteViewLocker.ts b/src/infrastructure/sqlite/SqliteViewLocker.ts index b29b5b1..a6a09af 100644 --- a/src/infrastructure/sqlite/SqliteViewLocker.ts +++ b/src/infrastructure/sqlite/SqliteViewLocker.ts @@ -103,7 +103,7 @@ export class SqliteViewLocker implements IViewLocker { } get ready(): boolean { - return !!this.#lockMarker; + return !this.#lockMarker; } async lock() { diff --git a/src/interfaces/IObjectStorage.ts b/src/interfaces/IObjectStorage.ts index 53c4a76..1207c37 100644 --- a/src/interfaces/IObjectStorage.ts +++ b/src/interfaces/IObjectStorage.ts @@ -1,11 +1,13 @@ +import { Identifier } from "./Identifier"; + export interface IObjectStorage { - get(id: string): Promise | TRecord | undefined; + get(id: Identifier): Promise | TRecord | undefined; - create(id: string, r: TRecord): Promise | any; + create(id: Identifier, r: TRecord): Promise | any; - update(id: string, cb: (r: TRecord) => TRecord): Promise | any; + update(id: Identifier, cb: (r: TRecord) => TRecord): Promise | any; - updateEnforcingNew(id: string, cb: (r?: TRecord) => TRecord): Promise | any; + updateEnforcingNew(id: Identifier, cb: (r?: TRecord) => TRecord): Promise | any; - delete(id: string): Promise | any; + delete(id: Identifier): Promise | any; } diff --git a/tests/unit/sqlite/SqliteObjectView.test.ts b/tests/unit/sqlite/SqliteObjectView.test.ts new file mode 100644 index 0000000..103b4c9 --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectView.test.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteObjectView } from '../../../src/infrastructure/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 index 43573a2..c7fecef 100644 --- a/tests/unit/sqlite/SqliteViewLocker.test.ts +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -44,6 +44,19 @@ describe('SqliteViewLocker', function () { 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(); From cb57919f0ddb4e9bf1c80fcfa5480724010b6682 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 20 Mar 2025 13:19:49 +0000 Subject: [PATCH 017/135] Tests --- jest.config.ts | 173 +--------------------- src/AbstractProjection.ts | 3 +- src/infrastructure/memory/InMemoryLock.ts | 13 +- tests/unit/memory/InMemoryLock.test.ts | 91 ++++++++++++ 4 files changed, 100 insertions(+), 180 deletions(-) create mode 100644 tests/unit/memory/InMemoryLock.test.ts diff --git a/jest.config.ts b/jest.config.ts index 0a40888..45e753a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,23 +4,14 @@ */ 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", @@ -31,147 +22,15 @@ export default { // ], // 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", - // A map from regular expressions to paths to transformers transform: { '^.+\\.tsx?$': [ @@ -180,23 +39,5 @@ export default { 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, + } }; diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index 87aaef7..ecb1cb7 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -9,8 +9,7 @@ import { IEventStore, IEvent, isViewLocker, - isEventLocker, - IViewFactory + isEventLocker } from './interfaces'; import { diff --git a/src/infrastructure/memory/InMemoryLock.ts b/src/infrastructure/memory/InMemoryLock.ts index 7203884..a8f2192 100644 --- a/src/infrastructure/memory/InMemoryLock.ts +++ b/src/infrastructure/memory/InMemoryLock.ts @@ -19,18 +19,7 @@ export class InMemoryLock { while (this.locked) await this.once('unlocked'); - try { - this.#lockMarker = new Deferred(); - } - catch (err: any) { - try { - await this.unlock(); - } - catch (unlockErr: any) { - // unlocking errors are ignored - } - throw err; - } + this.#lockMarker = new Deferred(); } /** diff --git a/tests/unit/memory/InMemoryLock.test.ts b/tests/unit/memory/InMemoryLock.test.ts new file mode 100644 index 0000000..63974e4 --- /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); + }); +}); From 405ade3461b0f0ef31f37bbbeacede661943d146 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 21 Mar 2025 18:25:35 +0000 Subject: [PATCH 018/135] Refactoring --- src/AbstractAggregate.ts | 17 ++++---- src/AbstractProjection.ts | 51 +++++++++++++++------- src/AggregateCommandHandler.ts | 3 +- src/CqrsContainerBuilder.ts | 3 +- src/index.ts | 1 - src/interfaces/IAggregate.ts | 2 +- src/utils/getHandledMessageTypes.ts | 20 --------- src/utils/getMessageHandlerNames.ts | 10 +---- src/utils/index.ts | 1 - src/utils/subscribe.ts | 20 +++++++-- tests/unit/AggregateCommandHandler.test.ts | 5 +-- tests/unit/CqrsContainerBuilder.test.ts | 2 +- 12 files changed, 68 insertions(+), 67 deletions(-) delete mode 100644 src/utils/getHandledMessageTypes.ts diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index a7da6ae..fd69a79 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -8,7 +8,7 @@ import { IAggregateConstructorParams } from "./interfaces"; -import { getClassName, validateHandlers, getHandler } from './utils'; +import { getClassName, validateHandlers, getHandler, getMessageHandlerNames } from './utils'; /** * Deep-clone simple JS object @@ -25,16 +25,15 @@ const SNAPSHOT_EVENT_TYPE = 'snapshot'; export abstract class AbstractAggregate implements IAggregate { /** - * Optional list of commands handled by Aggregate. - * - * If not overridden in Aggregate implementation, - * `AggregateCommandHandler` will treat all public methods as command handlers + * List of command names handled by the Aggregate. * - * @example - * return ['createUser', 'changePassword']; + * Can be overridden in the Aggregate implementation to explicitly define supported commands. + * If not overridden, all public methods will be treated as command handlers by default. + * + * @example ['createUser', 'changePassword']; */ - static get handles(): string[] | undefined { - return undefined; + static get handles(): string[] { + return getMessageHandlerNames(this); } #id: Identifier; diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index ecb1cb7..a999f50 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -16,24 +16,25 @@ import { getClassName, validateHandlers, getHandler, - getHandledMessageTypes, - subscribe + subscribe, + getMessageHandlerNames } from './utils'; export type AbstractProjectionParams = { /** - * (Optional) Default view associated with the projection + * The default view associated with the projection. + * Can optionally implement IViewLocker and/or IEventLocker. */ view?: T, /** - * Instance for managing view restoration state to prevent early access to an inconsistent view - * or conflicts caused by concurrent restoration by another process. + * Manages view restoration state to prevent early access to an inconsistent view + * or conflicts from concurrent restoration by other processes. */ viewLocker?: IViewLocker, /** - * Instance for tracking event processing state to prevent concurrent processing by multiple processes. + * Tracks event processing state to prevent concurrent handling by multiple processes. */ eventLocker?: IEventLocker, @@ -46,35 +47,53 @@ export type AbstractProjectionParams = { 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; + #view?: TView; #viewLocker?: IViewLocker; #eventLocker?: IEventLocker; protected _logger?: ILogger; + /** + * The default view associated with the projection. + * Can optionally implement IViewLocker and/or IEventLocker. + */ public get view(): TView { - return this.#view; + return this.#view ?? (this.#view = new InMemoryView() as TView); } protected set view(value: TView) { this.#view = value; } + /** + * Manages view restoration state to prevent early access to an inconsistent view + * or conflicts from concurrent restoration by other processes. + */ protected get _viewLocker(): IViewLocker | undefined { return this.#viewLocker ?? (isViewLocker(this.view) ? this.view : undefined); } + protected set _viewLocker(value: IViewLocker | undefined) { + this.#viewLocker = value; + } + + /** + * Tracks event processing state to prevent concurrent handling by multiple processes. + */ protected get _eventLocker(): IEventLocker | undefined { return this.#eventLocker ?? (isEventLocker(this.view) ? this.view : undefined); } + protected set _eventLocker(value: IEventLocker | undefined) { + this.#eventLocker = value; + } + constructor({ view, viewLocker, @@ -83,7 +102,7 @@ export abstract class AbstractProjection> implements I }: AbstractProjectionParams = {}) { validateHandlers(this); - this.#view = view ?? new InMemoryView() as any; + this.#view = view; this.#viewLocker = viewLocker; this.#eventLocker = eventLocker; @@ -158,7 +177,7 @@ export abstract class AbstractProjection> implements I this._logger?.debug(`retrieving ${lastEvent ? `events after ${describe(lastEvent)}` : 'all events'}...`); - const messageTypes = getHandledMessageTypes(this); + const messageTypes = (this.constructor as typeof AbstractProjection).handles; const eventsIterable = eventStore.getEventsByTypes(messageTypes, { afterEvent: lastEvent }); let eventsCount = 0; @@ -174,7 +193,7 @@ export abstract class AbstractProjection> implements I } } - this._logger?.info(`view restored (${this.#view}) from ${eventsCount} event(s) in ${Date.now() - startTs} ms`); + this._logger?.info(`view restored from ${eventsCount} event(s) in ${Date.now() - startTs} ms`); } /** Handle error on restoring. Logs and throws error by default */ diff --git a/src/AggregateCommandHandler.ts b/src/AggregateCommandHandler.ts index bf469c3..af886d7 100644 --- a/src/AggregateCommandHandler.ts +++ b/src/AggregateCommandHandler.ts @@ -15,7 +15,6 @@ import { import { iteratorToArray, getClassName, - getHandledMessageTypes, subscribe } from './utils'; @@ -58,7 +57,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) diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index a2ef76d..9aaecf8 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -6,7 +6,6 @@ import { EventStore } from './EventStore'; import { SagaEventHandler } from './SagaEventHandler'; import { - getHandledMessageTypes, isClass } from './utils'; @@ -94,7 +93,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { container.createInstance(AggregateCommandHandler, { aggregateFactory: (options: any) => container.createInstance(AggregateType, options), - handles: getHandledMessageTypes(AggregateType) + handles: AggregateType.handles }); return this.registerCommandHandler(commandHandlerFactory); diff --git a/src/index.ts b/src/index.ts index 31360d3..bd3b6bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,6 @@ export * as SQLite from './infrastructure/sqlite'; export * as Event from './Event'; export { getMessageHandlerNames, - getHandledMessageTypes, subscribe } from './utils'; diff --git a/src/interfaces/IAggregate.ts b/src/interfaces/IAggregate.ts index d5e924c..97aef7d 100644 --- a/src/interfaces/IAggregate.ts +++ b/src/interfaces/IAggregate.ts @@ -47,7 +47,7 @@ export type IAggregateConstructorParams { - readonly handles?: string[]; + readonly handles: string[]; new(options: IAggregateConstructorParams): IAggregate; } 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/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 848eb58..560c457 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,5 @@ export * from './CompoundEmitter'; export * from './getClassName'; -export * from './getHandledMessageTypes'; export * from './getHandler'; export * from './getMessageHandlerNames'; export * from './isClass'; diff --git a/src/utils/subscribe.ts b/src/utils/subscribe.ts index af09aca..28982b9 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 { getMessageHandlerNames } from "./getMessageHandlerNames"; const unique = (arr: T[]): T[] => [...new Set(arr)]; +/** + * Get a list of message types handled by observer + */ +export function getHandledMessageTypes(observerInstanceOrClass: (object | Function)): string[] { + if (!observerInstanceOrClass) + throw new TypeError('observerInstanceOrClass argument required'); + + const prototype = Object.getPrototypeOf(observerInstanceOrClass); + if (prototype && prototype.constructor && prototype.constructor.handles) + return prototype.constructor.handles; + + return getMessageHandlerNames(observerInstanceOrClass); +} + /** * Subscribe observer to observable */ @@ -35,11 +49,11 @@ export function subscribe( for (const messageType of unique(subscribeTo)) { const handler = masterHandler || getHandler(observer, messageType); - if (!handler) + if (!handler) throw new Error(`'${messageType}' handler is not defined or not a function`); if (queueName) { - if(!observable.queue) + if (!observable.queue) throw new TypeError('Observer does not support named queues'); observable.queue(queueName).on(messageType, handler); diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index e057c44..76bd7ec 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -9,7 +9,6 @@ import { EventStore, InMemorySnapshotStorage } from '../../src'; -import { getHandledMessageTypes } from '../../src/utils'; function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -123,7 +122,7 @@ describe('AggregateCommandHandler', function () { const handler = new AggregateCommandHandler({ eventStore, aggregateFactory: () => aggregate, - handles: getHandledMessageTypes(aggregate) + handles: MyAggregate.handles }); await handler.execute({ type: 'doSomething', payload: 'test' }); @@ -191,7 +190,7 @@ describe('AggregateCommandHandler', function () { const handler = new AggregateCommandHandler({ eventStore, aggregateFactory: () => aggregate, - handles: getHandledMessageTypes(aggregate) + handles: MyAggregate.handles }); // test diff --git a/tests/unit/CqrsContainerBuilder.test.ts b/tests/unit/CqrsContainerBuilder.test.ts index 02d2047..2a83b92 100644 --- a/tests/unit/CqrsContainerBuilder.test.ts +++ b/tests/unit/CqrsContainerBuilder.test.ts @@ -11,7 +11,7 @@ import { describe('CqrsContainerBuilder', function () { - let builder; + let builder: ContainerBuilder; beforeEach(() => { builder = new ContainerBuilder(); From cb6e9a64d3f6c6144d066fc8f4223de9450fc6c5 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 21 Mar 2025 21:30:56 +0000 Subject: [PATCH 019/135] Fix merge --- package-lock.json | 25 +------------------------ package.json | 6 ++---- tests/integration/SqliteView.test.ts | 9 ++++----- 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84415be..c9330af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,15 +17,13 @@ "@types/jest": "^29.5.13", "@types/node": "^20.16.9", "@types/sinon": "^17.0.4", - "@types/uuid": "^10.0.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "jest": "^29.7.0", "sinon": "^19.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.6.2", - "uuid": "^10.0.0" + "typescript": "^5.6.2" }, "engines": { "node": ">=10.3.0" @@ -1200,13 +1198,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -5948,20 +5939,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "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", diff --git a/package.json b/package.json index c3d17aa..70fcfdd 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "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", + "test:integration": "jest --verbose examples/user-domain-tests tests/integration", "changelog": "conventional-changelog -n ./scripts/changelog -i CHANGELOG.md -s", "clean": "tsc --build --clean", "build": "tsc --build", @@ -48,15 +48,13 @@ "@types/jest": "^29.5.13", "@types/node": "^20.16.9", "@types/sinon": "^17.0.4", - "@types/uuid": "^10.0.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "jest": "^29.7.0", "sinon": "^19.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.6.2", - "uuid": "^10.0.0" + "typescript": "^5.6.2" }, "peerDependencies": { "better-sqlite3": "^11.3.0", diff --git a/tests/integration/SqliteView.test.ts b/tests/integration/SqliteView.test.ts index b09e72d..c84797d 100644 --- a/tests/integration/SqliteView.test.ts +++ b/tests/integration/SqliteView.test.ts @@ -3,7 +3,6 @@ import { existsSync, unlinkSync } from 'fs'; import { AbstractProjection, IEvent } from '../../src'; import { SqliteObjectView } from '../../src/infrastructure/sqlite'; import * as createDb from 'better-sqlite3'; -import { v7 } from 'uuid'; type UserPayload = { name: string; @@ -87,10 +86,10 @@ describe.only('SqliteView', () => { await p.view.lock(); await p.view.unlock(); - const aggregateIds = Array.from({ length: 5_000 }, () => ({ - id1: v7(), - id2: v7(), - id3: v7() + const aggregateIds = Array.from({ length: 5_000 }, (v, i) => ({ + id1: `${i}A`, + id2: `${i}B`, + id3: `${i}C` })); console.time(); From b7cca4e1fa5e195819ba3dc31b90fcd89c59f41f Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 21 Mar 2025 21:54:14 +0000 Subject: [PATCH 020/135] Fix key types on InMemoryView --- src/infrastructure/memory/InMemoryView.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/infrastructure/memory/InMemoryView.ts b/src/infrastructure/memory/InMemoryView.ts index 34aae0b..9fc90f7 100644 --- a/src/infrastructure/memory/InMemoryView.ts +++ b/src/infrastructure/memory/InMemoryView.ts @@ -70,7 +70,7 @@ export class InMemoryView implements IViewLocker, IObjectStorage implements IViewLocker, IObjectStorage implements IViewLocker, IObjectStorage TRecord) { + async update(key: Identifier, update: (r: TRecord) => TRecord) { if (!key) throw new TypeError('key argument required'); if (typeof update !== 'function') @@ -144,7 +144,7 @@ export class InMemoryView implements IViewLocker, IObjectStorage TRecord) { + async updateEnforcingNew(key: Identifier, update: (r?: TRecord) => TRecord) { if (!key) throw new TypeError('key argument required'); if (typeof update !== 'function') From c235573678be349d031d1a696cab3993224979a2 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 21 Mar 2025 23:14:43 +0000 Subject: [PATCH 021/135] Change: Support persistent views; Add SQLite infrastructure --- .editorconfig | 1 + examples/user-domain-tests/index.test.js | 7 +- examples/user-domain/index.js | 4 +- jest.config.ts | 173 +- package-lock.json | 1582 +++++++++++++---- package.json | 21 +- src/AbstractAggregate.ts | 17 +- src/AbstractProjection.ts | 172 +- src/AggregateCommandHandler.ts | 8 +- src/CommandBus.ts | 2 +- src/CqrsContainerBuilder.ts | 4 +- src/EventStore.ts | 172 +- src/SagaEventHandler.ts | 6 +- src/index.ts | 9 +- src/infrastructure/InMemoryLock.ts | 73 - .../{ => memory}/InMemoryEventStorage.ts | 36 +- src/infrastructure/memory/InMemoryLock.ts | 43 + .../{ => memory}/InMemoryMessageBus.ts | 2 +- .../{ => memory}/InMemorySnapshotStorage.ts | 4 +- .../{ => memory}/InMemoryView.ts | 22 +- src/infrastructure/memory/index.ts | 6 + .../{ => memory}/utils/Deferred.ts | 0 .../{ => memory}/utils/index.ts | 0 .../{ => memory}/utils/nextCycle.ts | 0 .../sqlite/AbstractSqliteObjectProjection.ts | 27 + .../sqlite/AbstractSqliteView.ts | 52 + .../sqlite/SqliteEventLocker.ts | 131 ++ .../sqlite/SqliteObjectStorage.ts | 110 ++ src/infrastructure/sqlite/SqliteObjectView.ts | 73 + src/infrastructure/sqlite/SqliteViewLocker.ts | 173 ++ src/infrastructure/sqlite/commonParams.ts | 22 + src/infrastructure/sqlite/index.ts | 6 + .../sqlite/queries/eventLockTableInit.ts | 10 + src/infrastructure/sqlite/queries/index.ts | 2 + .../sqlite/queries/viewLockTableInit.ts | 9 + src/infrastructure/sqlite/utils/getEventId.ts | 8 + src/infrastructure/sqlite/utils/guid.ts | 4 + src/infrastructure/sqlite/utils/index.ts | 2 + src/interfaces.ts | 328 ---- src/interfaces/IAggregate.ts | 56 + src/interfaces/IAggregateSnapshotStorage.ts | 8 + src/interfaces/ICommand.ts | 3 + src/interfaces/ICommandBus.ts | 18 + src/interfaces/IEvent.ts | 6 + src/interfaces/IEventLocker.ts | 34 + src/interfaces/IEventReceptor.ts | 6 + src/interfaces/IEventSet.ts | 6 + src/interfaces/IEventStorage.ts | 29 + src/interfaces/IEventStore.ts | 26 + src/interfaces/IEventStream.ts | 3 + src/interfaces/ILogger.ts | 11 + src/interfaces/IMessage.ts | 15 + src/interfaces/IMessageBus.ts | 8 + src/interfaces/IObjectStorage.ts | 13 + src/interfaces/IObservable.ts | 20 + src/interfaces/IObserver.ts | 5 + src/interfaces/IProjection.ts | 20 + src/interfaces/ISaga.ts | 37 + src/interfaces/IViewLocker.ts | 46 + src/interfaces/Identifier.ts | 1 + src/interfaces/index.ts | 21 + src/interfaces/isObject.ts | 5 + src/utils/CompoundEmitter.ts | 45 + src/utils/getHandledMessageTypes.ts | 20 - src/utils/getMessageHandlerNames.ts | 10 +- src/utils/index.ts | 10 +- src/utils/isIEventStorage.ts | 8 + src/utils/isIMessageBus.ts | 9 + src/utils/isIObservable.ts | 7 + src/utils/iteratorToArray.ts | 6 + src/utils/subscribe.ts | 20 +- tests/integration/SqliteView.test.ts | 127 ++ tests/unit/AbstractProjection.test.ts | 37 +- tests/unit/AggregateCommandHandler.test.ts | 11 +- tests/unit/CommandBus.test.ts | 3 +- tests/unit/CqrsContainerBuilder.test.ts | 6 +- tests/unit/EventStore.test.ts | 87 +- tests/unit/SagaEventHandler.test.ts | 8 +- .../unit/memory/InMemoryEventStorage.test.ts | 127 ++ tests/unit/memory/InMemoryLock.test.ts | 91 + .../{ => memory}/InMemoryMessageBus.test.ts | 4 +- tests/unit/{ => memory}/InMemoryView.test.ts | 4 +- tests/unit/sqlite/SqliteEventLocker.test.ts | 97 + tests/unit/sqlite/SqliteObjectStorage.test.ts | 86 + tests/unit/sqlite/SqliteObjectView.test.ts | 73 + tests/unit/sqlite/SqliteViewLocker.test.ts | 122 ++ 86 files changed, 3440 insertions(+), 1296 deletions(-) delete mode 100644 src/infrastructure/InMemoryLock.ts rename src/infrastructure/{ => memory}/InMemoryEventStorage.ts (51%) create mode 100644 src/infrastructure/memory/InMemoryLock.ts rename src/infrastructure/{ => memory}/InMemoryMessageBus.ts (99%) rename src/infrastructure/{ => memory}/InMemorySnapshotStorage.ts (86%) rename src/infrastructure/{ => memory}/InMemoryView.ts (92%) create mode 100644 src/infrastructure/memory/index.ts rename src/infrastructure/{ => memory}/utils/Deferred.ts (100%) rename src/infrastructure/{ => memory}/utils/index.ts (100%) rename src/infrastructure/{ => memory}/utils/nextCycle.ts (100%) create mode 100644 src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts create mode 100644 src/infrastructure/sqlite/AbstractSqliteView.ts create mode 100644 src/infrastructure/sqlite/SqliteEventLocker.ts create mode 100644 src/infrastructure/sqlite/SqliteObjectStorage.ts create mode 100644 src/infrastructure/sqlite/SqliteObjectView.ts create mode 100644 src/infrastructure/sqlite/SqliteViewLocker.ts create mode 100644 src/infrastructure/sqlite/commonParams.ts create mode 100644 src/infrastructure/sqlite/index.ts create mode 100644 src/infrastructure/sqlite/queries/eventLockTableInit.ts create mode 100644 src/infrastructure/sqlite/queries/index.ts create mode 100644 src/infrastructure/sqlite/queries/viewLockTableInit.ts create mode 100644 src/infrastructure/sqlite/utils/getEventId.ts create mode 100644 src/infrastructure/sqlite/utils/guid.ts create mode 100644 src/infrastructure/sqlite/utils/index.ts delete mode 100644 src/interfaces.ts create mode 100644 src/interfaces/IAggregate.ts create mode 100644 src/interfaces/IAggregateSnapshotStorage.ts create mode 100644 src/interfaces/ICommand.ts create mode 100644 src/interfaces/ICommandBus.ts create mode 100644 src/interfaces/IEvent.ts create mode 100644 src/interfaces/IEventLocker.ts create mode 100644 src/interfaces/IEventReceptor.ts create mode 100644 src/interfaces/IEventSet.ts create mode 100644 src/interfaces/IEventStorage.ts create mode 100644 src/interfaces/IEventStore.ts create mode 100644 src/interfaces/IEventStream.ts create mode 100644 src/interfaces/ILogger.ts create mode 100644 src/interfaces/IMessage.ts create mode 100644 src/interfaces/IMessageBus.ts create mode 100644 src/interfaces/IObjectStorage.ts create mode 100644 src/interfaces/IObservable.ts create mode 100644 src/interfaces/IObserver.ts create mode 100644 src/interfaces/IProjection.ts create mode 100644 src/interfaces/ISaga.ts create mode 100644 src/interfaces/IViewLocker.ts create mode 100644 src/interfaces/Identifier.ts create mode 100644 src/interfaces/index.ts create mode 100644 src/interfaces/isObject.ts create mode 100644 src/utils/CompoundEmitter.ts delete mode 100644 src/utils/getHandledMessageTypes.ts create mode 100644 src/utils/isIEventStorage.ts create mode 100644 src/utils/isIMessageBus.ts create mode 100644 src/utils/isIObservable.ts create mode 100644 src/utils/iteratorToArray.ts create mode 100644 tests/integration/SqliteView.test.ts create mode 100644 tests/unit/memory/InMemoryEventStorage.test.ts create mode 100644 tests/unit/memory/InMemoryLock.test.ts rename tests/unit/{ => memory}/InMemoryMessageBus.test.ts (95%) rename tests/unit/{ => memory}/InMemoryView.test.ts (97%) create mode 100644 tests/unit/sqlite/SqliteEventLocker.test.ts create mode 100644 tests/unit/sqlite/SqliteObjectStorage.test.ts create mode 100644 tests/unit/sqlite/SqliteObjectView.test.ts create mode 100644 tests/unit/sqlite/SqliteViewLocker.test.ts 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/examples/user-domain-tests/index.test.js b/examples/user-domain-tests/index.test.js index 4dcb988..2b87755 100644 --- a/examples/user-domain-tests/index.test.js +++ b/examples/user-domain-tests/index.test.js @@ -2,7 +2,7 @@ const { expect } = require('chai'); const { createContainer, createBaseInstances } = require('../user-domain'); -const { nextCycle } = require('../../src/infrastructure/utils'); +const { nextCycle } = require('../../src/infrastructure/memory/utils'); describe('user-domain example', () => { @@ -55,8 +55,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 +64,7 @@ describe('user-domain example', () => { } }); - const userCreated = await eventStore.once('userCreated'); + const userCreated = await userCreatedPromise; const viewRecord = await users.get(userCreated.aggregateId); diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index 11e2bee..75d239c 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -19,7 +19,7 @@ exports.createContainer = () => { // register infrastructure services builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('messageBus'); + builder.register(InMemoryMessageBus).as('supplementaryEventBus'); // register domain entities builder.registerAggregate(UserAggregate); @@ -36,7 +36,7 @@ exports.createBaseInstances = () => { // create infrastructure services const messageBus = new InMemoryMessageBus(); const storage = new InMemoryEventStorage(); - const eventStore = new EventStore({ storage, messageBus }); + const eventStore = new EventStore({ storage, supplementaryEventBus: messageBus }); const commandBus = new CommandBus({ messageBus }); /** @type {IAggregateConstructor} */ diff --git a/jest.config.ts b/jest.config.ts index 0a40888..45e753a 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -4,23 +4,14 @@ */ 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", @@ -31,147 +22,15 @@ export default { // ], // 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", - // A map from regular expressions to paths to transformers transform: { '^.+\\.tsx?$': [ @@ -180,23 +39,5 @@ export default { 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, + } }; diff --git a/package-lock.json b/package-lock.json index 603dbe4..c9330af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,20 +12,25 @@ "di0": "^1.0.0" }, "devDependencies": { - "@types/chai": "^4.3.17", - "@types/jest": "^29.5.12", - "@types/node": "^20.14.14", - "@types/sinon": "^10.0.20", + "@types/better-sqlite3": "^7.6.11", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.13", + "@types/node": "^20.16.9", + "@types/sinon": "^17.0.4", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "jest": "^29.7.0", - "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "sinon": "^19.0.2", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.6.2" }, "engines": { "node": ">=10.3.0" + }, + "peerDependencies": { + "better-sqlite3": "^11.3.0", + "md5": "^2.3.0" } }, "node_modules/@ampproject/remapping": { @@ -33,6 +38,7 @@ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -57,30 +63,32 @@ } }, "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.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "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==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, + "license": "MIT", "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", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -96,29 +104,32 @@ } }, "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.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.0", + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "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.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "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.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -131,6 +142,7 @@ "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" } @@ -139,31 +151,33 @@ "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-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.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" }, "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.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", "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.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -173,23 +187,11 @@ } }, "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==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "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==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -215,10 +217,11 @@ } }, "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.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -258,6 +261,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" }, @@ -270,6 +274,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" }, @@ -282,6 +287,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" }, @@ -289,11 +295,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.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "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" }, @@ -306,6 +345,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" }, @@ -314,12 +354,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.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -333,6 +374,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" }, @@ -345,6 +387,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" }, @@ -357,6 +400,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" }, @@ -369,6 +413,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" }, @@ -381,6 +426,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" }, @@ -393,6 +439,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" }, @@ -400,11 +447,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" }, @@ -416,12 +480,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.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -446,16 +511,17 @@ } }, "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.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", "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", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -481,13 +547,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" }, @@ -500,6 +568,7 @@ "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" @@ -510,6 +579,7 @@ "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" } @@ -519,6 +589,7 @@ "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", @@ -535,6 +606,7 @@ "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" } @@ -544,6 +616,7 @@ "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": "*", @@ -561,6 +634,7 @@ "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", @@ -608,6 +682,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", @@ -623,6 +698,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" @@ -636,6 +712,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" }, @@ -648,6 +725,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", @@ -665,6 +743,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", @@ -680,6 +759,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", @@ -723,6 +803,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" }, @@ -735,6 +816,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", @@ -749,6 +831,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", @@ -764,6 +847,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", @@ -779,6 +863,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", @@ -805,6 +890,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", @@ -818,10 +904,11 @@ } }, "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.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -836,6 +923,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -845,6 +933,7 @@ "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" } @@ -853,13 +942,15 @@ "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 + "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==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -869,13 +960,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" } @@ -885,6 +978,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" } @@ -894,74 +988,64 @@ "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.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^2.0.0", + "@sinonjs/commons": "^3.0.1", "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" + "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 + "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/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", @@ -975,6 +1059,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -984,6 +1069,7 @@ "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" @@ -994,21 +1080,34 @@ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.12", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", + "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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/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": "*" } @@ -1017,13 +1116,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": "*" } @@ -1033,15 +1134,17 @@ "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" @@ -1051,28 +1154,32 @@ "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.17.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.25.tgz", + "integrity": "sha512-bT+r2haIlplJUYtlZrEanFHdPIZTeiMeh/fSOEbOOfWf9uTn+lg8g0KU6Q3iMgjd9FLuuMAgfCNSkjUbxL6E3Q==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "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": "*" } @@ -1081,19 +1188,22 @@ "version": "8.1.5", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -1102,13 +1212,15 @@ "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/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1117,10 +1229,11 @@ } }, "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" }, @@ -1132,13 +1245,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/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" }, @@ -1154,6 +1269,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" } @@ -1163,6 +1279,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" }, @@ -1178,6 +1295,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" @@ -1190,13 +1308,15 @@ "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==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } @@ -1205,13 +1325,15 @@ "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" } @@ -1221,21 +1343,24 @@ "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 + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "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", @@ -1257,6 +1382,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", @@ -1273,6 +1399,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", @@ -1289,6 +1416,7 @@ "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", @@ -1300,23 +1428,27 @@ } }, "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.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", "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" @@ -1327,6 +1459,7 @@ "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" @@ -1342,13 +1475,70 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/better-sqlite3": { + "version": "11.9.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz", + "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "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==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1359,6 +1549,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" }, @@ -1367,9 +1558,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.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -1385,11 +1576,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -1403,6 +1595,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" }, @@ -1415,21 +1608,49 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/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" } @@ -1439,6 +1660,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1448,6 +1670,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", @@ -1461,9 +1684,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.30001706", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz", + "integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==", "dev": true, "funding": [ { @@ -1478,13 +1701,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "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", @@ -1503,6 +1728,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" @@ -1519,15 +1745,27 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -1535,6 +1773,13 @@ "node": "*" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "peer": true + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -1546,21 +1791,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", @@ -1572,6 +1820,7 @@ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, + "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -1581,13 +1830,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1599,13 +1850,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", "dev": true, + "license": "MIT", "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" @@ -1615,13 +1868,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", @@ -1644,6 +1899,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" @@ -1657,6 +1913,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" }, @@ -1669,6 +1926,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" }, @@ -1681,6 +1939,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", @@ -1695,6 +1954,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", @@ -1720,6 +1980,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" }, @@ -1732,6 +1993,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" }, @@ -1744,6 +2006,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" }, @@ -1756,6 +2019,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" }, @@ -1768,6 +2032,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" @@ -1781,6 +2046,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" } @@ -1790,6 +2056,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", @@ -1813,6 +2080,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" @@ -1826,6 +2094,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", @@ -1845,19 +2114,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 + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "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", @@ -1878,7 +2150,8 @@ "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.6", @@ -1895,11 +2168,22 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1909,17 +2193,19 @@ "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.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1935,6 +2221,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" } @@ -1944,6 +2231,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" @@ -1960,15 +2248,33 @@ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", "dev": true, + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -1983,6 +2289,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" }, @@ -1990,20 +2297,42 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/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/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "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" } @@ -2011,13 +2340,15 @@ "node_modules/di0": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/di0/-/di0-1.0.0.tgz", - "integrity": "sha512-RRZsfbOmxiB0ZI+4ABfw/O7GUOnqmgFJGEPFzj7IX+mpm73Hkd38akjaTagaFmwzzRAqIIVR3uB3zSzwnt8ZFw==" + "integrity": "sha512-RRZsfbOmxiB0ZI+4ABfw/O7GUOnqmgFJGEPFzj7IX+mpm73Hkd38akjaTagaFmwzzRAqIIVR3uB3zSzwnt8ZFw==", + "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" } @@ -2027,6 +2358,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" } @@ -2036,6 +2368,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" }, @@ -2048,6 +2381,7 @@ "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "jake": "^10.8.5" }, @@ -2059,16 +2393,18 @@ } }, "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 + "version": "1.5.123", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.123.tgz", + "integrity": "sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==", + "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" }, @@ -2080,22 +2416,35 @@ "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 + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "once": "^1.4.0" + } }, "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==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "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" } @@ -2105,6 +2454,7 @@ "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" } @@ -2114,6 +2464,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" @@ -2127,6 +2478,7 @@ "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", @@ -2154,11 +2506,22 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -2174,22 +2537,32 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "peer": true + }, "node_modules/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, + "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } @@ -2199,6 +2572,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2208,6 +2582,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2220,6 +2595,7 @@ "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" }, @@ -2232,6 +2608,7 @@ "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" @@ -2240,11 +2617,19 @@ "node": ">=8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "peer": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -2252,6 +2637,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2265,6 +2651,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" } @@ -2274,6 +2661,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" } @@ -2283,6 +2671,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.*" } @@ -2292,6 +2681,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": "*" } @@ -2301,6 +2691,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" } @@ -2310,6 +2701,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", @@ -2328,6 +2720,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", @@ -2342,13 +2735,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" } @@ -2358,6 +2753,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" @@ -2368,6 +2764,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" }, @@ -2380,6 +2777,7 @@ "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", @@ -2399,6 +2797,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" @@ -2412,6 +2811,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" @@ -2428,16 +2828,25 @@ "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", "dev": true, + "license": "BSD", "dependencies": { "ini": "^1.3.2" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "peer": true + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2458,6 +2867,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -2466,13 +2876,15 @@ "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", @@ -2494,6 +2906,7 @@ "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" } @@ -2503,6 +2916,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" } @@ -2512,6 +2926,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" }, @@ -2524,6 +2939,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" }, @@ -2535,22 +2951,46 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, "node_modules/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" @@ -2570,6 +3010,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -2579,6 +3020,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" } @@ -2589,6 +3031,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" @@ -2598,25 +3041,34 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "license": "ISC" }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT", + "peer": true }, "node_modules/is-core-module": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", - "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -2632,6 +3084,7 @@ "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" } @@ -2641,6 +3094,7 @@ "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" } @@ -2650,6 +3104,7 @@ "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" } @@ -2659,6 +3114,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" } @@ -2668,6 +3124,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" } @@ -2677,6 +3134,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" }, @@ -2689,6 +3147,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" }, @@ -2700,19 +3159,22 @@ "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 + "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" } @@ -2722,6 +3184,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", @@ -2734,10 +3197,11 @@ } }, "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==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2750,6 +3214,7 @@ "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", @@ -2764,6 +3229,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", @@ -2778,6 +3244,7 @@ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -2791,6 +3258,7 @@ "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", @@ -2809,6 +3277,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -2835,6 +3304,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", @@ -2849,6 +3319,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", @@ -2880,6 +3351,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", @@ -2913,6 +3385,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", @@ -2927,6 +3400,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", @@ -2945,6 +3419,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" } @@ -2954,6 +3429,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", @@ -2999,6 +3475,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", @@ -3014,6 +3491,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" }, @@ -3026,6 +3504,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", @@ -3042,6 +3521,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", @@ -3059,6 +3539,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" } @@ -3068,6 +3549,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", @@ -3093,6 +3575,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" @@ -3106,6 +3589,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", @@ -3121,6 +3605,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", @@ -3141,6 +3626,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": "*", @@ -3155,6 +3641,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" }, @@ -3172,6 +3659,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" } @@ -3181,6 +3669,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", @@ -3201,6 +3690,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" @@ -3214,6 +3704,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", @@ -3246,6 +3737,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", @@ -3279,6 +3771,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", @@ -3306,10 +3799,11 @@ } }, "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==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3322,6 +3816,7 @@ "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": "*", @@ -3339,6 +3834,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", @@ -3356,6 +3852,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3368,6 +3865,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", @@ -3387,6 +3885,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", @@ -3402,6 +3901,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" }, @@ -3424,6 +3924,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -3433,40 +3934,45 @@ } }, "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-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 + "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" }, @@ -3481,13 +3987,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" @@ -3503,13 +4011,15 @@ "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/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" } @@ -3519,6 +4029,7 @@ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3528,6 +4039,7 @@ "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" } @@ -3536,13 +4048,15 @@ "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", @@ -3558,6 +4072,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" @@ -3571,6 +4086,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" } @@ -3580,6 +4096,7 @@ "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" } @@ -3589,6 +4106,7 @@ "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" }, @@ -3600,31 +4118,37 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "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 + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "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/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" } @@ -3634,6 +4158,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" }, @@ -3646,6 +4171,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" }, @@ -3657,10 +4183,11 @@ } }, "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==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3672,13 +4199,15 @@ "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" } @@ -3688,6 +4217,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" }, @@ -3695,11 +4225,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/minimist": "^1.2.0", "camelcase-keys": "^6.2.2", @@ -3724,13 +4267,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/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", @@ -3746,6 +4291,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", @@ -3763,6 +4309,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" } @@ -3772,6 +4319,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", @@ -3784,6 +4332,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" } @@ -3793,6 +4342,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" } @@ -3802,6 +4352,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" }, @@ -3813,7 +4364,8 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.8", @@ -3834,15 +4386,30 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=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" } @@ -3852,6 +4419,7 @@ "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" }, @@ -3863,7 +4431,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3873,6 +4441,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", @@ -3882,72 +4451,121 @@ "node": ">= 6" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "peer": true + }, "node_modules/modify-values": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "peer": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nise": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^11.2.2", - "@sinonjs/text-encoding": "^0.7.2", + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", "just-extend": "^6.2.0", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^8.1.0" } }, "node_modules/nise/node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "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.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "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", @@ -3959,10 +4577,11 @@ } }, "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==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -3975,6 +4594,7 @@ "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" } @@ -3984,6 +4604,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" }, @@ -3995,7 +4616,7 @@ "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" } @@ -4005,6 +4626,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" }, @@ -4020,6 +4642,7 @@ "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" }, @@ -4035,6 +4658,7 @@ "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" }, @@ -4047,6 +4671,7 @@ "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" }, @@ -4062,6 +4687,7 @@ "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" } @@ -4071,6 +4697,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", @@ -4089,6 +4716,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" } @@ -4098,6 +4726,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" } @@ -4107,6 +4736,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" } @@ -4115,20 +4745,25 @@ "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.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=16" + } }, "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" }, @@ -4141,6 +4776,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" } @@ -4150,21 +4786,24 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, "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" }, @@ -4177,6 +4816,7 @@ "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" } @@ -4186,6 +4826,7 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -4195,6 +4836,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" }, @@ -4202,11 +4844,39 @@ "node": ">=8" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "peer": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/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", @@ -4221,6 +4891,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" }, @@ -4232,13 +4903,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" @@ -4247,6 +4920,17 @@ "node": ">= 6" } }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -4261,7 +4945,8 @@ "type": "opencollective", "url": "https://opencollective.com/fast-check" } - ] + ], + "license": "MIT" }, "node_modules/q": { "version": "1.5.1", @@ -4269,6 +4954,7 @@ "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" @@ -4279,21 +4965,50 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "peer": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "dev": true, + "license": "MIT", "dependencies": { "load-json-file": "^4.0.0", "normalize-package-data": "^2.3.2", @@ -4308,6 +5023,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" @@ -4321,6 +5037,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" }, @@ -4333,6 +5050,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" @@ -4346,6 +5064,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" }, @@ -4358,6 +5077,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" }, @@ -4370,6 +5090,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" } @@ -4379,6 +5100,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" } @@ -4387,13 +5109,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", @@ -4406,6 +5130,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" } @@ -4414,7 +5139,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4429,6 +5154,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" @@ -4442,23 +5168,28 @@ "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/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4468,6 +5199,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" }, @@ -4480,15 +5212,17 @@ "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.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" } @@ -4497,7 +5231,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -4511,13 +5244,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -4527,6 +5262,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" }, @@ -4539,6 +5275,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" } @@ -4547,20 +5284,68 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "node_modules/sinon": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", - "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", - "deprecated": "16.1.1", + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.4.tgz", + "integrity": "sha512-myidFob7fjmYHJb+CHNLtAYScxn3sngGq4t75L2rCGGpE/k4OQVkN3KE5FsN+XkO2+fcDZ65PGvq3KHrlLAm7g==", "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": { @@ -4568,17 +5353,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" } @@ -4588,6 +5385,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" } @@ -4597,6 +5395,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" @@ -4607,6 +5406,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" @@ -4616,29 +5416,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.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "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" }, @@ -4651,6 +5455,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" } @@ -4659,13 +5464,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "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" }, @@ -4677,7 +5484,7 @@ "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" } @@ -4687,6 +5494,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" @@ -4700,6 +5508,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", @@ -4714,6 +5523,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" }, @@ -4726,6 +5536,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" } @@ -4735,6 +5546,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" } @@ -4744,6 +5556,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" }, @@ -4756,6 +5569,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" }, @@ -4768,6 +5582,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" }, @@ -4780,6 +5595,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" }, @@ -4787,11 +5603,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "peer": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -4806,6 +5653,7 @@ "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" } @@ -4814,13 +5662,15 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "3" } @@ -4829,13 +5679,15 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "dev": true, + "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" }, @@ -4848,25 +5700,27 @@ "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-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.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", "dev": true, + "license": "MIT", "dependencies": { - "bs-logger": "0.x", + "bs-logger": "^0.2.6", "ejs": "^3.1.10", - "fast-json-stable-stringify": "2.x", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "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.1", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" @@ -4901,10 +5755,11 @@ } }, "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==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4917,6 +5772,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" } @@ -4926,6 +5782,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -4969,15 +5826,30 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "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" } @@ -4987,6 +5859,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" }, @@ -4995,10 +5868,11 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5008,10 +5882,11 @@ } }, "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" @@ -5021,15 +5896,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.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -5045,9 +5921,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" @@ -5060,19 +5937,21 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "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", @@ -5087,6 +5966,7 @@ "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" @@ -5097,6 +5977,7 @@ "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" } @@ -5106,6 +5987,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" }, @@ -5120,13 +6002,15 @@ "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", @@ -5143,13 +6027,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -5163,6 +6048,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" } @@ -5172,6 +6058,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" } @@ -5180,13 +6067,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", @@ -5205,6 +6094,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" } @@ -5214,6 +6104,7 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5223,6 +6114,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 3247729..70fcfdd 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "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", + "test:integration": "jest --verbose examples/user-domain-tests tests/integration", "changelog": "conventional-changelog -n ./scripts/changelog -i CHANGELOG.md -s", "clean": "tsc --build --clean", "build": "tsc --build", @@ -43,16 +43,21 @@ "di0": "^1.0.0" }, "devDependencies": { - "@types/chai": "^4.3.17", - "@types/jest": "^29.5.12", - "@types/node": "^20.14.14", - "@types/sinon": "^10.0.20", + "@types/better-sqlite3": "^7.6.11", + "@types/chai": "^4.3.20", + "@types/jest": "^29.5.13", + "@types/node": "^20.16.9", + "@types/sinon": "^17.0.4", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", "jest": "^29.7.0", - "sinon": "^15.2.0", - "ts-jest": "^29.2.4", + "sinon": "^19.0.2", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.5.4" + "typescript": "^5.6.2" + }, + "peerDependencies": { + "better-sqlite3": "^11.3.0", + "md5": "^2.3.0" } } diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index a7da6ae..fd69a79 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -8,7 +8,7 @@ import { IAggregateConstructorParams } from "./interfaces"; -import { getClassName, validateHandlers, getHandler } from './utils'; +import { getClassName, validateHandlers, getHandler, getMessageHandlerNames } from './utils'; /** * Deep-clone simple JS object @@ -25,16 +25,15 @@ const SNAPSHOT_EVENT_TYPE = 'snapshot'; export abstract class AbstractAggregate implements IAggregate { /** - * Optional list of commands handled by Aggregate. - * - * If not overridden in Aggregate implementation, - * `AggregateCommandHandler` will treat all public methods as command handlers + * List of command names handled by the Aggregate. * - * @example - * return ['createUser', 'changePassword']; + * Can be overridden in the Aggregate implementation to explicitly define supported commands. + * If not overridden, all public methods will be treated as command handlers by default. + * + * @example ['createUser', 'changePassword']; */ - static get handles(): string[] | undefined { - return undefined; + static get handles(): string[] { + return getMessageHandlerNames(this); } #id: Identifier; diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index 54b52b4..a999f50 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -1,89 +1,110 @@ -import { InMemoryView } from './infrastructure/InMemoryView'; - +import { describe } from './Event'; +import { InMemoryView } from './infrastructure/memory/InMemoryView'; import { - IProjectionView, - IEvent, - IPersistentView, - IEventStore, - IExtendableLogger, - ILogger, + IViewLocker, + IEventLocker, IProjection, - IViewFactory -} from "./interfaces"; + ILogger, + IExtendableLogger, + IEventStore, + IEvent, + isViewLocker, + isEventLocker +} from './interfaces'; import { getClassName, validateHandlers, getHandler, - getHandledMessageTypes, - subscribe + subscribe, + getMessageHandlerNames } from './utils'; -const isProjectionView = (view: IProjectionView): view is IProjectionView => - 'ready' in view && - 'lock' in view && - 'unlock' in view && - 'once' in view; +export type AbstractProjectionParams = { + /** + * The default view associated with the projection. + * Can optionally implement IViewLocker and/or IEventLocker. + */ + view?: T, -const asProjectionView = (view: any): IProjectionView | undefined => - (isProjectionView(view) ? view : undefined); + /** + * Manages view restoration state to prevent early access to an inconsistent view + * or conflicts from concurrent restoration by other processes. + */ + viewLocker?: IViewLocker, + + /** + * Tracks event processing state to prevent concurrent handling by multiple processes. + */ + eventLocker?: IEventLocker, + + logger?: ILogger | IExtendableLogger +} /** * Base class for Projection definition */ -export abstract class AbstractProjection implements IProjection { +export abstract class AbstractProjection> implements IProjection { /** - * Optional list of event types being handled by projection. - * Can be overridden in projection implementation. - * If not overridden, will detect event types from event handlers declared on the Projection class + * List of event types handled by the projection. Can be overridden in the projection implementation. + * If not overridden, event types will be inferred from handler methods defined on the Projection class. */ - static get handles(): string[] | undefined { - return undefined; + static get handles(): string[] { + return getMessageHandlerNames(this); } + #view?: TView; + #viewLocker?: IViewLocker; + #eventLocker?: IEventLocker; + protected _logger?: ILogger; + /** - * Default view associated with projection + * The default view associated with the projection. + * Can optionally implement IViewLocker and/or IEventLocker. */ - get view(): TView { - if (!this.#view) - this.#view = this.#viewFactory(); - - return this.#view; + public get view(): TView { + return this.#view ?? (this.#view = new InMemoryView() as TView); } - #viewFactory: IViewFactory; - #view?: TView; + protected set view(value: TView) { + this.#view = value; + } - protected _logger?: ILogger; + /** + * Manages view restoration state to prevent early access to an inconsistent view + * or conflicts from concurrent restoration by other processes. + */ + protected get _viewLocker(): IViewLocker | undefined { + return this.#viewLocker ?? (isViewLocker(this.view) ? this.view : undefined); + } - get collectionName(): string { - return getClassName(this); + protected set _viewLocker(value: IViewLocker | undefined) { + this.#viewLocker = value; } /** - * Indicates if view should be restored from EventStore on start. - * Override for custom behavior. + * Tracks event processing state to prevent concurrent handling by multiple processes. */ - get shouldRestoreView(): boolean | Promise { - return (this.view instanceof Map) - || (this.view instanceof InMemoryView); + protected get _eventLocker(): IEventLocker | undefined { + return this.#eventLocker ?? (isEventLocker(this.view) ? this.view : undefined); + } + + protected set _eventLocker(value: IEventLocker | undefined) { + this.#eventLocker = value; } constructor({ view, - viewFactory = InMemoryView.factory, + viewLocker, + eventLocker, logger - }: { - view?: TView, - viewFactory?: IViewFactory, - logger?: ILogger | IExtendableLogger - } = {}) { + }: AbstractProjectionParams = {}) { validateHandlers(this); - this.#viewFactory = view ? - () => view : - viewFactory; + this.#view = view; + this.#viewLocker = viewLocker; + this.#eventLocker = eventLocker; this._logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : @@ -101,9 +122,10 @@ 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'); + await this._viewLocker.once('ready'); + } return this._project(event); } @@ -114,36 +136,50 @@ export abstract class AbstractProjection { // lock the view to ensure same restoring procedure // won't be performed by another projection instance - const concurrentView = asProjectionView(this.view); - if (concurrentView) - await concurrentView.lock(); + if (this._viewLocker) + await this._viewLocker.lock(); - const shouldRestore = await this.shouldRestoreView; - if (shouldRestore) - await this._restore(eventStore); + await this._restore(eventStore); - if (concurrentView) - concurrentView.unlock(); + if (this._viewLocker) + this._viewLocker.unlock(); } /** Restore projection view from event store */ protected async _restore(eventStore: IEventStore): Promise { if (!eventStore) throw new TypeError('eventStore argument required'); - if (typeof eventStore.getAllEvents !== 'function') - throw new TypeError('eventStore.getAllEvents must be a Function'); + if (typeof eventStore.getEventsByTypes !== 'function') + throw new TypeError('eventStore.getEventsByTypes must be a Function'); + + let lastEvent: IEvent | undefined; + + if (this._eventLocker) { + this._logger?.debug('retrieving last event projected'); + lastEvent = await this._eventLocker.getLastEvent(); + } + + this._logger?.debug(`retrieving ${lastEvent ? `events after ${describe(lastEvent)}` : 'all events'}...`); - this._logger?.debug('retrieving events and restoring projection...'); + const messageTypes = (this.constructor as typeof AbstractProjection).handles; + const eventsIterable = eventStore.getEventsByTypes(messageTypes, { afterEvent: lastEvent }); - const messageTypes = getHandledMessageTypes(this); - const eventsIterable = eventStore.getAllEvents(messageTypes); let eventsCount = 0; const startTs = Date.now(); @@ -157,7 +193,7 @@ export abstract class AbstractProjection new AggregateType(params); - this.#handles = getHandledMessageTypes(AggregateType); + this.#handles = AggregateType.handles; } else if (aggregateFactory) { if (!Array.isArray(handles) || !handles.length) @@ -84,7 +84,9 @@ export class AggregateCommandHandler implements ICommandHandler { if (!id) throw new TypeError('id argument required'); - const events = await this.#eventStore.getAggregateEvents(id); + const eventsIterable = this.#eventStore.getAggregateEvents(id); + const events = await iteratorToArray(eventsIterable); + const aggregate = this.#aggregateFactory({ id, events }); this.#logger?.info(`${aggregate} state restored from ${events.length} event(s)`); diff --git a/src/CommandBus.ts b/src/CommandBus.ts index 56b08f2..e7cfbbf 100644 --- a/src/CommandBus.ts +++ b/src/CommandBus.ts @@ -1,4 +1,4 @@ -import { InMemoryMessageBus } from "./infrastructure/InMemoryMessageBus"; +import { InMemoryMessageBus } from "./infrastructure/memory"; import { ICommand, ICommandBus, diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index e2c8e0d..9aaecf8 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -4,10 +4,8 @@ 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'; @@ -95,7 +93,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { container.createInstance(AggregateCommandHandler, { aggregateFactory: (options: any) => container.createInstance(AggregateType, options), - handles: getHandledMessageTypes(AggregateType) + handles: AggregateType.handles }); return this.registerCommandHandler(commandHandlerFactory); diff --git a/src/EventStore.ts b/src/EventStore.ts index 3a0c076..99d596a 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -1,8 +1,6 @@ import { IAggregateSnapshotStorage, - Identifier, IEvent, - IEventQueryFilter, IEventStorage, IEventSet, IExtendableLogger, @@ -11,46 +9,31 @@ import { IMessageHandler, IObservable, IEventStream, - IEventStore + IEventStore, + EventQueryAfter, + EventQueryBefore, + Identifier } from "./interfaces"; -import { getClassName, setupOneTimeEmitterSubscription } from "./utils"; +import { + getClassName, + setupOneTimeEmitterSubscription, + CompoundEmitter, + isIEventStorage, + isIMessageBus +} 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'; export class EventStore implements IEventStore { - #publishAsync: boolean; #validator: (event: IEvent) => void; #logger?: ILogger; #storage: IEventStorage; - #messageBus?: IMessageBus; + #supplementaryEventBus?: IMessageBus; #snapshotStorage: IAggregateSnapshotStorage | undefined; #sagaStarters: string[] = []; - #defaultEventEmitter: IObservable; + #compoundEmitter: CompoundEmitter; /** Whether storage supports aggregate snapshots */ get snapshotsSupported(): boolean { @@ -59,66 +42,57 @@ export class EventStore implements IEventStore { constructor({ storage, - messageBus, + supplementaryEventBus, snapshotStorage, eventValidator = Event.validate, - eventStoreConfig, logger }: { storage: IEventStorage, - messageBus?: IMessageBus, + + /** Optional event dispatcher for publishing persisted events externally */ + supplementaryEventBus?: IMessageBus, snapshotStorage?: IAggregateSnapshotStorage, eventValidator?: IMessageHandler, - eventStoreConfig?: { - publishAsync?: boolean - }, logger?: ILogger | IExtendableLogger }) { if (!storage) throw new TypeError('storage argument required'); if (!isIEventStorage(storage)) throw new TypeError('storage does not implement IEventStorage interface'); - if (messageBus && !isIMessageBus(messageBus)) - throw new TypeError('messageBus does not implement IMessageBus interface'); - if (messageBus && isIObservable(storage)) - throw new TypeError('both storage and messageBus implement IObservable interface, it is not yet supported'); - - const defaultEventEmitter = isIObservable(storage) ? storage : messageBus; - if (!defaultEventEmitter) - throw new TypeError('storage must implement IObservable if messageBus is not injected'); + if (supplementaryEventBus && !isIMessageBus(supplementaryEventBus)) + throw new TypeError('supplementaryEventBus does not implement IMessageBus interface'); - this.#publishAsync = eventStoreConfig?.publishAsync ?? true; this.#validator = eventValidator; this.#logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : logger; this.#storage = storage; this.#snapshotStorage = snapshotStorage; - this.#messageBus = messageBus; - this.#defaultEventEmitter = defaultEventEmitter; + this.#supplementaryEventBus = supplementaryEventBus; + this.#compoundEmitter = new CompoundEmitter(supplementaryEventBus, storage); } + /** Retrieve new ID from the storage */ async getNewId(): Promise { return this.#storage.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.#storage.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 +102,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); - 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 +125,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); - this.#logger?.debug(`${Event.describeMultiple(events)} retrieved`); + yield* eventsIterable; - return events; + this.#logger?.debug(`all events for saga ${sagaId} retrieved`); } /** @@ -176,10 +144,10 @@ export class EventStore implements IEventStore { /** * 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) { + async commit(events: IEventSet): Promise { if (!Array.isArray(events)) throw new TypeError('events argument must be an Array'); @@ -188,12 +156,12 @@ export class EventStore implements IEventStore { await this.#attachSagaIdToSagaStarterEvents(events) : events; - const eventStreamWithoutSnapshots = await this.save(augmentedEvents); + const eventStreamWithoutSnapshots = await this.persistEventsAndSnapshots(augmentedEvents); // after events are saved to the persistent storage, // publish them to the event bus (i.e. RabbitMq) - if (this.#messageBus) - await this.#publish(eventStreamWithoutSnapshots); + if (this.#supplementaryEventBus) + await this.publishEvents(eventStreamWithoutSnapshots); return eventStreamWithoutSnapshots; } @@ -218,8 +186,12 @@ export class EventStore implements IEventStore { return augmentedEvents; } - /** Save events to the persistent storage(s) */ - async save(events: IEventSet): Promise { + /** + * Save events and snapshots to the persistent storages + * + * @returns Event set without "snapshot" events + */ + protected async persistEventsAndSnapshots(events: IEventSet): Promise { if (!Array.isArray(events)) throw new TypeError('events argument must be an Array'); @@ -246,24 +218,15 @@ export class EventStore implements IEventStore { 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); - } - } + protected async publishEvents(events: IEventSet) { + if (!this.#supplementaryEventBus) + throw new Error('No supplementaryEventBus injected, events cannot be published'); - async #publishEvents(events: IEventSet) { - if (!this.#messageBus) - return; + this.#logger?.debug(`publishing ${Event.describeMultiple(events)}...`); try { - await Promise.all(events.map(event => - this.#messageBus?.publish(event))); + for (const event of events) + this.#supplementaryEventBus.publish(event); this.#logger?.debug(`${Event.describeMultiple(events)} published`); } @@ -275,41 +238,22 @@ export class EventStore implements IEventStore { } } - /** 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.#compoundEmitter.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.#compoundEmitter.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'); - - return this.#defaultEventEmitter.queue(name); + return this.#compoundEmitter.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.#compoundEmitter, subscribeTo, filter, handler, this.#logger); } } diff --git a/src/SagaEventHandler.ts b/src/SagaEventHandler.ts index b66c639..6b538b3 100644 --- a/src/SagaEventHandler.ts +++ b/src/SagaEventHandler.ts @@ -14,7 +14,8 @@ import { import { subscribe, - getClassName + getClassName, + iteratorToArray } from './utils'; /** @@ -151,7 +152,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/index.ts b/src/index.ts index 94b96da..bd3b6bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,17 +9,12 @@ export * from './AbstractSaga'; export * from './SagaEventHandler'; export * from './AbstractProjection'; -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 './infrastructure/memory'; +export * as SQLite from './infrastructure/sqlite'; export * as Event from './Event'; export { getMessageHandlerNames, - getHandledMessageTypes, subscribe } from './utils'; 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/InMemoryEventStorage.ts b/src/infrastructure/memory/InMemoryEventStorage.ts similarity index 51% rename from src/infrastructure/InMemoryEventStorage.ts rename to src/infrastructure/memory/InMemoryEventStorage.ts index 3fa6d95..c78dc36 100644 --- a/src/infrastructure/InMemoryEventStorage.ts +++ b/src/infrastructure/memory/InMemoryEventStorage.ts @@ -1,15 +1,14 @@ -import { IEvent, IEventStorage, IEventSet, IEventStream } from "../interfaces"; +import { IEvent } from "../../interfaces/IEvent"; +import { IEventSet } from "../../interfaces/IEventSet"; +import { EventQueryAfter, IEventStorage } from "../../interfaces/IEventStorage"; +import { IEventStream } from "../../interfaces/IEventStream"; 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 = []; @@ -23,11 +22,11 @@ export class InMemoryEventStorage implements IEventStorage { return events; } - async getAggregateEvents(aggregateId, options?: { snapshot: IEvent }): Promise { + async *getAggregateEvents(aggregateId, options?: { snapshot: IEvent }): IEventStream { await nextCycle(); const afterVersion = options?.snapshot?.aggregateVersion; - const result = !afterVersion ? + const results = !afterVersion ? this.#events.filter(e => e.aggregateId == aggregateId) : this.#events.filter(e => e.aggregateId == aggregateId && @@ -36,10 +35,10 @@ export class InMemoryEventStorage implements IEventStorage { await nextCycle(); - return result; + yield* results; } - async getSagaEvents(sagaId, { beforeEvent }): Promise { + async *getSagaEvents(sagaId, { beforeEvent }): IEventStream { await nextCycle(); const results = this.#events.filter(e => @@ -49,20 +48,27 @@ export class InMemoryEventStorage implements IEventStorage { await nextCycle(); - return results; + yield* results; } - async* getEvents(eventTypes): IEventStream { + async* getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream { await nextCycle(); - for await (const event of this.#events) { - if (!eventTypes || eventTypes.includes(event.type)) + 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; } } - getNewId(): number { + getNewId(): string { this.#nextId += 1; - return this.#nextId; + return String(this.#nextId); } } diff --git a/src/infrastructure/memory/InMemoryLock.ts b/src/infrastructure/memory/InMemoryLock.ts new file mode 100644 index 0000000..a8f2192 --- /dev/null +++ b/src/infrastructure/memory/InMemoryLock.ts @@ -0,0 +1,43 @@ +import { Deferred } from "./utils"; + +export class InMemoryLock { + + #lockMarker: Deferred | undefined; + + /** + * Indicates if lock is acquired + */ + get locked(): boolean { + return !!this.#lockMarker; + } + + /** + * Acquire the lock on the current instance. + * Resolves when the lock is successfully acquired + */ + async lock(): Promise { + while (this.locked) + await this.once('unlocked'); + + this.#lockMarker = new Deferred(); + } + + /** + * Release the lock acquired earlier + */ + async unlock(): Promise { + this.#lockMarker?.resolve(); + this.#lockMarker = undefined; + } + + /** + * Wait until the lock is released. + * Resolves immediately if the lock is not acquired + */ + once(event: 'unlocked'): Promise { + if (event !== 'unlocked') + throw new TypeError(`Unexpected event type: ${event}`); + + return this.#lockMarker?.promise ?? Promise.resolve(); + } +} diff --git a/src/infrastructure/InMemoryMessageBus.ts b/src/infrastructure/memory/InMemoryMessageBus.ts similarity index 99% rename from src/infrastructure/InMemoryMessageBus.ts rename to src/infrastructure/memory/InMemoryMessageBus.ts index c8f43f3..b14548e 100644 --- a/src/infrastructure/InMemoryMessageBus.ts +++ b/src/infrastructure/memory/InMemoryMessageBus.ts @@ -4,7 +4,7 @@ import { IMessageBus, IMessageHandler, IObservable -} from "../interfaces"; +} from "../../interfaces"; /** * Default implementation of the message bus. diff --git a/src/infrastructure/InMemorySnapshotStorage.ts b/src/infrastructure/memory/InMemorySnapshotStorage.ts similarity index 86% rename from src/infrastructure/InMemorySnapshotStorage.ts rename to src/infrastructure/memory/InMemorySnapshotStorage.ts index d3fd377..7943217 100644 --- a/src/infrastructure/InMemorySnapshotStorage.ts +++ b/src/infrastructure/memory/InMemorySnapshotStorage.ts @@ -1,4 +1,4 @@ -import { IAggregateSnapshotStorage, Identifier, IEvent } from "../interfaces"; +import { IAggregateSnapshotStorage, Identifier, IEvent } from "../../interfaces"; /** * In-memory storage for aggregate snapshots. @@ -11,7 +11,7 @@ export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { /** * Get latest aggregate snapshot */ - async getAggregateSnapshot(aggregateId: Identifier): Promise { + async getAggregateSnapshot(aggregateId: string): Promise { return this.#snapshots.get(aggregateId); } diff --git a/src/infrastructure/InMemoryView.ts b/src/infrastructure/memory/InMemoryView.ts similarity index 92% rename from src/infrastructure/InMemoryView.ts rename to src/infrastructure/memory/InMemoryView.ts index 20f528a..9fc90f7 100644 --- a/src/infrastructure/InMemoryView.ts +++ b/src/infrastructure/memory/InMemoryView.ts @@ -1,5 +1,5 @@ import { InMemoryLock } from './InMemoryLock'; -import { IProjectionView, Identifier } from "../interfaces"; +import { IViewLocker, Identifier, IObjectStorage } from "../../interfaces"; import { nextCycle } from './utils'; /** @@ -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`); @@ -186,9 +176,6 @@ export class InMemoryView implements IProjectionView { if (updatedValue === undefined) return; - if (this.#asyncWrites) - await nextCycle(); - this._map.set(key, updatedValue); } @@ -197,9 +184,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/infrastructure/memory/index.ts b/src/infrastructure/memory/index.ts new file mode 100644 index 0000000..3f6f779 --- /dev/null +++ b/src/infrastructure/memory/index.ts @@ -0,0 +1,6 @@ +export * from './InMemoryEventStorage'; +export * from './InMemoryLock'; +export * from './InMemoryMessageBus'; +export * from './InMemorySnapshotStorage'; +export * from './InMemoryView'; +export * from './utils/Deferred'; diff --git a/src/infrastructure/utils/Deferred.ts b/src/infrastructure/memory/utils/Deferred.ts similarity index 100% rename from src/infrastructure/utils/Deferred.ts rename to src/infrastructure/memory/utils/Deferred.ts diff --git a/src/infrastructure/utils/index.ts b/src/infrastructure/memory/utils/index.ts similarity index 100% rename from src/infrastructure/utils/index.ts rename to src/infrastructure/memory/utils/index.ts diff --git a/src/infrastructure/utils/nextCycle.ts b/src/infrastructure/memory/utils/nextCycle.ts similarity index 100% rename from src/infrastructure/utils/nextCycle.ts rename to src/infrastructure/memory/utils/nextCycle.ts diff --git a/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts b/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts new file mode 100644 index 0000000..5c13f9a --- /dev/null +++ b/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts @@ -0,0 +1,27 @@ +import { AbstractProjection } from "../../AbstractProjection"; +import { IExtendableLogger } from "../../interfaces"; +import { SqliteDbParams } from "./commonParams"; +import { SqliteObjectView } from "./SqliteObjectView"; + +export abstract class AbstractSqliteObjectProjection extends AbstractProjection> { + + static get tableName(): string { + throw new Error('tableName is not defined'); + } + + static get schemaVersion(): string { + throw new Error('schemaVersion is not defined'); + } + + constructor({ viewModelSqliteDb, logger }: SqliteDbParams & { logger?: IExtendableLogger }) { + super({ logger }); + + this.view = new SqliteObjectView({ + schemaVersion: new.target.schemaVersion, + projectionName: new.target.name, + viewModelSqliteDb, + tableNamePrefix: new.target.tableName, + logger + }); + } +} diff --git a/src/infrastructure/sqlite/AbstractSqliteView.ts b/src/infrastructure/sqlite/AbstractSqliteView.ts new file mode 100644 index 0000000..224aa97 --- /dev/null +++ b/src/infrastructure/sqlite/AbstractSqliteView.ts @@ -0,0 +1,52 @@ +import { IEvent, IEventLocker, ILogger } from '../../interfaces'; +import { Database } from 'better-sqlite3'; +import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker'; +import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker'; +import { IViewLocker } from '../../interfaces'; + +export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { + + protected readonly db: Database; + 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: SqliteEventLockerParams & SqliteViewLockerParams) { + this.db = options.viewModelSqliteDb; + 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/infrastructure/sqlite/SqliteEventLocker.ts b/src/infrastructure/sqlite/SqliteEventLocker.ts new file mode 100644 index 0000000..f99edcc --- /dev/null +++ b/src/infrastructure/sqlite/SqliteEventLocker.ts @@ -0,0 +1,131 @@ +import { Database, Statement } from 'better-sqlite3'; +import { IEvent, IEventLocker } from '../../interfaces'; +import { getEventId } from './utils'; +import { viewLockTableInit, eventLockTableInit } from './queries'; +import { SqliteViewLockerParams } from './SqliteViewLocker'; +import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; + +export type SqliteEventLockerParams = SqliteDbParams & SqliteProjectionDataParams & { + /** + * (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; +} + & Pick; + +export class SqliteEventLocker implements IEventLocker { + + #db: Database; + #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: SqliteEventLockerParams) { + if (!o.viewModelSqliteDb) + throw new TypeError('viewModelSqliteDb argument required'); + if (!o.projectionName) + throw new TypeError('projectionName argument required'); + if (!o.schemaVersion) + throw new TypeError('schemaVersion argument required'); + + this.#db = o.viewModelSqliteDb; + 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; + + this.#initialize(); + } + + #initialize() { + this.#db.exec(viewLockTableInit(this.#viewLockTableName)); + this.#db.exec(eventLockTableInit(this.#eventLockTableName)); + + this.#upsertLastEventQuery = this.#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 = this.#db.prepare(` + SELECT + last_event + FROM ${this.#viewLockTableName} + WHERE + projection_name = ? + AND schema_version =? + `); + + this.#lockEventQuery = this.#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(strftime('%f', 'now') * 1000 as INTEGER) + WHERE + processed_at IS NULL + AND processing_at <= cast(strftime('%f', 'now') * 1000 as INTEGER) - ${this.#eventLockTtl} + `); + + this.#finalizeEventLockQuery = this.#db.prepare(` + UPDATE ${this.#eventLockTableName} + SET + processed_at = (cast(strftime('%f', 'now') * 1000 as INTEGER)) + WHERE + projection_name = ? + AND schema_version = ? + AND event_id = ? + AND processed_at IS NULL + `); + } + + tryMarkAsProjecting(event: IEvent) { + const eventId = getEventId(event); + + const r = this.#lockEventQuery.run(this.#projectionName, this.#schemaVersion, eventId); + + return r.changes !== 0; + } + + markAsProjected(event: IEvent) { + 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(); + } + + getLastEvent(): IEvent | undefined { + 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/infrastructure/sqlite/SqliteObjectStorage.ts b/src/infrastructure/sqlite/SqliteObjectStorage.ts new file mode 100644 index 0000000..83a7347 --- /dev/null +++ b/src/infrastructure/sqlite/SqliteObjectStorage.ts @@ -0,0 +1,110 @@ +import { Statement, Database } from 'better-sqlite3'; +import { guid } from './utils'; +import { IObjectStorage } from '../../interfaces'; + +export class SqliteObjectStorage implements IObjectStorage { + + #db: Database; + #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: { + viewModelSqliteDb: Database, + tableName: string + }) { + if (!o.viewModelSqliteDb) + throw new TypeError('viewModelSqliteDb argument required'); + if (!o.tableName) + throw new TypeError('tableName argument required'); + + this.#db = o.viewModelSqliteDb; + this.#tableName = o.tableName; + + this.#initialize(); + } + + #initialize(): void { + this.#db.exec(`CREATE TABLE IF NOT EXISTS ${this.#tableName} ( + id BLOB PRIMARY KEY, + version INTEGER DEFAULT 1, + data TEXT NOT NULL + );`); + + this.#getQuery = this.#db.prepare(` + SELECT data, version + FROM ${this.#tableName} + WHERE id = ? + `); + + this.#insertQuery = this.#db.prepare(` + INSERT INTO ${this.#tableName} (id, data) + VALUES (?, ?) + `); + + this.#updateByIdAndVersionQuery = this.#db.prepare(` + UPDATE ${this.#tableName} + SET + data = ?, + version = version + 1 + WHERE + id = ? + AND version = ? + `); + + this.#deleteQuery = this.#db.prepare(` + DELETE FROM ${this.#tableName} + WHERE id = ? + `); + } + + get(id: string): TRecord | undefined { + const r = this.#getQuery.get(guid(id)); + if (!r) + return undefined; + + return JSON.parse(r.data); + } + + create(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`); + + } + + update(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`); + + 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`); + } + + updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + // Due to better-sqlite3 sync nature, + // it's safe to get then modify within this process + const record = this.#getQuery.get(guid(id)); + if (record) + this.update(id, update); + else + this.create(id, update()); + } + + delete(id: string): boolean { + const r = this.#deleteQuery.run(guid(id)); + return r.changes === 1; + } +} diff --git a/src/infrastructure/sqlite/SqliteObjectView.ts b/src/infrastructure/sqlite/SqliteObjectView.ts new file mode 100644 index 0000000..e7956a3 --- /dev/null +++ b/src/infrastructure/sqlite/SqliteObjectView.ts @@ -0,0 +1,73 @@ +import { AbstractSqliteView } from "./AbstractSqliteView"; +import { IObjectStorage, IEventLocker } from '../../interfaces'; +import { SqliteObjectStorage } from './SqliteObjectStorage'; + +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, + tableName: `${options.tableNamePrefix}_${options.schemaVersion}` + }); + } + + async get(id: string): Promise { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + if (!this.ready) + await this.once('ready'); + + return this.#sqliteObjectStorage.get(id); + } + + getSync(id: string) { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + return this.#sqliteObjectStorage.get(id); + } + + create(id: string, data: TRecord) { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + this.#sqliteObjectStorage.create(id, data); + } + + 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'); + + this.#sqliteObjectStorage.update(id, update); + } + + 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'); + + this.#sqliteObjectStorage.updateEnforcingNew(id, update); + } + + delete(id: string): boolean { + if (typeof id !== 'string' || !id.length) + throw new TypeError('id argument must be a non-empty String'); + + return this.#sqliteObjectStorage.delete(id); + } +} diff --git a/src/infrastructure/sqlite/SqliteViewLocker.ts b/src/infrastructure/sqlite/SqliteViewLocker.ts new file mode 100644 index 0000000..a6a09af --- /dev/null +++ b/src/infrastructure/sqlite/SqliteViewLocker.ts @@ -0,0 +1,173 @@ +import { Database, Statement } from 'better-sqlite3'; +import { IExtendableLogger, ILogger, IViewLocker } from '../../interfaces'; +import { Deferred } from '../memory'; +import { promisify } from 'util'; +import { viewLockTableInit } from './queries'; +import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; +const delay = promisify(setTimeout); + +export type SqliteViewLockerParams = SqliteDbParams & 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; + + /** + * (Optional) Logger instance for logging operations, + * can be an IExtendableLogger (Winston) + * or ILogger (Console) + */ + logger?: IExtendableLogger | ILogger; +}; + +export class SqliteViewLocker implements IViewLocker { + + #db: Database; + #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: SqliteViewLockerParams) { + if (!o.viewModelSqliteDb) + throw new TypeError('viewModelSqliteDb argument required'); + if (!o.projectionName) + throw new TypeError('projectionName argument required'); + if (!o.schemaVersion) + throw new TypeError('schemaVersion argument required'); + + this.#db = o.viewModelSqliteDb; + 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; + + this.#initialize(); + } + + #initialize() { + this.#db.exec(viewLockTableInit(this.#viewLockTableName)); + + this.#upsertTableLockQuery = this.#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 = this.#db.prepare(` + UPDATE ${this.#viewLockTableName} + SET + locked_till = ? + WHERE + projection_name = ? + AND schema_version = ? + AND locked_till IS NOT NULL + `); + + this.#removeTableLockQuery = this.#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(); + + 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 prolongLock() { + 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`); + } + + unlock() { + this.#lockMarker?.resolve(); + this.#lockMarker = undefined; + + this.cancelLockProlongation(); + + 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/infrastructure/sqlite/commonParams.ts b/src/infrastructure/sqlite/commonParams.ts new file mode 100644 index 0000000..9b51c7a --- /dev/null +++ b/src/infrastructure/sqlite/commonParams.ts @@ -0,0 +1,22 @@ +import { Database } from 'better-sqlite3'; + +export type SqliteDbParams = { + /** Configured instance of better-sqlite3.Database */ + viewModelSqliteDb: Database; +}; + +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/infrastructure/sqlite/index.ts b/src/infrastructure/sqlite/index.ts new file mode 100644 index 0000000..3eaf404 --- /dev/null +++ b/src/infrastructure/sqlite/index.ts @@ -0,0 +1,6 @@ +export * from './AbstractSqliteView'; +export * from './SqliteEventLocker'; +export * from './SqliteObjectStorage'; +export * from './SqliteObjectView'; +export * from './SqliteViewLocker'; +export * from './utils'; diff --git a/src/infrastructure/sqlite/queries/eventLockTableInit.ts b/src/infrastructure/sqlite/queries/eventLockTableInit.ts new file mode 100644 index 0000000..31a6b95 --- /dev/null +++ b/src/infrastructure/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(strftime('%f', 'now') * 1000 as INTEGER)), + processed_at INTEGER, + PRIMARY KEY (projection_name, schema_version, event_id) + ); +`; diff --git a/src/infrastructure/sqlite/queries/index.ts b/src/infrastructure/sqlite/queries/index.ts new file mode 100644 index 0000000..7edbb02 --- /dev/null +++ b/src/infrastructure/sqlite/queries/index.ts @@ -0,0 +1,2 @@ +export * from './eventLockTableInit'; +export * from './viewLockTableInit'; diff --git a/src/infrastructure/sqlite/queries/viewLockTableInit.ts b/src/infrastructure/sqlite/queries/viewLockTableInit.ts new file mode 100644 index 0000000..b3e707f --- /dev/null +++ b/src/infrastructure/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/infrastructure/sqlite/utils/getEventId.ts b/src/infrastructure/sqlite/utils/getEventId.ts new file mode 100644 index 0000000..62fc2ac --- /dev/null +++ b/src/infrastructure/sqlite/utils/getEventId.ts @@ -0,0 +1,8 @@ +import { IEvent } from "../../../interfaces"; +import * as md5 from 'md5'; +import { guid } from './guid'; + +/** + * 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/infrastructure/sqlite/utils/guid.ts b/src/infrastructure/sqlite/utils/guid.ts new file mode 100644 index 0000000..e8ce86c --- /dev/null +++ b/src/infrastructure/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/infrastructure/sqlite/utils/index.ts b/src/infrastructure/sqlite/utils/index.ts new file mode 100644 index 0000000..f27b49b --- /dev/null +++ b/src/infrastructure/sqlite/utils/index.ts @@ -0,0 +1,2 @@ +export * from './guid'; +export * from './getEventId'; 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..97aef7d --- /dev/null +++ b/src/interfaces/IAggregate.ts @@ -0,0 +1,56 @@ +import { ICommand } from "./ICommand"; +import { Identifier } from "./Identifier"; +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; + +/** + * 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; + diff --git a/src/interfaces/IAggregateSnapshotStorage.ts b/src/interfaces/IAggregateSnapshotStorage.ts new file mode 100644 index 0000000..41c293d --- /dev/null +++ b/src/interfaces/IAggregateSnapshotStorage.ts @@ -0,0 +1,8 @@ +import { Identifier } from "./Identifier"; +import { IEvent } from "./IEvent"; + +export interface IAggregateSnapshotStorage { + getAggregateSnapshot(aggregateId: Identifier): Promise | undefined> | IEvent | undefined; + + saveAggregateSnapshot(snapshotEvent: IEvent): Promise | void; +} diff --git a/src/interfaces/ICommand.ts b/src/interfaces/ICommand.ts new file mode 100644 index 0000000..95b5a2b --- /dev/null +++ b/src/interfaces/ICommand.ts @@ -0,0 +1,3 @@ +import { IMessage } from "./IMessage"; + +export type ICommand = IMessage; diff --git a/src/interfaces/ICommandBus.ts b/src/interfaces/ICommandBus.ts new file mode 100644 index 0000000..87e2f59 --- /dev/null +++ b/src/interfaces/ICommandBus.ts @@ -0,0 +1,18 @@ +import { ICommand } from "./ICommand"; +import { IEventSet } from "./IEventSet"; +import { IMessageHandler, IObservable } from "./IObservable"; +import { IObserver } from "./IObserver"; + +export interface ICommandBus extends IObservable { + send(commandType: string, aggregateId: string, 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; +} diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts new file mode 100644 index 0000000..4d54f07 --- /dev/null +++ b/src/interfaces/IEvent.ts @@ -0,0 +1,6 @@ +import { IMessage } from "./IMessage"; + +export type IEvent = IMessage & { + /** Unique event identifier */ + id?: string; +}; diff --git a/src/interfaces/IEventLocker.ts b/src/interfaces/IEventLocker.ts new file mode 100644 index 0000000..0d6c5a4 --- /dev/null +++ b/src/interfaces/IEventLocker.ts @@ -0,0 +1,34 @@ +import { IEvent } from "./IEvent"; +import { isObject } from "./isObject"; + +/** + * Interface for tracking event processing state to prevent concurrent processing + * by multiple processes. + */ +export interface IEventLocker { + + /** + * Retrieves the last projected event, + * allowing the projection state to be restored from subsequent events. + */ + getLastEvent(): Promise | IEvent | undefined; + + /** + * Marks an event as projecting to prevent it from being processed + * by another projection instance using the same storage. + * + * @returns `false` if the event is already being processed or has been processed. + */ + tryMarkAsProjecting(event: IEvent): Promise | boolean; + + /** + * Marks an event as projected. + */ + markAsProjected(event: IEvent): Promise | void; +} + +export const isEventLocker = (view: unknown): view is IEventLocker => + isObject(view) + && 'getLastEvent' in view + && 'tryMarkAsProjecting' in view + && 'markAsProjected' in view; diff --git a/src/interfaces/IEventReceptor.ts b/src/interfaces/IEventReceptor.ts new file mode 100644 index 0000000..6059e78 --- /dev/null +++ b/src/interfaces/IEventReceptor.ts @@ -0,0 +1,6 @@ +import { IEventStore } from "./IEventStore"; +import { IObserver } from "./IObserver"; + +export interface IEventReceptor extends IObserver { + subscribe(eventStore: IEventStore): void; +} diff --git a/src/interfaces/IEventSet.ts b/src/interfaces/IEventSet.ts new file mode 100644 index 0000000..fcde354 --- /dev/null +++ b/src/interfaces/IEventSet.ts @@ -0,0 +1,6 @@ +import { IEvent } from "./IEvent"; + +/** + * @deprecated Try to use `IEventStream` instead + */ +export type IEventSet = ReadonlyArray>; diff --git a/src/interfaces/IEventStorage.ts b/src/interfaces/IEventStorage.ts new file mode 100644 index 0000000..cbd1cfe --- /dev/null +++ b/src/interfaces/IEventStorage.ts @@ -0,0 +1,29 @@ +import { Identifier } from "./Identifier"; +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; +import { IEventStream } from "./IEventStream"; + +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 IEventStorage { + /** + * Create unique identifier + */ + getNewId(): Identifier | Promise; + + commitEvents(events: IEventSet): Promise; + + getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; + + getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): Promise | IEventStream; + + getSagaEvents(sagaId: Identifier, options: EventQueryBefore): Promise | IEventStream; +} diff --git a/src/interfaces/IEventStore.ts b/src/interfaces/IEventStore.ts new file mode 100644 index 0000000..21954f6 --- /dev/null +++ b/src/interfaces/IEventStore.ts @@ -0,0 +1,26 @@ +import { Identifier } from "./Identifier"; +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; +import { EventQueryAfter, EventQueryBefore } from "./IEventStorage"; +import { IEventStream } from "./IEventStream"; +import { IMessageHandler, IObservable } from "./IObservable"; + +export interface IEventStore extends IObservable { + readonly snapshotsSupported?: boolean; + + getNewId(): Identifier | Promise; + + commit(events: IEventSet): Promise; + + getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; + + getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): IEventStream; + + getSagaEvents(sagaId: Identifier, options: EventQueryBefore): IEventStream; + + once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise; + + queue(name: string): IObservable; + + registerSagaStarters(startsWith: string[] | undefined): void; +} diff --git a/src/interfaces/IEventStream.ts b/src/interfaces/IEventStream.ts new file mode 100644 index 0000000..f8c9337 --- /dev/null +++ b/src/interfaces/IEventStream.ts @@ -0,0 +1,3 @@ +import { IEvent } from "./IEvent"; + +export type IEventStream = AsyncIterableIterator>; 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..c40dc95 --- /dev/null +++ b/src/interfaces/IMessage.ts @@ -0,0 +1,15 @@ +import { Identifier } from "./Identifier"; + +export interface IMessage { + /** Event or command type */ + type: string; + + aggregateId?: Identifier; + aggregateVersion?: number; + + sagaId?: Identifier; + sagaVersion?: number; + + payload?: TPayload; + context?: any; +} diff --git a/src/interfaces/IMessageBus.ts b/src/interfaces/IMessageBus.ts new file mode 100644 index 0000000..c20af13 --- /dev/null +++ b/src/interfaces/IMessageBus.ts @@ -0,0 +1,8 @@ +import { ICommand } from "./ICommand"; +import { IEvent } from "./IEvent"; +import { IObservable } from "./IObservable"; + +export interface IMessageBus extends IObservable { + send(command: ICommand): Promise; + publish(event: IEvent): Promise; +} diff --git a/src/interfaces/IObjectStorage.ts b/src/interfaces/IObjectStorage.ts new file mode 100644 index 0000000..1207c37 --- /dev/null +++ b/src/interfaces/IObjectStorage.ts @@ -0,0 +1,13 @@ +import { Identifier } from "./Identifier"; + +export interface IObjectStorage { + get(id: Identifier): Promise | TRecord | undefined; + + create(id: Identifier, r: TRecord): Promise | any; + + update(id: Identifier, cb: (r: TRecord) => TRecord): Promise | any; + + updateEnforcingNew(id: Identifier, cb: (r?: TRecord) => TRecord): Promise | any; + + delete(id: Identifier): Promise | any; +} diff --git a/src/interfaces/IObservable.ts b/src/interfaces/IObservable.ts new file mode 100644 index 0000000..dc824fd --- /dev/null +++ b/src/interfaces/IObservable.ts @@ -0,0 +1,20 @@ +export interface IMessageHandler { + (...args: any[]): any | Promise +}; + +export interface IObservable { + /** + * Setup a listener for a specific event type + */ + on(type: string, handler: IMessageHandler): void; + + /** + * Remove previously installed listener + */ + off(type: string, handler: IMessageHandler): void; + + /** + * Get or create a named queue, which delivers events to a single handler only + */ + queue?(name: string): IObservable; +} diff --git a/src/interfaces/IObserver.ts b/src/interfaces/IObserver.ts new file mode 100644 index 0000000..6f1365f --- /dev/null +++ b/src/interfaces/IObserver.ts @@ -0,0 +1,5 @@ +import { IObservable } from "./IObservable"; + +export interface IObserver { + subscribe(observable: IObservable): void; +} diff --git a/src/interfaces/IProjection.ts b/src/interfaces/IProjection.ts new file mode 100644 index 0000000..aaa6721 --- /dev/null +++ b/src/interfaces/IProjection.ts @@ -0,0 +1,20 @@ +import { IEvent } from "./IEvent"; +import { IEventStore } from "./IEventStore"; +import { IObserver } from "./IObserver"; + +export interface IProjection extends IObserver { + readonly view: TView; + + subscribe(eventStore: IEventStore): Promise; + + project(event: IEvent): Promise; +} + +export interface IProjectionConstructor { + new(c?: any): IProjection; + readonly handles?: string[]; +} + +export interface IViewFactory { + (options: { schemaVersion: string }): TView; +} diff --git a/src/interfaces/ISaga.ts b/src/interfaces/ISaga.ts new file mode 100644 index 0000000..8507ac1 --- /dev/null +++ b/src/interfaces/ISaga.ts @@ -0,0 +1,37 @@ +import { ICommand } from "./ICommand"; +import { Identifier } from "./Identifier"; +import { IEvent } from "./IEvent"; +import { IEventSet } from "./IEventSet"; + +export interface ISaga { + /** Unique Saga ID */ + readonly id: Identifier; + + /** List of commands emitted by Saga */ + readonly uncommittedMessages: ICommand[]; + + /** Main entry point for Saga events */ + apply(event: IEvent): void | Promise; + + /** Reset emitted commands when they are not longer needed */ + resetUncommittedMessages(): void; + + onError?(error: Error, options: { event: IEvent, command: ICommand }): void; +} + +export type ISagaConstructorParams = { + id: Identifier, + events?: IEventSet +}; + +export type ISagaFactory = (options: ISagaConstructorParams) => ISaga; + +export interface ISagaConstructor { + new(options: ISagaConstructorParams): ISaga; + + /** List of event types that trigger new saga start */ + readonly startsWith: string[]; + + /** List of events being handled by Saga */ + readonly handles: string[]; +} diff --git a/src/interfaces/IViewLocker.ts b/src/interfaces/IViewLocker.ts new file mode 100644 index 0000000..238479d --- /dev/null +++ b/src/interfaces/IViewLocker.ts @@ -0,0 +1,46 @@ +import { isObject } from "./isObject"; + +/** + * Interface for managing view restoration state to prevent early access to an inconsistent view + * or concurrent restoration by another process. + */ +export interface IViewLocker { + + /** + * Indicates whether the view is fully restored and ready to accept new event projections. + */ + ready: boolean; + + /** + * Locks the view to prevent external read/write operations. + * + * @returns `true` if the lock is successfully acquired, `false` otherwise. + */ + lock(): Promise | boolean; + + /** + * Unlocks the view, allowing external read/write operations to resume. + */ + unlock(): Promise | void; + + /** + * Waits until the view is fully restored and ready to accept new events. + * + * @param eventType The event type to listen for (`"ready"`). + * @returns A promise that resolves when the view is ready. + */ + once(eventType: "ready"): Promise; +} + +/** + * Checks if a given object conforms to the `IViewLocker` interface. + * + * @param view The object to check. + * @returns `true` if the object implements `IViewLocker`, `false` otherwise. + */ +export const isViewLocker = (view: unknown): view is IViewLocker => + isObject(view) + && 'ready' in view + && 'lock' in view + && 'unlock' in view + && 'once' in view; diff --git a/src/interfaces/Identifier.ts b/src/interfaces/Identifier.ts new file mode 100644 index 0000000..f31f1fb --- /dev/null +++ b/src/interfaces/Identifier.ts @@ -0,0 +1 @@ +export type Identifier = string | number; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..b81c515 --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,21 @@ +export * from './IAggregate'; +export * from './IAggregateSnapshotStorage'; +export * from './ICommand'; +export * from './ICommandBus'; +export * from './Identifier'; +export * from './IEvent'; +export * from './IEventLocker'; +export * from './IEventReceptor'; +export * from './IEventSet'; +export * from './IEventStorage'; +export * from './IEventStore'; +export * from './IEventStream'; +export * from './ILogger'; +export * from './IMessage'; +export * from './IMessageBus'; +export * from './IObjectStorage'; +export * from './IObservable'; +export * from './IObserver'; +export * from './IProjection'; +export * from './ISaga'; +export * from './IViewLocker'; diff --git a/src/interfaces/isObject.ts b/src/interfaces/isObject.ts new file mode 100644 index 0000000..8c26dac --- /dev/null +++ b/src/interfaces/isObject.ts @@ -0,0 +1,5 @@ +export const isObject = (obj: unknown): obj is {} => + typeof obj === 'object' + && obj !== null + && !(obj instanceof Date) + && !Array.isArray(obj); diff --git a/src/utils/CompoundEmitter.ts b/src/utils/CompoundEmitter.ts new file mode 100644 index 0000000..3e2c2a5 --- /dev/null +++ b/src/utils/CompoundEmitter.ts @@ -0,0 +1,45 @@ +import { IObservable, IMessageHandler } from "../interfaces"; +import { isIObservable } from "./isIObservable"; + +interface IObservableQueueProvider extends Required> { } + +const isObservableQueueProvider = (obj: any): obj is IObservableQueueProvider => + obj + && 'queue' in obj + && typeof obj.queue === 'function'; + +export class CompoundEmitter implements IObservable { + + #emitters: IObservable[]; + #queueProvider?: IObservableQueueProvider; + + constructor(...emitters: any[]) { + const observableEmitters = emitters.filter(isIObservable); + if (!observableEmitters.length) + throw new TypeError('none of the arguments implement IObservable interface'); + + const queueProviders = emitters.filter(isObservableQueueProvider); + if (queueProviders.length > 1) + throw new TypeError('more than one argument implements IObservable `queue` method'); + + this.#emitters = observableEmitters; + this.#queueProvider = queueProviders[0]; + } + + on(type: string, handler: IMessageHandler): void { + for (const emitter of this.#emitters) + emitter.on(type, handler); + } + + off(type: string, handler: IMessageHandler): void { + for (const emitter of this.#emitters) + emitter.off(type, handler); + } + + queue(name: string): IObservable { + if (!this.#queueProvider) + throw new Error('none of the emitters support named queues'); + + return this.#queueProvider.queue(name); + } +} 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/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..560c457 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,12 @@ +export * from './CompoundEmitter'; export * from './getClassName'; export * from './getHandler'; -export * from './validateHandlers'; export * from './getMessageHandlerNames'; -export * from './getHandledMessageTypes'; +export * from './isClass'; +export * from './isIEventStorage'; +export * from './isIMessageBus'; +export * from './isIObservable'; +export * from './iteratorToArray'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; -export * from './isClass'; +export * from './validateHandlers'; diff --git a/src/utils/isIEventStorage.ts b/src/utils/isIEventStorage.ts new file mode 100644 index 0000000..bbe7cf2 --- /dev/null +++ b/src/utils/isIEventStorage.ts @@ -0,0 +1,8 @@ +import { IEventStorage } from "../interfaces"; + +export const isIEventStorage = (storage: IEventStorage): storage is IEventStorage => storage + && typeof storage.getNewId === 'function' + && typeof storage.commitEvents === 'function' + && typeof storage.getEventsByTypes === 'function' + && typeof storage.getAggregateEvents === 'function' + && typeof storage.getSagaEvents === 'function'; diff --git a/src/utils/isIMessageBus.ts b/src/utils/isIMessageBus.ts new file mode 100644 index 0000000..4a47228 --- /dev/null +++ b/src/utils/isIMessageBus.ts @@ -0,0 +1,9 @@ +import { IMessageBus } from "../interfaces"; +import { isIObservable } from "."; + +export 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'; diff --git a/src/utils/isIObservable.ts b/src/utils/isIObservable.ts new file mode 100644 index 0000000..191627c --- /dev/null +++ b/src/utils/isIObservable.ts @@ -0,0 +1,7 @@ +import { IObservable } from "../interfaces"; + +export const isIObservable = (obj: IObservable | any): obj is IObservable => obj + && 'on' in obj + && typeof obj.on === 'function' + && 'off' in obj + && typeof obj.off === 'function'; 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/subscribe.ts b/src/utils/subscribe.ts index af09aca..28982b9 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 { getMessageHandlerNames } from "./getMessageHandlerNames"; const unique = (arr: T[]): T[] => [...new Set(arr)]; +/** + * Get a list of message types handled by observer + */ +export function getHandledMessageTypes(observerInstanceOrClass: (object | Function)): string[] { + if (!observerInstanceOrClass) + throw new TypeError('observerInstanceOrClass argument required'); + + const prototype = Object.getPrototypeOf(observerInstanceOrClass); + if (prototype && prototype.constructor && prototype.constructor.handles) + return prototype.constructor.handles; + + return getMessageHandlerNames(observerInstanceOrClass); +} + /** * Subscribe observer to observable */ @@ -35,11 +49,11 @@ export function subscribe( for (const messageType of unique(subscribeTo)) { const handler = masterHandler || getHandler(observer, messageType); - if (!handler) + if (!handler) throw new Error(`'${messageType}' handler is not defined or not a function`); if (queueName) { - if(!observable.queue) + if (!observable.queue) throw new TypeError('Observer does not support named queues'); observable.queue(queueName).on(messageType, handler); diff --git a/tests/integration/SqliteView.test.ts b/tests/integration/SqliteView.test.ts new file mode 100644 index 0000000..c84797d --- /dev/null +++ b/tests/integration/SqliteView.test.ts @@ -0,0 +1,127 @@ + +import { existsSync, unlinkSync } from 'fs'; +import { AbstractProjection, IEvent } from '../../src'; +import { SqliteObjectView } from '../../src/infrastructure/sqlite'; +import * as createDb from 'better-sqlite3'; + +type UserPayload = { + name: string; +} + +class MyDumbProjection extends AbstractProjection> { + + async userCreated(e: IEvent) { + if (typeof e.aggregateId !== 'string') + throw new TypeError('e.aggregateId is required'); + if (!e.payload) + throw new TypeError('e.payload is required'); + + await this.view.create(e.aggregateId, e.payload); + } + + async userModified(e: IEvent) { + if (typeof e.aggregateId !== 'string') + throw new TypeError('e.aggregateId is required'); + if (!e.payload) + throw new TypeError('e.payload is required'); + + await this.view.update(e.aggregateId, u => e.payload); + } +} + + +describe.only('SqliteView', () => { + + let viewModelSqliteDb: import('better-sqlite3').Database; + + const logState = () => { + console.log({ + tbl_view_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_view_lock`).all(), + tbl_test_1_event_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1_event_lock`).all(), + tbl_test_1: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1`).all() + }); + } + + 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 10_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: 5_000 }, (v, i) => ({ + id1: `${i}A`, + id2: `${i}B`, + id3: `${i}C` + })); + + console.time(); + + for (const { id1: aggregateId, id2, id3 } of aggregateIds) { + await p.project({ + type: 'userCreated', + id: id2, + aggregateId, + payload: { + name: 'Jon' + } + }); + + await p.project({ + type: 'userModified', + id: id3, + aggregateId, + payload: { + name: 'Jon Doe' + } + }); + } + + console.timeEnd(); + + // logState(); + + // const user = await p.view.get(aggregateId); + + // expect(user).toEqual({ + // name: 'Jon Doe' + // }); + }); +}); diff --git a/tests/unit/AbstractProjection.test.ts b/tests/unit/AbstractProjection.test.ts index f5cff80..4abada0 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -2,7 +2,7 @@ import { expect, assert, AssertionError } from 'chai'; import * as sinon from 'sinon'; import { AbstractProjection, InMemoryView, InMemoryEventStorage, EventStore, InMemoryMessageBus } from '../../src'; -class MyProjection extends AbstractProjection { +class MyProjection extends AbstractProjection> { static get handles() { return ['somethingHappened']; } @@ -21,20 +21,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 +43,7 @@ describe('AbstractProjection', function () { beforeEach(() => { observable = { - getAllEvents() { + getEventsByTypes() { return []; }, on() { } @@ -54,7 +53,7 @@ describe('AbstractProjection', function () { it('subscribes to all handlers defined', () => { - class ProjectionWithoutHandles extends AbstractProjection { + class ProjectionWithoutHandles extends AbstractProjection { somethingHappened() { } somethingHappened2() { } } @@ -68,7 +67,7 @@ describe('AbstractProjection', function () { it('ignores overridden projection methods', () => { - class ProjectionWithoutHandles extends AbstractProjection { + class ProjectionWithoutHandles extends AbstractProjection { somethingHappened() { } /** overridden projection method */ @@ -85,7 +84,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 +105,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 +142,7 @@ describe('AbstractProjection', function () { it('throws, if projection error encountered', () => { es = { - async* getAllEvents() { + async* getEventsByTypes() { yield { type: 'unexpectedEvent' }; } }; @@ -163,8 +162,8 @@ 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 supplementaryEventBus = new InMemoryMessageBus(); + const es = new EventStore({ storage, supplementaryEventBus }); let restored = false; let projected = false; diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index 28698e0..76bd7ec 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -9,7 +9,6 @@ import { EventStore, InMemorySnapshotStorage } from '../../src'; -import { getHandledMessageTypes } from '../../src/utils'; function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); @@ -48,18 +47,18 @@ describe('AggregateCommandHandler', function () { let snapshotStorage: InMemorySnapshotStorage; let eventStore: IEventStore; let commandBus: ICommandBus; - let messageBus: IMessageBus; + let supplementaryEventBus: IMessageBus; let onSpy; let getNewIdSpy; let getAggregateEventsSpy; let commitSpy; beforeEach(() => { - messageBus = new InMemoryMessageBus(); + supplementaryEventBus = new InMemoryMessageBus(); storage = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); - eventStore = new EventStore({ storage, snapshotStorage, messageBus }); + eventStore = new EventStore({ storage, snapshotStorage, supplementaryEventBus }); getNewIdSpy = sinon.spy(eventStore, 'getNewId'); getAggregateEventsSpy = sinon.spy(eventStore, 'getAggregateEvents'); commitSpy = sinon.spy(eventStore, 'commit'); @@ -123,7 +122,7 @@ describe('AggregateCommandHandler', function () { const handler = new AggregateCommandHandler({ eventStore, aggregateFactory: () => aggregate, - handles: getHandledMessageTypes(aggregate) + handles: MyAggregate.handles }); await handler.execute({ type: 'doSomething', payload: 'test' }); @@ -191,7 +190,7 @@ describe('AggregateCommandHandler', function () { const handler = new AggregateCommandHandler({ eventStore, aggregateFactory: () => aggregate, - handles: getHandledMessageTypes(aggregate) + handles: MyAggregate.handles }); // test diff --git a/tests/unit/CommandBus.test.ts b/tests/unit/CommandBus.test.ts index d763a40..3181ec3 100644 --- a/tests/unit/CommandBus.test.ts +++ b/tests/unit/CommandBus.test.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { InMemoryMessageBus } from '../../src/infrastructure/InMemoryMessageBus'; -import { CommandBus } from '../../src/CommandBus'; +import { InMemoryMessageBus, CommandBus } from '../../src'; describe('CommandBus', function () { diff --git a/tests/unit/CqrsContainerBuilder.test.ts b/tests/unit/CqrsContainerBuilder.test.ts index ca3b63b..2a83b92 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(InMemoryMessageBus).as('supplementaryEventBus'); }); describe('registerAggregate(aggregateType) extension', () => { @@ -87,7 +87,7 @@ describe('CqrsContainerBuilder', function () { describe('registerProjection(typeOrFactory, exposedViewName) extension', () => { - class MyProjection extends AbstractProjection { + class MyProjection extends AbstractProjection { static get handles() { return ['somethingHappened']; } diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index b0a2ef5..5113df0 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -1,10 +1,9 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; 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'; +import { InMemoryEventStorage, InMemorySnapshotStorage, InMemoryMessageBus } from '../../src'; +import { IAggregateSnapshotStorage, IEvent, IEventStorage, IEventStore, IMessageBus } from '../../src/interfaces'; +import { iteratorToArray } from '../../src/utils'; const goodContext = { uid: '1', @@ -40,13 +39,13 @@ describe('EventStore', function () { let es: IEventStore; let storage: IEventStorage; let snapshotStorage: IAggregateSnapshotStorage; - let messageBus: IMessageBus; + let supplementaryEventBus: IMessageBus; beforeEach(() => { storage = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); - messageBus = new InMemoryMessageBus(); - es = new EventStore({ storage, snapshotStorage, messageBus }); + supplementaryEventBus = new InMemoryMessageBus(); + es = new EventStore({ storage, snapshotStorage, supplementaryEventBus }); }); describe('validator', () => { @@ -54,7 +53,7 @@ describe('EventStore', function () { it('allows to validate events before they are committed', () => { const events = [ - { type: 'somethingHappened', aggregateId: 1 } + { type: 'somethingHappened', aggregateId: '1' } ]; return es.commit(events).then(() => { @@ -64,7 +63,7 @@ describe('EventStore', function () { eventValidator: event => { throw new Error('test validation error'); }, - messageBus + supplementaryEventBus }); return es.commit(events).then(() => { @@ -99,7 +98,7 @@ describe('EventStore', function () { await es.commit([goodEvent]); const events: IEvent[] = []; - for await (const e of es.getAllEvents()) + for await (const e of es.getEventsByTypes(['somethingHappened'], {})) events.push(e); expect(events[0]).to.have.property('type', 'somethingHappened'); @@ -154,7 +153,7 @@ describe('EventStore', function () { } }); - es = new EventStore({ storage, messageBus }); + es = new EventStore({ storage, supplementaryEventBus }); return es.commit([goodEvent, goodEvent2]).then(() => { throw new Error('should fail'); @@ -163,33 +162,12 @@ describe('EventStore', function () { 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); - }); }); describe('getNewId', () => { it('retrieves a unique ID for new aggregate from storage', () => Promise.resolve(es.getNewId()).then(id => { - expect(id).to.equal(1); + expect(id).to.equal('1'); })); }); @@ -199,11 +177,12 @@ describe('EventStore', function () { await es.commit([goodEvent, goodEvent2]); - const events = await es.getAggregateEvents(goodEvent.aggregateId); + const events = 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'); + expect(events).to.be.have.property(Symbol.asyncIterator); + + const event = (await events.next()).value; + expect(event).to.have.nested.property('type', 'somethingHappened'); }); it('tries to retrieve aggregate snapshot', async () => { @@ -215,7 +194,7 @@ describe('EventStore', function () { expect(es).to.have.property('snapshotsSupported', true); - const events = await es.getAggregateEvents(goodEvent2.aggregateId); + const events = await iteratorToArray(es.getAggregateEvents(goodEvent2.aggregateId)); expect(snapshotStorage).to.have.nested.property('getAggregateSnapshot.calledOnce', true); expect(storage).to.have.nested.property('getAggregateEvents.calledOnce', true); @@ -231,33 +210,33 @@ describe('EventStore', function () { describe('getSagaEvents(sagaId, options)', () => { - it('returns events committed by saga prior to event that triggered saga execution', () => { + it('returns events committed by saga prior to event that triggered saga execution', async () => { const events = [ - { sagaId: 1, sagaVersion: 1, type: 'somethingHappened' }, - { sagaId: 1, sagaVersion: 2, type: 'anotherHappened' }, - { sagaId: 2, sagaVersion: 1, type: 'somethingHappened' } + { sagaId: '1', sagaVersion: 1, type: 'somethingHappened' }, + { sagaId: '1', sagaVersion: 2, type: 'anotherHappened' }, + { sagaId: '2', sagaVersion: 1, type: 'somethingHappened' } ]; const triggeredBy = events[1]; - return es.commit(events).then(() => es.getSagaEvents(1, { beforeEvent: triggeredBy }).then(events => { + await es.commit(events); - expect(events).to.be.an('Array'); - expect(events).to.have.length(1); - expect(events).to.have.nested.property('[0].type', 'somethingHappened'); - })); + const ii = es.getSagaEvents('1', { beforeEvent: triggeredBy }); + const retrievedEvents = await iteratorToArray(ii); + + expect(retrievedEvents).to.be.an('Array'); + expect(retrievedEvents).to.have.length(1); + expect(retrievedEvents).to.have.nested.property('[0].type', 'somethingHappened'); }); }); - describe('getAllEvents(eventTypes)', () => { + describe('getEventsByTypes(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); + const events = await iteratorToArray(es.getEventsByTypes(['somethingHappened'], {})); expect(events).to.have.length(2); expect(events).to.have.nested.property('[0].aggregateId', '1'); @@ -273,7 +252,7 @@ describe('EventStore', function () { it('fails, when trying to set up second messageType handler within the same node and named queue (Receptors)', () => { - es = new EventStore({ storage, messageBus }); + es = new EventStore({ storage, supplementaryEventBus }); expect(() => { es.queue('namedQueue').on('somethingHappened', () => { }); @@ -290,7 +269,7 @@ describe('EventStore', function () { it('sets up multiple handlers for same messageType, when queue name is not defined (Projections)', () => { - es = new EventStore({ storage, eventStoreConfig: { publishAsync: false }, messageBus }); + es = new EventStore({ storage, supplementaryEventBus }); const projection1Handler = sinon.spy(); const projection2Handler = sinon.spy(); @@ -299,7 +278,7 @@ describe('EventStore', function () { es.on('somethingHappened', projection2Handler); return es.commit([ - { type: 'somethingHappened', aggregateId: 1, aggregateVersion: 0 } + { type: 'somethingHappened', aggregateId: '1', aggregateVersion: 0 } ]).then(() => { expect(projection1Handler).to.have.property('calledOnce', true); expect(projection2Handler).to.have.property('calledOnce', true); diff --git a/tests/unit/SagaEventHandler.test.ts b/tests/unit/SagaEventHandler.test.ts index 83c0f02..5e13ece 100644 --- a/tests/unit/SagaEventHandler.test.ts +++ b/tests/unit/SagaEventHandler.test.ts @@ -22,7 +22,7 @@ class Saga extends AbstractSaga { } followingHappened() { super.enqueue('complete', undefined, { foo: 'bar' }); - } + } onError(error, { command, event }) { super.enqueue('fixError', undefined, { error, command, event }); } @@ -42,9 +42,9 @@ describe('SagaEventHandler', function () { let sagaEventHandler: SagaEventHandler; beforeEach(() => { - const messageBus = new InMemoryMessageBus(); - commandBus = new CommandBus({ messageBus }); - eventStore = new EventStore({ storage: new InMemoryEventStorage(), messageBus }); + const supplementaryEventBus = new InMemoryMessageBus(); + commandBus = new CommandBus({}); + eventStore = new EventStore({ storage: new InMemoryEventStorage(), supplementaryEventBus }); sagaEventHandler = new SagaEventHandler({ sagaType: Saga, eventStore, commandBus }); }); diff --git a/tests/unit/memory/InMemoryEventStorage.test.ts b/tests/unit/memory/InMemoryEventStorage.test.ts new file mode 100644 index 0000000..bd8079c --- /dev/null +++ b/tests/unit/memory/InMemoryEventStorage.test.ts @@ -0,0 +1,127 @@ +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..63974e4 --- /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 95% rename from tests/unit/InMemoryMessageBus.test.ts rename to tests/unit/memory/InMemoryMessageBus.test.ts index 511d5f5..fef5b23 100644 --- a/tests/unit/InMemoryMessageBus.test.ts +++ b/tests/unit/memory/InMemoryMessageBus.test.ts @@ -1,5 +1,5 @@ -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 () { diff --git a/tests/unit/InMemoryView.test.ts b/tests/unit/memory/InMemoryView.test.ts similarity index 97% rename from tests/unit/InMemoryView.test.ts rename to tests/unit/memory/InMemoryView.test.ts index 7bdb6e7..617b931 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/infrastructure/memory/utils'; describe('InMemoryView', function () { diff --git a/tests/unit/sqlite/SqliteEventLocker.test.ts b/tests/unit/sqlite/SqliteEventLocker.test.ts new file mode 100644 index 0000000..ee26dd4 --- /dev/null +++ b/tests/unit/sqlite/SqliteEventLocker.test.ts @@ -0,0 +1,97 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteEventLocker } from '../../../src/infrastructure/sqlite/SqliteEventLocker'; +import { IEvent } from '../../../src/interfaces'; +import { guid } from '../../../src/infrastructure/sqlite'; +import { promisify } from 'util'; +const delay = promisify(setTimeout); + +describe('SqliteEventLocker', function () { + + 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 + }); + jest.useFakeTimers(); + }); + + afterEach(() => { + db.close(); + jest.useRealTimers(); + }); + + it('allows marking an event as projecting', function () { + const result = locker.tryMarkAsProjecting(testEvent); + expect(result).to.be.true; + }); + + it('prevents re-locking an already locked event', function () { + locker.tryMarkAsProjecting(testEvent); + const result = locker.tryMarkAsProjecting(testEvent); + expect(result).to.be.false; + }); + + it('marks an event as projected', function () { + locker.tryMarkAsProjecting(testEvent); + locker.markAsProjected(testEvent); + + const row = db.prepare(`SELECT processed_at FROM test_event_lock WHERE event_id = ?`) + .get(guid(testEvent.id)) as any; + + expect(row).to.exist; + expect(row.processed_at).to.not.be.null; + }); + + it('retrieves the last projected event', function () { + + locker.tryMarkAsProjecting(testEvent); + locker.markAsProjected(testEvent); + + const lastEvent = locker.getLastEvent(); + + expect(lastEvent).to.deep.equal(testEvent); + }); + + it('returns undefined if no event has been projected', function () { + const lastEvent = locker.getLastEvent(); + expect(lastEvent).to.be.undefined; + }); + + it('fails to mark an event as projected if it was never locked', function () { + expect(() => locker.markAsProjected(testEvent)) + .to.throw(Error, `Event ${testEvent.id} could not be marked as processed`); + }); + + it('allows re-locking after TTL expires', async function () { + + locker.tryMarkAsProjecting(testEvent); + + await delay(51); + + const result = locker.tryMarkAsProjecting(testEvent); + expect(result).to.be.true; + }); + + it('fails to update an event if its version is modified in DB', function () { + + locker.tryMarkAsProjecting(testEvent); + + // Modify the event in DB to simulate an external change + db.prepare('UPDATE test_event_lock SET processed_at = ? WHERE event_id = ?') + .run(Date.now(), guid(testEvent.id)); + + // Attempt to finalize the event processing + expect(() => locker.markAsProjected(testEvent)) + .to.throw(Error, `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..ce2aca7 --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { guid, SqliteObjectStorage } from '../../../src/infrastructure/sqlite'; + +describe('SqliteObjectStorage', function () { + let db: import('better-sqlite3').Database; + let storage: SqliteObjectStorage<{ name: string; value: number }>; + + beforeEach(() => { + db = createDb(':memory:'); + storage = new SqliteObjectStorage<{ name: string; value: number }>({ + viewModelSqliteDb: db, + tableName: 'test_objects', + }); + }); + + afterEach(() => { + db.close(); + }); + + it('stores and retrieves an object', async function () { + + const obj = { name: 'Test Object', value: 42 }; + storage.create('0001', obj); + + const retrieved = storage.get('0001'); + expect(retrieved).to.deep.equal(obj); + }); + + it('returns undefined for a non-existent object', async function () { + const retrieved = storage.get('nonexistent'); + expect(retrieved).to.be.undefined; + }); + + it('updates an existing object', async function () { + + storage.create('0002', { name: 'Old Data', value: 5 }); + + storage.update('0002', (r) => ({ ...r, value: 99 })); + + const updated = storage.get('0002'); + expect(updated).to.deep.equal({ name: 'Old Data', value: 99 }); + }); + + it('throws an error when updating a non-existent object', async function () { + + expect(() => storage.update('nonexistent', (r) => ({ ...r, value: 99 }))) + .to.throw(Error, "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).to.be.true; + + const retrieved = storage.get('0003'); + expect(retrieved).to.be.undefined; + }); + + it('returns false when deleting a non-existent object', async function () { + + const deleted = storage.delete('0000'); + expect(deleted).to.be.false; + }); + + it('enforces updating or creating a new object', async function () { + + storage.updateEnforcingNew('0004', () => ({ name: 'Created', value: 1 })); + + let retrieved = storage.get('0004'); + expect(retrieved).to.deep.equal({ name: 'Created', value: 1 }); + + storage.updateEnforcingNew('0004', (r) => ({ ...r!, value: 100 })); + + retrieved = storage.get('0004'); + expect(retrieved).to.deep.equal({ 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'); + + expect(() => storage.get('0005')).to.throw(); + }); +}); diff --git a/tests/unit/sqlite/SqliteObjectView.test.ts b/tests/unit/sqlite/SqliteObjectView.test.ts new file mode 100644 index 0000000..103b4c9 --- /dev/null +++ b/tests/unit/sqlite/SqliteObjectView.test.ts @@ -0,0 +1,73 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteObjectView } from '../../../src/infrastructure/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..c7fecef --- /dev/null +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import * as createDb from 'better-sqlite3'; +import { SqliteViewLocker } from '../../../src/infrastructure/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(); + 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(); + 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'); + }); +}); From 9c8b4c79bd941cf9614ca8c3dc7019d2dbde6ca0 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 21 Mar 2025 23:15:15 +0000 Subject: [PATCH 022/135] 1.0.0-rc.6 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c5d1af..57c2567 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [1.0.0-rc.6](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.5...v1.0.0-rc.6) (2025-03-21) + + +### Changes + +* Support persistent views; Add SQLite infrastructure ([c235573](https://github.com/snatalenko/node-cqrs/commit/c235573678be349d031d1a696cab3993224979a2)) + + # [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) diff --git a/package-lock.json b/package-lock.json index c9330af..b547e2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.5", + "version": "1.0.0-rc.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.5", + "version": "1.0.0-rc.6", "license": "MIT", "dependencies": { "di0": "^1.0.0" diff --git a/package.json b/package.json index 70fcfdd..56f0886 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.5", + "version": "1.0.0-rc.6", "description": "Basic ES6 backbone for CQRS app development", "repository": { "type": "git", From e781f7c6c2e4f7c9f8c4615b170d0d29d3e8f133 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 30 Mar 2025 02:15:41 +0100 Subject: [PATCH 023/135] Change: Move validation, snapshot and event persistence to EventDispatcher pipeline --- docs/entities/Projection/README.md | 2 +- docs/infrastructure/README.md | 6 +- examples/user-domain-tests/index.test.js | 4 - examples/user-domain/index.js | 21 +- package-lock.json | 14 + package.json | 10 +- src/AbstractProjection.ts | 2 +- src/AggregateCommandHandler.ts | 4 +- src/CommandBus.ts | 15 +- src/CqrsContainerBuilder.ts | 17 + src/Event.ts | 11 - src/EventDispatcher.ts | 141 ++++++ src/EventStore.ts | 174 +++----- .../EventPersistenceProcessor.ts | 34 ++ .../EventValidationProcessor.ts | 27 ++ .../SnapshotPersistenceProcessor.ts | 66 +++ src/dispatch-pipeline/index.ts | 3 + .../InMemoryEventStorage.ts | 25 +- .../memory => in-memory}/InMemoryLock.ts | 0 .../InMemoryMessageBus.ts | 2 +- .../InMemorySnapshotStorage.ts | 12 +- .../memory => in-memory}/InMemoryView.ts | 2 +- .../memory => in-memory}/index.ts | 0 .../memory => in-memory}/utils/Deferred.ts | 0 .../memory => in-memory}/utils/index.ts | 0 .../memory => in-memory}/utils/nextCycle.ts | 0 src/index.ts | 4 +- src/interfaces/IAggregateSnapshotStorage.ts | 2 + src/interfaces/ICommandBus.ts | 6 +- src/interfaces/IEvent.ts | 1 + src/interfaces/IEventBus.ts | 11 + src/interfaces/IEventDispatcher.ts | 7 + src/interfaces/IEventProcessor.ts | 24 ++ src/interfaces/IEventSet.ts | 3 - src/interfaces/IEventStorage.ts | 34 +- src/interfaces/IEventStore.ts | 25 +- src/interfaces/IIdentifierProvider.ts | 16 + src/interfaces/IMessage.ts | 6 + src/interfaces/IObservable.ts | 12 +- src/interfaces/index.ts | 4 + .../sqlite/AbstractSqliteObjectProjection.ts | 4 +- .../sqlite/AbstractSqliteView.ts | 4 +- .../sqlite/SqliteEventLocker.ts | 2 +- .../sqlite/SqliteObjectStorage.ts | 2 +- .../sqlite/SqliteObjectView.ts | 2 +- .../sqlite/SqliteViewLocker.ts | 4 +- .../sqlite/commonParams.ts | 0 src/{infrastructure => }/sqlite/index.ts | 0 .../sqlite/queries/eventLockTableInit.ts | 0 .../sqlite/queries/index.ts | 0 .../sqlite/queries/viewLockTableInit.ts | 0 .../sqlite/utils/getEventId.ts | 4 +- src/{infrastructure => }/sqlite/utils/guid.ts | 0 .../sqlite/utils/index.ts | 0 src/utils/CompoundEmitter.ts | 45 -- src/utils/delay.ts | 8 + src/utils/index.ts | 6 +- src/utils/isIEventStorage.ts | 8 - src/utils/isIMessageBus.ts | 9 - src/utils/isIObservable.ts | 7 - src/utils/notEmpty.ts | 1 + .../{ => sqlite}/SqliteView.test.ts | 45 +- tests/unit/AbstractProjection.test.ts | 14 +- tests/unit/AggregateCommandHandler.test.ts | 11 +- tests/unit/CqrsContainerBuilder.test.ts | 6 +- tests/unit/EventDispatcher.test.ts | 100 +++++ tests/unit/EventStore.test.ts | 401 ++++++------------ tests/unit/SagaEventHandler.test.ts | 9 +- tests/unit/memory/InMemoryMessageBus.test.ts | 1 - tests/unit/memory/InMemoryView.test.ts | 2 +- tests/unit/sqlite/SqliteEventLocker.test.ts | 4 +- tests/unit/sqlite/SqliteObjectStorage.test.ts | 2 +- tests/unit/sqlite/SqliteObjectView.test.ts | 2 +- tests/unit/sqlite/SqliteViewLocker.test.ts | 2 +- 74 files changed, 840 insertions(+), 612 deletions(-) create mode 100644 src/EventDispatcher.ts create mode 100644 src/dispatch-pipeline/EventPersistenceProcessor.ts create mode 100644 src/dispatch-pipeline/EventValidationProcessor.ts create mode 100644 src/dispatch-pipeline/SnapshotPersistenceProcessor.ts create mode 100644 src/dispatch-pipeline/index.ts rename src/{infrastructure/memory => in-memory}/InMemoryEventStorage.ts (84%) rename src/{infrastructure/memory => in-memory}/InMemoryLock.ts (100%) rename src/{infrastructure/memory => in-memory}/InMemoryMessageBus.ts (99%) rename src/{infrastructure/memory => in-memory}/InMemorySnapshotStorage.ts (70%) rename src/{infrastructure/memory => in-memory}/InMemoryView.ts (98%) rename src/{infrastructure/memory => in-memory}/index.ts (100%) rename src/{infrastructure/memory => in-memory}/utils/Deferred.ts (100%) rename src/{infrastructure/memory => in-memory}/utils/index.ts (100%) rename src/{infrastructure/memory => in-memory}/utils/nextCycle.ts (100%) create mode 100644 src/interfaces/IEventBus.ts create mode 100644 src/interfaces/IEventDispatcher.ts create mode 100644 src/interfaces/IEventProcessor.ts create mode 100644 src/interfaces/IIdentifierProvider.ts rename src/{infrastructure => }/sqlite/AbstractSqliteObjectProjection.ts (85%) rename src/{infrastructure => }/sqlite/AbstractSqliteView.ts (92%) rename src/{infrastructure => }/sqlite/SqliteEventLocker.ts (98%) rename src/{infrastructure => }/sqlite/SqliteObjectStorage.ts (98%) rename src/{infrastructure => }/sqlite/SqliteObjectView.ts (97%) rename src/{infrastructure => }/sqlite/SqliteViewLocker.ts (97%) rename src/{infrastructure => }/sqlite/commonParams.ts (100%) rename src/{infrastructure => }/sqlite/index.ts (100%) rename src/{infrastructure => }/sqlite/queries/eventLockTableInit.ts (100%) rename src/{infrastructure => }/sqlite/queries/index.ts (100%) rename src/{infrastructure => }/sqlite/queries/viewLockTableInit.ts (100%) rename src/{infrastructure => }/sqlite/utils/getEventId.ts (83%) rename src/{infrastructure => }/sqlite/utils/guid.ts (100%) rename src/{infrastructure => }/sqlite/utils/index.ts (100%) delete mode 100644 src/utils/CompoundEmitter.ts create mode 100644 src/utils/delay.ts delete mode 100644 src/utils/isIEventStorage.ts delete mode 100644 src/utils/isIMessageBus.ts delete mode 100644 src/utils/isIObservable.ts create mode 100644 src/utils/notEmpty.ts rename tests/integration/{ => sqlite}/SqliteView.test.ts (73%) create mode 100644 tests/unit/EventDispatcher.test.ts diff --git a/docs/entities/Projection/README.md b/docs/entities/Projection/README.md index 3e797c7..0a94cf6 100644 --- a/docs/entities/Projection/README.md +++ b/docs/entities/Projection/README.md @@ -4,7 +4,7 @@ Projection is an Observer, that listens to events and updates an associated View ## Projection View Restoring -By default, an [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryViewStorage.js) is used. That means that upon application start, Projection queries all known events from the EventStore and projects them to the view. Once this process is complete, the view's `ready` property gets switched from *false* to *true*. +By default, an [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryViewStorage.js) is used. That means that upon application start, Projection queries all known events from the EventStore and projects them to the view. Once this process is complete, the view's `ready` property gets switched from *false* to *true*. ## Projection Event Handlers diff --git a/docs/infrastructure/README.md b/docs/infrastructure/README.md index 762deaf..93a194c 100644 --- a/docs/infrastructure/README.md +++ b/docs/infrastructure/README.md @@ -2,9 +2,9 @@ node-cqrs comes with a set of In-Memory infrastructure service implementations. They are suitable for test purposes, since all data is persisted in process memory only: -* [InMemoryEventStorage](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryEventStorage.js) -* [InMemoryMessageBus](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryMessageBus.js) -* [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/infrastructure/InMemoryView.js) +* [InMemoryEventStorage](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryEventStorage.js) +* [InMemoryMessageBus](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryMessageBus.js) +* [InMemoryView](https://github.com/snatalenko/node-cqrs/blob/master/src/in-memory/InMemoryView.js) The following storage/bus implementations persist data in external storages and can be used in production: diff --git a/examples/user-domain-tests/index.test.js b/examples/user-domain-tests/index.test.js index 2b87755..e5f4378 100644 --- a/examples/user-domain-tests/index.test.js +++ b/examples/user-domain-tests/index.test.js @@ -2,7 +2,6 @@ const { expect } = require('chai'); const { createContainer, createBaseInstances } = require('../user-domain'); -const { nextCycle } = require('../../src/infrastructure/memory/utils'); describe('user-domain example', () => { @@ -10,9 +9,6 @@ describe('user-domain example', () => { const { commandBus, eventStore } = container; - // HACK: let projection restoring to start before emitting new events - await nextCycle(); - // we send a command to an aggregate that does not exist yet (userAggregateId = undefined), // a new instance will be created automatically let userAggregateId; diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index 75d239c..6a66706 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -6,8 +6,10 @@ const { CommandBus, EventStore, AggregateCommandHandler, - InMemoryMessageBus + InMemoryMessageBus, + EventDispatcher } = require('../..'); // node-cqrs +const { EventPersistenceProcessor } = require('../../src/dispatch-pipeline'); const UserAggregate = require('./UserAggregate'); const UsersProjection = require('./UsersProjection'); @@ -19,7 +21,7 @@ exports.createContainer = () => { // register infrastructure services builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('supplementaryEventBus'); + builder.register(InMemoryMessageBus).as('eventBus'); // register domain entities builder.registerAggregate(UserAggregate); @@ -34,19 +36,22 @@ exports.createContainer = () => { */ exports.createBaseInstances = () => { // create infrastructure services - const messageBus = new InMemoryMessageBus(); + const eventBus = new InMemoryMessageBus(); const storage = new InMemoryEventStorage(); - const eventStore = new EventStore({ storage, supplementaryEventBus: messageBus }); - const commandBus = new CommandBus({ messageBus }); + const eventDispatcher = new EventDispatcher({ eventBus }) + eventDispatcher.addPipelineProcessor(new EventPersistenceProcessor({ storage })); - /** @type {IAggregateConstructor} */ + const eventStore = new EventStore({ storage, eventBus, eventDispatcher }); + const commandBus = new CommandBus(); + + /** @type {import('../..').IAggregateConstructor} */ const aggregateType = UserAggregate; - /** @type {ICommandHandler} */ + /** @type {import('../..').ICommandHandler} */ const userCommandHandler = new AggregateCommandHandler({ eventStore, aggregateType }); userCommandHandler.subscribe(commandBus); - /** @type {IProjection} */ + /** @type {import('../..').IProjection} */ const usersProjection = new UsersProjection(); usersProjection.subscribe(eventStore); diff --git a/package-lock.json b/package-lock.json index b547e2d..6a360a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0-rc.6", "license": "MIT", "dependencies": { + "async-iterable-buffer": "^1.0.0", + "async-parallel-pipe": "^1.0.1", "di0": "^1.0.0" }, "devDependencies": { @@ -1355,6 +1357,18 @@ "dev": true, "license": "MIT" }, + "node_modules/async-iterable-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-iterable-buffer/-/async-iterable-buffer-1.0.0.tgz", + "integrity": "sha512-pZn6MjtoJFyr+RVy3O0BSRb8ibjSX9BlEh8trEqdtpV4DdnH7oM28Ke14r4QZuzQnSGj3tg1CrEToIxuvsfqsw==", + "license": "MIT" + }, + "node_modules/async-parallel-pipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-parallel-pipe/-/async-parallel-pipe-1.0.1.tgz", + "integrity": "sha512-LnZtmPVzwMMvFywrQ2VKSsFlIWXuQogqkOfS5mxKoU3u0O9Di5zvVHMYW0HW/FdjTTz11l9a0pKw/mQ7DQgCww==", + "license": "MIT" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", diff --git a/package.json b/package.json index 56f0886..fd05acc 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,16 @@ ], "main": "./dist/index.js", "types": "./src/index.ts", + "exports": { + ".": "./dist/index.js", + "./sqlite": "./dist/sqlite/index.js" + }, "engines": { "node": ">=10.3.0" }, "scripts": { "pretest": "npm run build", - "test": "jest --verbose tests/unit", + "test": "jest tests/unit", "test:coverage": "jest --collect-coverage tests/unit", "pretest:integration": "npm run build", "test:integration": "jest --verbose examples/user-domain-tests tests/integration", @@ -40,6 +44,8 @@ "license": "MIT", "homepage": "https://github.com/snatalenko/node-cqrs#readme", "dependencies": { + "async-iterable-buffer": "^1.0.0", + "async-parallel-pipe": "^1.0.1", "di0": "^1.0.0" }, "devDependencies": { @@ -60,4 +66,4 @@ "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } -} +} \ No newline at end of file diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index a999f50..764b589 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -1,5 +1,5 @@ import { describe } from './Event'; -import { InMemoryView } from './infrastructure/memory/InMemoryView'; +import { InMemoryView } from './in-memory/InMemoryView'; import { IViewLocker, IEventLocker, diff --git a/src/AggregateCommandHandler.ts b/src/AggregateCommandHandler.ts index af886d7..7339890 100644 --- a/src/AggregateCommandHandler.ts +++ b/src/AggregateCommandHandler.ts @@ -119,12 +119,12 @@ export class AggregateCommandHandler implements ICommandHandler { if (!events.length) return events; - if (aggregate.shouldTakeSnapshot && this.#eventStore.snapshotsSupported) { + if (aggregate.shouldTakeSnapshot) { aggregate.takeSnapshot(); events = aggregate.changes; } - await this.#eventStore.commit(events); + await this.#eventStore.dispatch(events); return events; } diff --git a/src/CommandBus.ts b/src/CommandBus.ts index e7cfbbf..cf212b0 100644 --- a/src/CommandBus.ts +++ b/src/CommandBus.ts @@ -1,4 +1,4 @@ -import { InMemoryMessageBus } from "./infrastructure/memory"; +import { InMemoryMessageBus } from "./in-memory"; import { ICommand, ICommandBus, @@ -14,18 +14,15 @@ 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; } /** diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 9aaecf8..cefc476 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -4,6 +4,13 @@ import { AggregateCommandHandler } from './AggregateCommandHandler'; import { CommandBus } from './CommandBus'; import { EventStore } from './EventStore'; import { SagaEventHandler } from './SagaEventHandler'; +import { EventDispatcher } from './EventDispatcher'; +import { InMemoryMessageBus } from './in-memory'; +import { + EventValidationProcessor, + SnapshotPersistenceProcessor, + EventPersistenceProcessor +} from './dispatch-pipeline'; import { isClass @@ -32,8 +39,18 @@ export class CqrsContainerBuilder extends ContainerBuilder { singletones: object }) { super(options); + super.register(InMemoryMessageBus).as('eventBus'); super.register(EventStore).as('eventStore'); super.register(CommandBus).as('commandBus'); + + super.register(container => { + const eventDispatcher = new EventDispatcher(container); + eventDispatcher.addPipelineProcessor(new EventValidationProcessor(container)); + eventDispatcher.addPipelineProcessor(new SnapshotPersistenceProcessor(container)); + eventDispatcher.addPipelineProcessor(new EventPersistenceProcessor(container)); + + return eventDispatcher; + }).as('eventDispatcher'); } /** Register command handler, which will be subscribed to commandBus upon instance creation */ diff --git a/src/Event.ts b/src/Event.ts index 83b2043..0dea892 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(/==$/, ''); /** * Get text description of an event for logging purposes @@ -37,7 +30,3 @@ export function validate(event: IEvent) { 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/EventDispatcher.ts b/src/EventDispatcher.ts new file mode 100644 index 0000000..2a29fc3 --- /dev/null +++ b/src/EventDispatcher.ts @@ -0,0 +1,141 @@ +import { + EventBatch, + IEvent, + IEventDispatcher, + IEventProcessor, + IEventSet, + IEventBus +} from "./interfaces"; +import { parallelPipe } from 'async-parallel-pipe'; +import { AsyncIterableBuffer } from 'async-iterable-buffer'; +import { notEmpty } from "./utils"; +import { InMemoryMessageBus } from "./in-memory"; + +type EventBatchEnvelope = { + data: EventBatch<{ event?: IEvent }>; + error?: Error; + resolve: (event: IEvent[]) => void; + reject: (error: Error) => void; +} + +export class EventDispatcher implements IEventDispatcher { + + #pipelineInput = new AsyncIterableBuffer(); + #processors: Array = []; + #pipeline: AsyncIterableIterator | IterableIterator = this.#pipelineInput; + + /** + * Event bus where dispatched messages are delivered after processing. + * + * If not provided in the constructor, defaults to an instance of `InMemoryMessageBus`. + */ + eventBus: IEventBus; + + /** + * Maximum number of event batches that each pipeline processor can handle in parallel. + */ + concurrentLimit: number; + + constructor(o?: { + eventBus?: IEventBus, + eventDispatcherConfig?: { + concurrentLimit?: number + } + }) { + this.eventBus = o?.eventBus ?? new InMemoryMessageBus(); + this.concurrentLimit = o?.eventDispatcherConfig?.concurrentLimit ?? 100; + } + + /** + * Adds a preprocessor to the event dispatch pipeline. + * + * Preprocessors run in order they are added but process separate batches in parallel, maintaining FIFO order. + */ + addPipelineProcessor(preprocessor: IEventProcessor) { + if (this.#pipelineProcessing) + throw new Error('pipeline processing already started'); + + this.#processors.push(preprocessor); + + // Build a processing pipeline that runs preprocessors concurrently + // while preserving first-in-first-out 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) { + return { ...envelope, error }; + } + }); + } + + #pipelineProcessing = false; + + /** + * Consume the pipeline, publish events, and resolve/reject each batch + */ + async #startPipelineProcessing() { + if (this.#pipelineProcessing) // should never happen + throw new Error('pipeline processing already started'); + + this.#pipelineProcessing = true; + + for await (const { error, reject, data, resolve } of this.#pipeline) { + if (error) { // some of the preprocessors failed + await this.#revert(data); + reject(error); + continue; + } + + const events = data.map(e => e.event).filter(notEmpty); + + try { + for (const event of events) { + this.eventBus.publish(event); + } + resolve(events); + } + catch (publishError) { + reject(publishError); + } + } + } + + /** + * Revert side effects made by pipeline processors in case of a batch processing failure + */ + async #revert(batch: EventBatch) { + for (const processor of this.#processors) + await processor.revert?.(batch); + } + + /** + * Dispatch a set of events through the processing pipeline. + * + * Returns a promise that resolves after all events are processed and published. + */ + async dispatch(events: IEventSet) { + if (!Array.isArray(events) || events.length === 0) + throw new Error('dispatch requires a non-empty array of events'); + + const { promise, resolve, reject } = Promise.withResolvers(); + const envelope: EventBatchEnvelope = { + data: events.map(event => ({ event })), + resolve, + reject + }; + + if (!this.#pipelineProcessing) + this.#startPipelineProcessing(); + + this.#pipelineInput.push(envelope); + + return promise; + } +} diff --git a/src/EventStore.ts b/src/EventStore.ts index 99d596a..b1cdc28 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -1,81 +1,81 @@ import { IAggregateSnapshotStorage, IEvent, - IEventStorage, + IEventStoreReader, IEventSet, IExtendableLogger, ILogger, - IMessageBus, IMessageHandler, IObservable, IEventStream, IEventStore, EventQueryAfter, EventQueryBefore, - Identifier + Identifier, + IIdentifierProvider, + isIdentifierProvider, + IEventDispatcher, + IEventBus, + isIEventBus, + isIEventStoreReader } from "./interfaces"; import { getClassName, - setupOneTimeEmitterSubscription, - CompoundEmitter, - isIEventStorage, - isIMessageBus + setupOneTimeEmitterSubscription } from "./utils"; -import * as Event from './Event'; - -const SNAPSHOT_EVENT_TYPE = 'snapshot'; +import { EventDispatcher } from "./EventDispatcher"; export class EventStore implements IEventStore { - #validator: (event: IEvent) => void; - #logger?: ILogger; - #storage: IEventStorage; - #supplementaryEventBus?: IMessageBus; + #identifierProvider: IIdentifierProvider; + #storage: IEventStoreReader; #snapshotStorage: IAggregateSnapshotStorage | undefined; - #sagaStarters: string[] = []; - #compoundEmitter: CompoundEmitter; - - /** Whether storage supports aggregate snapshots */ - get snapshotsSupported(): boolean { - return Boolean(this.#snapshotStorage); - } + eventBus: IEventBus; + #eventDispatcher: IEventDispatcher; + #sagaStarters: Set = new Set(); + #logger?: ILogger; constructor({ storage, - supplementaryEventBus, + identifierProvider = isIdentifierProvider(storage) ? storage : undefined, snapshotStorage, - eventValidator = Event.validate, - logger + eventBus, + eventDispatcher, + logger, }: { - storage: IEventStorage, - - /** Optional event dispatcher for publishing persisted events externally */ - supplementaryEventBus?: IMessageBus, + storage: IEventStoreReader, + identifierProvider?: IIdentifierProvider, snapshotStorage?: IAggregateSnapshotStorage, - eventValidator?: IMessageHandler, - logger?: ILogger | IExtendableLogger + eventBus?: IEventBus, + eventDispatcher?: IEventDispatcher, + logger?: ILogger | IExtendableLogger, }) { if (!storage) throw new TypeError('storage argument required'); - if (!isIEventStorage(storage)) + if (!identifierProvider) + throw new TypeError('identifierProvider argument required'); + if (!isIEventStoreReader(storage)) throw new TypeError('storage does not implement IEventStorage interface'); - if (supplementaryEventBus && !isIMessageBus(supplementaryEventBus)) - throw new TypeError('supplementaryEventBus does not implement IMessageBus interface'); + if (eventBus && !isIEventBus(eventBus)) + throw new TypeError('eventBus does not implement IMessageBus interface'); - this.#validator = eventValidator; + this.#storage = storage; + this.#identifierProvider = identifierProvider; + this.#snapshotStorage = snapshotStorage; + this.#eventDispatcher = eventDispatcher ?? new EventDispatcher({ eventBus }); + this.eventBus = eventBus ?? this.#eventDispatcher.eventBus; this.#logger = logger && 'child' in logger ? logger.child({ service: getClassName(this) }) : logger; - this.#storage = storage; - this.#snapshotStorage = snapshotStorage; - this.#supplementaryEventBus = supplementaryEventBus; - this.#compoundEmitter = new CompoundEmitter(supplementaryEventBus, storage); } - - /** 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(); } async* getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream { @@ -137,8 +137,8 @@ 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); } /** @@ -147,30 +147,25 @@ export class EventStore implements IEventStore { * @param events - a set of events to commit * @returns Signed and committed events */ - async commit(events: IEventSet): Promise { + async dispatch(events: IEventSet): Promise { 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.persistEventsAndSnapshots(augmentedEvents); + const augmentedEvents = await this.#attachSagaIdToSagaStarterEvents(events); - // after events are saved to the persistent storage, - // publish them to the event bus (i.e. RabbitMq) - if (this.#supplementaryEventBus) - await this.publishEvents(eventStreamWithoutSnapshots); - - return eventStreamWithoutSnapshots; + return this.#eventDispatcher.dispatch(augmentedEvents); } - /** 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`); @@ -186,74 +181,25 @@ export class EventStore implements IEventStore { return augmentedEvents; } - /** - * Save events and snapshots to the persistent storages - * - * @returns Event set without "snapshot" events - */ - protected async persistEventsAndSnapshots(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; - } - - protected async publishEvents(events: IEventSet) { - if (!this.#supplementaryEventBus) - throw new Error('No supplementaryEventBus injected, events cannot be published'); - - this.#logger?.debug(`publishing ${Event.describeMultiple(events)}...`); - - try { - for (const event of events) - this.#supplementaryEventBus.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; - } - } - on(messageType: string, handler: IMessageHandler) { - this.#compoundEmitter.on(messageType, handler); + this.eventBus.on(messageType, handler); } off(messageType: string, handler: IMessageHandler) { - this.#compoundEmitter.off(messageType, handler); + this.eventBus.off(messageType, handler); } queue(name: string): IObservable { - return this.#compoundEmitter.queue(name); + if (!this.eventBus.queue) + throw new Error('Injected eventBus does not support named queues'); + + 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 { const subscribeTo = Array.isArray(messageTypes) ? messageTypes : [messageTypes]; - return setupOneTimeEmitterSubscription(this.#compoundEmitter, subscribeTo, filter, handler, this.#logger); + return setupOneTimeEmitterSubscription(this.eventBus, subscribeTo, filter, handler, this.#logger); } } diff --git a/src/dispatch-pipeline/EventPersistenceProcessor.ts b/src/dispatch-pipeline/EventPersistenceProcessor.ts new file mode 100644 index 0000000..aea2ff3 --- /dev/null +++ b/src/dispatch-pipeline/EventPersistenceProcessor.ts @@ -0,0 +1,34 @@ +import { EventBatch, IEvent, IEventProcessor, IEventStoreWriter } from '../interfaces'; + +/** + * Processor responsible for persisting events using an in-memory event storage. + * Typically used for testing or ephemeral scenarios where durability isn't required. + */ +export class EventPersistenceProcessor implements IEventProcessor { + + #storage: IEventStoreWriter; + + constructor(options: { storage: IEventStoreWriter }) { + if (!options.storage) + throw new TypeError('storage argument required'); + + this.#storage = options.storage; + } + + async process(batch: EventBatch): Promise { + if(!this.#storage) + return batch; + + const events: IEvent[] = []; + for(const { event } of batch) { + if(!event) + throw new Error('Event batch does not contain event'); + + events.push(event); + } + + await this.#storage.commitEvents(events); + + return batch; + } +} diff --git a/src/dispatch-pipeline/EventValidationProcessor.ts b/src/dispatch-pipeline/EventValidationProcessor.ts new file mode 100644 index 0000000..72090c6 --- /dev/null +++ b/src/dispatch-pipeline/EventValidationProcessor.ts @@ -0,0 +1,27 @@ +import { EventBatch, IEvent, IEventProcessor } from '../interfaces'; +import { validate as defaultValidator } from '../Event'; + +export type EventValidator = (event: IEvent) => void; + +/** + * Processor that validates the format of events. + * Rejects the batch if any event fails validation. + */ +export class EventValidationProcessor implements IEventProcessor { + + #validate: EventValidator; + + constructor(o?: { + eventFormatValidator: EventValidator + }) { + this.#validate = o?.eventFormatValidator ?? defaultValidator; + } + + async process(batch: EventBatch): Promise { + for (const { event } of batch) { + if (event) + this.#validate(event); + } + return batch; + } +} diff --git a/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts b/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts new file mode 100644 index 0000000..dc6088f --- /dev/null +++ b/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts @@ -0,0 +1,66 @@ +import { + EventBatch, + IAggregateSnapshotStorage, + IEvent, + IEventProcessor, + IExtendableLogger, + ILogger +} from "../interfaces"; +import * as Event from '../Event'; + +const SNAPSHOT_EVENT_TYPE = 'snapshot'; + +export class SnapshotPersistenceProcessor implements IEventProcessor { + + #snapshotStorage?: IAggregateSnapshotStorage; + #logger?: ILogger; + + constructor(options: { + snapshotStorage?: IAggregateSnapshotStorage; + logger?: ILogger | IExtendableLogger; + }) { + this.#snapshotStorage = options.snapshotStorage; + this.#logger = options.logger && 'child' in options.logger ? + options.logger.child({ service: new.target.name }) : + options.logger; + } + + #extractSnapshotEvent(batch: EventBatch): IEvent | undefined { + if (!Array.isArray(batch)) + throw new TypeError('batch argument must be an Array'); + + const snapshotEvents = batch.filter(({ event }) => event?.type === SNAPSHOT_EVENT_TYPE); + if (snapshotEvents.length > 1) + throw new Error(`Cannot process more than one "${SNAPSHOT_EVENT_TYPE}" event per batch`); + + return snapshotEvents[0].event; + } + + async process(batch: EventBatch): Promise { + if (!this.#snapshotStorage) + return batch; + + const snapshotEvent = this.#extractSnapshotEvent(batch); + if (!snapshotEvent) + return batch; + + this.#logger?.debug(`Persisting ${Event.describe(snapshotEvent)}`); + + await this.#snapshotStorage.saveAggregateSnapshot(snapshotEvent); + + return batch.filter(e => e !== snapshotEvent); + } + + async revert(batch: EventBatch): Promise { + if (!this.#snapshotStorage) + return; + + const snapshotEvent = this.#extractSnapshotEvent(batch); + if (!snapshotEvent) + return; + + this.#logger?.debug(`Removing ${Event.describe(snapshotEvent)}`); + + await this.#snapshotStorage?.deleteAggregateSnapshot(snapshotEvent); + } +} diff --git a/src/dispatch-pipeline/index.ts b/src/dispatch-pipeline/index.ts new file mode 100644 index 0000000..43ec39a --- /dev/null +++ b/src/dispatch-pipeline/index.ts @@ -0,0 +1,3 @@ +export * from './EventPersistenceProcessor'; +export * from './EventValidationProcessor'; +export * from './SnapshotPersistenceProcessor'; diff --git a/src/infrastructure/memory/InMemoryEventStorage.ts b/src/in-memory/InMemoryEventStorage.ts similarity index 84% rename from src/infrastructure/memory/InMemoryEventStorage.ts rename to src/in-memory/InMemoryEventStorage.ts index c78dc36..a77def8 100644 --- a/src/infrastructure/memory/InMemoryEventStorage.ts +++ b/src/in-memory/InMemoryEventStorage.ts @@ -1,17 +1,27 @@ -import { IEvent } from "../../interfaces/IEvent"; -import { IEventSet } from "../../interfaces/IEventSet"; -import { EventQueryAfter, IEventStorage } from "../../interfaces/IEventStorage"; -import { IEventStream } from "../../interfaces/IEventStream"; +import { + IIdentifierProvider, + IEvent, + IEventSet, + EventQueryAfter, + IEventStoreReader, + IEventStream, + IEventStoreWriter +} from "../interfaces"; import { nextCycle } from "./utils"; /** * A simple event storage implementation intended to use for tests only. * Storage content resets on each app restart. */ -export class InMemoryEventStorage implements IEventStorage { +export class InMemoryEventStorage implements IEventStoreReader, IEventStoreWriter, IIdentifierProvider { #nextId: number = 0; #events: IEventSet = []; + getNewId(): string { + this.#nextId += 1; + return String(this.#nextId); + } + async commitEvents(events: IEventSet): Promise { await nextCycle(); @@ -66,9 +76,4 @@ export class InMemoryEventStorage implements IEventStorage { yield event; } } - - getNewId(): string { - this.#nextId += 1; - return String(this.#nextId); - } } diff --git a/src/infrastructure/memory/InMemoryLock.ts b/src/in-memory/InMemoryLock.ts similarity index 100% rename from src/infrastructure/memory/InMemoryLock.ts rename to src/in-memory/InMemoryLock.ts diff --git a/src/infrastructure/memory/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts similarity index 99% rename from src/infrastructure/memory/InMemoryMessageBus.ts rename to src/in-memory/InMemoryMessageBus.ts index b14548e..c8f43f3 100644 --- a/src/infrastructure/memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -4,7 +4,7 @@ import { IMessageBus, IMessageHandler, IObservable -} from "../../interfaces"; +} from "../interfaces"; /** * Default implementation of the message bus. diff --git a/src/infrastructure/memory/InMemorySnapshotStorage.ts b/src/in-memory/InMemorySnapshotStorage.ts similarity index 70% rename from src/infrastructure/memory/InMemorySnapshotStorage.ts rename to src/in-memory/InMemorySnapshotStorage.ts index 7943217..5ac8183 100644 --- a/src/infrastructure/memory/InMemorySnapshotStorage.ts +++ b/src/in-memory/InMemorySnapshotStorage.ts @@ -1,4 +1,4 @@ -import { IAggregateSnapshotStorage, Identifier, IEvent } from "../../interfaces"; +import { IAggregateSnapshotStorage, Identifier, IEvent } from "../interfaces"; /** * In-memory storage for aggregate snapshots. @@ -24,4 +24,14 @@ export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { 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.#snapshots.delete(snapshotEvent.aggregateId); + } } diff --git a/src/infrastructure/memory/InMemoryView.ts b/src/in-memory/InMemoryView.ts similarity index 98% rename from src/infrastructure/memory/InMemoryView.ts rename to src/in-memory/InMemoryView.ts index 9fc90f7..abd6e1b 100644 --- a/src/infrastructure/memory/InMemoryView.ts +++ b/src/in-memory/InMemoryView.ts @@ -1,5 +1,5 @@ import { InMemoryLock } from './InMemoryLock'; -import { IViewLocker, Identifier, IObjectStorage } from "../../interfaces"; +import { IViewLocker, Identifier, IObjectStorage } from "../interfaces"; import { nextCycle } from './utils'; /** diff --git a/src/infrastructure/memory/index.ts b/src/in-memory/index.ts similarity index 100% rename from src/infrastructure/memory/index.ts rename to src/in-memory/index.ts diff --git a/src/infrastructure/memory/utils/Deferred.ts b/src/in-memory/utils/Deferred.ts similarity index 100% rename from src/infrastructure/memory/utils/Deferred.ts rename to src/in-memory/utils/Deferred.ts diff --git a/src/infrastructure/memory/utils/index.ts b/src/in-memory/utils/index.ts similarity index 100% rename from src/infrastructure/memory/utils/index.ts rename to src/in-memory/utils/index.ts diff --git a/src/infrastructure/memory/utils/nextCycle.ts b/src/in-memory/utils/nextCycle.ts similarity index 100% rename from src/infrastructure/memory/utils/nextCycle.ts rename to src/in-memory/utils/nextCycle.ts diff --git a/src/index.ts b/src/index.ts index bd3b6bd..27d5d0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,9 +8,9 @@ export * from './AggregateCommandHandler'; export * from './AbstractSaga'; export * from './SagaEventHandler'; export * from './AbstractProjection'; +export * from './EventDispatcher'; -export * from './infrastructure/memory'; -export * as SQLite from './infrastructure/sqlite'; +export * from './in-memory'; export * as Event from './Event'; export { diff --git a/src/interfaces/IAggregateSnapshotStorage.ts b/src/interfaces/IAggregateSnapshotStorage.ts index 41c293d..e938b4c 100644 --- a/src/interfaces/IAggregateSnapshotStorage.ts +++ b/src/interfaces/IAggregateSnapshotStorage.ts @@ -5,4 +5,6 @@ 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/ICommandBus.ts b/src/interfaces/ICommandBus.ts index 87e2f59..38907f8 100644 --- a/src/interfaces/ICommandBus.ts +++ b/src/interfaces/ICommandBus.ts @@ -1,16 +1,14 @@ import { ICommand } from "./ICommand"; import { IEventSet } from "./IEventSet"; -import { IMessageHandler, IObservable } from "./IObservable"; +import { IObservable } from "./IObservable"; import { IObserver } from "./IObserver"; export interface ICommandBus extends IObservable { - send(commandType: string, aggregateId: string, options: { payload?: object, context?: object }): + send(commandType: string, aggregateId: string | undefined, options: { payload?: object, context?: object }): Promise; sendRaw(command: ICommand): Promise; - - on(type: string, handler: IMessageHandler): void; } export interface ICommandHandler extends IObserver { diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts index 4d54f07..70af6ee 100644 --- a/src/interfaces/IEvent.ts +++ b/src/interfaces/IEvent.ts @@ -1,4 +1,5 @@ import { IMessage } from "./IMessage"; +import { isObject } from "./isObject"; export type IEvent = IMessage & { /** Unique event identifier */ diff --git a/src/interfaces/IEventBus.ts b/src/interfaces/IEventBus.ts new file mode 100644 index 0000000..c358a06 --- /dev/null +++ b/src/interfaces/IEventBus.ts @@ -0,0 +1,11 @@ +import { IEvent } from "./IEvent"; +import { IObservable, isIObservable } from "./IObservable"; + +export interface IEventBus extends IObservable { + publish(event: IEvent): 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..9acfda5 --- /dev/null +++ b/src/interfaces/IEventDispatcher.ts @@ -0,0 +1,7 @@ +import { IEventSet } from "./IEventSet"; +import { IEventBus } from "./IEventBus"; + +export interface IEventDispatcher { + readonly eventBus: IEventBus; + dispatch(events: IEventSet): Promise; +} diff --git a/src/interfaces/IEventProcessor.ts b/src/interfaces/IEventProcessor.ts new file mode 100644 index 0000000..ee7c28b --- /dev/null +++ b/src/interfaces/IEventProcessor.ts @@ -0,0 +1,24 @@ +import { IEvent } from "./IEvent"; + +/** + * Represents a wrapper for an event that can optionally contain additional metadata. + * Used to extend event processing with context-specific data required by processors. + */ +type EventEnvelope = { + event?: IEvent; +} + +/** + * A batch of event envelopes. Can contain custom envelope types extending EventEnvelope. + */ +export type EventBatch = Readonly>; + +/** + * Defines a processor that operates on a batch of event envelopes. + * Allows transformations, side-effects, or filtering of events during dispatch. + */ +export interface IEventProcessor { + process(batch: EventBatch): Promise>; + revert?(batch: EventBatch): Promise; +} + diff --git a/src/interfaces/IEventSet.ts b/src/interfaces/IEventSet.ts index fcde354..e54caf5 100644 --- a/src/interfaces/IEventSet.ts +++ b/src/interfaces/IEventSet.ts @@ -1,6 +1,3 @@ import { IEvent } from "./IEvent"; -/** - * @deprecated Try to use `IEventStream` instead - */ export type IEventSet = ReadonlyArray>; diff --git a/src/interfaces/IEventStorage.ts b/src/interfaces/IEventStorage.ts index cbd1cfe..9fa0dca 100644 --- a/src/interfaces/IEventStorage.ts +++ b/src/interfaces/IEventStorage.ts @@ -2,6 +2,7 @@ import { Identifier } from "./Identifier"; import { IEvent } from "./IEvent"; import { IEventSet } from "./IEventSet"; import { IEventStream } from "./IEventStream"; +import { isObject } from "./isObject"; export type EventQueryAfter = { /** Get events emitted after this specific event */ @@ -13,17 +14,36 @@ export type EventQueryBefore = { beforeEvent?: IEvent; } -export interface IEventStorage { +export interface IEventStoreReader { /** - * Create unique identifier + * Retrieves events of specified types that were emitted after a given event. */ - getNewId(): Identifier | Promise; + getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; - commitEvents(events: IEventSet): Promise; + /** + * Retrieves all events (and optionally a snapshot) associated with a specific aggregate. + */ + getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): IEventStream; - getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; + /** + * Retrieves events associated with a saga, with optional filtering by version or timestamp. + */ + getSagaEvents(sagaId: Identifier, options: EventQueryBefore): IEventStream; +} - getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): Promise | IEventStream; +export const isIEventStoreReader = (storage: unknown): storage is IEventStoreReader => + isObject(storage) + && 'getEventsByTypes' in storage + && typeof storage.getEventsByTypes === 'function' + && 'getAggregateEvents' in storage + && typeof storage.getAggregateEvents === 'function' + && 'getSagaEvents' in storage + && typeof storage.getSagaEvents === 'function'; - getSagaEvents(sagaId: Identifier, options: EventQueryBefore): Promise | IEventStream; +export interface IEventStoreWriter { + /** + * 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 index 21954f6..67ace1e 100644 --- a/src/interfaces/IEventStore.ts +++ b/src/interfaces/IEventStore.ts @@ -1,26 +1,13 @@ -import { Identifier } from "./Identifier"; +import { IEventDispatcher } from "./IEventDispatcher"; import { IEvent } from "./IEvent"; -import { IEventSet } from "./IEventSet"; -import { EventQueryAfter, EventQueryBefore } from "./IEventStorage"; -import { IEventStream } from "./IEventStream"; +import { IEventStoreReader } from "./IEventStorage"; +import { IIdentifierProvider } from "./IIdentifierProvider"; import { IMessageHandler, IObservable } from "./IObservable"; -export interface IEventStore extends IObservable { - readonly snapshotsSupported?: boolean; +export interface IEventStore + extends IObservable, IEventDispatcher, IEventStoreReader, IIdentifierProvider { - getNewId(): Identifier | Promise; - - commit(events: IEventSet): Promise; - - getEventsByTypes(eventTypes: Readonly, options?: EventQueryAfter): IEventStream; - - getAggregateEvents(aggregateId: Identifier, options?: { snapshot?: IEvent }): IEventStream; - - getSagaEvents(sagaId: Identifier, options: EventQueryBefore): IEventStream; + registerSagaStarters(startsWith: string[] | undefined): void; once(messageTypes: string | string[], handler?: IMessageHandler, filter?: (e: IEvent) => boolean): Promise; - - queue(name: string): IObservable; - - registerSagaStarters(startsWith: string[] | undefined): void; } diff --git a/src/interfaces/IIdentifierProvider.ts b/src/interfaces/IIdentifierProvider.ts new file mode 100644 index 0000000..30b8be9 --- /dev/null +++ b/src/interfaces/IIdentifierProvider.ts @@ -0,0 +1,16 @@ +import { Identifier } from "./Identifier"; +import { isObject } from "./isObject"; + +export interface IIdentifierProvider { + /** + * Generates and returns a new unique identifier suitable for aggregates, sagas, and events. + * + * @returns A promise resolving to an identifier or an identifier itself. + */ + getNewId(): Identifier | Promise; +} + +export const isIdentifierProvider = (obj: any): obj is IIdentifierProvider => + isObject(obj) + && 'getNewId' in obj + && typeof obj.getNewId === 'function'; diff --git a/src/interfaces/IMessage.ts b/src/interfaces/IMessage.ts index c40dc95..c474e3b 100644 --- a/src/interfaces/IMessage.ts +++ b/src/interfaces/IMessage.ts @@ -1,4 +1,5 @@ import { Identifier } from "./Identifier"; +import { isObject } from "./isObject"; export interface IMessage { /** Event or command type */ @@ -13,3 +14,8 @@ export interface IMessage { payload?: TPayload; context?: any; } + +export const isMessage = (obj: unknown): obj is IMessage => + isObject(obj) + && 'type' in obj + && typeof obj.type === 'string'; diff --git a/src/interfaces/IObservable.ts b/src/interfaces/IObservable.ts index dc824fd..076b85b 100644 --- a/src/interfaces/IObservable.ts +++ b/src/interfaces/IObservable.ts @@ -1,5 +1,8 @@ +import { IMessage } from "./IMessage"; +import { isObject } from "./isObject"; + export interface IMessageHandler { - (...args: any[]): any | Promise + (message: IMessage): any | Promise }; export interface IObservable { @@ -18,3 +21,10 @@ export interface IObservable { */ queue?(name: string): IObservable; } + +export const isIObservable = (obj: unknown): obj is IObservable => + isObject(obj) + && 'on' in obj + && typeof obj.on === 'function' + && 'off' in obj + && typeof obj.off === 'function'; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index b81c515..a78ae56 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -4,12 +4,16 @@ export * from './ICommand'; export * from './ICommandBus'; export * from './Identifier'; export * from './IEvent'; +export * from './IEventBus'; +export * from './IEventDispatcher'; export * from './IEventLocker'; +export * from './IEventProcessor'; export * from './IEventReceptor'; export * from './IEventSet'; export * from './IEventStorage'; export * from './IEventStore'; export * from './IEventStream'; +export * from './IIdentifierProvider'; export * from './ILogger'; export * from './IMessage'; export * from './IMessageBus'; diff --git a/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts b/src/sqlite/AbstractSqliteObjectProjection.ts similarity index 85% rename from src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts rename to src/sqlite/AbstractSqliteObjectProjection.ts index 5c13f9a..59efb63 100644 --- a/src/infrastructure/sqlite/AbstractSqliteObjectProjection.ts +++ b/src/sqlite/AbstractSqliteObjectProjection.ts @@ -1,5 +1,5 @@ -import { AbstractProjection } from "../../AbstractProjection"; -import { IExtendableLogger } from "../../interfaces"; +import { AbstractProjection } from "../AbstractProjection"; +import { IExtendableLogger } from "../interfaces"; import { SqliteDbParams } from "./commonParams"; import { SqliteObjectView } from "./SqliteObjectView"; diff --git a/src/infrastructure/sqlite/AbstractSqliteView.ts b/src/sqlite/AbstractSqliteView.ts similarity index 92% rename from src/infrastructure/sqlite/AbstractSqliteView.ts rename to src/sqlite/AbstractSqliteView.ts index 224aa97..e4a01e4 100644 --- a/src/infrastructure/sqlite/AbstractSqliteView.ts +++ b/src/sqlite/AbstractSqliteView.ts @@ -1,8 +1,8 @@ -import { IEvent, IEventLocker, ILogger } from '../../interfaces'; +import { IEvent, IEventLocker, ILogger } from '../interfaces'; import { Database } from 'better-sqlite3'; import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker'; import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker'; -import { IViewLocker } from '../../interfaces'; +import { IViewLocker } from '../interfaces'; export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { diff --git a/src/infrastructure/sqlite/SqliteEventLocker.ts b/src/sqlite/SqliteEventLocker.ts similarity index 98% rename from src/infrastructure/sqlite/SqliteEventLocker.ts rename to src/sqlite/SqliteEventLocker.ts index f99edcc..95cb6b1 100644 --- a/src/infrastructure/sqlite/SqliteEventLocker.ts +++ b/src/sqlite/SqliteEventLocker.ts @@ -1,5 +1,5 @@ import { Database, Statement } from 'better-sqlite3'; -import { IEvent, IEventLocker } from '../../interfaces'; +import { IEvent, IEventLocker } from '../interfaces'; import { getEventId } from './utils'; import { viewLockTableInit, eventLockTableInit } from './queries'; import { SqliteViewLockerParams } from './SqliteViewLocker'; diff --git a/src/infrastructure/sqlite/SqliteObjectStorage.ts b/src/sqlite/SqliteObjectStorage.ts similarity index 98% rename from src/infrastructure/sqlite/SqliteObjectStorage.ts rename to src/sqlite/SqliteObjectStorage.ts index 83a7347..ea0fc49 100644 --- a/src/infrastructure/sqlite/SqliteObjectStorage.ts +++ b/src/sqlite/SqliteObjectStorage.ts @@ -1,6 +1,6 @@ import { Statement, Database } from 'better-sqlite3'; import { guid } from './utils'; -import { IObjectStorage } from '../../interfaces'; +import { IObjectStorage } from '../interfaces'; export class SqliteObjectStorage implements IObjectStorage { diff --git a/src/infrastructure/sqlite/SqliteObjectView.ts b/src/sqlite/SqliteObjectView.ts similarity index 97% rename from src/infrastructure/sqlite/SqliteObjectView.ts rename to src/sqlite/SqliteObjectView.ts index e7956a3..9a99f6a 100644 --- a/src/infrastructure/sqlite/SqliteObjectView.ts +++ b/src/sqlite/SqliteObjectView.ts @@ -1,5 +1,5 @@ import { AbstractSqliteView } from "./AbstractSqliteView"; -import { IObjectStorage, IEventLocker } from '../../interfaces'; +import { IObjectStorage, IEventLocker } from '../interfaces'; import { SqliteObjectStorage } from './SqliteObjectStorage'; export class SqliteObjectView extends AbstractSqliteView implements IObjectStorage, IEventLocker { diff --git a/src/infrastructure/sqlite/SqliteViewLocker.ts b/src/sqlite/SqliteViewLocker.ts similarity index 97% rename from src/infrastructure/sqlite/SqliteViewLocker.ts rename to src/sqlite/SqliteViewLocker.ts index a6a09af..2073698 100644 --- a/src/infrastructure/sqlite/SqliteViewLocker.ts +++ b/src/sqlite/SqliteViewLocker.ts @@ -1,6 +1,6 @@ import { Database, Statement } from 'better-sqlite3'; -import { IExtendableLogger, ILogger, IViewLocker } from '../../interfaces'; -import { Deferred } from '../memory'; +import { IExtendableLogger, ILogger, IViewLocker } from '../interfaces'; +import { Deferred } from '../in-memory'; import { promisify } from 'util'; import { viewLockTableInit } from './queries'; import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; diff --git a/src/infrastructure/sqlite/commonParams.ts b/src/sqlite/commonParams.ts similarity index 100% rename from src/infrastructure/sqlite/commonParams.ts rename to src/sqlite/commonParams.ts diff --git a/src/infrastructure/sqlite/index.ts b/src/sqlite/index.ts similarity index 100% rename from src/infrastructure/sqlite/index.ts rename to src/sqlite/index.ts diff --git a/src/infrastructure/sqlite/queries/eventLockTableInit.ts b/src/sqlite/queries/eventLockTableInit.ts similarity index 100% rename from src/infrastructure/sqlite/queries/eventLockTableInit.ts rename to src/sqlite/queries/eventLockTableInit.ts diff --git a/src/infrastructure/sqlite/queries/index.ts b/src/sqlite/queries/index.ts similarity index 100% rename from src/infrastructure/sqlite/queries/index.ts rename to src/sqlite/queries/index.ts diff --git a/src/infrastructure/sqlite/queries/viewLockTableInit.ts b/src/sqlite/queries/viewLockTableInit.ts similarity index 100% rename from src/infrastructure/sqlite/queries/viewLockTableInit.ts rename to src/sqlite/queries/viewLockTableInit.ts diff --git a/src/infrastructure/sqlite/utils/getEventId.ts b/src/sqlite/utils/getEventId.ts similarity index 83% rename from src/infrastructure/sqlite/utils/getEventId.ts rename to src/sqlite/utils/getEventId.ts index 62fc2ac..6d86ec3 100644 --- a/src/infrastructure/sqlite/utils/getEventId.ts +++ b/src/sqlite/utils/getEventId.ts @@ -1,6 +1,6 @@ -import { IEvent } from "../../../interfaces"; -import * as md5 from 'md5'; +import { IEvent } from "../../interfaces"; import { guid } from './guid'; +import * as md5 from 'md5'; /** * Get assigned or generate new event ID from event content diff --git a/src/infrastructure/sqlite/utils/guid.ts b/src/sqlite/utils/guid.ts similarity index 100% rename from src/infrastructure/sqlite/utils/guid.ts rename to src/sqlite/utils/guid.ts diff --git a/src/infrastructure/sqlite/utils/index.ts b/src/sqlite/utils/index.ts similarity index 100% rename from src/infrastructure/sqlite/utils/index.ts rename to src/sqlite/utils/index.ts diff --git a/src/utils/CompoundEmitter.ts b/src/utils/CompoundEmitter.ts deleted file mode 100644 index 3e2c2a5..0000000 --- a/src/utils/CompoundEmitter.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { IObservable, IMessageHandler } from "../interfaces"; -import { isIObservable } from "./isIObservable"; - -interface IObservableQueueProvider extends Required> { } - -const isObservableQueueProvider = (obj: any): obj is IObservableQueueProvider => - obj - && 'queue' in obj - && typeof obj.queue === 'function'; - -export class CompoundEmitter implements IObservable { - - #emitters: IObservable[]; - #queueProvider?: IObservableQueueProvider; - - constructor(...emitters: any[]) { - const observableEmitters = emitters.filter(isIObservable); - if (!observableEmitters.length) - throw new TypeError('none of the arguments implement IObservable interface'); - - const queueProviders = emitters.filter(isObservableQueueProvider); - if (queueProviders.length > 1) - throw new TypeError('more than one argument implements IObservable `queue` method'); - - this.#emitters = observableEmitters; - this.#queueProvider = queueProviders[0]; - } - - on(type: string, handler: IMessageHandler): void { - for (const emitter of this.#emitters) - emitter.on(type, handler); - } - - off(type: string, handler: IMessageHandler): void { - for (const emitter of this.#emitters) - emitter.off(type, handler); - } - - queue(name: string): IObservable { - if (!this.#queueProvider) - throw new Error('none of the emitters support named queues'); - - return this.#queueProvider.queue(name); - } -} diff --git a/src/utils/delay.ts b/src/utils/delay.ts new file mode 100644 index 0000000..8663de4 --- /dev/null +++ b/src/utils/delay.ts @@ -0,0 +1,8 @@ +/** + * Returns a promise that resolves after the specified number of milliseconds. + * The internal timeout is unref'd to avoid blocking Node.js process termination. + */ +export const delay = (ms: number) => new Promise(resolve => { + const t = setTimeout(resolve, ms); + t.unref(); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 560c457..0ec92ea 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,12 +1,10 @@ -export * from './CompoundEmitter'; +export * from './delay'; export * from './getClassName'; export * from './getHandler'; export * from './getMessageHandlerNames'; export * from './isClass'; -export * from './isIEventStorage'; -export * from './isIMessageBus'; -export * from './isIObservable'; export * from './iteratorToArray'; +export * from './notEmpty'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; export * from './validateHandlers'; diff --git a/src/utils/isIEventStorage.ts b/src/utils/isIEventStorage.ts deleted file mode 100644 index bbe7cf2..0000000 --- a/src/utils/isIEventStorage.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { IEventStorage } from "../interfaces"; - -export const isIEventStorage = (storage: IEventStorage): storage is IEventStorage => storage - && typeof storage.getNewId === 'function' - && typeof storage.commitEvents === 'function' - && typeof storage.getEventsByTypes === 'function' - && typeof storage.getAggregateEvents === 'function' - && typeof storage.getSagaEvents === 'function'; diff --git a/src/utils/isIMessageBus.ts b/src/utils/isIMessageBus.ts deleted file mode 100644 index 4a47228..0000000 --- a/src/utils/isIMessageBus.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IMessageBus } from "../interfaces"; -import { isIObservable } from "."; - -export 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'; diff --git a/src/utils/isIObservable.ts b/src/utils/isIObservable.ts deleted file mode 100644 index 191627c..0000000 --- a/src/utils/isIObservable.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IObservable } from "../interfaces"; - -export const isIObservable = (obj: IObservable | any): obj is IObservable => obj - && 'on' in obj - && typeof obj.on === 'function' - && 'off' in obj - && typeof obj.off === 'function'; 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/tests/integration/SqliteView.test.ts b/tests/integration/sqlite/SqliteView.test.ts similarity index 73% rename from tests/integration/SqliteView.test.ts rename to tests/integration/sqlite/SqliteView.test.ts index c84797d..c50916e 100644 --- a/tests/integration/SqliteView.test.ts +++ b/tests/integration/sqlite/SqliteView.test.ts @@ -1,7 +1,6 @@ - import { existsSync, unlinkSync } from 'fs'; -import { AbstractProjection, IEvent } from '../../src'; -import { SqliteObjectView } from '../../src/infrastructure/sqlite'; +import { AbstractProjection, IEvent } from '../../../src'; +import { SqliteObjectView } from '../../../src/sqlite'; import * as createDb from 'better-sqlite3'; type UserPayload = { @@ -34,14 +33,6 @@ describe.only('SqliteView', () => { let viewModelSqliteDb: import('better-sqlite3').Database; - const logState = () => { - console.log({ - tbl_view_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_view_lock`).all(), - tbl_test_1_event_lock: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1_event_lock`).all(), - tbl_test_1: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1`).all() - }); - } - const fileName = './test.sqlite'; beforeEach(() => { @@ -72,7 +63,7 @@ describe.only('SqliteView', () => { // 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 10_000 events within 0.5 seconds', async () => { + it('handles 1_000 events within 0.5 seconds', async () => { const p = new MyDumbProjection({ view: new SqliteObjectView({ @@ -86,18 +77,17 @@ describe.only('SqliteView', () => { await p.view.lock(); await p.view.unlock(); - const aggregateIds = Array.from({ length: 5_000 }, (v, i) => ({ - id1: `${i}A`, - id2: `${i}B`, - id3: `${i}C` + const aggregateIds = Array.from({ length: 1_000 }, (v, i) => ({ + aggregateId: `${i}A`.padStart(32, '0'), + eventId: `${i}B`.padStart(32, '0') })); - console.time(); + const startTs = Date.now(); - for (const { id1: aggregateId, id2, id3 } of aggregateIds) { + for (const { aggregateId, eventId } of aggregateIds) { await p.project({ type: 'userCreated', - id: id2, + id: eventId, aggregateId, payload: { name: 'Jon' @@ -106,7 +96,6 @@ describe.only('SqliteView', () => { await p.project({ type: 'userModified', - id: id3, aggregateId, payload: { name: 'Jon Doe' @@ -114,14 +103,18 @@ describe.only('SqliteView', () => { }); } - console.timeEnd(); - - // logState(); + const totalMs = Date.now() - startTs; + expect(totalMs).toBeLessThan(500); - // const user = await p.view.get(aggregateId); + const user = await p.view.get('0000000000000000000000000000999A'); + expect(user).toEqual({ + name: 'Jon Doe' + }); - // 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/AbstractProjection.test.ts b/tests/unit/AbstractProjection.test.ts index 4abada0..818ee8f 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -1,6 +1,13 @@ import { expect, assert, AssertionError } from 'chai'; import * as sinon from 'sinon'; -import { AbstractProjection, InMemoryView, InMemoryEventStorage, EventStore, InMemoryMessageBus } from '../../src'; +import { + AbstractProjection, + InMemoryView, + InMemoryEventStorage, + EventStore, + InMemoryMessageBus, + EventDispatcher +} from '../../src'; class MyProjection extends AbstractProjection> { static get handles() { @@ -162,8 +169,9 @@ describe('AbstractProjection', function () { it('waits until the restoring process is done', async () => { const storage = new InMemoryEventStorage(); - const supplementaryEventBus = new InMemoryMessageBus(); - const es = new EventStore({ storage, supplementaryEventBus }); + const eventBus = new InMemoryMessageBus(); + const eventDispatcher = new EventDispatcher({ eventBus }); + const es = new EventStore({ storage, eventBus, eventDispatcher, identifierProvider: storage }); let restored = false; let projected = false; diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index 76bd7ec..c19f82f 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -1,6 +1,6 @@ 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 } from '../../src'; import { AggregateCommandHandler, @@ -47,21 +47,22 @@ describe('AggregateCommandHandler', function () { let snapshotStorage: InMemorySnapshotStorage; let eventStore: IEventStore; let commandBus: ICommandBus; - let supplementaryEventBus: IMessageBus; + let eventBus: IEventBus; let onSpy; let getNewIdSpy; let getAggregateEventsSpy; let commitSpy; beforeEach(() => { - supplementaryEventBus = new InMemoryMessageBus(); + eventBus = new InMemoryMessageBus(); storage = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); + const eventDispatcher = new EventDispatcher({ eventBus }); - eventStore = new EventStore({ storage, snapshotStorage, supplementaryEventBus }); + eventStore = new EventStore({ storage, snapshotStorage, eventBus, eventDispatcher, identifierProvider: storage }); 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'); diff --git a/tests/unit/CqrsContainerBuilder.test.ts b/tests/unit/CqrsContainerBuilder.test.ts index 2a83b92..37fe640 100644 --- a/tests/unit/CqrsContainerBuilder.test.ts +++ b/tests/unit/CqrsContainerBuilder.test.ts @@ -15,8 +15,8 @@ describe('CqrsContainerBuilder', function () { beforeEach(() => { builder = new ContainerBuilder(); - builder.register(InMemoryEventStorage).as('storage'); - builder.register(InMemoryMessageBus).as('supplementaryEventBus'); + builder.register(InMemoryEventStorage).as('storage').as('identifierProvider'); + builder.register(InMemoryMessageBus).as('eventBus'); }); describe('registerAggregate(aggregateType) extension', () => { @@ -81,7 +81,7 @@ describe('CqrsContainerBuilder', function () { { type: 'somethingHappened', aggregateId: 1 } ]; - container.eventStore.commit(events).catch(done); + container.eventStore.dispatch(events).catch(done); }); }); diff --git a/tests/unit/EventDispatcher.test.ts b/tests/unit/EventDispatcher.test.ts new file mode 100644 index 0000000..45bde3f --- /dev/null +++ b/tests/unit/EventDispatcher.test.ts @@ -0,0 +1,100 @@ +import { IEvent, IEventBus, IEventProcessor } 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: IEventProcessor = { + 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: IEventProcessor = { + 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: IEventProcessor = { + 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: IEventProcessor = { + 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', + ]); + }); +}); diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index 5113df0..16c89a4 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -1,331 +1,178 @@ -import { expect } from 'chai'; -import * as sinon from 'sinon'; +import { EventDispatcher } from '../../dist/EventDispatcher'; +import { IEventDispatcher, InMemoryMessageBus } from '../../src'; import { EventStore } from '../../src/EventStore'; -import { InMemoryEventStorage, InMemorySnapshotStorage, InMemoryMessageBus } from '../../src'; -import { IAggregateSnapshotStorage, IEvent, IEventStorage, IEventStore, IMessageBus } from '../../src/interfaces'; -import { iteratorToArray } from '../../src/utils'; - -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 supplementaryEventBus: IMessageBus; +import { + IEvent, + IEventBus, + IEventStoreReader, + 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(); - supplementaryEventBus = new InMemoryMessageBus(); - es = new EventStore({ storage, snapshotStorage, supplementaryEventBus }); - }); - - 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'); - }, - supplementaryEventBus - }); - - 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, + storage: mockStorage, + identifierProvider: mockIdentifierProvider, + snapshotStorage: mockSnapshotStorage, + logger: undefined }); }); - describe('commit', () => { + describe('dispatch', () => { - it('validates event format', () => { + it('throws error when called with non-array argument', async () => { - const badEvent = { - type: 'somethingHappened', - context: goodContext - }; - - 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'); - }); - }); - - it('commits events to storage', async () => { - - await es.commit([goodEvent]); - - const events: IEvent[] = []; - for await (const e of es.getEventsByTypes(['somethingHappened'], {})) - events.push(e); - - 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); + await expect(store.dispatch(null as any)).rejects.toThrow(TypeError); }); - it('submits aggregate snapshot to storage.saveAggregateSnapshot, when provided', async () => { - - snapshotStorage.getAggregateSnapshot = () => snapshotEvent as IEvent; + it('augments saga starter events with new sagaId and version', async () => { - // storage.saveAggregateSnapshot = () => { }; - const saveAggregateSnapshotSpy = sinon.spy(snapshotStorage, 'saveAggregateSnapshot'); - const commitEventsSpy = sinon.spy(storage, 'commitEvents'); + store.registerSagaStarters(['StartSaga']); + const event: IEvent = { type: 'StartSaga' } as IEvent; + const dispatchSpy = jest.spyOn(eventDispatcher, 'dispatch'); - expect(es).to.have.property('snapshotsSupported', true); + await store.dispatch([event]); - es.commit([goodEvent]); - expect(snapshotStorage).to.have.nested.property('saveAggregateSnapshot.called', false); - - es.commit([goodEvent2, snapshotEvent]); - expect(snapshotStorage).to.have.nested.property('saveAggregateSnapshot.calledOnce', true); - - { - 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); - } + expect(event.sagaId).toBe(mockId); + expect(event.sagaVersion).toBe(0); + expect(dispatchSpy).toHaveBeenCalledWith([event]); }); - 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('does not modify non-saga starter events', async () => { - 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'); - } - } - }); + const event: IEvent = { type: 'RegularEvent' } as IEvent; + const dispatchSpy = jest.spyOn(eventDispatcher, 'dispatch'); - es = new EventStore({ storage, supplementaryEventBus }); + await store.dispatch([event]); - 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'); - }); + expect(event.sagaId).toBeUndefined(); + expect(event.sagaVersion).toBeUndefined(); + expect(dispatchSpy).toHaveBeenCalledWith([event]); }); }); - describe('getNewId', () => { - - 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 = es.getAggregateEvents(goodEvent.aggregateId); + describe('getAggregateEvents', () => { - expect(events).to.be.have.property(Symbol.asyncIterator); + 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); - const event = (await events.next()).value; - expect(event).to.have.nested.property('type', 'somethingHappened'); - }); - - it('tries to retrieve aggregate snapshot', async () => { - - snapshotStorage.getAggregateSnapshot = () => snapshotEvent as IEvent; - snapshotStorage.saveAggregateSnapshot = () => { }; - sinon.spy(snapshotStorage, 'getAggregateSnapshot'); - const getAggregateEventsSpy = sinon.spy(storage, 'getAggregateEvents'); - - expect(es).to.have.property('snapshotsSupported', true); - - const events = await iteratorToArray(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; + const result: IEvent[] = []; + for await (const event of store.getAggregateEvents('aggregate-1')) { + result.push(event); + } - 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)', () => { + describe('getSagaEvents', () => { - it('returns events committed by saga prior to event that triggered saga execution', async () => { + it('retrieves saga events with provided filter', async () => { + const sagaEvents = [{ type: 'SagaEvent1' }] as IEvent[]; + mockStorage.getSagaEvents.mockResolvedValueOnce(sagaEvents); + const filter = { beforeEvent: { sagaVersion: 1 } }; - const events = [ - { sagaId: '1', sagaVersion: 1, type: 'somethingHappened' }, - { sagaId: '1', sagaVersion: 2, type: 'anotherHappened' }, - { sagaId: '2', sagaVersion: 1, type: 'somethingHappened' } - ]; - - const triggeredBy = events[1]; - - await es.commit(events); - - const ii = es.getSagaEvents('1', { beforeEvent: triggeredBy }); - const retrievedEvents = await iteratorToArray(ii); + const result: IEvent[] = []; + for await (const event of store.getSagaEvents('saga-1', filter)) { + result.push(event); + } - expect(retrievedEvents).to.be.an('Array'); - expect(retrievedEvents).to.have.length(1); - expect(retrievedEvents).to.have.nested.property('[0].type', 'somethingHappened'); + expect(result).toEqual(sagaEvents); + expect(mockStorage.getSagaEvents).toHaveBeenCalledWith('saga-1', filter); }); }); - describe('getEventsByTypes(eventTypes)', () => { - - it('returns a promise that resolves to all committed events of specific types', async () => { - await es.commit([goodEvent, goodEvent2]); - - const events = await iteratorToArray(es.getEventsByTypes(['somethingHappened'], {})); + 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, supplementaryEventBus }); - - 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, supplementaryEventBus }); + 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); + 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]); + + expect(promise).resolves.toBe(testEvent); + expect(callCount).toBe(1); }); }); }); diff --git a/tests/unit/SagaEventHandler.test.ts b/tests/unit/SagaEventHandler.test.ts index 5e13ece..f84f167 100644 --- a/tests/unit/SagaEventHandler.test.ts +++ b/tests/unit/SagaEventHandler.test.ts @@ -7,7 +7,8 @@ import { CommandBus, AbstractSaga, InMemoryMessageBus, - Deferred + Deferred, + EventDispatcher } from '../../src'; class Saga extends AbstractSaga { @@ -42,9 +43,11 @@ describe('SagaEventHandler', function () { let sagaEventHandler: SagaEventHandler; beforeEach(() => { - const supplementaryEventBus = new InMemoryMessageBus(); + const eventBus = new InMemoryMessageBus(); + const eventDispatcher = new EventDispatcher({ eventBus }); + const storage = new InMemoryEventStorage(); commandBus = new CommandBus({}); - eventStore = new EventStore({ storage: new InMemoryEventStorage(), supplementaryEventBus }); + eventStore = new EventStore({ storage, identifierProvider: storage, eventBus, eventDispatcher }); sagaEventHandler = new SagaEventHandler({ sagaType: Saga, eventStore, commandBus }); }); diff --git a/tests/unit/memory/InMemoryMessageBus.test.ts b/tests/unit/memory/InMemoryMessageBus.test.ts index fef5b23..e80c385 100644 --- a/tests/unit/memory/InMemoryMessageBus.test.ts +++ b/tests/unit/memory/InMemoryMessageBus.test.ts @@ -13,7 +13,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/InMemoryView.test.ts b/tests/unit/memory/InMemoryView.test.ts index 617b931..3c0e7eb 100644 --- a/tests/unit/memory/InMemoryView.test.ts +++ b/tests/unit/memory/InMemoryView.test.ts @@ -1,6 +1,6 @@ import { InMemoryView } from '../../../src'; import { expect, assert } from 'chai'; -import { nextCycle } from '../../../src/infrastructure/memory/utils'; +import { nextCycle } from '../../../src/in-memory/utils'; describe('InMemoryView', function () { diff --git a/tests/unit/sqlite/SqliteEventLocker.test.ts b/tests/unit/sqlite/SqliteEventLocker.test.ts index ee26dd4..e894fdd 100644 --- a/tests/unit/sqlite/SqliteEventLocker.test.ts +++ b/tests/unit/sqlite/SqliteEventLocker.test.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; import * as createDb from 'better-sqlite3'; -import { SqliteEventLocker } from '../../../src/infrastructure/sqlite/SqliteEventLocker'; +import { SqliteEventLocker } from '../../../src/sqlite/SqliteEventLocker'; import { IEvent } from '../../../src/interfaces'; -import { guid } from '../../../src/infrastructure/sqlite'; +import { guid } from '../../../src/sqlite'; import { promisify } from 'util'; const delay = promisify(setTimeout); diff --git a/tests/unit/sqlite/SqliteObjectStorage.test.ts b/tests/unit/sqlite/SqliteObjectStorage.test.ts index ce2aca7..6da8010 100644 --- a/tests/unit/sqlite/SqliteObjectStorage.test.ts +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import * as createDb from 'better-sqlite3'; -import { guid, SqliteObjectStorage } from '../../../src/infrastructure/sqlite'; +import { guid, SqliteObjectStorage } from '../../../src/sqlite'; describe('SqliteObjectStorage', function () { let db: import('better-sqlite3').Database; diff --git a/tests/unit/sqlite/SqliteObjectView.test.ts b/tests/unit/sqlite/SqliteObjectView.test.ts index 103b4c9..694b0fe 100644 --- a/tests/unit/sqlite/SqliteObjectView.test.ts +++ b/tests/unit/sqlite/SqliteObjectView.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import * as createDb from 'better-sqlite3'; -import { SqliteObjectView } from '../../../src/infrastructure/sqlite'; +import { SqliteObjectView } from '../../../src/sqlite'; import { promisify } from 'util'; const delay = promisify(setTimeout); diff --git a/tests/unit/sqlite/SqliteViewLocker.test.ts b/tests/unit/sqlite/SqliteViewLocker.test.ts index c7fecef..4ce335f 100644 --- a/tests/unit/sqlite/SqliteViewLocker.test.ts +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import * as createDb from 'better-sqlite3'; -import { SqliteViewLocker } from '../../../src/infrastructure/sqlite'; +import { SqliteViewLocker } from '../../../src/sqlite'; describe('SqliteViewLocker', function () { From 991c2233185d3610a2b8930f6930a03c0cdea01d Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 2 Apr 2025 02:40:34 +0100 Subject: [PATCH 024/135] New: RabbitMQ integration classes to support event publishing and subscription --- jest.config.ts | 10 +- package-lock.json | 83 ++++- package.json | 24 +- src/CqrsContainerBuilder.ts | 4 +- .../ExternalEventPublishingProcessor.ts | 29 ++ src/rabbitmq/RabbitMqEventBus.ts | 82 +++++ src/rabbitmq/RabbitMqGateway.ts | 339 ++++++++++++++++++ src/rabbitmq/index.ts | 2 + .../rabbitmq/RabbitMqEventBus.test.ts | 190 ++++++++++ .../rabbitmq/RabbitMqGateway.test.ts | 273 ++++++++++++++ tests/integration/rabbitmq/docker-compose.yml | 17 + 11 files changed, 1035 insertions(+), 18 deletions(-) create mode 100644 src/dispatch-pipeline/ExternalEventPublishingProcessor.ts create mode 100644 src/rabbitmq/RabbitMqEventBus.ts create mode 100644 src/rabbitmq/RabbitMqGateway.ts create mode 100644 src/rabbitmq/index.ts create mode 100644 tests/integration/rabbitmq/RabbitMqEventBus.test.ts create mode 100644 tests/integration/rabbitmq/RabbitMqGateway.test.ts create mode 100644 tests/integration/rabbitmq/docker-compose.yml diff --git a/jest.config.ts b/jest.config.ts index 45e753a..5b1e81b 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -17,9 +17,13 @@ export default { 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", diff --git a/package-lock.json b/package-lock.json index 6a360a4..e7b4b0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "di0": "^1.0.0" }, "devDependencies": { + "@types/amqplib": "^0.10.7", "@types/better-sqlite3": "^7.6.11", "@types/chai": "^4.3.20", "@types/jest": "^29.5.13", @@ -31,10 +32,33 @@ "node": ">=10.3.0" }, "peerDependencies": { + "amqplib": "^0.10.5", "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } }, + "node_modules/@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT", + "peer": true + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1042,6 +1066,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/amqplib": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.7.tgz", + "integrity": "sha512-IVj3avf9AQd2nXCx0PGk/OYq7VmHiyNxWFSb5HhU9ATh+i+gHWvVcljFTcTWQ/dyHJCTrzCixde+r/asL2ErDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1250,6 +1284,21 @@ "dev": true, "license": "MIT" }, + "node_modules/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-Dx5zmy0Ur+Q7LPPdhz+jx5IzmJBoHd15tOeAfQ8SuvEtyPJ20hBemhOBA4b1WeORCRa0ENM/kHCzmem1w/zHvQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "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", @@ -1659,6 +1708,13 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", + "license": "MIT", + "peer": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2216,7 +2272,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4486,7 +4541,6 @@ "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": { @@ -4974,6 +5028,13 @@ "teleport": ">=0.2.0" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT", + "peer": true + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -5187,6 +5248,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT", + "peer": true + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -5947,6 +6015,17 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index fd05acc..0cb2672 100644 --- a/package.json +++ b/package.json @@ -2,28 +2,26 @@ "name": "node-cqrs", "version": "1.0.0-rc.6", "description": "Basic ES6 backbone for CQRS app development", + "keywords": [ + "cqrs", + "eventsourcing" + ], "repository": { "type": "git", "url": "https://github.com/snatalenko/node-cqrs.git" }, - "directories": { - "doc": "docs", - "example": "examples", - "test": "tests" - }, - "keywords": [ - "cqrs", - "eventsourcing", - "ddd", - "domain", - "eventstore" - ], "main": "./dist/index.js", "types": "./src/index.ts", "exports": { ".": "./dist/index.js", + "./rabbitmq": "./dist/rabbitmq/index.js", "./sqlite": "./dist/sqlite/index.js" }, + "directories": { + "doc": "docs", + "example": "examples", + "test": "tests" + }, "engines": { "node": ">=10.3.0" }, @@ -49,6 +47,7 @@ "di0": "^1.0.0" }, "devDependencies": { + "@types/amqplib": "^0.10.7", "@types/better-sqlite3": "^7.6.11", "@types/chai": "^4.3.20", "@types/jest": "^29.5.13", @@ -63,6 +62,7 @@ "typescript": "^5.6.2" }, "peerDependencies": { + "amqplib": "^0.10.5", "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index cefc476..57c337f 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -26,6 +26,7 @@ import { IProjectionConstructor, ISagaConstructor } from './interfaces'; +import { ExternalEventPublishingProcessor } from './dispatch-pipeline/ExternalEventPublishingProcessor'; interface CqrsContainer extends Container { eventStore: IEventStore; @@ -46,8 +47,9 @@ export class CqrsContainerBuilder extends ContainerBuilder { super.register(container => { const eventDispatcher = new EventDispatcher(container); eventDispatcher.addPipelineProcessor(new EventValidationProcessor(container)); - eventDispatcher.addPipelineProcessor(new SnapshotPersistenceProcessor(container)); + eventDispatcher.addPipelineProcessor(new ExternalEventPublishingProcessor(container)); eventDispatcher.addPipelineProcessor(new EventPersistenceProcessor(container)); + eventDispatcher.addPipelineProcessor(new SnapshotPersistenceProcessor(container)); return eventDispatcher; }).as('eventDispatcher'); diff --git a/src/dispatch-pipeline/ExternalEventPublishingProcessor.ts b/src/dispatch-pipeline/ExternalEventPublishingProcessor.ts new file mode 100644 index 0000000..8cfbad3 --- /dev/null +++ b/src/dispatch-pipeline/ExternalEventPublishingProcessor.ts @@ -0,0 +1,29 @@ +import { IEventProcessor, IEventBus, EventBatch } from '../interfaces'; + +/** + * Event dispatcher processor that publishes events to an external RabbitMQ event bus if provided. + */ +export class ExternalEventPublishingProcessor implements IEventProcessor { + + #externalEventBus?: IEventBus; + + constructor(options: { externalEventBus?: IEventBus }) { + this.#externalEventBus = options.externalEventBus; + } + + async process(batch: EventBatch): Promise { + if (!this.#externalEventBus) + return batch; + + // TODO: ignore external events + + for (const { event } of batch) { + if (!event) + throw new Error('Event batch does not contain `event`'); + + await this.#externalEventBus.publish(event); + } + + return batch; + } +} diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts new file mode 100644 index 0000000..1946e67 --- /dev/null +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -0,0 +1,82 @@ +import { IEvent, IEventBus, IMessage, IMessageHandler, IObservable } from "../interfaces"; +import { RabbitMqGateway } from "./RabbitMqGateway"; + +const ALL_EVENTS_WILDCARD = '*'; + +export class RabbitMqEventBus implements IEventBus { + + static get allEventsWildcard(): '*' { + return ALL_EVENTS_WILDCARD; + } + + #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 ?? 'node-cqrs.events'; + 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 + }); + } + + /** + * Removes a previously registered message handler for a specific event type. + */ + off(eventType: string, handler: IMessageHandler): void { + 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..455c83e --- /dev/null +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -0,0 +1,339 @@ +import amqp, { Channel, ChannelModel, ConfirmChannel, ConsumeMessage, Message } from 'amqplib'; +import { + IExtendableLogger, + ILogger, + IMessage, + isMessage +} from '../interfaces'; +import * as Event from '../Event'; +import { delay } from '../utils'; + +/** 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; +type Subscription = { + exchange: string, + queueName?: string, + eventType?: string, + handler: MessageHandler, + ignoreOwn?: boolean +}; + +const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); + +/** + * RabbitMqGateway implements the IObservable interface using RabbitMQ. + * + * It uses a fanout exchange to broadcast messages to all connected subscribers. + * The `on` and `off` methods allow you to register and remove handlers for specific event types. + * The `queue(name)` method creates or returns a durable queue with the given name, ensuring that + * all messages delivered to the fanout exchange are also routed to this queue. + */ +export class RabbitMqGateway { + + #connectionFactory: () => Promise; + #appId: string; + #logger: ILogger | undefined; + + #connecting = false; + #connection: ChannelModel | undefined; + #pubChannel: ConfirmChannel | undefined; + #exclusiveQueueName: string = ''; + #queueChannels = new Map(); + #queueConsumers = new Map(); + + #subscriptions: Array = []; + #handlers: Map>> = new Map(); + + get connection() { + return this.#connection; + } + + get channel() { + return this.#pubChannel; + } + + constructor(o: { + rabbitMqConnectionFactory?: () => Promise, + logger?: ILogger | IExtendableLogger + }) { + if (!o.rabbitMqConnectionFactory) + throw new TypeError('rabbitMqConnectionFactory argument required'); + + this.#connectionFactory = o.rabbitMqConnectionFactory; + this.#appId = getRandomAppId(); + this.#logger = o.logger && 'child' in o.logger ? + o.logger.child({ service: new.target.name }) : + o.logger; + } + + async connect(): Promise { + while (this.#connecting) + await delay(1_000); + + this.#connecting = true; + + while (!this.#connection) { + try { + this.#connection = await this.#connectionFactory(); + this.#connection.on('error', err => this.#onConnectionError(err)); + this.#connection.on('close', () => this.#onConnectionClosed()); + this.#logger?.info(`${this.#appId}: Connection established`); + + this.#handlers.clear(); + const subscriptionsToRestore = this.#subscriptions.splice(0); + for (const subscription of subscriptionsToRestore) + await this.subscribe(subscription); + } + catch (err) { + this.#logger?.warn(`${this.#appId}: Connection attempt failed: ${err.message}`); + await delay(5_000); + } + } + + this.#connecting = false; + + return this.#connection; + } + + async disconnect() { + await this.#connection?.close(); + if (this.#connection) // clean up in case 'close' event was not triggered + this.#onConnectionClosed(); + } + + #onConnectionError(err: Error) { + this.#logger?.warn(`${this.#appId}: Connection error: ${err.message}`); + } + + #onConnectionClosed() { + this.#logger?.warn('Connection closed'); + this.#connection = undefined; + this.#pubChannel = undefined; + this.#exclusiveQueueName = ''; + this.#queueChannels.clear(); + this.#queueConsumers.clear(); + } + + #getHandlers(queueGivenName: string = '', eventType: string = '*') { + return this.#subscriptions + .filter(s => + s.queueGivenName === queueGivenName && ( + !s.eventType + || s.eventType === '*' + || s.eventType === eventType)) + .map(s => s.handler); + } + + async subscribeToQueue(exchange: string, queueName: string, handler: MessageHandler) { + return this.subscribe({ exchange, queueName, handler }); + } + + /** + * 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) { + return this.subscribe({ exchange, handler }); + } + + async subscribe(subscription: Subscription) { + let { queueName } = subscription; + const { + exchange, + eventType, + ignoreOwn = !queueName + } = subscription; + + const channel = await this.#assertChannel(queueName); + + if (!queueName) { + if (!this.#exclusiveQueueName) { + // Assert temporary "exclusive" queue that will be destroyed on connection termination + const queueGivenName = await this.#assetQueue(channel, exchange, '', eventType, { + exclusive: true, + durable: false + }); + + this.#exclusiveQueueName = queueGivenName; + } + else { + this.#assertBinding(channel, exchange, this.#exclusiveQueueName, eventType); + } + + queueName = this.#exclusiveQueueName; + } + else { + const deadLetterExchangeName = `${exchange}.failed`; + + // Assert dead letter queue for rejected or timed out messages + await this.#assetQueue(channel, deadLetterExchangeName, `${queueName}.failed`); + + // Assert durable queue that will survive broker restart + await this.#assetQueue(channel, exchange, queueName, eventType, { deadLetterExchangeName }); + } + + await this.#assertConsumer(queueName, ignoreOwn, channel); + + this.#subscriptions.push({ ...subscription, queueGivenName: queueName }); + } + + async unsubscribe(d: Subscription) { + this.#subscriptions = this.#subscriptions.filter(s => !( + s.exchange === d.exchange && + s.queueName === d.queueName && + s.eventType === d.eventType && + s.handler === d.handler)); + } + + async #assertConnection() { + return this.#connection ?? await this.connect(); + } + + /** Get existing or open a new channel for a given queue name */ + async #assertChannel(queueName: string = ''): Promise { + const connection = await this.#assertConnection(); + let channel = this.#queueChannels.get(queueName); + if (!channel) { + channel = await 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, + ...deadLetterExchangeName && { + arguments: { + 'x-dead-letter-exchange': deadLetterExchangeName + } + } + }); + + await this.#assertBinding(channel, exchange, queueGivenName, eventType); + + return queueGivenName; + } + + async #assertBinding(channel: Channel, exchange: string, queueGivenName: string, eventType?: string) { + if (!eventType || eventType === '*') + eventType = '#'; + + await channel.bindQueue(queueGivenName, exchange, eventType); + + this.#logger?.debug(`${this.#appId}: Queue "${queueGivenName}" bound to exchange "${exchange}" with pattern "${eventType}"`); + } + + async #assertConsumer(queueGivenName: string, ignoreOwn: boolean, channel: Channel) { + if (this.#queueConsumers.has(queueGivenName)) + return; + + const c = await channel.consume(queueGivenName, async (msg: ConsumeMessage | null) => { + if (!msg) + return; + + const { consumerTag, routingKey } = msg.fields; + const { appId, messageId, correlationId } = msg.properties; + + this.#logger?.debug(`${this.#appId}: Message received`, { + queueName: queueGivenName, + consumerTag, + routingKey, + messageId, + correlationId, + appId + }); + + if (ignoreOwn && appId === this.#appId) + return; + + try { + 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 of handlers) + await handler(message); + + channel?.ack(msg); + } + catch (err) { + this.#logger?.error(`${this.#appId}: Message processing failed: ${err.message}`); + + // Redirect message to dead letter queue, if `{ noAck: true }` was not set on consumption + channel?.nack(msg, false, false); + } + }); + + this.#logger?.debug(`${this.#appId}: Consumer "${c.consumerTag}" registered on queue "${queueGivenName}"`); + + this.#queueConsumers.set(queueGivenName, c.consumerTag); + } + + /** + * 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) => { + const published = this.#pubChannel!.publish(exchange, message.type, content, properties, err => + err ? reject(err) : resolve()); + if (!published) + throw new Error(`${this.#appId}: Failed to send event ${Event.describe(message)}, channel buffer is full`); + }); + } +} diff --git a/src/rabbitmq/index.ts b/src/rabbitmq/index.ts new file mode 100644 index 0000000..79404df --- /dev/null +++ b/src/rabbitmq/index.ts @@ -0,0 +1,2 @@ +export * from './RabbitMqEventBus'; +export * from './RabbitMqGateway'; diff --git a/tests/integration/rabbitmq/RabbitMqEventBus.test.ts b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts new file mode 100644 index 0000000..ee99a10 --- /dev/null +++ b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts @@ -0,0 +1,190 @@ +import * as amqplib from 'amqplib'; +import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; +import { RabbitMqEventBus } from '../../../src/rabbitmq/RabbitMqEventBus'; +import { IMessage, IEvent } from '../../../src/interfaces'; + +const delay = (ms: number) => new Promise(res => { + const t = setTimeout(res, ms); + t.unref(); +}); + +describe('RabbitMqEventBus', () => { + + let gateway1: RabbitMqGateway; + let gateway2: RabbitMqGateway; + let gateway3: RabbitMqGateway; + let eventBus1: RabbitMqEventBus; + let eventBus2: RabbitMqEventBus; + let eventBus3: RabbitMqEventBus; + + const queueName = 'test-bus-queue'; + const exchangeName = 'test-bus-exchange'; + const eventType = 'test-bus-event'; + + beforeEach(async () => { + const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); + gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory }); + gateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory }); + gateway3 = new RabbitMqGateway({ rabbitMqConnectionFactory }); + eventBus1 = new RabbitMqEventBus({ rabbitMqGateway: gateway1, exchange: exchangeName }); + eventBus2 = new RabbitMqEventBus({ rabbitMqGateway: gateway2, exchange: exchangeName }); + eventBus3 = new RabbitMqEventBus({ rabbitMqGateway: gateway3, exchange: exchangeName }); + }); + + afterEach(async () => { + const ch = await gateway1.connection.createChannel(); + await ch.deleteQueue(queueName); + await ch.deleteQueue(`${queueName}.failed`); + await ch.deleteExchange(exchangeName); + await gateway1.disconnect(); + await gateway2.disconnect(); + await gateway3.disconnect(); + }); + + describe('publish()', () => { + + it('publishes without throwing', async () => { + + await eventBus1.publish({ type: eventType }); + }); + }); + + describe('on()', () => { + + it('subscribes to events so that they are delivered to every subscriber except sender', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + const received3: IMessage[] = []; + + await eventBus1.on(eventType, e => { + received1.push(e); + }); + + await eventBus2.on(eventType, e => { + received2.push(e); + }); + + await eventBus3.on(eventType, e => { + received3.push(e); + }); + + const event: IEvent = { + type: eventType, + payload: { ok: true } + }; + + await eventBus2.publish(event); + await delay(50); + + expect(received1).toEqual([event]); + expect(received2).toEqual([]); + expect(received3).toEqual([event]); + }); + + it('allows to subscribe to all events', async () => { + + const received1: IMessage[] = []; + + await eventBus1.on(RabbitMqEventBus.allEventsWildcard, e => { + received1.push(e); + }); + + const event1: IEvent = { type: `${eventType}1` }; + const event2: IEvent = { type: `${eventType}2` }; + + await eventBus2.publish(event1); + await eventBus3.publish(event2); + + await delay(50); + + expect(received1).toEqual([event1, event2]); + }); + }); + + describe('queue()', () => { + + it('creates an isolated queue where published messages delivered to only one recipient', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + await eventBus1.queue(queueName).on(eventType, (msg) => { + received1.push(msg); + }); + + await eventBus2.queue(queueName).on(eventType, (msg) => { + received2.push(msg); + }); + + const event: IEvent = { + type: eventType, + payload: { ok: true } + }; + + await eventBus1.publish(event); + await delay(50); + + expect([...received1, ...received2]).toEqual([ + event + ]); + }); + + it('allows to subscribe to all events in the queue', async () => { + + const received1: IMessage[] = []; + const received2: IMessage[] = []; + + await eventBus1.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, (msg) => { + received1.push(msg); + }); + + await eventBus2.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, (msg) => { + received2.push(msg); + }); + + const event1: IEvent = { + type: `${eventType}1` + }; + + const event2: IEvent = { + type: `${eventType}2` + }; + + await eventBus1.publish(event1); + await eventBus1.publish(event2); + + await delay(50); + + expect([...received1, ...received2]).toEqual([ + event1, + event2 + ]); + }); + + }); + + describe('off()', () => { + + it('removes previously added handler', async () => { + + const received1: IMessage[] = []; + const handler1 = (msg: IMessage) => received1.push(msg); + await eventBus1.on(eventType, handler1); + + const received2: IMessage[] = []; + const handler2 = (msg: IMessage) => received2.push(msg); + await eventBus2.on(eventType, handler2); + + eventBus2.off(eventType, handler2); + + const event = { type: eventType, payload: { removed: true } }; + await eventBus3.publish(event); + + await delay(50); + + expect(received1).toEqual([event]); + expect(received2).toEqual([]); + }); + }); +}); diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts new file mode 100644 index 0000000..5bf6ccd --- /dev/null +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -0,0 +1,273 @@ +import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; +import { IEvent, IMessage } from '../../../src/interfaces'; +import * as amqplib from 'amqplib'; +import { delay } from '../../../src/utils'; + +describe('RabbitMqGateway', () => { + + let gateway1: RabbitMqGateway; + let gateway2: RabbitMqGateway | undefined; + let gateway3: RabbitMqGateway | undefined; + const exchange = 'test-exchange'; + const queueName = 'test-queue'; + + beforeEach(async () => { + const logger = undefined; + // const logger = console; + const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); + gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); + gateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); + gateway3 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); + }); + + 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 ]); + }); + }); + + 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]); + }); + }); +}); 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: From d680d9589d24bd72a7a995ee70523ec605779772 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 2 Apr 2025 18:06:38 +0100 Subject: [PATCH 025/135] Fix `ignoreOwn` resolving per rabbitmq message handler --- src/rabbitmq/RabbitMqEventBus.ts | 3 ++- src/rabbitmq/RabbitMqGateway.ts | 36 +++++++++++++++----------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts index 1946e67..6b5c813 100644 --- a/src/rabbitmq/RabbitMqEventBus.ts +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -44,7 +44,8 @@ export class RabbitMqEventBus implements IEventBus { exchange: this.#exchange, queueName: this.#queueName, eventType, - handler + handler, + ignoreOwn: !this.#queueName }); } diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 455c83e..275efc1 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -118,13 +118,14 @@ export class RabbitMqGateway { } #getHandlers(queueGivenName: string = '', eventType: string = '*') { - return this.#subscriptions - .filter(s => - s.queueGivenName === queueGivenName && ( - !s.eventType - || s.eventType === '*' - || s.eventType === eventType)) - .map(s => s.handler); + return this.#subscriptions.filter(s => + s.queueGivenName === queueGivenName + && ( + !s.eventType + || s.eventType === '*' + || s.eventType === eventType + ) + ); } async subscribeToQueue(exchange: string, queueName: string, handler: MessageHandler) { @@ -138,16 +139,12 @@ export class RabbitMqGateway { * Failed message processing does not result in redelivery or dead-lettering. */ async subscribeToFanout(exchange: string, handler: MessageHandler) { - return this.subscribe({ exchange, handler }); + return this.subscribe({ exchange, handler, ignoreOwn: true }); } async subscribe(subscription: Subscription) { let { queueName } = subscription; - const { - exchange, - eventType, - ignoreOwn = !queueName - } = subscription; + const { exchange, eventType } = subscription; const channel = await this.#assertChannel(queueName); @@ -177,7 +174,7 @@ export class RabbitMqGateway { await this.#assetQueue(channel, exchange, queueName, eventType, { deadLetterExchangeName }); } - await this.#assertConsumer(queueName, ignoreOwn, channel); + await this.#assertConsumer(queueName, channel); this.#subscriptions.push({ ...subscription, queueGivenName: queueName }); } @@ -249,7 +246,7 @@ export class RabbitMqGateway { this.#logger?.debug(`${this.#appId}: Queue "${queueGivenName}" bound to exchange "${exchange}" with pattern "${eventType}"`); } - async #assertConsumer(queueGivenName: string, ignoreOwn: boolean, channel: Channel) { + async #assertConsumer(queueGivenName: string, channel: Channel) { if (this.#queueConsumers.has(queueGivenName)) return; @@ -269,9 +266,6 @@ export class RabbitMqGateway { appId }); - if (ignoreOwn && appId === this.#appId) - return; - try { const jsonContent = msg.content.toString(); const message: IMessage = JSON.parse(jsonContent); @@ -280,8 +274,12 @@ export class RabbitMqGateway { 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 of handlers) + for (const { handler, ignoreOwn } of handlers) { + if (ignoreOwn && appId === this.#appId) + continue; + await handler(message); + } channel?.ack(msg); } From 8109ae958c9de1f2162ef8d8daaaa8e31137737d Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 2 Apr 2025 19:02:20 +0100 Subject: [PATCH 026/135] Add `concurrentLimit` to rabbitmq subscriptions --- src/rabbitmq/RabbitMqGateway.ts | 45 +++++++++++-------- .../rabbitmq/RabbitMqGateway.test.ts | 43 +++++++++++++++--- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 275efc1..4969fcc 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -1,4 +1,4 @@ -import amqp, { Channel, ChannelModel, ConfirmChannel, ConsumeMessage, Message } from 'amqplib'; +import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; import { IExtendableLogger, ILogger, @@ -18,7 +18,8 @@ type Subscription = { queueName?: string, eventType?: string, handler: MessageHandler, - ignoreOwn?: boolean + ignoreOwn?: boolean, + concurrentLimit?: number }; const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); @@ -40,7 +41,7 @@ export class RabbitMqGateway { #connecting = false; #connection: ChannelModel | undefined; #pubChannel: ConfirmChannel | undefined; - #exclusiveQueueName: string = ''; + #exclusiveQueueName: string | undefined; #queueChannels = new Map(); #queueConsumers = new Map(); @@ -112,7 +113,7 @@ export class RabbitMqGateway { this.#logger?.warn('Connection closed'); this.#connection = undefined; this.#pubChannel = undefined; - this.#exclusiveQueueName = ''; + this.#exclusiveQueueName = undefined; this.#queueChannels.clear(); this.#queueConsumers.clear(); } @@ -143,40 +144,45 @@ export class RabbitMqGateway { } async subscribe(subscription: Subscription) { - let { queueName } = subscription; - const { exchange, eventType } = subscription; + const { + exchange, + queueName, + eventType, + concurrentLimit + } = subscription; const channel = await this.#assertChannel(queueName); - if (!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 - const queueGivenName = await this.#assetQueue(channel, exchange, '', eventType, { + this.#exclusiveQueueName = await this.#assetQueue(channel, exchange, '', eventType, { exclusive: true, durable: false }); - - this.#exclusiveQueueName = queueGivenName; } else { - this.#assertBinding(channel, exchange, this.#exclusiveQueueName, eventType); + // If exclusive queue already exists, ensure it is bound with the current event type + await this.#assertBinding(channel, exchange, this.#exclusiveQueueName, eventType); } - - queueName = this.#exclusiveQueueName; + 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, `${queueName}.failed`); + await this.#assetQueue(channel, deadLetterExchangeName, `${queueGivenName}.failed`); // Assert durable queue that will survive broker restart - await this.#assetQueue(channel, exchange, queueName, eventType, { deadLetterExchangeName }); + await this.#assetQueue(channel, exchange, queueGivenName, eventType, { deadLetterExchangeName }); } - await this.#assertConsumer(queueName, channel); + await this.#assertConsumer(queueGivenName, channel, concurrentLimit); - this.#subscriptions.push({ ...subscription, queueGivenName: queueName }); + this.#subscriptions.push({ ...subscription, queueGivenName }); } async unsubscribe(d: Subscription) { @@ -246,10 +252,13 @@ export class RabbitMqGateway { this.#logger?.debug(`${this.#appId}: Queue "${queueGivenName}" bound to exchange "${exchange}" with pattern "${eventType}"`); } - async #assertConsumer(queueGivenName: string, channel: Channel) { + async #assertConsumer(queueGivenName: string, channel: Channel, concurrentLimit?: number) { if (this.#queueConsumers.has(queueGivenName)) return; + if (concurrentLimit) + await channel.prefetch(concurrentLimit); + const c = await channel.consume(queueGivenName, async (msg: ConsumeMessage | null) => { if (!msg) return; diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index 5bf6ccd..6999fda 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -1,5 +1,5 @@ import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; -import { IEvent, IMessage } from '../../../src/interfaces'; +import { IMessage } from '../../../src/interfaces'; import * as amqplib from 'amqplib'; import { delay } from '../../../src/utils'; @@ -206,8 +206,8 @@ describe('RabbitMqGateway', () => { await delay(50); - expect(received1).toEqual([ event1, event2 ]); - expect(received2).toEqual([ event2, event3 ]); + expect(received1).toEqual([event1, event2]); + expect(received2).toEqual([event2, event3]); }); it('subscribe queue to given event types, when specified', async () => { @@ -230,8 +230,41 @@ describe('RabbitMqGateway', () => { await delay(50); - expect(received1).toEqual([ event1 ]); - expect(received2).toEqual([ event2 ]); + 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]); }); }); From f6616f590ccb7f62241e2ba8bc012efc3ebabdfc Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 2 Apr 2025 20:53:07 +0100 Subject: [PATCH 027/135] Fix package exports --- package.json | 10 ++++++++++ src/dispatch-pipeline/EventPersistenceProcessor.ts | 3 +-- src/dispatch-pipeline/index.ts | 1 + src/index.ts | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0cb2672..cb8cef6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,16 @@ "./rabbitmq": "./dist/rabbitmq/index.js", "./sqlite": "./dist/sqlite/index.js" }, + "typesVersions": { + "*": { + "rabbitmq": [ + "src/rabbitmq/index.ts" + ], + "sqlite": [ + "src/sqlite/index.ts" + ] + } + }, "directories": { "doc": "docs", "example": "examples", diff --git a/src/dispatch-pipeline/EventPersistenceProcessor.ts b/src/dispatch-pipeline/EventPersistenceProcessor.ts index aea2ff3..cfd93a1 100644 --- a/src/dispatch-pipeline/EventPersistenceProcessor.ts +++ b/src/dispatch-pipeline/EventPersistenceProcessor.ts @@ -1,8 +1,7 @@ import { EventBatch, IEvent, IEventProcessor, IEventStoreWriter } from '../interfaces'; /** - * Processor responsible for persisting events using an in-memory event storage. - * Typically used for testing or ephemeral scenarios where durability isn't required. + * Processor responsible for persisting events to IEventStoreWriter. */ export class EventPersistenceProcessor implements IEventProcessor { diff --git a/src/dispatch-pipeline/index.ts b/src/dispatch-pipeline/index.ts index 43ec39a..c536281 100644 --- a/src/dispatch-pipeline/index.ts +++ b/src/dispatch-pipeline/index.ts @@ -1,3 +1,4 @@ export * from './EventPersistenceProcessor'; export * from './EventValidationProcessor'; +export * from './ExternalEventPublishingProcessor'; export * from './SnapshotPersistenceProcessor'; diff --git a/src/index.ts b/src/index.ts index 27d5d0b..a1e1c55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ export * from './AbstractProjection'; export * from './EventDispatcher'; export * from './in-memory'; +export * from './dispatch-pipeline'; export * as Event from './Event'; export { From 4f2281b043a5a41436f0acdb965764d8004c69b7 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 4 Apr 2025 01:22:01 +0100 Subject: [PATCH 028/135] Fix typings when installed in project with "Node" moduleResolution --- package.json | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index cb8cef6..d19af1c 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,6 @@ }, "main": "./dist/index.js", "types": "./src/index.ts", - "exports": { - ".": "./dist/index.js", - "./rabbitmq": "./dist/rabbitmq/index.js", - "./sqlite": "./dist/sqlite/index.js" - }, "typesVersions": { "*": { "rabbitmq": [ @@ -27,6 +22,20 @@ ] } }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./src/index.ts" + }, + "./rabbitmq": { + "import": "./dist/rabbitmq/index.js", + "types": "./src/rabbitmq/index.ts" + }, + "./sqlite": { + "import": "./dist/sqlite/index.js", + "types": "./src/sqlite/index.ts" + } + }, "directories": { "doc": "docs", "example": "examples", From e955eff465196003390e4f5fa1f5e1150747ae1c Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 4 Apr 2025 01:24:44 +0100 Subject: [PATCH 029/135] Fix internal typings --- src/AbstractProjection.ts | 2 +- src/EventDispatcher.ts | 3 +- src/EventStore.ts | 10 ++--- .../EventPersistenceProcessor.ts | 12 +++--- .../EventValidationProcessor.ts | 2 +- .../SnapshotPersistenceProcessor.ts | 41 +++++++------------ src/in-memory/InMemoryEventStorage.ts | 6 +-- src/interfaces/IEvent.ts | 9 ++++ src/interfaces/IEventSet.ts | 6 ++- src/interfaces/IEventStorage.ts | 20 ++++----- src/interfaces/IEventStore.ts | 4 +- 11 files changed, 58 insertions(+), 57 deletions(-) diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index 764b589..c0fb2f0 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -44,7 +44,7 @@ export type AbstractProjectionParams = { /** * Base class for Projection definition */ -export abstract class AbstractProjection> implements IProjection { +export abstract class AbstractProjection implements IProjection { /** * List of event types handled by the projection. Can be overridden in the projection implementation. diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 2a29fc3..ac10bfa 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -4,7 +4,8 @@ import { IEventDispatcher, IEventProcessor, IEventSet, - IEventBus + IEventBus, + isEventSet } from "./interfaces"; import { parallelPipe } from 'async-parallel-pipe'; import { AsyncIterableBuffer } from 'async-iterable-buffer'; diff --git a/src/EventStore.ts b/src/EventStore.ts index b1cdc28..d54c988 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -1,7 +1,7 @@ import { IAggregateSnapshotStorage, IEvent, - IEventStoreReader, + IEventStorageReader, IEventSet, IExtendableLogger, ILogger, @@ -17,7 +17,7 @@ import { IEventDispatcher, IEventBus, isIEventBus, - isIEventStoreReader + isIEventStorageReader } from "./interfaces"; import { getClassName, @@ -28,7 +28,7 @@ import { EventDispatcher } from "./EventDispatcher"; export class EventStore implements IEventStore { #identifierProvider: IIdentifierProvider; - #storage: IEventStoreReader; + #storage: IEventStorageReader; #snapshotStorage: IAggregateSnapshotStorage | undefined; eventBus: IEventBus; #eventDispatcher: IEventDispatcher; @@ -43,7 +43,7 @@ export class EventStore implements IEventStore { eventDispatcher, logger, }: { - storage: IEventStoreReader, + storage: IEventStorageReader, identifierProvider?: IIdentifierProvider, snapshotStorage?: IAggregateSnapshotStorage, eventBus?: IEventBus, @@ -54,7 +54,7 @@ export class EventStore implements IEventStore { throw new TypeError('storage argument required'); if (!identifierProvider) throw new TypeError('identifierProvider argument required'); - if (!isIEventStoreReader(storage)) + if (!isIEventStorageReader(storage)) throw new TypeError('storage does not implement IEventStorage interface'); if (eventBus && !isIEventBus(eventBus)) throw new TypeError('eventBus does not implement IMessageBus interface'); diff --git a/src/dispatch-pipeline/EventPersistenceProcessor.ts b/src/dispatch-pipeline/EventPersistenceProcessor.ts index cfd93a1..eb63599 100644 --- a/src/dispatch-pipeline/EventPersistenceProcessor.ts +++ b/src/dispatch-pipeline/EventPersistenceProcessor.ts @@ -1,13 +1,13 @@ -import { EventBatch, IEvent, IEventProcessor, IEventStoreWriter } from '../interfaces'; +import { EventBatch, IEvent, IEventProcessor, IEventStorageWriter } from '../interfaces'; /** * Processor responsible for persisting events to IEventStoreWriter. */ export class EventPersistenceProcessor implements IEventProcessor { - #storage: IEventStoreWriter; + #storage: IEventStorageWriter; - constructor(options: { storage: IEventStoreWriter }) { + constructor(options: { storage: IEventStorageWriter }) { if (!options.storage) throw new TypeError('storage argument required'); @@ -15,12 +15,12 @@ export class EventPersistenceProcessor implements IEventProcessor { } async process(batch: EventBatch): Promise { - if(!this.#storage) + if (!this.#storage) return batch; const events: IEvent[] = []; - for(const { event } of batch) { - if(!event) + for (const { event } of batch) { + if (!event) throw new Error('Event batch does not contain event'); events.push(event); diff --git a/src/dispatch-pipeline/EventValidationProcessor.ts b/src/dispatch-pipeline/EventValidationProcessor.ts index 72090c6..7f24245 100644 --- a/src/dispatch-pipeline/EventValidationProcessor.ts +++ b/src/dispatch-pipeline/EventValidationProcessor.ts @@ -12,7 +12,7 @@ export class EventValidationProcessor implements IEventProcessor { #validate: EventValidator; constructor(o?: { - eventFormatValidator: EventValidator + eventFormatValidator?: EventValidator }) { this.#validate = o?.eventFormatValidator ?? defaultValidator; } diff --git a/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts b/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts index dc6088f..eeab2ce 100644 --- a/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts +++ b/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts @@ -9,8 +9,10 @@ import { import * as Event from '../Event'; const SNAPSHOT_EVENT_TYPE = 'snapshot'; +const isSnapshotEvent = (event?: IEvent): event is IEvent & { type: 'snapshot' } => + (!!event && event.type === SNAPSHOT_EVENT_TYPE); -export class SnapshotPersistenceProcessor implements IEventProcessor { +export class SnapshotPersistenceProcessor implements IEventProcessor<{ event?: IEvent }> { #snapshotStorage?: IAggregateSnapshotStorage; #logger?: ILogger; @@ -25,42 +27,27 @@ export class SnapshotPersistenceProcessor implements IEventProcessor { options.logger; } - #extractSnapshotEvent(batch: EventBatch): IEvent | undefined { - if (!Array.isArray(batch)) - throw new TypeError('batch argument must be an Array'); - - const snapshotEvents = batch.filter(({ event }) => event?.type === SNAPSHOT_EVENT_TYPE); - if (snapshotEvents.length > 1) - throw new Error(`Cannot process more than one "${SNAPSHOT_EVENT_TYPE}" event per batch`); - - return snapshotEvents[0].event; - } - async process(batch: EventBatch): Promise { if (!this.#snapshotStorage) return batch; - const snapshotEvent = this.#extractSnapshotEvent(batch); - if (!snapshotEvent) - return batch; - - this.#logger?.debug(`Persisting ${Event.describe(snapshotEvent)}`); - - await this.#snapshotStorage.saveAggregateSnapshot(snapshotEvent); + const snapshotEvents = batch.map(e => e.event).filter(isSnapshotEvent); + for (const event of snapshotEvents) { + this.#logger?.debug(`Persisting ${Event.describe(event)}`); + await this.#snapshotStorage.saveAggregateSnapshot(event); + } - return batch.filter(e => e !== snapshotEvent); + return batch.filter(e => !isSnapshotEvent(e.event)); } async revert(batch: EventBatch): Promise { if (!this.#snapshotStorage) return; - const snapshotEvent = this.#extractSnapshotEvent(batch); - if (!snapshotEvent) - return; - - this.#logger?.debug(`Removing ${Event.describe(snapshotEvent)}`); - - await this.#snapshotStorage?.deleteAggregateSnapshot(snapshotEvent); + const snapshotEvents = batch.map(e => e.event).filter(isSnapshotEvent); + for (const snapshotEvent of snapshotEvents) { + this.#logger?.debug(`Removing ${Event.describe(snapshotEvent)}`); + await this.#snapshotStorage.deleteAggregateSnapshot(snapshotEvent); + } } } diff --git a/src/in-memory/InMemoryEventStorage.ts b/src/in-memory/InMemoryEventStorage.ts index a77def8..bd3b3ed 100644 --- a/src/in-memory/InMemoryEventStorage.ts +++ b/src/in-memory/InMemoryEventStorage.ts @@ -3,9 +3,9 @@ import { IEvent, IEventSet, EventQueryAfter, - IEventStoreReader, + IEventStorageReader, IEventStream, - IEventStoreWriter + IEventStorageWriter } from "../interfaces"; import { nextCycle } from "./utils"; @@ -13,7 +13,7 @@ import { nextCycle } from "./utils"; * A simple event storage implementation intended to use for tests only. * Storage content resets on each app restart. */ -export class InMemoryEventStorage implements IEventStoreReader, IEventStoreWriter, IIdentifierProvider { +export class InMemoryEventStorage implements IEventStorageReader, IEventStorageWriter, IIdentifierProvider { #nextId: number = 0; #events: IEventSet = []; diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts index 70af6ee..16f3118 100644 --- a/src/interfaces/IEvent.ts +++ b/src/interfaces/IEvent.ts @@ -5,3 +5,12 @@ 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' + && ( + 'aggregateId' in event + || 'sagaId' in event + ); diff --git a/src/interfaces/IEventSet.ts b/src/interfaces/IEventSet.ts index e54caf5..b65cc0a 100644 --- a/src/interfaces/IEventSet.ts +++ b/src/interfaces/IEventSet.ts @@ -1,3 +1,7 @@ -import { IEvent } from "./IEvent"; +import { IEvent, isEvent } from "./IEvent"; export type IEventSet = ReadonlyArray>; + +export const isEventSet = (arr: unknown): arr is IEventSet => + Array.isArray(arr) + && arr.every(isEvent); diff --git a/src/interfaces/IEventStorage.ts b/src/interfaces/IEventStorage.ts index 9fa0dca..cd55ade 100644 --- a/src/interfaces/IEventStorage.ts +++ b/src/interfaces/IEventStorage.ts @@ -14,7 +14,7 @@ export type EventQueryBefore = { beforeEvent?: IEvent; } -export interface IEventStoreReader { +export interface IEventStorageReader { /** * Retrieves events of specified types that were emitted after a given event. */ @@ -31,7 +31,15 @@ export interface IEventStoreReader { getSagaEvents(sagaId: Identifier, options: EventQueryBefore): IEventStream; } -export const isIEventStoreReader = (storage: unknown): storage is IEventStoreReader => +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; +} + +export const isIEventStorageReader = (storage: unknown): storage is IEventStorageReader => isObject(storage) && 'getEventsByTypes' in storage && typeof storage.getEventsByTypes === 'function' @@ -39,11 +47,3 @@ export const isIEventStoreReader = (storage: unknown): storage is IEventStoreRea && typeof storage.getAggregateEvents === 'function' && 'getSagaEvents' in storage && typeof storage.getSagaEvents === 'function'; - -export interface IEventStoreWriter { - /** - * 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 index 67ace1e..c647010 100644 --- a/src/interfaces/IEventStore.ts +++ b/src/interfaces/IEventStore.ts @@ -1,11 +1,11 @@ import { IEventDispatcher } from "./IEventDispatcher"; import { IEvent } from "./IEvent"; -import { IEventStoreReader } from "./IEventStorage"; +import { IEventStorageReader } from "./IEventStorage"; import { IIdentifierProvider } from "./IIdentifierProvider"; import { IMessageHandler, IObservable } from "./IObservable"; export interface IEventStore - extends IObservable, IEventDispatcher, IEventStoreReader, IIdentifierProvider { + extends IObservable, IEventDispatcher, IEventStorageReader, IIdentifierProvider { registerSagaStarters(startsWith: string[] | undefined): void; From f37ad5e488b272a84e1b9e94eea54758f9f9cb90 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 4 Apr 2025 01:26:38 +0100 Subject: [PATCH 030/135] Add support for event metadata in event dispatcher --- src/EventDispatcher.ts | 9 ++++++--- src/in-memory/InMemoryMessageBus.ts | 6 +++--- src/interfaces/IObservable.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index ac10bfa..bd1bba8 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -121,13 +121,16 @@ export class EventDispatcher implements IEventDispatcher { * * Returns a promise that resolves after all events are processed and published. */ - async dispatch(events: IEventSet) { - if (!Array.isArray(events) || events.length === 0) + async dispatch(events: IEventSet, meta?: Record) { + if (!isEventSet(events) || events.length === 0) throw new Error('dispatch requires a non-empty array of events'); const { promise, resolve, reject } = Promise.withResolvers(); const envelope: EventBatchEnvelope = { - data: events.map(event => ({ event })), + data: events.map(event => ({ + event, + ...meta + })), resolve, reject }; diff --git a/src/in-memory/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts index c8f43f3..31eab51 100644 --- a/src/in-memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -101,7 +101,7 @@ export class InMemoryMessageBus implements IMessageBus { /** * 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) @@ -110,9 +110,9 @@ export class InMemoryMessageBus implements IMessageBus { const handlers = [ ...this.#handlers.get(event.type) || [], ...Array.from(this.#queues.values()).map(namedQueue => - (e: IEvent) => namedQueue.publish(e)) + (e: IEvent, m?: Record) => namedQueue.publish(e, m)) ]; - return Promise.all(handlers.map(handler => handler(event))); + return Promise.all(handlers.map(handler => handler(event, meta))); } } diff --git a/src/interfaces/IObservable.ts b/src/interfaces/IObservable.ts index 076b85b..79774ba 100644 --- a/src/interfaces/IObservable.ts +++ b/src/interfaces/IObservable.ts @@ -2,7 +2,7 @@ import { IMessage } from "./IMessage"; import { isObject } from "./isObject"; export interface IMessageHandler { - (message: IMessage): any | Promise + (message: IMessage, meta?: Record): any | Promise }; export interface IObservable { From 8945398e688c425c32913ed5f36919867b0007a4 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 5 Apr 2025 00:33:02 +0100 Subject: [PATCH 031/135] Enforce strict types --- package-lock.json | 16 ++++++++++++---- package.json | 5 +++-- src/AbstractAggregate.ts | 4 ++-- src/AbstractProjection.ts | 2 +- src/EventDispatcher.ts | 9 ++++++--- src/in-memory/InMemoryEventStorage.ts | 8 +++++--- src/in-memory/InMemoryMessageBus.ts | 2 +- src/in-memory/InMemoryView.ts | 7 +++++-- src/interfaces/IEventDispatcher.ts | 2 +- src/rabbitmq/RabbitMqGateway.ts | 4 ++-- src/sqlite/SqliteEventLocker.ts | 3 --- src/sqlite/SqliteObjectStorage.ts | 3 --- src/sqlite/SqliteViewLocker.ts | 3 --- tsconfig.json | 10 +++++++--- 14 files changed, 45 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index e7b4b0e..723858f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.0.0", - "async-parallel-pipe": "^1.0.1", + "async-parallel-pipe": "^1.0.2", "di0": "^1.0.0" }, "devDependencies": { @@ -18,6 +18,7 @@ "@types/better-sqlite3": "^7.6.11", "@types/chai": "^4.3.20", "@types/jest": "^29.5.13", + "@types/md5": "^2.3.5", "@types/node": "^20.16.9", "@types/sinon": "^17.0.4", "chai": "^4.5.0", @@ -1186,6 +1187,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/md5": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz", + "integrity": "sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -1413,9 +1421,9 @@ "license": "MIT" }, "node_modules/async-parallel-pipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-parallel-pipe/-/async-parallel-pipe-1.0.1.tgz", - "integrity": "sha512-LnZtmPVzwMMvFywrQ2VKSsFlIWXuQogqkOfS5mxKoU3u0O9Di5zvVHMYW0HW/FdjTTz11l9a0pKw/mQ7DQgCww==", + "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": { diff --git a/package.json b/package.json index d19af1c..5bcb35f 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "homepage": "https://github.com/snatalenko/node-cqrs#readme", "dependencies": { "async-iterable-buffer": "^1.0.0", - "async-parallel-pipe": "^1.0.1", + "async-parallel-pipe": "^1.0.2", "di0": "^1.0.0" }, "devDependencies": { @@ -70,6 +70,7 @@ "@types/better-sqlite3": "^7.6.11", "@types/chai": "^4.3.20", "@types/jest": "^29.5.13", + "@types/md5": "^2.3.5", "@types/node": "^20.16.9", "@types/sinon": "^17.0.4", "chai": "^4.5.0", @@ -85,4 +86,4 @@ "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } -} \ No newline at end of file +} diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index fd69a79..3d87b08 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -42,7 +42,7 @@ export abstract class AbstractAggregate implements IProjection e.sagaId == sagaId && e.sagaVersion !== undefined && + beforeEvent.sagaVersion !== undefined && e.sagaVersion < beforeEvent.sagaVersion); await nextCycle(); diff --git a/src/in-memory/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts index 31eab51..3a0bc54 100644 --- a/src/in-memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -95,7 +95,7 @@ export class InMemoryMessageBus implements IMessageBus { const commandHandler = handlers.values().next().value; - return commandHandler(command); + return commandHandler!(command); } /** diff --git a/src/in-memory/InMemoryView.ts b/src/in-memory/InMemoryView.ts index abd6e1b..4ef35ae 100644 --- a/src/in-memory/InMemoryView.ts +++ b/src/in-memory/InMemoryView.ts @@ -6,7 +6,7 @@ import { nextCycle } from './utils'; * Update given value with an update Cb and return updated value. * Wrapper is needed for backward compatibility with update methods that were modifying the passed in objects directly */ -function applyUpdate(view: T | undefined, update: (r?: T) => T | undefined): T | undefined { +function applyUpdate(view: T, update: (r: T) => T): T { const valueReturnedByUpdate = update(view); return valueReturnedByUpdate === undefined ? view : @@ -170,8 +170,11 @@ export class InMemoryView implements IViewLocker, IObjectStorage 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; diff --git a/src/interfaces/IEventDispatcher.ts b/src/interfaces/IEventDispatcher.ts index 9acfda5..bb5a0ba 100644 --- a/src/interfaces/IEventDispatcher.ts +++ b/src/interfaces/IEventDispatcher.ts @@ -3,5 +3,5 @@ import { IEventBus } from "./IEventBus"; export interface IEventDispatcher { readonly eventBus: IEventBus; - dispatch(events: IEventSet): Promise; + dispatch(events: IEventSet, meta?: Record): Promise; } diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 4969fcc..3fffd23 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -88,7 +88,7 @@ export class RabbitMqGateway { for (const subscription of subscriptionsToRestore) await this.subscribe(subscription); } - catch (err) { + catch (err: any) { this.#logger?.warn(`${this.#appId}: Connection attempt failed: ${err.message}`); await delay(5_000); } @@ -292,7 +292,7 @@ export class RabbitMqGateway { channel?.ack(msg); } - catch (err) { + catch (err: any) { this.#logger?.error(`${this.#appId}: Message processing failed: ${err.message}`); // Redirect message to dead letter queue, if `{ noAck: true }` was not set on consumption diff --git a/src/sqlite/SqliteEventLocker.ts b/src/sqlite/SqliteEventLocker.ts index 95cb6b1..81db266 100644 --- a/src/sqlite/SqliteEventLocker.ts +++ b/src/sqlite/SqliteEventLocker.ts @@ -52,10 +52,7 @@ export class SqliteEventLocker implements IEventLocker { this.#eventLockTableName = o.eventLockTableName ?? 'tbl_event_lock'; this.#eventLockTtl = o.eventLockTtl ?? 15_000; - this.#initialize(); - } - #initialize() { this.#db.exec(viewLockTableInit(this.#viewLockTableName)); this.#db.exec(eventLockTableInit(this.#eventLockTableName)); diff --git a/src/sqlite/SqliteObjectStorage.ts b/src/sqlite/SqliteObjectStorage.ts index ea0fc49..b780f2c 100644 --- a/src/sqlite/SqliteObjectStorage.ts +++ b/src/sqlite/SqliteObjectStorage.ts @@ -23,10 +23,7 @@ export class SqliteObjectStorage implements IObjectStorage { this.#db = o.viewModelSqliteDb; this.#tableName = o.tableName; - this.#initialize(); - } - #initialize(): void { this.#db.exec(`CREATE TABLE IF NOT EXISTS ${this.#tableName} ( id BLOB PRIMARY KEY, version INTEGER DEFAULT 1, diff --git a/src/sqlite/SqliteViewLocker.ts b/src/sqlite/SqliteViewLocker.ts index 2073698..9320adf 100644 --- a/src/sqlite/SqliteViewLocker.ts +++ b/src/sqlite/SqliteViewLocker.ts @@ -64,10 +64,7 @@ export class SqliteViewLocker implements IViewLocker { o.logger.child({ service: this.constructor.name }) : o.logger; - this.#initialize(); - } - #initialize() { this.#db.exec(viewLockTableInit(this.#viewLockTableName)); this.#upsertTableLockQuery = this.#db.prepare(` diff --git a/tsconfig.json b/tsconfig.json index 6f3a80f..b0180a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,9 +8,13 @@ "outDir": "./dist", "target": "ESNext", "declaration": false, - "strictNullChecks": true, "allowSyntheticDefaultImports": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, }, "include": [ "src/**/*" @@ -19,4 +23,4 @@ "node_modules", "**/*.spec.ts" ] -} +} \ No newline at end of file From 922be7b75ea57d42f95587e9d624c82def37bc21 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 5 Apr 2025 00:50:48 +0100 Subject: [PATCH 032/135] Fix tests --- src/interfaces/IEvent.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts index 16f3118..ec08092 100644 --- a/src/interfaces/IEvent.ts +++ b/src/interfaces/IEvent.ts @@ -9,8 +9,4 @@ export type IEvent = IMessage & { export const isEvent = (event: unknown): event is IEvent => isObject(event) && 'type' in event - && typeof event.type === 'string' - && ( - 'aggregateId' in event - || 'sagaId' in event - ); + && typeof event.type === 'string'; From 892366d027234673dd012b26cb355b5c6a455449 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 5 Apr 2025 01:17:58 +0100 Subject: [PATCH 033/135] Fix typing error --- src/sqlite/utils/getEventId.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sqlite/utils/getEventId.ts b/src/sqlite/utils/getEventId.ts index 6d86ec3..309d49f 100644 --- a/src/sqlite/utils/getEventId.ts +++ b/src/sqlite/utils/getEventId.ts @@ -1,6 +1,6 @@ import { IEvent } from "../../interfaces"; import { guid } from './guid'; -import * as md5 from 'md5'; +import md5 = require('md5'); /** * Get assigned or generate new event ID from event content From aa5ea950023a62a758ee3562fe3fe8a27beaeb07 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 5 Apr 2025 18:05:02 +0100 Subject: [PATCH 034/135] Split `storage` onto separate dependencies: `eventStorageReader`, `eventStorageWriter` --- src/EventStore.ts | 22 +++++++++---------- .../EventPersistenceProcessor.ts | 14 ++++++------ tests/unit/AbstractProjection.test.ts | 9 ++++++-- tests/unit/AggregateCommandHandler.test.ts | 12 +++++++--- tests/unit/CqrsContainerBuilder.test.ts | 2 +- tests/unit/EventStore.test.ts | 6 ++--- tests/unit/SagaEventHandler.test.ts | 9 ++++++-- 7 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/EventStore.ts b/src/EventStore.ts index d54c988..44daf80 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -28,7 +28,7 @@ import { EventDispatcher } from "./EventDispatcher"; export class EventStore implements IEventStore { #identifierProvider: IIdentifierProvider; - #storage: IEventStorageReader; + #eventStorageReader: IEventStorageReader; #snapshotStorage: IAggregateSnapshotStorage | undefined; eventBus: IEventBus; #eventDispatcher: IEventDispatcher; @@ -36,30 +36,30 @@ export class EventStore implements IEventStore { #logger?: ILogger; constructor({ - storage, - identifierProvider = isIdentifierProvider(storage) ? storage : undefined, + eventStorageReader, + identifierProvider = isIdentifierProvider(eventStorageReader) ? eventStorageReader : undefined, snapshotStorage, eventBus, eventDispatcher, logger, }: { - storage: IEventStorageReader, + eventStorageReader: IEventStorageReader, identifierProvider?: IIdentifierProvider, snapshotStorage?: IAggregateSnapshotStorage, eventBus?: IEventBus, eventDispatcher?: IEventDispatcher, logger?: ILogger | IExtendableLogger, }) { - if (!storage) - throw new TypeError('storage argument required'); + if (!eventStorageReader) + throw new TypeError('eventStorageReader argument required'); if (!identifierProvider) throw new TypeError('identifierProvider argument required'); - if (!isIEventStorageReader(storage)) + if (!isIEventStorageReader(eventStorageReader)) throw new TypeError('storage does not implement IEventStorage interface'); if (eventBus && !isIEventBus(eventBus)) throw new TypeError('eventBus does not implement IMessageBus interface'); - this.#storage = storage; + this.#eventStorageReader = eventStorageReader; this.#identifierProvider = identifierProvider; this.#snapshotStorage = snapshotStorage; this.#eventDispatcher = eventDispatcher ?? new EventDispatcher({ eventBus }); @@ -84,7 +84,7 @@ export class EventStore implements IEventStore { this.#logger?.debug(`retrieving ${eventTypes.join(', ')} events...`); - const eventsIterable = await this.#storage.getEventsByTypes(eventTypes, options); + const eventsIterable = await this.#eventStorageReader.getEventsByTypes(eventTypes, options); yield* eventsIterable; @@ -105,7 +105,7 @@ export class EventStore implements IEventStore { if (snapshot) yield snapshot; - const eventsIterable = await this.#storage.getAggregateEvents(aggregateId, { snapshot }); + const eventsIterable = await this.#eventStorageReader.getAggregateEvents(aggregateId, { snapshot }); yield* eventsIterable; @@ -125,7 +125,7 @@ export class EventStore implements IEventStore { this.#logger?.debug(`retrieving event stream for saga ${sagaId}, v${filter.beforeEvent.sagaVersion}...`); - const eventsIterable = await this.#storage.getSagaEvents(sagaId, filter); + const eventsIterable = await this.#eventStorageReader.getSagaEvents(sagaId, filter); yield* eventsIterable; diff --git a/src/dispatch-pipeline/EventPersistenceProcessor.ts b/src/dispatch-pipeline/EventPersistenceProcessor.ts index eb63599..d91e89f 100644 --- a/src/dispatch-pipeline/EventPersistenceProcessor.ts +++ b/src/dispatch-pipeline/EventPersistenceProcessor.ts @@ -5,17 +5,17 @@ import { EventBatch, IEvent, IEventProcessor, IEventStorageWriter } from '../int */ export class EventPersistenceProcessor implements IEventProcessor { - #storage: IEventStorageWriter; + #storageWriter: IEventStorageWriter; - constructor(options: { storage: IEventStorageWriter }) { - if (!options.storage) - throw new TypeError('storage argument required'); + constructor(options: { eventStorageWriter: IEventStorageWriter }) { + if (!options.eventStorageWriter) + throw new TypeError('eventStorageWriter argument required'); - this.#storage = options.storage; + this.#storageWriter = options.eventStorageWriter; } async process(batch: EventBatch): Promise { - if (!this.#storage) + if (!this.#storageWriter) return batch; const events: IEvent[] = []; @@ -26,7 +26,7 @@ export class EventPersistenceProcessor implements IEventProcessor { events.push(event); } - await this.#storage.commitEvents(events); + await this.#storageWriter.commitEvents(events); return batch; } diff --git a/tests/unit/AbstractProjection.test.ts b/tests/unit/AbstractProjection.test.ts index 818ee8f..af37fba 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -168,10 +168,15 @@ describe('AbstractProjection', function () { it('waits until the restoring process is done', async () => { - const storage = new InMemoryEventStorage(); + const eventStorageReader = new InMemoryEventStorage(); const eventBus = new InMemoryMessageBus(); const eventDispatcher = new EventDispatcher({ eventBus }); - const es = new EventStore({ storage, eventBus, eventDispatcher, identifierProvider: storage }); + const es = new EventStore({ + eventStorageReader, + eventBus, + eventDispatcher, + identifierProvider: eventStorageReader + }); let restored = false; let projected = false; diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index c19f82f..0e5ead3 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -43,7 +43,7 @@ describe('AggregateCommandHandler', function () { // this.timeout(500); // this.slow(300); - let storage: InMemoryEventStorage; + let eventStorageReader: InMemoryEventStorage; let snapshotStorage: InMemorySnapshotStorage; let eventStore: IEventStore; let commandBus: ICommandBus; @@ -55,11 +55,17 @@ describe('AggregateCommandHandler', function () { beforeEach(() => { eventBus = new InMemoryMessageBus(); - storage = new InMemoryEventStorage(); + eventStorageReader = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); const eventDispatcher = new EventDispatcher({ eventBus }); - eventStore = new EventStore({ storage, snapshotStorage, eventBus, eventDispatcher, identifierProvider: storage }); + eventStore = new EventStore({ + eventStorageReader, + snapshotStorage, + eventBus, + eventDispatcher, + identifierProvider: eventStorageReader + }); getNewIdSpy = sinon.spy(eventStore, 'getNewId'); getAggregateEventsSpy = sinon.spy(eventStore, 'getAggregateEvents'); commitSpy = sinon.spy(eventStore, 'dispatch'); diff --git a/tests/unit/CqrsContainerBuilder.test.ts b/tests/unit/CqrsContainerBuilder.test.ts index 37fe640..844af3f 100644 --- a/tests/unit/CqrsContainerBuilder.test.ts +++ b/tests/unit/CqrsContainerBuilder.test.ts @@ -15,7 +15,7 @@ describe('CqrsContainerBuilder', function () { beforeEach(() => { builder = new ContainerBuilder(); - builder.register(InMemoryEventStorage).as('storage').as('identifierProvider'); + builder.register(InMemoryEventStorage).as('eventStorageWriter').as('eventStorageReader').as('identifierProvider'); builder.register(InMemoryMessageBus).as('eventBus'); }); diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index 16c89a4..dcd1048 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -4,7 +4,7 @@ import { EventStore } from '../../src/EventStore'; import { IEvent, IEventBus, - IEventStoreReader, + IEventStorageReader, IAggregateSnapshotStorage, IIdentifierProvider } from '../../src/interfaces'; @@ -14,7 +14,7 @@ describe('EventStore', () => { let store: EventStore; let eventBus: IEventBus; let eventDispatcher: IEventDispatcher; - let mockStorage: jest.Mocked; + let mockStorage: jest.Mocked; let mockSnapshotStorage: jest.Mocked; let mockIdentifierProvider: jest.Mocked; const mockId = 'test-id'; @@ -40,7 +40,7 @@ describe('EventStore', () => { store = new EventStore({ eventBus, eventDispatcher, - storage: mockStorage, + eventStorageReader: mockStorage, identifierProvider: mockIdentifierProvider, snapshotStorage: mockSnapshotStorage, logger: undefined diff --git a/tests/unit/SagaEventHandler.test.ts b/tests/unit/SagaEventHandler.test.ts index f84f167..1556bc2 100644 --- a/tests/unit/SagaEventHandler.test.ts +++ b/tests/unit/SagaEventHandler.test.ts @@ -45,9 +45,14 @@ describe('SagaEventHandler', function () { beforeEach(() => { const eventBus = new InMemoryMessageBus(); const eventDispatcher = new EventDispatcher({ eventBus }); - const storage = new InMemoryEventStorage(); + const eventStorageReader = new InMemoryEventStorage(); commandBus = new CommandBus({}); - eventStore = new EventStore({ storage, identifierProvider: storage, eventBus, eventDispatcher }); + eventStore = new EventStore({ + eventStorageReader, + identifierProvider: eventStorageReader, + eventBus, + eventDispatcher + }); sagaEventHandler = new SagaEventHandler({ sagaType: Saga, eventStore, commandBus }); }); From 4f3b8b797fb37f37c86db026dcf0ffb14988bfd4 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 5 Apr 2025 18:47:47 +0100 Subject: [PATCH 035/135] Add missing export instructions for CJS loader --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5bcb35f..07a4600 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,17 @@ }, "exports": { ".": { + "require": "./dist/index.js", "import": "./dist/index.js", "types": "./src/index.ts" }, "./rabbitmq": { + "require": "./dist/rabbitmq/index.js", "import": "./dist/rabbitmq/index.js", "types": "./src/rabbitmq/index.ts" }, "./sqlite": { + "require": "./dist/sqlite/index.js", "import": "./dist/sqlite/index.js", "types": "./src/sqlite/index.ts" } @@ -86,4 +89,4 @@ "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } -} +} \ No newline at end of file From 1e98a9e550521bc7991afc8b773bc248d1727c10 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 6 Apr 2025 01:58:46 +0100 Subject: [PATCH 036/135] Export `IContainer` interface --- src/AggregateCommandHandler.ts | 7 ++-- src/CqrsContainerBuilder.ts | 37 +++++++------------ src/EventDispatcher.ts | 11 ++++-- src/EventStore.ts | 20 +++++----- src/SagaEventHandler.ts | 6 +-- .../EventPersistenceProcessor.ts | 9 ++--- src/interfaces/IContainer.ts | 25 +++++++++++++ src/interfaces/index.ts | 1 + 8 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 src/interfaces/IContainer.ts diff --git a/src/AggregateCommandHandler.ts b/src/AggregateCommandHandler.ts index 7339890..f15d402 100644 --- a/src/AggregateCommandHandler.ts +++ b/src/AggregateCommandHandler.ts @@ -5,6 +5,7 @@ import { ICommand, ICommandBus, ICommandHandler, + IContainer, Identifier, IEventSet, IEventStore, @@ -39,12 +40,10 @@ export class AggregateCommandHandler implements ICommandHandler { aggregateFactory, handles, logger - }: { - eventStore: IEventStore, + }: Pick & { aggregateType?: IAggregateConstructor, aggregateFactory?: IAggregateFactory, - handles?: string[], - logger?: ILogger | IExtendableLogger + handles?: string[] }) { if (!eventStore) throw new TypeError('eventStore argument required'); diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 57c337f..4ed14fe 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -1,4 +1,4 @@ -import { ContainerBuilder, Container, TypeConfig, TClassOrFactory } from 'di0'; +import { ContainerBuilder, TypeConfig, TClassOrFactory } from 'di0'; import { AggregateCommandHandler } from './AggregateCommandHandler'; import { CommandBus } from './CommandBus'; @@ -18,21 +18,15 @@ import { import { IAggregateConstructor, - ICommandBus, ICommandHandler, + IContainer, IEventReceptor, - IEventStore, IProjection, IProjectionConstructor, ISagaConstructor } from './interfaces'; import { ExternalEventPublishingProcessor } from './dispatch-pipeline/ExternalEventPublishingProcessor'; -interface CqrsContainer extends Container { - eventStore: IEventStore; - commandBus: ICommandBus; -} - export class CqrsContainerBuilder extends ContainerBuilder { constructor(options?: { @@ -43,22 +37,19 @@ export class CqrsContainerBuilder extends ContainerBuilder { super.register(InMemoryMessageBus).as('eventBus'); super.register(EventStore).as('eventStore'); super.register(CommandBus).as('commandBus'); - - super.register(container => { - const eventDispatcher = new EventDispatcher(container); - eventDispatcher.addPipelineProcessor(new EventValidationProcessor(container)); - eventDispatcher.addPipelineProcessor(new ExternalEventPublishingProcessor(container)); - eventDispatcher.addPipelineProcessor(new EventPersistenceProcessor(container)); - eventDispatcher.addPipelineProcessor(new SnapshotPersistenceProcessor(container)); - - return eventDispatcher; - }).as('eventDispatcher'); + super.register(EventDispatcher).as('eventDispatcher'); + super.register(container => [ + new EventValidationProcessor(container), + new ExternalEventPublishingProcessor(container), + new EventPersistenceProcessor(container), + new SnapshotPersistenceProcessor(container) + ]).as('eventDispatchProcessors'); } /** Register command handler, which will be subscribed to commandBus upon instance creation */ registerCommandHandler(typeOrFactory: TClassOrFactory) { return super.register( - (container: CqrsContainer) => { + (container: IContainer) => { const handler = container.createInstance(typeOrFactory); handler.subscribe(container.commandBus); return handler; @@ -69,7 +60,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { /** Register event receptor, which will be subscribed to eventStore upon instance creation */ registerEventReceptor(typeOrFactory: TClassOrFactory) { return super.register( - (container: CqrsContainer) => { + (container: IContainer) => { const receptor = container.createInstance(typeOrFactory); receptor.subscribe(container.eventStore); return receptor; @@ -85,7 +76,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { if (!isClass(ProjectionType)) throw new TypeError('ProjectionType argument must be a constructor function'); - const projectionFactory = (container: CqrsContainer): IProjection => { + const projectionFactory = (container: IContainer): IProjection => { const projection = container.createInstance(ProjectionType); projection.subscribe(container.eventStore); @@ -108,7 +99,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { if (!isClass(AggregateType)) throw new TypeError('AggregateType argument must be a constructor function'); - const commandHandlerFactory = (container: CqrsContainer): ICommandHandler => + const commandHandlerFactory = (container: IContainer): ICommandHandler => container.createInstance(AggregateCommandHandler, { aggregateFactory: (options: any) => container.createInstance(AggregateType, options), @@ -124,7 +115,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: IContainer): IEventReceptor => container.createInstance(SagaEventHandler, { sagaFactory: (options: any) => container.createInstance(SagaType, options), handles: SagaType.handles, diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 82444d7..10c1fdb 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -5,7 +5,8 @@ import { IEventProcessor, IEventSet, IEventBus, - isEventSet + isEventSet, + IContainer } from "./interfaces"; import { parallelPipe } from 'async-parallel-pipe'; import { AsyncIterableBuffer } from 'async-iterable-buffer'; @@ -37,14 +38,18 @@ export class EventDispatcher implements IEventDispatcher { */ concurrentLimit: number; - constructor(o?: { - eventBus?: IEventBus, + constructor(o?: Pick & { eventDispatcherConfig?: { concurrentLimit?: number } }) { this.eventBus = o?.eventBus ?? new InMemoryMessageBus(); this.concurrentLimit = o?.eventDispatcherConfig?.concurrentLimit ?? 100; + + if (o?.eventDispatchProcessors) { + for (const processor of o.eventDispatchProcessors) + this.addPipelineProcessor(processor); + } } /** diff --git a/src/EventStore.ts b/src/EventStore.ts index 44daf80..7f269a1 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -3,7 +3,6 @@ import { IEvent, IEventStorageReader, IEventSet, - IExtendableLogger, ILogger, IMessageHandler, IObservable, @@ -17,7 +16,8 @@ import { IEventDispatcher, IEventBus, isIEventBus, - isIEventStorageReader + isIEventStorageReader, + IContainer } from "./interfaces"; import { getClassName, @@ -42,14 +42,14 @@ export class EventStore implements IEventStore { eventBus, eventDispatcher, logger, - }: { - eventStorageReader: IEventStorageReader, - identifierProvider?: IIdentifierProvider, - snapshotStorage?: IAggregateSnapshotStorage, - eventBus?: IEventBus, - eventDispatcher?: IEventDispatcher, - logger?: ILogger | IExtendableLogger, - }) { + }: Pick) { if (!eventStorageReader) throw new TypeError('eventStorageReader argument required'); if (!identifierProvider) diff --git a/src/SagaEventHandler.ts b/src/SagaEventHandler.ts index 6b538b3..a367a7f 100644 --- a/src/SagaEventHandler.ts +++ b/src/SagaEventHandler.ts @@ -1,6 +1,7 @@ import * as Event from './Event'; import { ICommandBus, + IContainer, IEvent, IEventReceptor, IEventStore, @@ -34,12 +35,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[] diff --git a/src/dispatch-pipeline/EventPersistenceProcessor.ts b/src/dispatch-pipeline/EventPersistenceProcessor.ts index d91e89f..d5ed7b6 100644 --- a/src/dispatch-pipeline/EventPersistenceProcessor.ts +++ b/src/dispatch-pipeline/EventPersistenceProcessor.ts @@ -1,16 +1,13 @@ -import { EventBatch, IEvent, IEventProcessor, IEventStorageWriter } from '../interfaces'; +import { EventBatch, IContainer, IEvent, IEventProcessor, IEventStorageWriter } from '../interfaces'; /** * Processor responsible for persisting events to IEventStoreWriter. */ export class EventPersistenceProcessor implements IEventProcessor { - #storageWriter: IEventStorageWriter; - - constructor(options: { eventStorageWriter: IEventStorageWriter }) { - if (!options.eventStorageWriter) - throw new TypeError('eventStorageWriter argument required'); + #storageWriter: IEventStorageWriter | undefined; + constructor(options: Pick) { this.#storageWriter = options.eventStorageWriter; } diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts new file mode 100644 index 0000000..0d657a0 --- /dev/null +++ b/src/interfaces/IContainer.ts @@ -0,0 +1,25 @@ +import { Container } from "di0"; +import { ICommandBus } from "./ICommandBus"; +import { IEventDispatcher } from "./IEventDispatcher"; +import { IEventStore } from "./IEventStore"; +import { IEventBus } from "./IEventBus"; +import { IEventProcessor } from "./IEventProcessor"; +import { IEventStorageReader, IEventStorageWriter } from "./IEventStorage"; +import { IAggregateSnapshotStorage } from "./IAggregateSnapshotStorage"; +import { IIdentifierProvider } from "./IIdentifierProvider"; +import { IExtendableLogger, ILogger } from "./ILogger"; + +export interface IContainer extends Container { + eventBus: IEventBus; + eventStore: IEventStore + eventStorageReader: IEventStorageReader; + eventStorageWriter?: IEventStorageWriter; + identifierProvider?: IIdentifierProvider; + snapshotStorage?: IAggregateSnapshotStorage; + + commandBus: ICommandBus; + eventDispatcher: IEventDispatcher; + eventDispatchProcessors?: IEventProcessor[]; + + logger?: ILogger | IExtendableLogger; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index a78ae56..bb70131 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -2,6 +2,7 @@ export * from './IAggregate'; export * from './IAggregateSnapshotStorage'; export * from './ICommand'; export * from './ICommandBus'; +export * from './IContainer'; export * from './Identifier'; export * from './IEvent'; export * from './IEventBus'; From 2d4265de6740eb254df97db52d440cafb877b424 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Apr 2025 00:59:53 +0100 Subject: [PATCH 037/135] Add RabbitMqEventInjector --- src/rabbitmq/IContainer.ts | 10 ++ src/rabbitmq/RabbitMqEventInjector.ts | 65 +++++++++++++ src/rabbitmq/index.ts | 1 + .../rabbitmq/RabbitMqEventInjector.test.ts | 91 +++++++++++++++++++ 4 files changed, 167 insertions(+) create mode 100644 src/rabbitmq/IContainer.ts create mode 100644 src/rabbitmq/RabbitMqEventInjector.ts create mode 100644 tests/integration/rabbitmq/RabbitMqEventInjector.test.ts diff --git a/src/rabbitmq/IContainer.ts b/src/rabbitmq/IContainer.ts new file mode 100644 index 0000000..18da704 --- /dev/null +++ b/src/rabbitmq/IContainer.ts @@ -0,0 +1,10 @@ +import { RabbitMqEventInjector } from "./RabbitMqEventInjector"; +import { RabbitMqGateway } from "./RabbitMqGateway"; + +declare module '../interfaces/IContainer' { + interface IContainer { + rabbitMqGateway?: RabbitMqGateway; + rabbitMqEventInjector?: RabbitMqEventInjector; + rabbitMqEventBus?: RabbitMqEventInjector; + } +} diff --git a/src/rabbitmq/RabbitMqEventInjector.ts b/src/rabbitmq/RabbitMqEventInjector.ts new file mode 100644 index 0000000..f6ae456 --- /dev/null +++ b/src/rabbitmq/RabbitMqEventInjector.ts @@ -0,0 +1,65 @@ +import { IContainer } from "../interfaces/IContainer"; +import { IMessage } from "../interfaces/IMessage"; +import { RabbitMqGateway } from "./RabbitMqGateway"; +import { IEventDispatcher, isEvent } from "../interfaces"; +import * as Event from '../Event'; + +export class RabbitMqEventInjector { + #rabbitMqGateway: RabbitMqGateway; + #eventDispatcher: IEventDispatcher; + #logger: IContainer['logger']; + + #exchangeName: string; + #queueName: string; + #messageHandler: (message: IMessage) => Promise; + + constructor(container: Partial> & { + exchange?: string; + queueName?: string; + }) { + if (!container.eventDispatcher) + throw new Error("eventDispatcher is required in the container."); + if (!container.rabbitMqGateway) + throw new Error("rabbitMqGateway is required in the container."); + + this.#rabbitMqGateway = container.rabbitMqGateway; + this.#eventDispatcher = container.eventDispatcher; + + this.#logger = container.logger && 'child' in container.logger ? + container.logger.child({ service: new.target.name }) : + container.logger; + + this.#exchangeName = container.exchange ?? 'node-cqrs.events'; + this.#queueName = container.queueName ?? 'node-cqrs.persistence'; + this.#messageHandler = this.#handleMessage.bind(this); + + this.start(); + } + + async start(): Promise { + this.#logger?.info(`Starting event injection from queue "${this.#queueName}"`); + + await this.#rabbitMqGateway.subscribeToQueue( + this.#exchangeName, + this.#queueName, + this.#messageHandler + ); + + this.#logger?.info(`Subscribed to queue "${this.#queueName}" on exchange "${this.#exchangeName}"`); + } + + async #handleMessage(message: IMessage): Promise { + this.#logger?.debug(`Received message from queue "${this.#queueName}": ${message.type}`); + try { + // EventDispatcher expects an array of events (IEventSet) + // Assuming IMessage is compatible with IEvent or needs transformation + await this.#eventDispatcher.dispatch([message]); + this.#logger?.debug(`Event ${Event.describe(message)} dispatched successfully`); + } + catch (error: any) { + this.#logger?.error(`Failed to dispatch event ${message.type}: ${error.message}`, { stack: error.stack }); + + throw error; // Re-throw to ensure message is nack'd by the gateway + } + } +} diff --git a/src/rabbitmq/index.ts b/src/rabbitmq/index.ts index 79404df..26b4d04 100644 --- a/src/rabbitmq/index.ts +++ b/src/rabbitmq/index.ts @@ -1,2 +1,3 @@ export * from './RabbitMqEventBus'; +export * from './RabbitMqEventInjector'; export * from './RabbitMqGateway'; diff --git a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts new file mode 100644 index 0000000..b733d98 --- /dev/null +++ b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts @@ -0,0 +1,91 @@ +import * as amqplib from 'amqplib'; +import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; +import { RabbitMqEventInjector } from '../../../src/rabbitmq/RabbitMqEventInjector'; +import { IEvent, IEventDispatcher, IMessage } from '../../../src/interfaces'; +import { jest } from '@jest/globals'; +import { delay } from '../../../src/utils'; + +describe('RabbitMqEventInjector', () => { + let rabbitMqGateway: RabbitMqGateway; + let eventDispatcher: jest.Mocked; + let injector: RabbitMqEventInjector; + + const exchange = 'node-cqrs.events'; + const queueName = 'test-injector-queue'; + const deadLetterQueueName = `${queueName}.failed`; + const eventType = 'test-injector-event'; + + beforeEach(async () => { + const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); + rabbitMqGateway = new RabbitMqGateway({ rabbitMqConnectionFactory }); + + eventDispatcher = { + dispatch: jest.fn().mockResolvedValue(undefined), + } as unknown as jest.Mocked; + + injector = new RabbitMqEventInjector({ + rabbitMqGateway, + eventDispatcher, + queueName, + exchange + }); + + await delay(50); // Allow time for subscription setup + }); + + afterEach(async () => { + try { + const ch = await rabbitMqGateway.connection?.createChannel(); + if (ch) { + await ch.deleteQueue(queueName); + await ch.deleteQueue(`${queueName}.failed`); + await ch.deleteExchange(exchange); + await ch.close(); + } + } + catch (error) { + console.warn('Error during RabbitMQ cleanup:', error); + } + finally { + await rabbitMqGateway.disconnect(); + } + }); + + it('receives a message from the queue and dispatch it via EventDispatcher', async () => { + const testEvent: IEvent = { + type: eventType, + payload: { data: 'test-payload' }, + id: 'test-id-123', + }; + + await rabbitMqGateway.publish(exchange, testEvent); + + await delay(50); + + expect(eventDispatcher.dispatch).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatch).toHaveBeenCalledWith([testEvent]); + }); + + it('handles errors during event dispatch and nack the message', async () => { + const testEvent: IEvent = { + type: 'error-event', + payload: { data: 'trigger-error' }, + id: 'error-id-456', + }; + const dispatchError = new Error('Dispatch failed'); + eventDispatcher.dispatch.mockRejectedValueOnce(dispatchError); + + // Publish the event + await rabbitMqGateway.publish(exchange, testEvent); + + await delay(100); + + const ch = await rabbitMqGateway.connection!.createChannel(); + const deadLetterMessage = await ch.get(deadLetterQueueName, { noAck: true }); + if (!deadLetterMessage) + throw new Error('Dead letter message not found'); + + const messageContent = JSON.parse(deadLetterMessage.content.toString()); + expect(messageContent).toEqual(testEvent); + }); +}); From 252bba6175b64421c06ee2a7113fe63707cdffa3 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Apr 2025 01:45:47 +0100 Subject: [PATCH 038/135] Fix integration tests --- examples/user-domain/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index 6a66706..32126eb 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -20,7 +20,7 @@ exports.createContainer = () => { const builder = new ContainerBuilder(); // register infrastructure services - builder.register(InMemoryEventStorage).as('storage'); + builder.register(InMemoryEventStorage).as('eventStorageReader').as('eventStorageWriter'); builder.register(InMemoryMessageBus).as('eventBus'); // register domain entities @@ -39,9 +39,9 @@ exports.createBaseInstances = () => { const eventBus = new InMemoryMessageBus(); const storage = new InMemoryEventStorage(); const eventDispatcher = new EventDispatcher({ eventBus }) - eventDispatcher.addPipelineProcessor(new EventPersistenceProcessor({ storage })); + eventDispatcher.addPipelineProcessor(new EventPersistenceProcessor({ eventStorageWriter: storage })); - const eventStore = new EventStore({ storage, eventBus, eventDispatcher }); + const eventStore = new EventStore({ eventStorageReader: storage, eventBus, eventDispatcher }); const commandBus = new CommandBus(); /** @type {import('../..').IAggregateConstructor} */ From 4d1c63d440061593bf817e73435596109c15c5a4 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Apr 2025 01:46:20 +0100 Subject: [PATCH 039/135] Enhance RabbitMqGateway to handle SIGINT for graceful shutdown and update IContainer interface --- src/interfaces/IContainer.ts | 1 + src/rabbitmq/RabbitMqGateway.ts | 54 +++++++++++++++---- .../rabbitmq/RabbitMqGateway.test.ts | 39 +++++++++++++- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index 0d657a0..c2e0c3f 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -22,4 +22,5 @@ export interface IContainer extends Container { eventDispatchProcessors?: IEventProcessor[]; logger?: ILogger | IExtendableLogger; + process?: NodeJS.Process } diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 3fffd23..ab00b06 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -1,6 +1,6 @@ import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; import { - IExtendableLogger, + IContainer, ILogger, IMessage, isMessage @@ -43,7 +43,7 @@ export class RabbitMqGateway { #pubChannel: ConfirmChannel | undefined; #exclusiveQueueName: string | undefined; #queueChannels = new Map(); - #queueConsumers = new Map(); + #queueConsumers = new Map(); #subscriptions: Array = []; #handlers: Map>> = new Map(); @@ -56,9 +56,8 @@ export class RabbitMqGateway { return this.#pubChannel; } - constructor(o: { - rabbitMqConnectionFactory?: () => Promise, - logger?: ILogger | IExtendableLogger + constructor(o: Partial> & { + rabbitMqConnectionFactory?: () => Promise }) { if (!o.rabbitMqConnectionFactory) throw new TypeError('rabbitMqConnectionFactory argument required'); @@ -68,6 +67,9 @@ export class RabbitMqGateway { this.#logger = o.logger && 'child' in o.logger ? o.logger.child({ service: new.target.name }) : o.logger; + + if (o.process) + o.process.on('SIGINT', () => this.#stopConsuming()); } async connect(): Promise { @@ -100,9 +102,40 @@ export class RabbitMqGateway { } async disconnect() { - await this.#connection?.close(); - if (this.#connection) // clean up in case 'close' event was not triggered - this.#onConnectionClosed(); + try { + this.#logger?.debug(`${this.#appId}: Disconnecting from RabbitMQ...`); + + await this.#stopConsuming(); + await this.#connection?.close(); + if (this.#connection) // clean up in case 'close' event was not triggered + this.#onConnectionClosed(); + + this.#logger?.debug(`${this.#appId}: Disconnected from RabbitMQ`); + } + catch (err: any) { + this.#logger?.error(`${this.#appId}: Failed to disconnect from RabbitMQ: ${err.message}`, { + stack: err.stack + }); + } + } + + async #stopConsuming() { + this.#logger?.info(`${this.#appId}: Stopping all consumers...`); + + const cancellations = this.#queueConsumers.entries().map(async ([queueName, { channel, consumerTag }]) => { + this.#logger?.debug(`${this.#appId}: Cancelling consumer "${consumerTag}" for queue "${queueName}"`); + try { + await channel.cancel(consumerTag); + this.#logger?.debug(`${this.#appId}: Consumer "${consumerTag}" on queue "${queueName}" cancelled successfully`); + this.#queueConsumers.delete(queueName); + } + catch (err: any) { + this.#logger?.error(`${this.#appId}: Failed to cancel consumer "${consumerTag}" for queue "${queueName}": ${err.message}`); + } + }); + + await Promise.all(cancellations); + this.#logger?.info(`${this.#appId}: All consumers stopped.`); } #onConnectionError(err: Error) { @@ -302,7 +335,10 @@ export class RabbitMqGateway { this.#logger?.debug(`${this.#appId}: Consumer "${c.consumerTag}" registered on queue "${queueGivenName}"`); - this.#queueConsumers.set(queueGivenName, c.consumerTag); + this.#queueConsumers.set(queueGivenName, { + channel, + consumerTag: c.consumerTag + }); } /** diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index 6999fda..271d90d 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -2,6 +2,8 @@ import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; import { IMessage } from '../../../src/interfaces'; import * as amqplib from 'amqplib'; import { delay } from '../../../src/utils'; +import { Deferred } from '../../../dist/in-memory/utils/Deferred'; +import { EventEmitter } from 'stream'; describe('RabbitMqGateway', () => { @@ -10,11 +12,11 @@ describe('RabbitMqGateway', () => { let gateway3: RabbitMqGateway | undefined; const exchange = 'test-exchange'; const queueName = 'test-queue'; + const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); beforeEach(async () => { const logger = undefined; // const logger = console; - const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); gateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); gateway3 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); @@ -302,5 +304,40 @@ describe('RabbitMqGateway', () => { expect(fanoutReceived).toEqual([message]); expect(queueReceived).toEqual([message]); }); + + it('stops receiving messages on SIGINT', async () => { + + const received: IMessage[] = []; + const process = new EventEmitter(); + 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); + }); }); }); From 46c4a97ac451cbb8057b5db9a4b5c7de9175c9e7 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Apr 2025 03:28:02 +0100 Subject: [PATCH 040/135] Add eslint --- eslint.config.mjs | 837 +++++++++++ jest.config.ts | 18 +- package-lock.json | 1240 ++++++++++++++++- package.json | 13 +- scripts/changelog/index.js | 2 +- src/AbstractAggregate.ts | 3 +- src/AbstractProjection.ts | 1 + src/AbstractSaga.ts | 6 +- src/AggregateCommandHandler.ts | 9 +- src/CommandBus.ts | 11 +- src/Event.ts | 2 +- src/EventDispatcher.ts | 12 +- src/EventStore.ts | 8 +- src/SagaEventHandler.ts | 1 - .../SnapshotPersistenceProcessor.ts | 2 +- src/in-memory/InMemoryEventStorage.ts | 14 +- src/in-memory/InMemoryLock.ts | 2 +- src/in-memory/InMemoryMessageBus.ts | 7 +- src/in-memory/InMemorySnapshotStorage.ts | 2 +- src/in-memory/InMemoryView.ts | 2 +- src/interfaces/IAggregate.ts | 10 +- src/interfaces/IAggregateSnapshotStorage.ts | 7 +- src/interfaces/ICommand.ts | 2 +- src/interfaces/ICommandBus.ts | 8 +- src/interfaces/IContainer.ts | 22 +- src/interfaces/IEvent.ts | 5 +- src/interfaces/IEventBus.ts | 4 +- src/interfaces/IEventDispatcher.ts | 4 +- src/interfaces/IEventLocker.ts | 4 +- src/interfaces/IEventProcessor.ts | 2 +- src/interfaces/IEventReceptor.ts | 4 +- src/interfaces/IEventSet.ts | 2 +- src/interfaces/IEventStorage.ts | 14 +- src/interfaces/IEventStore.ts | 10 +- src/interfaces/IEventStream.ts | 2 +- src/interfaces/IIdentifierProvider.ts | 5 +- src/interfaces/IMessage.ts | 5 +- src/interfaces/IMessageBus.ts | 6 +- src/interfaces/IObjectStorage.ts | 2 +- src/interfaces/IObservable.ts | 7 +- src/interfaces/IObserver.ts | 2 +- src/interfaces/IProjection.ts | 6 +- src/interfaces/ISaga.ts | 9 +- src/interfaces/IViewLocker.ts | 10 +- src/rabbitmq/IContainer.ts | 4 +- src/rabbitmq/RabbitMqEventBus.ts | 4 +- src/rabbitmq/RabbitMqEventInjector.ts | 12 +- src/rabbitmq/RabbitMqGateway.ts | 5 +- src/sqlite/AbstractSqliteObjectProjection.ts | 8 +- src/sqlite/AbstractSqliteView.ts | 5 +- src/sqlite/SqliteEventLocker.ts | 3 +- src/sqlite/SqliteObjectView.ts | 2 +- src/sqlite/SqliteViewLocker.ts | 6 +- src/sqlite/commonParams.ts | 2 + src/sqlite/utils/getEventId.ts | 2 +- src/utils/getHandler.ts | 4 +- src/utils/setupOneTimeEmitterSubscription.ts | 8 +- src/utils/subscribe.ts | 4 +- src/utils/validateHandlers.ts | 3 +- .../rabbitmq/RabbitMqEventBus.test.ts | 8 +- .../rabbitmq/RabbitMqEventInjector.test.ts | 12 +- .../rabbitmq/RabbitMqGateway.test.ts | 9 +- tests/integration/sqlite/SqliteView.test.ts | 5 +- tests/unit/AbstractAggregate.test.ts | 18 +- tests/unit/AbstractProjection.test.ts | 9 +- tests/unit/AbstractSaga.test.ts | 2 +- tests/unit/AggregateCommandHandler.test.ts | 54 +- tests/unit/EventDispatcher.test.ts | 12 +- tests/unit/EventStore.test.ts | 16 +- tests/unit/SagaEventHandler.test.ts | 6 +- .../unit/memory/InMemoryEventStorage.test.ts | 23 +- tests/unit/memory/InMemoryLock.test.ts | 4 +- tests/unit/memory/InMemoryMessageBus.test.ts | 4 +- tests/unit/memory/InMemoryView.test.ts | 59 +- tests/unit/sqlite/SqliteEventLocker.test.ts | 6 +- tests/unit/sqlite/SqliteObjectStorage.test.ts | 8 +- tests/unit/sqlite/SqliteObjectView.test.ts | 2 +- tests/unit/sqlite/SqliteViewLocker.test.ts | 6 +- 78 files changed, 2375 insertions(+), 304 deletions(-) create mode 100644 eslint.config.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..b000df0 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,837 @@ +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/*" + ]), + { + files: [ + "**/*.ts" + ], + languageOptions: { + parser: tsParser, + globals: { + ...globals.node + } + }, + plugins: { + "@typescript-eslint": tsPlugin, + }, + "rules": { + "no-explicit-any": "off", + "no-unused-vars": "off", + "no-use-before-define": "warn", + "strict": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "vars": "local", + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "^(_|err)" + } + ], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-empty-object-type": "off", + "padding-line-between-statements": [ + "warn", + { + "blankLine": "always", + "prev": "if", + "next": "return" + }, + { + "blankLine": "any", + "prev": "block-like", + "next": "return" + }, + { + "blankLine": "always", + "prev": "if", + "next": "const" + }, + { + "blankLine": "any", + "prev": "block-like", + "next": "const" + } + ], + "nonblock-statement-body-position": [ + "error", + "below" + ], + "accessor-pairs": "off", + "array-callback-return": "error", + "block-scoped-var": "error", + "complexity": [ + "off", + 11 + ], + "class-methods-use-this": "warn", + "consistent-return": "error", + "curly": [ + "error", + "multi-or-nest", + "consistent" + ], + "default-case": [ + "error", + { + "commentPattern": "^no default$" + } + ], + "dot-notation": [ + "error", + { + "allowKeywords": true + } + ], + "dot-location": [ + "error", + "property" + ], + "eqeqeq": [ + "error", + "allow-null" + ], + "guard-for-in": "error", + "no-alert": "error", + "no-caller": "error", + "no-case-declarations": "error", + "no-div-regex": "off", + "no-else-return": "off", + "no-empty-function": [ + "error", + { + "allow": [ + "arrowFunctions", + "methods", + "getters" + ] + } + ], + "no-empty-pattern": "error", + "no-eq-null": "off", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-extra-label": "error", + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-global-assign": [ + "error", + { + "exceptions": [] + } + ], + "no-native-reassign": "off", + "no-implicit-coercion": [ + "off", + { + "boolean": false, + "number": true, + "string": true, + "allow": [] + } + ], + "no-implicit-globals": "off", + "no-implied-eval": "error", + "no-invalid-this": "off", + "no-iterator": "error", + "no-labels": [ + "error", + { + "allowLoop": false, + "allowSwitch": false + } + ], + "no-lone-blocks": "error", + "no-loop-func": "error", + "no-magic-numbers": [ + "off", + { + "ignore": [], + "ignoreArrayIndexes": true, + "enforceConst": true, + "detectObjects": false + } + ], + "no-multi-spaces": "error", + "no-multi-str": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-octal": "error", + "no-octal-escape": "error", + "no-param-reassign": [ + "off", + { + "props": true + } + ], + "no-proto": "error", + "no-redeclare": "error", + "no-restricted-properties": [ + "error", + { + "object": "arguments", + "property": "callee", + "message": "arguments.callee is deprecated" + }, + { + "property": "__defineGetter__", + "message": "Please use Object.defineProperty instead." + }, + { + "property": "__defineSetter__", + "message": "Please use Object.defineProperty instead." + }, + { + "object": "Math", + "property": "pow", + "message": "Use the exponentiation operator (**) instead." + } + ], + "no-return-assign": "error", + "no-return-await": "error", + "no-script-url": "error", + "no-self-assign": "error", + "no-self-compare": "error", + "no-sequences": "error", + "no-throw-literal": "error", + "no-unmodified-loop-condition": "off", + "no-unused-expressions": "off", + "no-unused-labels": "error", + "no-useless-call": "off", + "no-useless-concat": "error", + "no-useless-escape": "error", + "no-useless-return": "error", + "no-void": "error", + "no-warning-comments": [ + "off", + { + "terms": [ + "todo", + "fixme", + "xxx" + ], + "location": "start" + } + ], + "no-with": "error", + "radix": "error", + "vars-on-top": "error", + "wrap-iife": [ + "error", + "outside", + { + "functionPrototypeMethods": false + } + ], + "yoda": "error", + "no-mixed-requires": "error", + "callback-return": "off", + "global-require": "error", + "handle-callback-err": "off", + "no-new-require": "error", + "no-path-concat": "error", + "no-process-env": "off", + "no-process-exit": "off", + "no-restricted-modules": "off", + "no-sync": "off", + "arrow-body-style": [ + "error", + "as-needed" + ], + "arrow-parens": [ + "error", + "as-needed" + ], + "arrow-spacing": [ + "error", + { + "before": true, + "after": true + } + ], + "constructor-super": "error", + "generator-star-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "no-class-assign": "error", + "no-confusing-arrow": [ + "error", + { + "allowParens": true + } + ], + "no-const-assign": "error", + "no-dupe-class-members": "error", + "no-duplicate-imports": "error", + "no-new-symbol": "error", + "no-restricted-imports": "off", + "no-this-before-super": "error", + "no-useless-computed-key": "error", + "no-useless-constructor": "error", + "no-useless-rename": [ + "error", + { + "ignoreDestructuring": false, + "ignoreImport": false, + "ignoreExport": false + } + ], + "no-var": "error", + "object-shorthand": [ + "error", + "always", + { + "ignoreConstructors": false, + "avoidQuotes": true + } + ], + "prefer-arrow-callback": "off", + "prefer-const": [ + "error", + { + "destructuring": "any", + "ignoreReadBeforeAssign": true + } + ], + "prefer-numeric-literals": "error", + "prefer-reflect": "off", + "prefer-rest-params": "error", + "prefer-spread": "error", + "prefer-template": "error", + "require-yield": "error", + "rest-spread-spacing": [ + "error", + "never" + ], + "sort-imports": [ + "off", + { + "ignoreCase": false, + "ignoreMemberSort": false, + "memberSyntaxSortOrder": [ + "none", + "all", + "multiple", + "single" + ] + } + ], + "symbol-description": "error", + "template-curly-spacing": "error", + "yield-star-spacing": [ + "error", + "after" + ], + "comma-dangle": [ + "error", + "never" + ], + "no-cond-assign": [ + "error", + "always" + ], + "no-console": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-debugger": "error", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": [ + "off", + "all", + { + "conditionalAssign": true, + "nestedBinaryExpressions": false, + "returnAssign": false + } + ], + "no-extra-semi": "error", + "no-func-assign": "error", + "no-inner-declarations": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-obj-calls": "error", + "no-prototype-builtins": "error", + "no-regex-spaces": "error", + "no-sparse-arrays": "error", + "no-template-curly-in-string": "error", + "no-unexpected-multiline": "error", + "no-unsafe-finally": "error", + "no-unsafe-negation": "error", + "no-negated-in-lhs": "off", + "use-isnan": "error", + "valid-jsdoc": "off", + "valid-typeof": [ + "error", + { + "requireStringLiterals": true + } + ], + "array-bracket-spacing": [ + "error", + "never" + ], + "block-spacing": [ + "error", + "always" + ], + "brace-style": [ + "error", + "stroustrup", + { + "allowSingleLine": false + } + ], + "camelcase": [ + "error", + { + "properties": "never" + } + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "comma-style": [ + "error", + "last" + ], + "computed-property-spacing": [ + "error", + "never" + ], + "consistent-this": "off", + "eol-last": [ + "error", + "always" + ], + "func-call-spacing": [ + "error", + "never" + ], + "func-name-matching": [ + "off", + "always", + { + "includeCommonJSModuleExports": false + } + ], + "func-style": [ + "off", + "expression" + ], + "id-blacklist": "off", + "id-length": "off", + "id-match": "off", + "indent": [ + "error", + "tab", + { + "SwitchCase": 1, + "VariableDeclarator": 1, + "outerIIFEBody": 1, + "FunctionDeclaration": { + "parameters": 1, + "body": 1 + }, + "FunctionExpression": { + "parameters": 1, + "body": 1 + } + } + ], + "jsx-quotes": [ + "off", + "prefer-double" + ], + "key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true + } + ], + "keyword-spacing": [ + "error", + { + "before": true, + "after": true, + "overrides": { + "return": { + "after": true + }, + "throw": { + "after": true + }, + "case": { + "after": true + } + } + } + ], + "line-comment-position": [ + "off", + { + "position": "above", + "ignorePattern": "", + "applyDefaultPatterns": true + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "lines-around-comment": [ + "error", + { + "beforeBlockComment": true, + "afterBlockComment": false, + "beforeLineComment": true, + "afterLineComment": false, + "allowBlockStart": true, + "allowObjectStart": true, + "allowArrayStart": true + } + ], + "lines-around-directive": [ + "error", + { + "before": "never", + "after": "always" + } + ], + "max-depth": [ + "off", + 4 + ], + "max-len": [ + "warn", + 120, + 4, + { + "ignoreUrls": true, + "ignoreComments": false, + "ignoreRegExpLiterals": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true + } + ], + "max-lines": [ + "off", + { + "max": 300, + "skipBlankLines": true, + "skipComments": true + } + ], + "max-nested-callbacks": "off", + "max-params": [ + "warn", + 5 + ], + "max-statements": [ + "off", + 10 + ], + "max-statements-per-line": [ + "off", + { + "max": 1 + } + ], + "multiline-ternary": [ + "off", + "never" + ], + "new-cap": [ + "error", + { + "newIsCap": true, + "newIsCapExceptions": [], + "capIsNew": false, + "capIsNewExceptions": [ + "Immutable.Map", + "Immutable.Set", + "Immutable.List" + ] + } + ], + "new-parens": "error", + "newline-after-var": "off", + "newline-before-return": "off", + "newline-per-chained-call": [ + "error", + { + "ignoreChainWithDepth": 4 + } + ], + "no-array-constructor": "error", + "no-bitwise": "error", + "no-continue": "off", + "no-inline-comments": "off", + "no-lonely-if": "error", + "no-mixed-operators": [ + "error", + { + "groups": [ + [ + "+", + "-", + "*", + "/", + "%", + "**" + ], + [ + "&", + "|", + "^", + "~", + "<<", + ">>", + ">>>" + ], + [ + "==", + "!=", + "===", + "!==", + ">", + ">=", + "<", + "<=" + ], + [ + "&&", + "||" + ], + [ + "in", + "instanceof" + ] + ], + "allowSamePrecedence": true + } + ], + "no-mixed-spaces-and-tabs": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 2, + "maxEOF": 1 + } + ], + "no-negated-condition": "off", + "no-nested-ternary": "error", + "no-new-object": "error", + "no-restricted-syntax": [ + "error", + "ForInStatement", + "LabeledStatement", + "WithStatement" + ], + "no-spaced-func": "error", + "no-ternary": "off", + "no-trailing-spaces": "error", + "no-underscore-dangle": [ + "off", + { + "allowAfterThis": true + } + ], + "no-unneeded-ternary": [ + "error", + { + "defaultAssignment": false + } + ], + "no-whitespace-before-property": "error", + "object-curly-spacing": [ + "error", + "always" + ], + "object-curly-newline": [ + "off", + { + "ObjectExpression": { + "minProperties": 0, + "multiline": true + }, + "ObjectPattern": { + "minProperties": 0, + "multiline": true + } + } + ], + "object-property-newline": [ + "error", + { + "allowMultiplePropertiesPerLine": true + } + ], + "one-var": [ + "error", + "never" + ], + "one-var-declaration-per-line": [ + "error", + "always" + ], + "operator-assignment": [ + "error", + "always" + ], + "operator-linebreak": "off", + "padded-blocks": [ + "off", + "never" + ], + "quote-props": [ + "error", + "as-needed", + { + "keywords": false, + "unnecessary": true, + "numbers": false + } + ], + "quotes": [ + "error", + "single", + { + "avoidEscape": true + } + ], + "require-jsdoc": "off", + "semi": [ + "error", + "always" + ], + "semi-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "sort-keys": [ + "off", + "asc", + { + "caseSensitive": false, + "natural": true + } + ], + "sort-vars": "off", + "space-before-blocks": "error", + "space-before-function-paren": [ + "error", + { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + } + ], + "space-in-parens": [ + "error", + "never" + ], + "space-infix-ops": "error", + "space-unary-ops": [ + "error", + { + "words": true, + "nonwords": false, + "overrides": {} + } + ], + "spaced-comment": [ + "error", + "always", + { + "line": { + "exceptions": [ + "-", + "+" + ], + "markers": [ + "/", + "=", + "!" + ] + }, + "block": { + "exceptions": [ + "-", + "+" + ], + "markers": [ + "=", + "!" + ], + "balanced": false + } + } + ], + "unicode-bom": [ + "error", + "never" + ], + "wrap-regex": "off", + "init-declarations": "off", + "no-catch-shadow": "off", + "no-delete-var": "error", + "no-label-var": "error", + "no-restricted-globals": "off", + "no-shadow": "error", + "no-shadow-restricted-names": "error", + "no-undef": "error", + "no-undef-init": "error", + "no-undefined": "off" + } + }, { + files: [ + 'tests/**/*.ts' + ], + plugins: { + jest: jestPlugin, + }, + languageOptions: { + globals: jestPlugin.environments.globals.globals, + }, + rules: { + 'jest/no-disabled-tests': 'warn', + 'jest/no-focused-tests': 'error', + 'jest/no-identical-title': 'error', + 'jest/prefer-to-have-length': 'warn', + 'jest/valid-expect': 'error', + 'class-methods-use-this': 'off', + 'no-loop-func': 'off', + 'no-return-assign': 'off', + 'no-console': 'off' + } + } +]); diff --git a/jest.config.ts b/jest.config.ts index 5b1e81b..469c8e1 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,20 +9,20 @@ export default { // An array of glob patterns indicating a set of files for which coverage information should be collected collectCoverageFrom: [ - "src/**/*.ts", // Only collect coverage from TypeScript source - "!src/**/*.d.ts", // Ignore TypeScript type declaration files + '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: [ - "/dist/", - "/examples/", - "/node_modules/", - "/src/rabbitmq/", - "/tests/" + '/dist/', + '/examples/', + '/node_modules/', + '/src/rabbitmq/', + '/tests/' ], // Indicates which provider should be used to instrument code for coverage @@ -33,7 +33,7 @@ export default { }, // The test environment that will be used for testing - testEnvironment: "node", + testEnvironment: 'node', // A map from regular expressions to paths to transformers transform: { diff --git a/package-lock.json b/package-lock.json index 723858f..dcbe83c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "di0": "^1.0.0" }, "devDependencies": { + "@stylistic/eslint-plugin-ts": "^4.2.0", "@types/amqplib": "^0.10.7", "@types/better-sqlite3": "^7.6.11", "@types/chai": "^4.3.20", @@ -21,13 +22,18 @@ "@types/md5": "^2.3.5", "@types/node": "^20.16.9", "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", + "eslint": "^9.24.0", + "eslint-plugin-jest": "^28.11.0", "jest": "^29.7.0", "sinon": "^19.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "typescript-eslint": "^8.29.0" }, "engines": { "node": ">=10.3.0" @@ -601,6 +607,256 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "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/@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.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "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, + "license": "Python-2.0" + }, + "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/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@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.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "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", @@ -983,6 +1239,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1039,6 +1333,24 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-4.2.0.tgz", + "integrity": "sha512-j2o2GvOx9v66x8hmp/HJ+0T+nOppiO5ycGsCkifh7JPGgjxEhpkGmIGx3RWsoxpWbad3VCX8e8/T8n3+7ze1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.23.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1139,6 +1451,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "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", @@ -1187,6 +1506,13 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/md5": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.5.tgz", @@ -1259,6 +1585,225 @@ "dev": true, "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", + "integrity": "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/type-utils": "8.29.0", + "@typescript-eslint/utils": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "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.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", + "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", + "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz", + "integrity": "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.29.0", + "@typescript-eslint/utils": "8.29.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.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 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", + "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", + "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.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", + "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/visitor-keys": "8.29.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "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 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "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": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", + "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.29.0", + "@typescript-eslint/types": "8.29.0", + "@typescript-eslint/typescript-estree": "8.29.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 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", + "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.29.0", + "eslint-visitor-keys": "^4.2.0" + }, + "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.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -1272,6 +1817,16 @@ "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.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", @@ -1292,6 +1847,23 @@ "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", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/amqplib": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.5.tgz", @@ -2384,6 +2956,13 @@ "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", @@ -2536,18 +3115,261 @@ "node": ">=8" } }, + "node_modules/eslint": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.24.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.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.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/node_modules/find-up": { + "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": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "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": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "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-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "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" - }, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, "node_modules/execa": { @@ -2610,6 +3432,43 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.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, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2617,6 +3476,23 @@ "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/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2627,6 +3503,19 @@ "bser": "2.1.1" } }, + "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": { + "flat-cache": "^4.0.0" + }, + "engines": { + "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", @@ -2694,6 +3583,27 @@ "node": ">=8" } }, + "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": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "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", @@ -2939,6 +3849,19 @@ "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/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2956,6 +3879,13 @@ "dev": true, "license": "ISC" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -3062,6 +3992,43 @@ "license": "BSD-3-Clause", "peer": true }, + "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/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-fresh/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/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -3156,6 +4123,16 @@ "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", @@ -3176,6 +4153,19 @@ "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", @@ -4023,6 +5013,13 @@ "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", @@ -4037,6 +5034,20 @@ "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, + "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", @@ -4091,6 +5102,16 @@ "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", @@ -4121,6 +5142,20 @@ "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", @@ -4220,6 +5255,13 @@ "dev": true, "license": "MIT" }, + "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, + "license": "MIT" + }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -4444,6 +5486,16 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -4713,6 +5765,24 @@ "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", @@ -4768,6 +5838,19 @@ "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" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -4947,6 +6030,16 @@ "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", @@ -5007,6 +6100,16 @@ "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" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -5043,6 +6146,27 @@ "license": "MIT", "peer": true }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -5317,6 +6441,41 @@ "node": ">=10" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5795,6 +6954,19 @@ "node": ">=8" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-jest": { "version": "29.2.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", @@ -5934,6 +7106,19 @@ "node": "*" } }, + "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", @@ -5971,6 +7156,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.0.tgz", + "integrity": "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.29.0", + "@typescript-eslint/parser": "8.29.0", + "@typescript-eslint/utils": "8.29.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 <5.9.0" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -6023,6 +7231,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "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", @@ -6099,6 +7317,16 @@ "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", diff --git a/package.json b/package.json index 07a4600..dbb6319 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "build": "tsc --build", "prepare": "npm run build", "preversion": "npm test", - "version": "npm run changelog && git add CHANGELOG.md" + "version": "npm run changelog && git add CHANGELOG.md", + "lint": "eslint" }, "author": "@snatalenko", "license": "MIT", @@ -69,6 +70,7 @@ "di0": "^1.0.0" }, "devDependencies": { + "@stylistic/eslint-plugin-ts": "^4.2.0", "@types/amqplib": "^0.10.7", "@types/better-sqlite3": "^7.6.11", "@types/chai": "^4.3.20", @@ -76,17 +78,22 @@ "@types/md5": "^2.3.5", "@types/node": "^20.16.9", "@types/sinon": "^17.0.4", + "@typescript-eslint/eslint-plugin": "^8.29.0", + "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", + "eslint": "^9.24.0", + "eslint-plugin-jest": "^28.11.0", "jest": "^29.7.0", "sinon": "^19.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.6.2" + "typescript": "^5.6.2", + "typescript-eslint": "^8.29.0" }, "peerDependencies": { "amqplib": "^0.10.5", "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } -} \ No newline at end of file +} diff --git a/scripts/changelog/index.js b/scripts/changelog/index.js index 887d0f0..1028fe2 100644 --- a/scripts/changelog/index.js +++ b/scripts/changelog/index.js @@ -19,7 +19,7 @@ const TITLES = [ function transform(commit) { if (known[commit.hash]) - // eslint-disable-next-line no-param-reassign + commit = { ...commit, ...known[commit.hash] }; if (!commit.tag) return undefined; diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index 3d87b08..1c3fdff 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -6,7 +6,7 @@ import { IEvent, IEventSet, IAggregateConstructorParams -} from "./interfaces"; +} from './interfaces'; import { getClassName, validateHandlers, getHandler, getMessageHandlerNames } from './utils'; @@ -74,6 +74,7 @@ export abstract class AbstractAggregate = { + /** * The default view associated with the projection. * Can optionally implement IViewLocker and/or IEventLocker. diff --git a/src/AbstractSaga.ts b/src/AbstractSaga.ts index 972516d..fa08b72 100644 --- a/src/AbstractSaga.ts +++ b/src/AbstractSaga.ts @@ -1,4 +1,4 @@ -import { ICommand, Identifier, IEvent, ISaga, ISagaConstructorParams } from "./interfaces"; +import { ICommand, Identifier, IEvent, ISaga, ISagaConstructorParams } from './interfaces'; import { getClassName, validateHandlers, getHandler } from './utils'; @@ -97,7 +97,7 @@ export abstract class AbstractSaga implements ISaga { } /** Put a command to the execution queue */ - protected enqueueRaw(command: ICommand) { + protected enqueueRaw(command: ICommand) { if (typeof command !== 'object' || !command) throw new TypeError('command argument must be an Object'); if (typeof command.type !== 'string' || !command.type.length) @@ -107,7 +107,7 @@ export abstract class AbstractSaga implements ISaga { } /** Clear the execution queue */ - resetUncommittedMessages() { + resetUncommittedMessages() { this.#messages.length = 0; } diff --git a/src/AggregateCommandHandler.ts b/src/AggregateCommandHandler.ts index f15d402..e404a52 100644 --- a/src/AggregateCommandHandler.ts +++ b/src/AggregateCommandHandler.ts @@ -9,9 +9,8 @@ import { Identifier, IEventSet, IEventStore, - IExtendableLogger, ILogger -} from "./interfaces"; +} from './interfaces'; import { iteratorToArray, @@ -104,8 +103,10 @@ export class AggregateCommandHandler implements ICommandHandler { /** 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) : diff --git a/src/CommandBus.ts b/src/CommandBus.ts index cf212b0..0a2104d 100644 --- a/src/CommandBus.ts +++ b/src/CommandBus.ts @@ -1,4 +1,4 @@ -import { InMemoryMessageBus } from "./in-memory"; +import { InMemoryMessageBus } from './in-memory'; import { ICommand, ICommandBus, @@ -7,7 +7,7 @@ import { ILogger, IMessageBus, IMessageHandler -} from "./interfaces"; +} from './interfaces'; export class CommandBus implements ICommandBus { @@ -52,7 +52,12 @@ export class CommandBus implements ICommandBus { /** * Format and send a command for execution */ - send(type: string, aggregateId: string, options: { payload: TPayload, context: object }, ...otherArgs: object[]): Promise { + send( + type: string, + aggregateId: string, + options: { payload: TPayload, context: object }, + ...otherArgs: object[] + ): Promise { if (typeof type !== 'string' || !type.length) throw new TypeError('type argument must be a non-empty String'); if (options && typeof options !== 'object') diff --git a/src/Event.ts b/src/Event.ts index 0dea892..ab2abc8 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -1,4 +1,4 @@ -import { IEvent } from "./interfaces"; +import { IEvent } from './interfaces'; /** * Get text description of an event for logging purposes diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 10c1fdb..ceeb6b2 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -7,11 +7,11 @@ import { IEventBus, isEventSet, IContainer -} from "./interfaces"; +} from './interfaces'; import { parallelPipe } from 'async-parallel-pipe'; import { AsyncIterableBuffer } from 'async-iterable-buffer'; -import { notEmpty } from "./utils"; -import { InMemoryMessageBus } from "./in-memory"; +import { notEmpty } from './utils'; +import { InMemoryMessageBus } from './in-memory'; type EventBatchEnvelope = { data: EventBatch<{ event?: IEvent }>; @@ -28,7 +28,7 @@ export class EventDispatcher implements IEventDispatcher { /** * Event bus where dispatched messages are delivered after processing. - * + * * If not provided in the constructor, defaults to an instance of `InMemoryMessageBus`. */ eventBus: IEventBus; @@ -105,9 +105,9 @@ export class EventDispatcher implements IEventDispatcher { const events = data.map(e => e.event).filter(notEmpty); try { - for (const event of events) { + for (const event of events) this.eventBus.publish(event); - } + resolve(events); } catch (publishError: any) { diff --git a/src/EventStore.ts b/src/EventStore.ts index 7f269a1..5a1e1e5 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -18,12 +18,12 @@ import { isIEventBus, isIEventStorageReader, IContainer -} from "./interfaces"; +} from './interfaces'; import { getClassName, setupOneTimeEmitterSubscription -} from "./utils"; -import { EventDispatcher } from "./EventDispatcher"; +} from './utils'; +import { EventDispatcher } from './EventDispatcher'; export class EventStore implements IEventStore { @@ -41,7 +41,7 @@ export class EventStore implements IEventStore { snapshotStorage, eventBus, eventDispatcher, - logger, + logger }: Pick e.aggregateId == aggregateId) : + this.#events.filter(e => e.aggregateId === aggregateId) : this.#events.filter(e => - e.aggregateId == aggregateId && + e.aggregateId === aggregateId && e.aggregateVersion !== undefined && e.aggregateVersion > afterVersion); @@ -49,11 +49,11 @@ export class InMemoryEventStorage implements IEventStorageReader, IEventStorageW yield* results; } - async *getSagaEvents(sagaId: Identifier, { beforeEvent }: { beforeEvent: IEvent }): IEventStream { + async* getSagaEvents(sagaId: Identifier, { beforeEvent }: { beforeEvent: IEvent }): IEventStream { await nextCycle(); const results = this.#events.filter(e => - e.sagaId == sagaId && + e.sagaId === sagaId && e.sagaVersion !== undefined && beforeEvent.sagaVersion !== undefined && e.sagaVersion < beforeEvent.sagaVersion); diff --git a/src/in-memory/InMemoryLock.ts b/src/in-memory/InMemoryLock.ts index a8f2192..84456bd 100644 --- a/src/in-memory/InMemoryLock.ts +++ b/src/in-memory/InMemoryLock.ts @@ -1,4 +1,4 @@ -import { Deferred } from "./utils"; +import { Deferred } from './utils'; export class InMemoryLock { diff --git a/src/in-memory/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts index 3a0bc54..69b8af6 100644 --- a/src/in-memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -4,7 +4,7 @@ import { IMessageBus, IMessageHandler, IObservable -} from "../interfaces"; +} from '../interfaces'; /** * Default implementation of the message bus. @@ -15,6 +15,7 @@ export class InMemoryMessageBus implements IMessageBus { #handlers: Map> = new Map(); #name: string | undefined; #uniqueEventHandlers: boolean; + // eslint-disable-next-line no-use-before-define #queues: Map = new Map(); constructor({ name, uniqueEventHandlers = !!name }: { @@ -38,8 +39,8 @@ 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 + // 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) diff --git a/src/in-memory/InMemorySnapshotStorage.ts b/src/in-memory/InMemorySnapshotStorage.ts index 5ac8183..3fb4947 100644 --- a/src/in-memory/InMemorySnapshotStorage.ts +++ b/src/in-memory/InMemorySnapshotStorage.ts @@ -1,4 +1,4 @@ -import { IAggregateSnapshotStorage, Identifier, IEvent } from "../interfaces"; +import { IAggregateSnapshotStorage, Identifier, IEvent } from '../interfaces'; /** * In-memory storage for aggregate snapshots. diff --git a/src/in-memory/InMemoryView.ts b/src/in-memory/InMemoryView.ts index 4ef35ae..6e2f429 100644 --- a/src/in-memory/InMemoryView.ts +++ b/src/in-memory/InMemoryView.ts @@ -1,5 +1,5 @@ import { InMemoryLock } from './InMemoryLock'; -import { IViewLocker, Identifier, IObjectStorage } from "../interfaces"; +import { IViewLocker, Identifier, IObjectStorage } from '../interfaces'; import { nextCycle } from './utils'; /** diff --git a/src/interfaces/IAggregate.ts b/src/interfaces/IAggregate.ts index 97aef7d..8dcc883 100644 --- a/src/interfaces/IAggregate.ts +++ b/src/interfaces/IAggregate.ts @@ -1,7 +1,7 @@ -import { ICommand } from "./ICommand"; -import { Identifier } from "./Identifier"; -import { IEvent } from "./IEvent"; -import { IEventSet } from "./IEventSet"; +import { ICommand } from './ICommand'; +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; +import { IEventSet } from './IEventSet'; /** * Minimum aggregate interface, as it's used by default `AggregateCommandHandler` @@ -25,6 +25,7 @@ export interface IAggregate { } export interface IMutableAggregateState { + // schemaVersion?: number; // constructor: IAggregateStateConstructor; mutate(event: IEvent): void; @@ -36,6 +37,7 @@ export interface IMutableAggregateState { // } export type IAggregateConstructorParams = { + /** Unique aggregate identifier */ id: Identifier, diff --git a/src/interfaces/IAggregateSnapshotStorage.ts b/src/interfaces/IAggregateSnapshotStorage.ts index e938b4c..1fb937a 100644 --- a/src/interfaces/IAggregateSnapshotStorage.ts +++ b/src/interfaces/IAggregateSnapshotStorage.ts @@ -1,8 +1,9 @@ -import { Identifier } from "./Identifier"; -import { IEvent } from "./IEvent"; +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; export interface IAggregateSnapshotStorage { - getAggregateSnapshot(aggregateId: Identifier): Promise | undefined> | IEvent | undefined; + getAggregateSnapshot(aggregateId: Identifier): + Promise | undefined> | IEvent | undefined; saveAggregateSnapshot(snapshotEvent: IEvent): Promise | void; diff --git a/src/interfaces/ICommand.ts b/src/interfaces/ICommand.ts index 95b5a2b..94efa95 100644 --- a/src/interfaces/ICommand.ts +++ b/src/interfaces/ICommand.ts @@ -1,3 +1,3 @@ -import { IMessage } from "./IMessage"; +import { IMessage } from './IMessage'; export type ICommand = IMessage; diff --git a/src/interfaces/ICommandBus.ts b/src/interfaces/ICommandBus.ts index 38907f8..53c4a7d 100644 --- a/src/interfaces/ICommandBus.ts +++ b/src/interfaces/ICommandBus.ts @@ -1,7 +1,7 @@ -import { ICommand } from "./ICommand"; -import { IEventSet } from "./IEventSet"; -import { IObservable } from "./IObservable"; -import { IObserver } from "./IObserver"; +import { ICommand } from './ICommand'; +import { IEventSet } from './IEventSet'; +import { IObservable } from './IObservable'; +import { IObserver } from './IObserver'; export interface ICommandBus extends IObservable { send(commandType: string, aggregateId: string | undefined, options: { payload?: object, context?: object }): diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index c2e0c3f..7110fa6 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -1,13 +1,13 @@ -import { Container } from "di0"; -import { ICommandBus } from "./ICommandBus"; -import { IEventDispatcher } from "./IEventDispatcher"; -import { IEventStore } from "./IEventStore"; -import { IEventBus } from "./IEventBus"; -import { IEventProcessor } from "./IEventProcessor"; -import { IEventStorageReader, IEventStorageWriter } from "./IEventStorage"; -import { IAggregateSnapshotStorage } from "./IAggregateSnapshotStorage"; -import { IIdentifierProvider } from "./IIdentifierProvider"; -import { IExtendableLogger, ILogger } from "./ILogger"; +import { Container } from 'di0'; +import { ICommandBus } from './ICommandBus'; +import { IEventDispatcher } from './IEventDispatcher'; +import { IEventStore } from './IEventStore'; +import { IEventBus } from './IEventBus'; +import { IEventProcessor } from './IEventProcessor'; +import { IEventStorageReader, IEventStorageWriter } from './IEventStorage'; +import { IAggregateSnapshotStorage } from './IAggregateSnapshotStorage'; +import { IIdentifierProvider } from './IIdentifierProvider'; +import { IExtendableLogger, ILogger } from './ILogger'; export interface IContainer extends Container { eventBus: IEventBus; @@ -22,5 +22,7 @@ export interface IContainer extends Container { eventDispatchProcessors?: IEventProcessor[]; logger?: ILogger | IExtendableLogger; + + // eslint-disable-next-line no-undef process?: NodeJS.Process } diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts index ec08092..0afe504 100644 --- a/src/interfaces/IEvent.ts +++ b/src/interfaces/IEvent.ts @@ -1,7 +1,8 @@ -import { IMessage } from "./IMessage"; -import { isObject } from "./isObject"; +import { IMessage } from './IMessage'; +import { isObject } from './isObject'; export type IEvent = IMessage & { + /** Unique event identifier */ id?: string; }; diff --git a/src/interfaces/IEventBus.ts b/src/interfaces/IEventBus.ts index c358a06..f36c593 100644 --- a/src/interfaces/IEventBus.ts +++ b/src/interfaces/IEventBus.ts @@ -1,5 +1,5 @@ -import { IEvent } from "./IEvent"; -import { IObservable, isIObservable } from "./IObservable"; +import { IEvent } from './IEvent'; +import { IObservable, isIObservable } from './IObservable'; export interface IEventBus extends IObservable { publish(event: IEvent): Promise; diff --git a/src/interfaces/IEventDispatcher.ts b/src/interfaces/IEventDispatcher.ts index bb5a0ba..60a1ce8 100644 --- a/src/interfaces/IEventDispatcher.ts +++ b/src/interfaces/IEventDispatcher.ts @@ -1,5 +1,5 @@ -import { IEventSet } from "./IEventSet"; -import { IEventBus } from "./IEventBus"; +import { IEventSet } from './IEventSet'; +import { IEventBus } from './IEventBus'; export interface IEventDispatcher { readonly eventBus: IEventBus; diff --git a/src/interfaces/IEventLocker.ts b/src/interfaces/IEventLocker.ts index 0d6c5a4..d3388b1 100644 --- a/src/interfaces/IEventLocker.ts +++ b/src/interfaces/IEventLocker.ts @@ -1,5 +1,5 @@ -import { IEvent } from "./IEvent"; -import { isObject } from "./isObject"; +import { IEvent } from './IEvent'; +import { isObject } from './isObject'; /** * Interface for tracking event processing state to prevent concurrent processing diff --git a/src/interfaces/IEventProcessor.ts b/src/interfaces/IEventProcessor.ts index ee7c28b..7bd448c 100644 --- a/src/interfaces/IEventProcessor.ts +++ b/src/interfaces/IEventProcessor.ts @@ -1,4 +1,4 @@ -import { IEvent } from "./IEvent"; +import { IEvent } from './IEvent'; /** * Represents a wrapper for an event that can optionally contain additional metadata. diff --git a/src/interfaces/IEventReceptor.ts b/src/interfaces/IEventReceptor.ts index 6059e78..722cbde 100644 --- a/src/interfaces/IEventReceptor.ts +++ b/src/interfaces/IEventReceptor.ts @@ -1,5 +1,5 @@ -import { IEventStore } from "./IEventStore"; -import { IObserver } from "./IObserver"; +import { IEventStore } from './IEventStore'; +import { IObserver } from './IObserver'; export interface IEventReceptor extends IObserver { subscribe(eventStore: IEventStore): void; diff --git a/src/interfaces/IEventSet.ts b/src/interfaces/IEventSet.ts index b65cc0a..c06ac83 100644 --- a/src/interfaces/IEventSet.ts +++ b/src/interfaces/IEventSet.ts @@ -1,4 +1,4 @@ -import { IEvent, isEvent } from "./IEvent"; +import { IEvent, isEvent } from './IEvent'; export type IEventSet = ReadonlyArray>; diff --git a/src/interfaces/IEventStorage.ts b/src/interfaces/IEventStorage.ts index cd55ade..bc7dbdb 100644 --- a/src/interfaces/IEventStorage.ts +++ b/src/interfaces/IEventStorage.ts @@ -1,20 +1,23 @@ -import { Identifier } from "./Identifier"; -import { IEvent } from "./IEvent"; -import { IEventSet } from "./IEventSet"; -import { IEventStream } from "./IEventStream"; -import { isObject } from "./isObject"; +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; +import { IEventSet } from './IEventSet'; +import { IEventStream } from './IEventStream'; +import { isObject } from './isObject'; export type EventQueryAfter = { + /** Get events emitted after this specific event */ afterEvent?: IEvent; } export type EventQueryBefore = { + /** Get events emitted before this specific event */ beforeEvent?: IEvent; } export interface IEventStorageReader { + /** * Retrieves events of specified types that were emitted after a given event. */ @@ -32,6 +35,7 @@ export interface IEventStorageReader { } export interface IEventStorageWriter { + /** * Persists a set of events to the event store. * Returns the persisted event set (potentially enriched or normalized). diff --git a/src/interfaces/IEventStore.ts b/src/interfaces/IEventStore.ts index c647010..1320e0d 100644 --- a/src/interfaces/IEventStore.ts +++ b/src/interfaces/IEventStore.ts @@ -1,8 +1,8 @@ -import { IEventDispatcher } from "./IEventDispatcher"; -import { IEvent } from "./IEvent"; -import { IEventStorageReader } from "./IEventStorage"; -import { IIdentifierProvider } from "./IIdentifierProvider"; -import { IMessageHandler, IObservable } from "./IObservable"; +import { IEventDispatcher } from './IEventDispatcher'; +import { IEvent } from './IEvent'; +import { IEventStorageReader } from './IEventStorage'; +import { IIdentifierProvider } from './IIdentifierProvider'; +import { IMessageHandler, IObservable } from './IObservable'; export interface IEventStore extends IObservable, IEventDispatcher, IEventStorageReader, IIdentifierProvider { diff --git a/src/interfaces/IEventStream.ts b/src/interfaces/IEventStream.ts index f8c9337..1f11e35 100644 --- a/src/interfaces/IEventStream.ts +++ b/src/interfaces/IEventStream.ts @@ -1,3 +1,3 @@ -import { IEvent } from "./IEvent"; +import { IEvent } from './IEvent'; export type IEventStream = AsyncIterableIterator>; diff --git a/src/interfaces/IIdentifierProvider.ts b/src/interfaces/IIdentifierProvider.ts index 30b8be9..2c73090 100644 --- a/src/interfaces/IIdentifierProvider.ts +++ b/src/interfaces/IIdentifierProvider.ts @@ -1,7 +1,8 @@ -import { Identifier } from "./Identifier"; -import { isObject } from "./isObject"; +import { Identifier } from './Identifier'; +import { isObject } from './isObject'; export interface IIdentifierProvider { + /** * Generates and returns a new unique identifier suitable for aggregates, sagas, and events. * diff --git a/src/interfaces/IMessage.ts b/src/interfaces/IMessage.ts index c474e3b..40c78f0 100644 --- a/src/interfaces/IMessage.ts +++ b/src/interfaces/IMessage.ts @@ -1,7 +1,8 @@ -import { Identifier } from "./Identifier"; -import { isObject } from "./isObject"; +import { Identifier } from './Identifier'; +import { isObject } from './isObject'; export interface IMessage { + /** Event or command type */ type: string; diff --git a/src/interfaces/IMessageBus.ts b/src/interfaces/IMessageBus.ts index c20af13..d986b20 100644 --- a/src/interfaces/IMessageBus.ts +++ b/src/interfaces/IMessageBus.ts @@ -1,6 +1,6 @@ -import { ICommand } from "./ICommand"; -import { IEvent } from "./IEvent"; -import { IObservable } from "./IObservable"; +import { ICommand } from './ICommand'; +import { IEvent } from './IEvent'; +import { IObservable } from './IObservable'; export interface IMessageBus extends IObservable { send(command: ICommand): Promise; diff --git a/src/interfaces/IObjectStorage.ts b/src/interfaces/IObjectStorage.ts index 1207c37..e4b651a 100644 --- a/src/interfaces/IObjectStorage.ts +++ b/src/interfaces/IObjectStorage.ts @@ -1,4 +1,4 @@ -import { Identifier } from "./Identifier"; +import { Identifier } from './Identifier'; export interface IObjectStorage { get(id: Identifier): Promise | TRecord | undefined; diff --git a/src/interfaces/IObservable.ts b/src/interfaces/IObservable.ts index 79774ba..a04387a 100644 --- a/src/interfaces/IObservable.ts +++ b/src/interfaces/IObservable.ts @@ -1,11 +1,12 @@ -import { IMessage } from "./IMessage"; -import { isObject } from "./isObject"; +import { IMessage } from './IMessage'; +import { isObject } from './isObject'; export interface IMessageHandler { (message: IMessage, meta?: Record): any | Promise -}; +} export interface IObservable { + /** * Setup a listener for a specific event type */ diff --git a/src/interfaces/IObserver.ts b/src/interfaces/IObserver.ts index 6f1365f..d822cea 100644 --- a/src/interfaces/IObserver.ts +++ b/src/interfaces/IObserver.ts @@ -1,4 +1,4 @@ -import { IObservable } from "./IObservable"; +import { IObservable } from './IObservable'; export interface IObserver { subscribe(observable: IObservable): void; diff --git a/src/interfaces/IProjection.ts b/src/interfaces/IProjection.ts index aaa6721..4c260d7 100644 --- a/src/interfaces/IProjection.ts +++ b/src/interfaces/IProjection.ts @@ -1,6 +1,6 @@ -import { IEvent } from "./IEvent"; -import { IEventStore } from "./IEventStore"; -import { IObserver } from "./IObserver"; +import { IEvent } from './IEvent'; +import { IEventStore } from './IEventStore'; +import { IObserver } from './IObserver'; export interface IProjection extends IObserver { readonly view: TView; diff --git a/src/interfaces/ISaga.ts b/src/interfaces/ISaga.ts index 8507ac1..f4920ba 100644 --- a/src/interfaces/ISaga.ts +++ b/src/interfaces/ISaga.ts @@ -1,9 +1,10 @@ -import { ICommand } from "./ICommand"; -import { Identifier } from "./Identifier"; -import { IEvent } from "./IEvent"; -import { IEventSet } from "./IEventSet"; +import { ICommand } from './ICommand'; +import { Identifier } from './Identifier'; +import { IEvent } from './IEvent'; +import { IEventSet } from './IEventSet'; export interface ISaga { + /** Unique Saga ID */ readonly id: Identifier; diff --git a/src/interfaces/IViewLocker.ts b/src/interfaces/IViewLocker.ts index 238479d..fefcd2d 100644 --- a/src/interfaces/IViewLocker.ts +++ b/src/interfaces/IViewLocker.ts @@ -1,4 +1,4 @@ -import { isObject } from "./isObject"; +import { isObject } from './isObject'; /** * Interface for managing view restoration state to prevent early access to an inconsistent view @@ -13,7 +13,7 @@ export interface IViewLocker { /** * Locks the view to prevent external read/write operations. - * + * * @returns `true` if the lock is successfully acquired, `false` otherwise. */ lock(): Promise | boolean; @@ -25,16 +25,16 @@ export interface IViewLocker { /** * 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; + 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. */ diff --git a/src/rabbitmq/IContainer.ts b/src/rabbitmq/IContainer.ts index 18da704..6c43fb0 100644 --- a/src/rabbitmq/IContainer.ts +++ b/src/rabbitmq/IContainer.ts @@ -1,5 +1,5 @@ -import { RabbitMqEventInjector } from "./RabbitMqEventInjector"; -import { RabbitMqGateway } from "./RabbitMqGateway"; +import { RabbitMqEventInjector } from './RabbitMqEventInjector'; +import { RabbitMqGateway } from './RabbitMqGateway'; declare module '../interfaces/IContainer' { interface IContainer { diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts index 6b5c813..130c43f 100644 --- a/src/rabbitmq/RabbitMqEventBus.ts +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -1,5 +1,5 @@ -import { IEvent, IEventBus, IMessage, IMessageHandler, IObservable } from "../interfaces"; -import { RabbitMqGateway } from "./RabbitMqGateway"; +import { IEvent, IEventBus, IMessageHandler, IObservable } from '../interfaces'; +import { RabbitMqGateway } from './RabbitMqGateway'; const ALL_EVENTS_WILDCARD = '*'; diff --git a/src/rabbitmq/RabbitMqEventInjector.ts b/src/rabbitmq/RabbitMqEventInjector.ts index f6ae456..f8e4850 100644 --- a/src/rabbitmq/RabbitMqEventInjector.ts +++ b/src/rabbitmq/RabbitMqEventInjector.ts @@ -1,7 +1,7 @@ -import { IContainer } from "../interfaces/IContainer"; -import { IMessage } from "../interfaces/IMessage"; -import { RabbitMqGateway } from "./RabbitMqGateway"; -import { IEventDispatcher, isEvent } from "../interfaces"; +import { IContainer } from '../interfaces/IContainer'; +import { IMessage } from '../interfaces/IMessage'; +import { RabbitMqGateway } from './RabbitMqGateway'; +import { IEventDispatcher } from '../interfaces'; import * as Event from '../Event'; export class RabbitMqEventInjector { @@ -18,9 +18,9 @@ export class RabbitMqEventInjector { queueName?: string; }) { if (!container.eventDispatcher) - throw new Error("eventDispatcher is required in the container."); + throw new Error('eventDispatcher is required in the container.'); if (!container.rabbitMqGateway) - throw new Error("rabbitMqGateway is required in the container."); + throw new Error('rabbitMqGateway is required in the container.'); this.#rabbitMqGateway = container.rabbitMqGateway; this.#eventDispatcher = container.eventDispatcher; diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index ab00b06..fad8d2f 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -227,7 +227,7 @@ export class RabbitMqGateway { } async #assertConnection() { - return this.#connection ?? await this.connect(); + return this.#connection ?? this.connect(); } /** Get existing or open a new channel for a given queue name */ @@ -245,6 +245,7 @@ export class RabbitMqGateway { * 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, @@ -374,7 +375,7 @@ export class RabbitMqGateway { return new Promise((resolve, reject) => { const published = this.#pubChannel!.publish(exchange, message.type, content, properties, err => - err ? reject(err) : resolve()); + (err ? reject(err) : resolve())); if (!published) throw new Error(`${this.#appId}: Failed to send event ${Event.describe(message)}, channel buffer is full`); }); diff --git a/src/sqlite/AbstractSqliteObjectProjection.ts b/src/sqlite/AbstractSqliteObjectProjection.ts index 59efb63..3cb996c 100644 --- a/src/sqlite/AbstractSqliteObjectProjection.ts +++ b/src/sqlite/AbstractSqliteObjectProjection.ts @@ -1,7 +1,7 @@ -import { AbstractProjection } from "../AbstractProjection"; -import { IExtendableLogger } from "../interfaces"; -import { SqliteDbParams } from "./commonParams"; -import { SqliteObjectView } from "./SqliteObjectView"; +import { AbstractProjection } from '../AbstractProjection'; +import { IExtendableLogger } from '../interfaces'; +import { SqliteDbParams } from './commonParams'; +import { SqliteObjectView } from './SqliteObjectView'; export abstract class AbstractSqliteObjectProjection extends AbstractProjection> { diff --git a/src/sqlite/AbstractSqliteView.ts b/src/sqlite/AbstractSqliteView.ts index e4a01e4..697ec15 100644 --- a/src/sqlite/AbstractSqliteView.ts +++ b/src/sqlite/AbstractSqliteView.ts @@ -1,8 +1,7 @@ -import { IEvent, IEventLocker, ILogger } from '../interfaces'; +import { IEvent, IEventLocker, ILogger, IViewLocker } from '../interfaces'; import { Database } from 'better-sqlite3'; import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker'; import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker'; -import { IViewLocker } from '../interfaces'; export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { @@ -21,7 +20,7 @@ export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { this.schemaVersion = options.schemaVersion; this.viewLocker = new SqliteViewLocker(options); this.eventLocker = new SqliteEventLocker(options); - this.logger = options.logger && 'child' in options.logger ? + this.logger = options.logger && 'child' in options.logger ? options.logger.child({ serviceName: new.target.name }) : options.logger; } diff --git a/src/sqlite/SqliteEventLocker.ts b/src/sqlite/SqliteEventLocker.ts index 81db266..3d26171 100644 --- a/src/sqlite/SqliteEventLocker.ts +++ b/src/sqlite/SqliteEventLocker.ts @@ -6,9 +6,10 @@ import { SqliteViewLockerParams } from './SqliteViewLocker'; import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; export type SqliteEventLockerParams = SqliteDbParams & SqliteProjectionDataParams & { + /** * (Optional) SQLite table name where event locks are stored - * + * * @default "tbl_event_lock" */ eventLockTableName?: string; diff --git a/src/sqlite/SqliteObjectView.ts b/src/sqlite/SqliteObjectView.ts index 9a99f6a..31165f4 100644 --- a/src/sqlite/SqliteObjectView.ts +++ b/src/sqlite/SqliteObjectView.ts @@ -1,4 +1,4 @@ -import { AbstractSqliteView } from "./AbstractSqliteView"; +import { AbstractSqliteView } from './AbstractSqliteView'; import { IObjectStorage, IEventLocker } from '../interfaces'; import { SqliteObjectStorage } from './SqliteObjectStorage'; diff --git a/src/sqlite/SqliteViewLocker.ts b/src/sqlite/SqliteViewLocker.ts index 9320adf..9907aee 100644 --- a/src/sqlite/SqliteViewLocker.ts +++ b/src/sqlite/SqliteViewLocker.ts @@ -7,16 +7,17 @@ import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; const delay = promisify(setTimeout); export type SqliteViewLockerParams = SqliteDbParams & 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; @@ -44,6 +45,7 @@ export class SqliteViewLocker implements IViewLocker { #removeTableLockQuery: Statement<[string, string], void>; #lockMarker: Deferred | undefined; + // eslint-disable-next-line no-undef #lockProlongationTimeout: NodeJS.Timeout | undefined; constructor(o: SqliteViewLockerParams) { diff --git a/src/sqlite/commonParams.ts b/src/sqlite/commonParams.ts index 9b51c7a..f26118e 100644 --- a/src/sqlite/commonParams.ts +++ b/src/sqlite/commonParams.ts @@ -1,11 +1,13 @@ import { Database } from 'better-sqlite3'; export type SqliteDbParams = { + /** Configured instance of better-sqlite3.Database */ viewModelSqliteDb: Database; }; export type SqliteProjectionDataParams = { + /** * Unique identifier for the projection, used with the schema version to distinguish data ownership. */ diff --git a/src/sqlite/utils/getEventId.ts b/src/sqlite/utils/getEventId.ts index 309d49f..2a99f75 100644 --- a/src/sqlite/utils/getEventId.ts +++ b/src/sqlite/utils/getEventId.ts @@ -1,4 +1,4 @@ -import { IEvent } from "../../interfaces"; +import { IEvent } from '../../interfaces'; import { guid } from './guid'; import md5 = require('md5'); diff --git a/src/utils/getHandler.ts b/src/utils/getHandler.ts index 957bbaf..8df01a1 100644 --- a/src/utils/getHandler.ts +++ b/src/utils/getHandler.ts @@ -1,4 +1,4 @@ -import { IMessageHandler } from "../interfaces"; +import { IMessageHandler } from '../interfaces'; /** * Gets a handler for a specific message type, prefers a public (w\o _ prefix) method, if available @@ -17,4 +17,4 @@ export function getHandler(context: { [key: string]: any }, messageType: string) return context[privateHandlerName].bind(context); return null; -}; +} diff --git a/src/utils/setupOneTimeEmitterSubscription.ts b/src/utils/setupOneTimeEmitterSubscription.ts index 4fe28a0..554d8bf 100644 --- a/src/utils/setupOneTimeEmitterSubscription.ts +++ b/src/utils/setupOneTimeEmitterSubscription.ts @@ -1,4 +1,4 @@ -import { IEvent, ILogger, IObservable } from "../interfaces"; +import { IEvent, ILogger, IObservable } from '../interfaces'; /** * Create one-time eventEmitter subscription for one or multiple events that match a filter @@ -34,8 +34,10 @@ export function setupOneTimeEmitterSubscription( let handled = false; function filteredHandler(event: IEvent) { - if (filter && !filter(event)) return; - if (handled) return; + if (filter && !filter(event)) + return; + if (handled) + return; handled = true; for (const messageType of messageTypes) diff --git a/src/utils/subscribe.ts b/src/utils/subscribe.ts index 28982b9..dcb3965 100644 --- a/src/utils/subscribe.ts +++ b/src/utils/subscribe.ts @@ -1,6 +1,6 @@ -import { IMessageHandler, IObservable } from "../interfaces"; +import { IMessageHandler, IObservable } from '../interfaces'; import { getHandler } from './getHandler'; -import { getMessageHandlerNames } from "./getMessageHandlerNames"; +import { getMessageHandlerNames } from './getMessageHandlerNames'; const unique = (arr: T[]): T[] => [...new Set(arr)]; diff --git a/src/utils/validateHandlers.ts b/src/utils/validateHandlers.ts index e6d5c96..7061c34 100644 --- a/src/utils/validateHandlers.ts +++ b/src/utils/validateHandlers.ts @@ -4,7 +4,8 @@ import { getHandler } from './getHandler'; * Ensure instance has handlers declared for all handled message types */ export function validateHandlers(instance: object, handlesFieldName = 'handles') { - if (!instance) throw new TypeError('instance argument required'); + if (!instance) + throw new TypeError('instance argument required'); const messageTypes = Object.getPrototypeOf(instance).constructor[handlesFieldName]; if (messageTypes === undefined) diff --git a/tests/integration/rabbitmq/RabbitMqEventBus.test.ts b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts index ee99a10..10c31ad 100644 --- a/tests/integration/rabbitmq/RabbitMqEventBus.test.ts +++ b/tests/integration/rabbitmq/RabbitMqEventBus.test.ts @@ -109,11 +109,11 @@ describe('RabbitMqEventBus', () => { const received1: IMessage[] = []; const received2: IMessage[] = []; - await eventBus1.queue(queueName).on(eventType, (msg) => { + await eventBus1.queue(queueName).on(eventType, msg => { received1.push(msg); }); - await eventBus2.queue(queueName).on(eventType, (msg) => { + await eventBus2.queue(queueName).on(eventType, msg => { received2.push(msg); }); @@ -135,11 +135,11 @@ describe('RabbitMqEventBus', () => { const received1: IMessage[] = []; const received2: IMessage[] = []; - await eventBus1.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, (msg) => { + await eventBus1.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, msg => { received1.push(msg); }); - await eventBus2.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, (msg) => { + await eventBus2.queue(queueName).on(RabbitMqEventBus.allEventsWildcard, msg => { received2.push(msg); }); diff --git a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts index b733d98..f111718 100644 --- a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts +++ b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts @@ -1,14 +1,13 @@ import * as amqplib from 'amqplib'; import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; import { RabbitMqEventInjector } from '../../../src/rabbitmq/RabbitMqEventInjector'; -import { IEvent, IEventDispatcher, IMessage } from '../../../src/interfaces'; +import { IEvent, IEventDispatcher } from '../../../src/interfaces'; import { jest } from '@jest/globals'; import { delay } from '../../../src/utils'; describe('RabbitMqEventInjector', () => { let rabbitMqGateway: RabbitMqGateway; let eventDispatcher: jest.Mocked; - let injector: RabbitMqEventInjector; const exchange = 'node-cqrs.events'; const queueName = 'test-injector-queue'; @@ -20,10 +19,11 @@ describe('RabbitMqEventInjector', () => { rabbitMqGateway = new RabbitMqGateway({ rabbitMqConnectionFactory }); eventDispatcher = { - dispatch: jest.fn().mockResolvedValue(undefined), + dispatch: jest.fn().mockResolvedValue(undefined) } as unknown as jest.Mocked; - injector = new RabbitMqEventInjector({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const injector = new RabbitMqEventInjector({ rabbitMqGateway, eventDispatcher, queueName, @@ -55,7 +55,7 @@ describe('RabbitMqEventInjector', () => { const testEvent: IEvent = { type: eventType, payload: { data: 'test-payload' }, - id: 'test-id-123', + id: 'test-id-123' }; await rabbitMqGateway.publish(exchange, testEvent); @@ -70,7 +70,7 @@ describe('RabbitMqEventInjector', () => { const testEvent: IEvent = { type: 'error-event', payload: { data: 'trigger-error' }, - id: 'error-id-456', + id: 'error-id-456' }; const dispatchError = new Error('Dispatch failed'); eventDispatcher.dispatch.mockRejectedValueOnce(dispatchError); diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index 271d90d..12ec6fa 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -16,6 +16,7 @@ describe('RabbitMqGateway', () => { beforeEach(async () => { const logger = undefined; + // const logger = console; gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); gateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); @@ -28,7 +29,7 @@ describe('RabbitMqGateway', () => { await ch.deleteQueue(queueName); await ch.deleteQueue(`${queueName}.failed`); await ch.deleteExchange(exchange); - await gateway1.disconnect() + await gateway1.disconnect(); } await gateway2?.disconnect(); await gateway3?.disconnect(); @@ -40,7 +41,7 @@ describe('RabbitMqGateway', () => { const message: IMessage = { type: 'test.confirm', - payload: { msg: 'confirmed' }, + payload: { msg: 'confirmed' } }; await gateway1.publish(exchange, message); @@ -59,7 +60,7 @@ describe('RabbitMqGateway', () => { const message: IMessage = { type: 'test.event', - payload: { msg: 'self-test' }, + payload: { msg: 'self-test' } }; // publish from the same instance — should be ignored @@ -83,7 +84,7 @@ describe('RabbitMqGateway', () => { const message: IMessage = { type: 'test.event', - payload: { from: 'external' }, + payload: { from: 'external' } }; gateway3.publish(exchange, message); diff --git a/tests/integration/sqlite/SqliteView.test.ts b/tests/integration/sqlite/SqliteView.test.ts index c50916e..eaf116c 100644 --- a/tests/integration/sqlite/SqliteView.test.ts +++ b/tests/integration/sqlite/SqliteView.test.ts @@ -24,12 +24,11 @@ class MyDumbProjection extends AbstractProjection> { if (!e.payload) throw new TypeError('e.payload is required'); - await this.view.update(e.aggregateId, u => e.payload); + await this.view.update(e.aggregateId, _u => e.payload); } } - -describe.only('SqliteView', () => { +describe('SqliteView', () => { let viewModelSqliteDb: import('better-sqlite3').Database; diff --git a/tests/unit/AbstractAggregate.test.ts b/tests/unit/AbstractAggregate.test.ts index a74ac4e..7cfcf73 100644 --- a/tests/unit/AbstractAggregate.test.ts +++ b/tests/unit/AbstractAggregate.test.ts @@ -81,7 +81,9 @@ 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); }); }); @@ -94,7 +96,9 @@ describe('AbstractAggregate', function () { 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); + expect(() => { + (agg as any).changes = []; + }).to.throw(TypeError); return agg.doSomething({}).then(() => { @@ -110,7 +114,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', () => { @@ -227,7 +233,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'); @@ -303,7 +309,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 af37fba..5701e5f 100644 --- a/tests/unit/AbstractProjection.test.ts +++ b/tests/unit/AbstractProjection.test.ts @@ -14,12 +14,13 @@ class MyProjection extends AbstractProjection { if (v.somethingHappenedCnt) v.somethingHappenedCnt += 1; else v.somethingHappenedCnt = 1; + return v; }); } @@ -210,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 0e5ead3..837d9b1 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -1,8 +1,13 @@ import { expect, assert } from 'chai'; import * as sinon from 'sinon'; -import { EventDispatcher, ICommandBus, Identifier, IEventBus, IEventSet, IEventStore, InMemoryMessageBus } from '../../src'; - import { + EventDispatcher, + ICommandBus, + Identifier, + IEventBus, + IEventSet, + IEventStore, + InMemoryMessageBus, AggregateCommandHandler, AbstractAggregate, InMemoryEventStorage, @@ -141,8 +146,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 @@ -222,47 +225,4 @@ describe('AggregateCommandHandler', function () { expect(eventStream[2]).to.have.property('aggregateVersion', 2); expect(eventStream[2]).to.have.property('payload'); }); - - it.skip('executes concurrent commands on same aggregate instance', async () => { - - // setup - - class PersistedAggregate extends MyAggregate { - get shouldTakeSnapshot() { - return this.version > 2; - } - } - - const handler = new AggregateCommandHandler({ eventStore, aggregateType: PersistedAggregate }); - - const getAggregateEventsSpy = sinon.spy(storage, 'getAggregateEvents'); - const commitEventsSpy = sinon.spy(storage, 'commitEvents'); - - // test - - const cmd0 = { type: 'createAggregate' }; - - const [{ aggregateId }] = await handler.execute(cmd0); - - expect(storage).to.have.nested.property('getAggregateEvents.callCount', 0); - expect(storage).to.have.nested.property('commitEvents.callCount', 1); - - const cmd1 = { aggregateId, type: 'doSomething' }; - const cmd2 = { aggregateId, type: 'doSomething' }; - const cmd3 = { aggregateId, type: 'doSomething' }; - - 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); - - const events = await eventStore.getAggregateEvents(aggregateId as Identifier); - - 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); - }); }); diff --git a/tests/unit/EventDispatcher.test.ts b/tests/unit/EventDispatcher.test.ts index 45bde3f..53f3107 100644 --- a/tests/unit/EventDispatcher.test.ts +++ b/tests/unit/EventDispatcher.test.ts @@ -16,7 +16,7 @@ describe('EventDispatcher', () => { const event2: IEvent = { type: 'test-event-2' }; const processorMock: IEventProcessor = { - process: jest.fn(batch => Promise.resolve(batch)), + process: jest.fn(batch => Promise.resolve(batch)) }; dispatcher.addPipelineProcessor(processorMock); @@ -36,7 +36,7 @@ describe('EventDispatcher', () => { const processorMock: IEventProcessor = { process: jest.fn().mockRejectedValue(error), - revert: jest.fn().mockResolvedValue(undefined), + revert: jest.fn().mockResolvedValue(undefined) }; dispatcher.addPipelineProcessor(processorMock); @@ -63,7 +63,7 @@ describe('EventDispatcher', () => { await new Promise(res => setTimeout(res, 5)); executionOrder.push(`A-end-${batch[0].event.type}`); return batch; - }), + }) }; const processorB: IEventProcessor = { @@ -72,7 +72,7 @@ describe('EventDispatcher', () => { await new Promise(res => setTimeout(res, 5)); executionOrder.push(`B-end-${batch[0].event.type}`); return batch; - }), + }) }; dispatcher.addPipelineProcessor(processorA); @@ -83,7 +83,7 @@ describe('EventDispatcher', () => { await Promise.all([ dispatcher.dispatch([event1]), - dispatcher.dispatch([event2]), + dispatcher.dispatch([event2]) ]); expect(executionOrder).toEqual([ @@ -94,7 +94,7 @@ describe('EventDispatcher', () => { 'A-end-event-2', 'B-start-event-2', 'B-end-event-1', - 'B-end-event-2', + 'B-end-event-2' ]); }); }); diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index dcd1048..8cb1bb1 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -89,9 +89,9 @@ describe('EventStore', () => { mockStorage.getAggregateEvents.mockResolvedValueOnce(storedEvents); const result: IEvent[] = []; - for await (const event of store.getAggregateEvents('aggregate-1')) { + for await (const event of store.getAggregateEvents('aggregate-1')) result.push(event); - } + expect(result).toEqual([snapshotEvent, ...storedEvents]); expect(mockSnapshotStorage.getAggregateSnapshot).toHaveBeenCalledWith('aggregate-1'); @@ -107,9 +107,9 @@ describe('EventStore', () => { const filter = { beforeEvent: { sagaVersion: 1 } }; const result: IEvent[] = []; - for await (const event of store.getSagaEvents('saga-1', filter)) { + for await (const event of store.getSagaEvents('saga-1', filter)) result.push(event); - } + expect(result).toEqual(sagaEvents); expect(mockStorage.getSagaEvents).toHaveBeenCalledWith('saga-1', filter); @@ -150,13 +150,13 @@ describe('EventStore', () => { 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) => { + const promise = store.once('onceEvent', (_e: IEvent) => { callCount++; }); await store.dispatch([testEvent]); - expect(promise).resolves.toBe(testEvent); + await expect(promise).resolves.toBe(testEvent); expect(callCount).toBe(1); }); @@ -164,14 +164,14 @@ describe('EventStore', () => { let callCount = 0; const testEvent = { type: 'onceEvent' } as IEvent; const testEvent2 = { type: 'onceEvent' } as IEvent; - const promise = store.once('onceEvent', (e: IEvent) => { + const promise = store.once('onceEvent', (_e: IEvent) => { callCount++; }); await store.dispatch([testEvent, testEvent2]); await store.dispatch([testEvent2]); - expect(promise).resolves.toBe(testEvent); + await expect(promise).resolves.toBe(testEvent); expect(callCount).toBe(1); }); }); diff --git a/tests/unit/SagaEventHandler.test.ts b/tests/unit/SagaEventHandler.test.ts index 1556bc2..0aac4e9 100644 --- a/tests/unit/SagaEventHandler.test.ts +++ b/tests/unit/SagaEventHandler.test.ts @@ -18,7 +18,7 @@ class Saga extends AbstractSaga { static get handles(): string[] { return ['followingHappened']; } - somethingHappened(event) { + somethingHappened(_event) { super.enqueue('doSomething', undefined, { foo: 'bar' }); } followingHappened() { @@ -66,7 +66,7 @@ describe('SagaEventHandler', function () { commandBus.on('complete', () => { deferred.resolve(undefined); - }) + }); sinon.spy(eventStore, 'getSagaEvents'); @@ -94,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/memory/InMemoryEventStorage.test.ts b/tests/unit/memory/InMemoryEventStorage.test.ts index bd8079c..ca47e57 100644 --- a/tests/unit/memory/InMemoryEventStorage.test.ts +++ b/tests/unit/memory/InMemoryEventStorage.test.ts @@ -27,9 +27,9 @@ describe('InMemoryEventStorage', () => { await storage.commitEvents([event1, event2]); const results = []; - for await (const event of storage.getAggregateEvents('agg1')) { + for await (const event of storage.getAggregateEvents('agg1')) results.push(event); - } + expect(results).to.deep.equal([event1]); }); @@ -41,9 +41,9 @@ describe('InMemoryEventStorage', () => { const snapshot = { aggregateVersion: 1 }; const results = []; - for await (const event of storage.getAggregateEvents('agg1', { snapshot })) { + for await (const event of storage.getAggregateEvents('agg1', { snapshot })) results.push(event); - } + expect(results).to.deep.equal([event2]); }); }); @@ -59,9 +59,9 @@ describe('InMemoryEventStorage', () => { const beforeEvent = { sagaVersion: 3 }; const results = []; - for await (const event of storage.getSagaEvents('saga1', { beforeEvent })) { + for await (const event of storage.getSagaEvents('saga1', { beforeEvent })) results.push(event); - } + expect(results).to.deep.equal([event1, event2]); }); }); @@ -76,9 +76,9 @@ describe('InMemoryEventStorage', () => { await storage.commitEvents([event1, event2, event3]); const results = []; - for await (const event of storage.getEventsByTypes(['A'])) { + for await (const event of storage.getEventsByTypes(['A'])) results.push(event); - } + expect(results).to.deep.equal([event1, event3]); }); @@ -91,9 +91,9 @@ describe('InMemoryEventStorage', () => { const options = { afterEvent: { id: '1' } }; const results = []; - for await (const event of storage.getEventsByTypes(['A'], options)) { + for await (const event of storage.getEventsByTypes(['A'], options)) results.push(event); - } + expect(results).to.deep.equal([event2, event3]); }); @@ -107,7 +107,8 @@ describe('InMemoryEventStorage', () => { try { await gen.next(); throw new Error('Expected error was not thrown'); - } catch (err) { + } + catch (err) { expect(err).to.be.instanceOf(TypeError); expect(err.message).to.equal('options.afterEvent.id is required'); } diff --git a/tests/unit/memory/InMemoryLock.test.ts b/tests/unit/memory/InMemoryLock.test.ts index 63974e4..ce9cd91 100644 --- a/tests/unit/memory/InMemoryLock.test.ts +++ b/tests/unit/memory/InMemoryLock.test.ts @@ -33,7 +33,7 @@ describe('InMemoryLock', () => { }); // Ensure second lock() is still waiting - await new Promise((resolve) => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 100)); expect(secondLockAcquired).to.be.false; // Unlock and allow second lock to proceed @@ -69,7 +69,7 @@ describe('InMemoryLock', () => { }); // Ensure it's still waiting - await new Promise((resolve) => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 100)); expect(resolved).to.be.false; // Unlock and verify resolution diff --git a/tests/unit/memory/InMemoryMessageBus.test.ts b/tests/unit/memory/InMemoryMessageBus.test.ts index e80c385..f260a82 100644 --- a/tests/unit/memory/InMemoryMessageBus.test.ts +++ b/tests/unit/memory/InMemoryMessageBus.test.ts @@ -5,7 +5,9 @@ import { spy } from 'sinon'; describe('InMemoryMessageBus', function () { let bus: IMessageBus; - beforeEach(() => bus = new InMemoryMessageBus()); + beforeEach(() => { + bus = new InMemoryMessageBus(); + }); describe('send(command)', function () { diff --git a/tests/unit/memory/InMemoryView.test.ts b/tests/unit/memory/InMemoryView.test.ts index 3c0e7eb..a233997 100644 --- a/tests/unit/memory/InMemoryView.test.ts +++ b/tests/unit/memory/InMemoryView.test.ts @@ -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 index e894fdd..99bb6e2 100644 --- a/tests/unit/sqlite/SqliteEventLocker.test.ts +++ b/tests/unit/sqlite/SqliteEventLocker.test.ts @@ -45,7 +45,7 @@ describe('SqliteEventLocker', function () { locker.tryMarkAsProjecting(testEvent); locker.markAsProjected(testEvent); - const row = db.prepare(`SELECT processed_at FROM test_event_lock WHERE event_id = ?`) + const row = db.prepare('SELECT processed_at FROM test_event_lock WHERE event_id = ?') .get(guid(testEvent.id)) as any; expect(row).to.exist; @@ -53,7 +53,7 @@ describe('SqliteEventLocker', function () { }); it('retrieves the last projected event', function () { - + locker.tryMarkAsProjecting(testEvent); locker.markAsProjected(testEvent); @@ -83,7 +83,7 @@ describe('SqliteEventLocker', function () { }); it('fails to update an event if its version is modified in DB', function () { - + locker.tryMarkAsProjecting(testEvent); // Modify the event in DB to simulate an external change diff --git a/tests/unit/sqlite/SqliteObjectStorage.test.ts b/tests/unit/sqlite/SqliteObjectStorage.test.ts index 6da8010..56bc72f 100644 --- a/tests/unit/sqlite/SqliteObjectStorage.test.ts +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -10,7 +10,7 @@ describe('SqliteObjectStorage', function () { db = createDb(':memory:'); storage = new SqliteObjectStorage<{ name: string; value: number }>({ viewModelSqliteDb: db, - tableName: 'test_objects', + tableName: 'test_objects' }); }); @@ -36,7 +36,7 @@ describe('SqliteObjectStorage', function () { storage.create('0002', { name: 'Old Data', value: 5 }); - storage.update('0002', (r) => ({ ...r, value: 99 })); + storage.update('0002', r => ({ ...r, value: 99 })); const updated = storage.get('0002'); expect(updated).to.deep.equal({ name: 'Old Data', value: 99 }); @@ -44,7 +44,7 @@ describe('SqliteObjectStorage', function () { it('throws an error when updating a non-existent object', async function () { - expect(() => storage.update('nonexistent', (r) => ({ ...r, value: 99 }))) + expect(() => storage.update('nonexistent', r => ({ ...r, value: 99 }))) .to.throw(Error, "Record 'nonexistent' does not exist"); }); @@ -71,7 +71,7 @@ describe('SqliteObjectStorage', function () { let retrieved = storage.get('0004'); expect(retrieved).to.deep.equal({ name: 'Created', value: 1 }); - storage.updateEnforcingNew('0004', (r) => ({ ...r!, value: 100 })); + storage.updateEnforcingNew('0004', r => ({ ...r!, value: 100 })); retrieved = storage.get('0004'); expect(retrieved).to.deep.equal({ name: 'Created', value: 100 }); diff --git a/tests/unit/sqlite/SqliteObjectView.test.ts b/tests/unit/sqlite/SqliteObjectView.test.ts index 694b0fe..3aa5de4 100644 --- a/tests/unit/sqlite/SqliteObjectView.test.ts +++ b/tests/unit/sqlite/SqliteObjectView.test.ts @@ -15,7 +15,7 @@ describe('SqliteObjectView', function () { projectionName: 'test', tableNamePrefix: 'tbl_test', schemaVersion: '1' - }) + }); }); describe('get', () => { diff --git a/tests/unit/sqlite/SqliteViewLocker.test.ts b/tests/unit/sqlite/SqliteViewLocker.test.ts index 4ce335f..9c868ca 100644 --- a/tests/unit/sqlite/SqliteViewLocker.test.ts +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -81,14 +81,14 @@ describe('SqliteViewLocker', function () { 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 = ?`) + 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 = ?`) + 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); @@ -98,7 +98,7 @@ describe('SqliteViewLocker', function () { await firstLock.lock(); firstLock.unlock(); - const row = viewModelSqliteDb.prepare(`SELECT * FROM tbl_view_lock WHERE projection_name = ? AND schema_version = ?`) + 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; From 109c022d0cdfad934ac2b0edbf24503eb85d41df Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 7 Apr 2025 20:49:24 +0100 Subject: [PATCH 041/135] Change RabbitMqEventInjector to subscribe to fanout messages instead of a named queue --- src/rabbitmq/RabbitMqEventBus.ts | 3 +- src/rabbitmq/RabbitMqEventInjector.ts | 22 +++++--------- src/rabbitmq/constants.ts | 1 + .../rabbitmq/RabbitMqEventInjector.test.ts | 30 +------------------ 4 files changed, 12 insertions(+), 44 deletions(-) create mode 100644 src/rabbitmq/constants.ts diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts index 130c43f..a707779 100644 --- a/src/rabbitmq/RabbitMqEventBus.ts +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -1,4 +1,5 @@ import { IEvent, IEventBus, IMessageHandler, IObservable } from '../interfaces'; +import { DEFAULT_EXCHANGE } from './constants'; import { RabbitMqGateway } from './RabbitMqGateway'; const ALL_EVENTS_WILDCARD = '*'; @@ -20,7 +21,7 @@ export class RabbitMqEventBus implements IEventBus { queueName?: string }) { this.#gateway = o.rabbitMqGateway; - this.#exchange = o.exchange ?? 'node-cqrs.events'; + this.#exchange = o.exchange ?? DEFAULT_EXCHANGE; this.#queueName = o.queueName; } diff --git a/src/rabbitmq/RabbitMqEventInjector.ts b/src/rabbitmq/RabbitMqEventInjector.ts index f8e4850..9bae093 100644 --- a/src/rabbitmq/RabbitMqEventInjector.ts +++ b/src/rabbitmq/RabbitMqEventInjector.ts @@ -3,6 +3,7 @@ import { IMessage } from '../interfaces/IMessage'; import { RabbitMqGateway } from './RabbitMqGateway'; import { IEventDispatcher } from '../interfaces'; import * as Event from '../Event'; +import { DEFAULT_EXCHANGE } from './constants'; export class RabbitMqEventInjector { #rabbitMqGateway: RabbitMqGateway; @@ -10,7 +11,6 @@ export class RabbitMqEventInjector { #logger: IContainer['logger']; #exchangeName: string; - #queueName: string; #messageHandler: (message: IMessage) => Promise; constructor(container: Partial> & { @@ -29,32 +29,26 @@ export class RabbitMqEventInjector { container.logger.child({ service: new.target.name }) : container.logger; - this.#exchangeName = container.exchange ?? 'node-cqrs.events'; - this.#queueName = container.queueName ?? 'node-cqrs.persistence'; + this.#exchangeName = container.exchange ?? DEFAULT_EXCHANGE; this.#messageHandler = this.#handleMessage.bind(this); this.start(); } async start(): Promise { - this.#logger?.info(`Starting event injection from queue "${this.#queueName}"`); + this.#logger?.debug(`Subscribing to messages from exchange "${this.#exchangeName}"...`); - await this.#rabbitMqGateway.subscribeToQueue( - this.#exchangeName, - this.#queueName, - this.#messageHandler - ); + await this.#rabbitMqGateway.subscribeToFanout(this.#exchangeName, this.#messageHandler); - this.#logger?.info(`Subscribed to queue "${this.#queueName}" on exchange "${this.#exchangeName}"`); + this.#logger?.debug(`Listening to messages from exchange "${this.#exchangeName}"`); } async #handleMessage(message: IMessage): Promise { - this.#logger?.debug(`Received message from queue "${this.#queueName}": ${message.type}`); + this.#logger?.debug(`Received "${Event.describe(message)}" message from exchange "${this.#exchangeName}"`); try { - // EventDispatcher expects an array of events (IEventSet) - // Assuming IMessage is compatible with IEvent or needs transformation await this.#eventDispatcher.dispatch([message]); - this.#logger?.debug(`Event ${Event.describe(message)} dispatched successfully`); + + this.#logger?.debug(`${Event.describe(message)} dispatched successfully`); } catch (error: any) { this.#logger?.error(`Failed to dispatch event ${message.type}: ${error.message}`, { stack: error.stack }); diff --git a/src/rabbitmq/constants.ts b/src/rabbitmq/constants.ts new file mode 100644 index 0000000..d303442 --- /dev/null +++ b/src/rabbitmq/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_EXCHANGE = 'node-cqrs.events'; diff --git a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts index f111718..28da91b 100644 --- a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts +++ b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts @@ -10,8 +10,6 @@ describe('RabbitMqEventInjector', () => { let eventDispatcher: jest.Mocked; const exchange = 'node-cqrs.events'; - const queueName = 'test-injector-queue'; - const deadLetterQueueName = `${queueName}.failed`; const eventType = 'test-injector-event'; beforeEach(async () => { @@ -26,7 +24,6 @@ describe('RabbitMqEventInjector', () => { const injector = new RabbitMqEventInjector({ rabbitMqGateway, eventDispatcher, - queueName, exchange }); @@ -37,8 +34,6 @@ describe('RabbitMqEventInjector', () => { try { const ch = await rabbitMqGateway.connection?.createChannel(); if (ch) { - await ch.deleteQueue(queueName); - await ch.deleteQueue(`${queueName}.failed`); await ch.deleteExchange(exchange); await ch.close(); } @@ -51,7 +46,7 @@ describe('RabbitMqEventInjector', () => { } }); - it('receives a message from the queue and dispatch it via EventDispatcher', async () => { + it('receives messages and dispatches them via EventDispatcher', async () => { const testEvent: IEvent = { type: eventType, payload: { data: 'test-payload' }, @@ -65,27 +60,4 @@ describe('RabbitMqEventInjector', () => { expect(eventDispatcher.dispatch).toHaveBeenCalledTimes(1); expect(eventDispatcher.dispatch).toHaveBeenCalledWith([testEvent]); }); - - it('handles errors during event dispatch and nack the message', async () => { - const testEvent: IEvent = { - type: 'error-event', - payload: { data: 'trigger-error' }, - id: 'error-id-456' - }; - const dispatchError = new Error('Dispatch failed'); - eventDispatcher.dispatch.mockRejectedValueOnce(dispatchError); - - // Publish the event - await rabbitMqGateway.publish(exchange, testEvent); - - await delay(100); - - const ch = await rabbitMqGateway.connection!.createChannel(); - const deadLetterMessage = await ch.get(deadLetterQueueName, { noAck: true }); - if (!deadLetterMessage) - throw new Error('Dead letter message not found'); - - const messageContent = JSON.parse(deadLetterMessage.content.toString()); - expect(messageContent).toEqual(testEvent); - }); }); From ea5682a18afb6e08bc835c6f3e0f89453f1b02ef Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 8 Apr 2025 16:34:47 +0100 Subject: [PATCH 042/135] Fix eslint --- eslint.config.mjs | 3 ++- src/interfaces/IContainer.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index b000df0..fe2f193 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -16,7 +16,8 @@ export default defineConfig([ languageOptions: { parser: tsParser, globals: { - ...globals.node + ...globals.node, + NodeJS: true } }, plugins: { diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index 7110fa6..770b8af 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -23,6 +23,5 @@ export interface IContainer extends Container { logger?: ILogger | IExtendableLogger; - // eslint-disable-next-line no-undef process?: NodeJS.Process } From 948bb977153566442d0acf433706c91793d781c5 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 8 Apr 2025 16:39:50 +0100 Subject: [PATCH 043/135] Enhance RabbitMqGateway with detailed subscription management and error handling --- src/rabbitmq/RabbitMqGateway.ts | 69 +++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index fad8d2f..f231883 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -13,13 +13,29 @@ 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 = { - exchange: string, - queueName?: string, - eventType?: string, - handler: MessageHandler, - ignoreOwn?: boolean, - concurrentLimit?: number + + /** 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; }; const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); @@ -176,7 +192,26 @@ export class RabbitMqGateway { return this.subscribe({ exchange, handler, ignoreOwn: true }); } + /** + * 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) { + const subscriptionExists = !!this.#findSubscription(subscription); + if (subscriptionExists) + throw new Error('Subscription already exists'); + const { exchange, queueName, @@ -218,12 +253,22 @@ export class RabbitMqGateway { this.#subscriptions.push({ ...subscription, queueGivenName }); } - async unsubscribe(d: Subscription) { - this.#subscriptions = this.#subscriptions.filter(s => !( - s.exchange === d.exchange && - s.queueName === d.queueName && - s.eventType === d.eventType && - s.handler === d.handler)); + #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.queueGivenName); } async #assertConnection() { From e523cf276b5202b78104e1a816350fc701b7bf3f Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 8 Apr 2025 16:41:41 +0100 Subject: [PATCH 044/135] Implement graceful shutdown handling in RabbitMqGateway --- src/rabbitmq/RabbitMqGateway.ts | 89 +++++++++---- src/rabbitmq/TerminationHandler.ts | 29 +++++ src/rabbitmq/constants.ts | 1 + .../rabbitmq/RabbitMqGateway.test.ts | 121 +++++++++++++++++- 4 files changed, 213 insertions(+), 27 deletions(-) create mode 100644 src/rabbitmq/TerminationHandler.ts diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index f231883..48121b6 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -1,12 +1,9 @@ import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; -import { - IContainer, - ILogger, - IMessage, - isMessage -} from '../interfaces'; +import { IContainer, ILogger, IMessage, isMessage } from '../interfaces'; import * as Event from '../Event'; import { delay } from '../utils'; +import { HANDLER_PROCESS_TIMEOUT } from './constants'; +import { TerminationHandler } from './TerminationHandler'; /** Generate a short pseudo-unique identifier using a truncated timestamp and random component */ const getRandomAppId = () => @@ -64,14 +61,13 @@ export class RabbitMqGateway { #subscriptions: Array = []; #handlers: Map>> = new Map(); + /** Handles termination signals for graceful shutdown */ + #terminationHandler: TerminationHandler | undefined; + get connection() { return this.#connection; } - get channel() { - return this.#pubChannel; - } - constructor(o: Partial> & { rabbitMqConnectionFactory?: () => Promise }) { @@ -85,9 +81,19 @@ export class RabbitMqGateway { o.logger; if (o.process) - o.process.on('SIGINT', () => this.#stopConsuming()); + this.#terminationHandler = new TerminationHandler(o.process, () => this.#stopConsuming()); } + /** + * 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 { while (this.#connecting) await delay(1_000); @@ -342,19 +348,31 @@ export class RabbitMqGateway { if (!msg) return; - const { consumerTag, routingKey } = msg.fields; - const { appId, messageId, correlationId } = msg.properties; + const { consumerTag, routingKey } = msg.fields ?? {}; + const { messageId, correlationId, appId } = msg.properties ?? {}; - this.#logger?.debug(`${this.#appId}: Message received`, { - queueName: queueGivenName, - consumerTag, - routingKey, - messageId, - correlationId, - appId - }); + // Keep the process alive while waiting for the handler to finish + const keepAliveTimeout = setTimeout(() => { + this.#logger?.warn(`${this.#appId}: Message processing timed out`, { + queueName: queueGivenName, + consumerTag, + routingKey, + messageId + }); + channel.nack(msg, false, false); + }, HANDLER_PROCESS_TIMEOUT); try { + + this.#logger?.debug(`${this.#appId}: Message received`, { + queueName: queueGivenName, + consumerTag, + routingKey, + messageId, + correlationId, + appId + }); + const jsonContent = msg.content.toString(); const message: IMessage = JSON.parse(jsonContent); @@ -377,6 +395,9 @@ export class RabbitMqGateway { // Redirect message to dead letter queue, if `{ noAck: true }` was not set on consumption channel?.nack(msg, false, false); } + finally { + clearTimeout(keepAliveTimeout); + } }); this.#logger?.debug(`${this.#appId}: Consumer "${c.consumerTag}" registered on queue "${queueGivenName}"`); @@ -385,6 +406,25 @@ export class RabbitMqGateway { channel, consumerTag: c.consumerTag }); + + this.#terminationHandler?.on(); + } + + async #tryDropConsumer(queueGivenName: string) { + const queueStillUsed = this.#subscriptions.some(s => s.queueGivenName === queueGivenName); + if (queueStillUsed) + return; + + const consumer = this.#queueConsumers.get(queueGivenName); + if (!consumer) + return; + + this.#queueConsumers.delete(queueGivenName); + await consumer.channel.cancel(consumer.consumerTag); + + // If no consumers are active anymore, disable the termination handler + if (!this.#queueConsumers.size) + this.#terminationHandler?.off(); } /** @@ -419,7 +459,12 @@ export class RabbitMqGateway { }; return new Promise((resolve, reject) => { - const published = this.#pubChannel!.publish(exchange, message.type, content, properties, err => + if (!this.#pubChannel) + throw new Error(`${this.#appId}: No channel available for publishing`); + + this.#logger?.debug(`${this.#appId}: 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(`${this.#appId}: Failed to send event ${Event.describe(message)}, channel buffer is full`); diff --git a/src/rabbitmq/TerminationHandler.ts b/src/rabbitmq/TerminationHandler.ts new file mode 100644 index 0000000..5ba3e64 --- /dev/null +++ b/src/rabbitmq/TerminationHandler.ts @@ -0,0 +1,29 @@ +/** + * Handles graceful termination of a Node.js process. + * Listens for SIGINT and executes a cleanup routine before allowing the process to exit. + */ +export class TerminationHandler { + + #process: NodeJS.Process; + #cleanupHandler: () => Promise; + #terminationHandler: () => Promise; + + constructor(process: NodeJS.Process, cleanupHandler: () => Promise) { + this.#process = process; + this.#cleanupHandler = cleanupHandler; + this.#terminationHandler = this.#onProcessTermination.bind(this); + } + + on() { + this.#process.on('SIGINT', this.#terminationHandler); + } + + off() { + this.#process.off('SIGINT', this.#terminationHandler); + } + + async #onProcessTermination() { + await this.#cleanupHandler(); + this.off(); + } +} diff --git a/src/rabbitmq/constants.ts b/src/rabbitmq/constants.ts index d303442..2f3a455 100644 --- a/src/rabbitmq/constants.ts +++ b/src/rabbitmq/constants.ts @@ -1 +1,2 @@ export const DEFAULT_EXCHANGE = 'node-cqrs.events'; +export const HANDLER_PROCESS_TIMEOUT = 60 * 60 * 1000; // 1 hour diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index 12ec6fa..51c5dc5 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -14,13 +14,16 @@ describe('RabbitMqGateway', () => { const queueName = 'test-queue'; const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); + let process: EventEmitter; + beforeEach(async () => { + // const logger = console; const logger = undefined; - // const logger = console; - gateway1 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); - gateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); - gateway3 = new RabbitMqGateway({ rabbitMqConnectionFactory, logger }); + 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 () => { @@ -115,7 +118,6 @@ describe('RabbitMqGateway', () => { expect(received1).toHaveLength(1); expect(received2).toHaveLength(1); }); - }); describe('subscribeToQueue', () => { @@ -271,6 +273,115 @@ describe('RabbitMqGateway', () => { }); }); + 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 () => { + + const processOnSpy = jest.spyOn(process, 'on'); + const processOffSpy = jest.spyOn(process, 'off'); + + 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 handler2' }, + context: { ts: Date.now() } + }; + + // Subscribe both handlers to the same durable queue + await gateway1.subscribe({ + exchange, + eventType: event1.type, + handler: handler1 + }); + await gateway1.subscribe({ + exchange, + eventType: event1.type, + handler: handler2 + }); + + expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + expect(processOnSpy).toHaveBeenCalledTimes(1); + expect(processOffSpy).not.toHaveBeenCalled(); + + // Unsubscribe one handler; the queue should still be active + await gateway1.unsubscribe({ + exchange, + eventType: event1.type, + handler: handler1 + }); + + expect(processOffSpy).not.toHaveBeenCalled(); + + // Publish an event; only handler2 should receive it + + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received1).toEqual([]); + expect(received2).toEqual([event1]); + + // Now unsubscribe the last handler; this should cancel the consumer for the queue + await gateway1.unsubscribe({ + exchange, + eventType: event1.type, + handler: handler2 + }); + + expect(processOffSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + + // Publish a second event; no handler should receive it because the consumer is cancelled + received2.length = 0; + + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received1).toEqual([]); + }); + }); + describe('connect()', () => { it('retains subscriptions after reconnect', async () => { From dea0f4b889bb7931f3147ac2e10072dd9ebdc853 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 8 Apr 2025 17:08:23 +0100 Subject: [PATCH 045/135] Refactor RabbitMqEventInjector to improve message handling and logging --- src/rabbitmq/RabbitMqEventInjector.ts | 39 +++++++++------- .../rabbitmq/RabbitMqEventInjector.test.ts | 45 ++++++++++--------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/rabbitmq/RabbitMqEventInjector.ts b/src/rabbitmq/RabbitMqEventInjector.ts index 9bae093..ddeb713 100644 --- a/src/rabbitmq/RabbitMqEventInjector.ts +++ b/src/rabbitmq/RabbitMqEventInjector.ts @@ -5,46 +5,53 @@ import { IEventDispatcher } from '../interfaces'; import * as Event from '../Event'; import { DEFAULT_EXCHANGE } from './constants'; +/** + * Injects events received from a RabbitMQ exchange into the local event dispatcher. + * + * It subscribes to a specified fanout exchange on RabbitMQ and dispatches + * any received messages as events using the provided event dispatcher. + */ export class RabbitMqEventInjector { #rabbitMqGateway: RabbitMqGateway; + #messageHandler: (message: IMessage) => Promise; #eventDispatcher: IEventDispatcher; #logger: IContainer['logger']; - #exchangeName: string; - #messageHandler: (message: IMessage) => Promise; - - constructor(container: Partial> & { - exchange?: string; - queueName?: string; - }) { + constructor(container: Partial>) { if (!container.eventDispatcher) throw new Error('eventDispatcher is required in the container.'); if (!container.rabbitMqGateway) throw new Error('rabbitMqGateway is required in the container.'); this.#rabbitMqGateway = container.rabbitMqGateway; + this.#messageHandler = (msg: IMessage) => this.#handleMessage(msg); this.#eventDispatcher = container.eventDispatcher; - this.#logger = container.logger && 'child' in container.logger ? container.logger.child({ service: new.target.name }) : container.logger; + } + + async start(exchange: string = DEFAULT_EXCHANGE): Promise { + this.#logger?.debug(`Subscribing to messages from exchange "${exchange}"...`); - this.#exchangeName = container.exchange ?? DEFAULT_EXCHANGE; - this.#messageHandler = this.#handleMessage.bind(this); + await this.#rabbitMqGateway.subscribeToFanout(exchange, this.#messageHandler); - this.start(); + this.#logger?.debug(`Listening to messages from exchange "${exchange}"`); } - async start(): Promise { - this.#logger?.debug(`Subscribing to messages from exchange "${this.#exchangeName}"...`); + async stop(exchange: string = DEFAULT_EXCHANGE): Promise { + this.#logger?.debug(`Unsubscribing from messages from exchange "${exchange}"...`); - await this.#rabbitMqGateway.subscribeToFanout(this.#exchangeName, this.#messageHandler); + await this.#rabbitMqGateway.unsubscribe({ + exchange, + handler: this.#messageHandler + }); - this.#logger?.debug(`Listening to messages from exchange "${this.#exchangeName}"`); + this.#logger?.debug(`Stopped listening to messages from exchange "${exchange}"`); } async #handleMessage(message: IMessage): Promise { - this.#logger?.debug(`Received "${Event.describe(message)}" message from exchange "${this.#exchangeName}"`); + this.#logger?.debug(`"${Event.describe(message)}" received`); try { await this.#eventDispatcher.dispatch([message]); diff --git a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts index 28da91b..1946dad 100644 --- a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts +++ b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts @@ -7,6 +7,7 @@ import { delay } from '../../../src/utils'; describe('RabbitMqEventInjector', () => { let rabbitMqGateway: RabbitMqGateway; + let rabbitMqGateway2: RabbitMqGateway; let eventDispatcher: jest.Mocked; const exchange = 'node-cqrs.events'; @@ -15,38 +16,26 @@ describe('RabbitMqEventInjector', () => { beforeEach(async () => { const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); rabbitMqGateway = new RabbitMqGateway({ rabbitMqConnectionFactory }); + rabbitMqGateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory }); eventDispatcher = { dispatch: jest.fn().mockResolvedValue(undefined) } as unknown as jest.Mocked; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const injector = new RabbitMqEventInjector({ - rabbitMqGateway, - eventDispatcher, - exchange - }); + const injector = new RabbitMqEventInjector({ rabbitMqGateway, eventDispatcher }); - await delay(50); // Allow time for subscription setup + await injector.start(exchange); }); afterEach(async () => { - try { - const ch = await rabbitMqGateway.connection?.createChannel(); - if (ch) { - await ch.deleteExchange(exchange); - await ch.close(); - } - } - catch (error) { - console.warn('Error during RabbitMQ cleanup:', error); - } - finally { - await rabbitMqGateway.disconnect(); - } + const ch = await rabbitMqGateway.connection?.createChannel(); + await ch.deleteExchange(exchange); + await ch.close(); + await rabbitMqGateway.disconnect(); + await rabbitMqGateway2.disconnect(); }); - it('receives messages and dispatches them via EventDispatcher', async () => { + it('does not receive messages published to own gateway', async () => { const testEvent: IEvent = { type: eventType, payload: { data: 'test-payload' }, @@ -57,6 +46,20 @@ describe('RabbitMqEventInjector', () => { await delay(50); + expect(eventDispatcher.dispatch).not.toHaveBeenCalled(); + }); + + it('receives messages published to other gateway, dispatches to eventDispatcher', async () => { + const testEvent: IEvent = { + type: eventType, + payload: { data: 'test-payload' }, + id: 'test-id-123' + }; + + await rabbitMqGateway2.publish(exchange, testEvent); + + await delay(50); + expect(eventDispatcher.dispatch).toHaveBeenCalledTimes(1); expect(eventDispatcher.dispatch).toHaveBeenCalledWith([testEvent]); }); From cae8e8140fe31cd24df6c650e7e377767f945a43 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 8 Apr 2025 17:47:38 +0100 Subject: [PATCH 046/135] Refactor TerminationHandler to use 'once' for signal handling and ensure cleanup occurs after deregistration --- src/rabbitmq/TerminationHandler.ts | 6 ++++-- tests/integration/rabbitmq/RabbitMqGateway.test.ts | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/rabbitmq/TerminationHandler.ts b/src/rabbitmq/TerminationHandler.ts index 5ba3e64..f5e63f3 100644 --- a/src/rabbitmq/TerminationHandler.ts +++ b/src/rabbitmq/TerminationHandler.ts @@ -15,15 +15,17 @@ export class TerminationHandler { } on() { - this.#process.on('SIGINT', this.#terminationHandler); + this.#process.once('SIGINT', this.#terminationHandler); + this.#process.once('SIGTERM', this.#terminationHandler); } off() { this.#process.off('SIGINT', this.#terminationHandler); + this.#process.off('SIGTERM', this.#terminationHandler); } async #onProcessTermination() { - await this.#cleanupHandler(); this.off(); + await this.#cleanupHandler(); } } diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index 51c5dc5..c1f15bb 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -311,7 +311,7 @@ describe('RabbitMqGateway', () => { it('cancels consumer when unsubscribing the last subscription on a queue', async () => { - const processOnSpy = jest.spyOn(process, 'on'); + const processOnSpy = jest.spyOn(process, 'once'); const processOffSpy = jest.spyOn(process, 'off'); const received1: IMessage[] = []; @@ -343,7 +343,8 @@ describe('RabbitMqGateway', () => { }); expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); - expect(processOnSpy).toHaveBeenCalledTimes(1); + expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); + expect(processOnSpy).toHaveBeenCalledTimes(2); expect(processOffSpy).not.toHaveBeenCalled(); // Unsubscribe one handler; the queue should still be active From 34659dfce032e6962181e6ba6cf8a76345e92094 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 10 Apr 2025 20:22:56 +0100 Subject: [PATCH 047/135] Make SQLite locker and storage classes async to support async DB initialization --- src/in-memory/InMemoryLock.ts | 2 +- src/in-memory/index.ts | 1 - src/in-memory/utils/index.ts | 1 - src/sqlite/AbstractSqliteAccessor.ts | 55 ++++++ src/sqlite/AbstractSqliteObjectProjection.ts | 10 +- src/sqlite/AbstractSqliteView.ts | 10 +- src/sqlite/IContainer.ts | 8 + src/sqlite/SqliteEventLocker.ts | 54 ++--- src/sqlite/SqliteObjectStorage.ts | 87 +++++--- src/sqlite/SqliteObjectView.ts | 41 +--- ...arams.ts => SqliteProjectionDataParams.ts} | 8 - src/sqlite/SqliteViewLocker.ts | 18 +- src/sqlite/index.ts | 2 + src/{in-memory => }/utils/Deferred.ts | 0 src/utils/Lock.ts | 53 +++++ src/utils/index.ts | 2 + tests/unit/Lock.test.ts | 185 ++++++++++++++++++ tests/unit/SagaEventHandler.test.ts | 2 +- tests/unit/sqlite/SqliteEventLocker.test.ts | 75 ++++--- tests/unit/sqlite/SqliteObjectStorage.test.ts | 48 ++--- 20 files changed, 483 insertions(+), 179 deletions(-) create mode 100644 src/sqlite/AbstractSqliteAccessor.ts create mode 100644 src/sqlite/IContainer.ts rename src/sqlite/{commonParams.ts => SqliteProjectionDataParams.ts} (75%) rename src/{in-memory => }/utils/Deferred.ts (100%) create mode 100644 src/utils/Lock.ts create mode 100644 tests/unit/Lock.test.ts diff --git a/src/in-memory/InMemoryLock.ts b/src/in-memory/InMemoryLock.ts index 84456bd..8b853d1 100644 --- a/src/in-memory/InMemoryLock.ts +++ b/src/in-memory/InMemoryLock.ts @@ -1,4 +1,4 @@ -import { Deferred } from './utils'; +import { Deferred } from '../utils'; export class InMemoryLock { diff --git a/src/in-memory/index.ts b/src/in-memory/index.ts index 3f6f779..3ef457a 100644 --- a/src/in-memory/index.ts +++ b/src/in-memory/index.ts @@ -3,4 +3,3 @@ export * from './InMemoryLock'; export * from './InMemoryMessageBus'; export * from './InMemorySnapshotStorage'; export * from './InMemoryView'; -export * from './utils/Deferred'; diff --git a/src/in-memory/utils/index.ts b/src/in-memory/utils/index.ts index 3622022..d504dca 100644 --- a/src/in-memory/utils/index.ts +++ b/src/in-memory/utils/index.ts @@ -1,2 +1 @@ -export * from './Deferred'; export * from './nextCycle'; diff --git a/src/sqlite/AbstractSqliteAccessor.ts b/src/sqlite/AbstractSqliteAccessor.ts new file mode 100644 index 0000000..23c009c --- /dev/null +++ b/src/sqlite/AbstractSqliteAccessor.ts @@ -0,0 +1,55 @@ +import { IContainer } from '../interfaces'; +import { Lock } from '../utils'; +import { Database } from 'better-sqlite3'; + +/** + * Abstract base class for accessing a SQLite database. + * + * Manages the database connection lifecycle, ensuring initialization via `assertDb`. + * Supports providing a database instance directly or a factory function for lazy initialization. + * + * Subclasses must implement the `initialize` method for specific setup tasks. + */ +export abstract class AbstractSqliteAccessor { + + protected db: Database | undefined; + #dbFactory: (() => Promise | Database) | undefined; + #initLocker = new Lock(); + #initialized = false; + + constructor(c: Pick) { + 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() { + try { + 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 index 3cb996c..acc3c2b 100644 --- a/src/sqlite/AbstractSqliteObjectProjection.ts +++ b/src/sqlite/AbstractSqliteObjectProjection.ts @@ -1,6 +1,5 @@ import { AbstractProjection } from '../AbstractProjection'; -import { IExtendableLogger } from '../interfaces'; -import { SqliteDbParams } from './commonParams'; +import { IContainer } from '../interfaces'; import { SqliteObjectView } from './SqliteObjectView'; export abstract class AbstractSqliteObjectProjection extends AbstractProjection> { @@ -13,13 +12,18 @@ export abstract class AbstractSqliteObjectProjection extends AbstractProjecti throw new Error('schemaVersion is not defined'); } - constructor({ viewModelSqliteDb, logger }: SqliteDbParams & { logger?: IExtendableLogger }) { + 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 index 697ec15..63b26dd 100644 --- a/src/sqlite/AbstractSqliteView.ts +++ b/src/sqlite/AbstractSqliteView.ts @@ -1,11 +1,9 @@ -import { IEvent, IEventLocker, ILogger, IViewLocker } from '../interfaces'; -import { Database } from 'better-sqlite3'; +import { IContainer, IEvent, IEventLocker, ILogger, IViewLocker } from '../interfaces'; import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker'; import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker'; export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { - protected readonly db: Database; protected readonly schemaVersion: string; protected readonly viewLocker: SqliteViewLocker; protected readonly eventLocker: SqliteEventLocker; @@ -15,8 +13,10 @@ export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { return this.viewLocker.ready; } - constructor(options: SqliteEventLockerParams & SqliteViewLockerParams) { - this.db = options.viewModelSqliteDb; + constructor(options: Pick + & SqliteEventLockerParams + & SqliteViewLockerParams) { + this.schemaVersion = options.schemaVersion; this.viewLocker = new SqliteViewLocker(options); this.eventLocker = new SqliteEventLocker(options); diff --git a/src/sqlite/IContainer.ts b/src/sqlite/IContainer.ts new file mode 100644 index 0000000..f24f6d6 --- /dev/null +++ b/src/sqlite/IContainer.ts @@ -0,0 +1,8 @@ +import { Database } from 'better-sqlite3'; + +declare module '../interfaces/IContainer' { + interface IContainer { + viewModelSqliteDbFactory?: () => Promise | Database; + viewModelSqliteDb?: Database; + } +} diff --git a/src/sqlite/SqliteEventLocker.ts b/src/sqlite/SqliteEventLocker.ts index 3d26171..4322cec 100644 --- a/src/sqlite/SqliteEventLocker.ts +++ b/src/sqlite/SqliteEventLocker.ts @@ -1,11 +1,12 @@ import { Database, Statement } from 'better-sqlite3'; -import { IEvent, IEventLocker } from '../interfaces'; +import { IContainer, IEvent, IEventLocker } from '../interfaces'; import { getEventId } from './utils'; import { viewLockTableInit, eventLockTableInit } from './queries'; import { SqliteViewLockerParams } from './SqliteViewLocker'; -import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; +import { SqliteProjectionDataParams } from './SqliteProjectionDataParams'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; -export type SqliteEventLockerParams = SqliteDbParams & SqliteProjectionDataParams & { +export type SqliteEventLockerParams = SqliteProjectionDataParams & { /** * (Optional) SQLite table name where event locks are stored @@ -24,40 +25,39 @@ export type SqliteEventLockerParams = SqliteDbParams & SqliteProjectionDataParam } & Pick; -export class SqliteEventLocker implements IEventLocker { +export class SqliteEventLocker extends AbstractSqliteAccessor implements IEventLocker { - #db: Database; #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>; + #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); - constructor(o: SqliteEventLockerParams) { - if (!o.viewModelSqliteDb) - throw new TypeError('viewModelSqliteDb argument required'); if (!o.projectionName) throw new TypeError('projectionName argument required'); if (!o.schemaVersion) throw new TypeError('schemaVersion argument required'); - this.#db = o.viewModelSqliteDb; 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.#db.exec(viewLockTableInit(this.#viewLockTableName)); - this.#db.exec(eventLockTableInit(this.#eventLockTableName)); - - this.#upsertLastEventQuery = this.#db.prepare(` + this.#upsertLastEventQuery = db.prepare(` INSERT INTO ${this.#viewLockTableName} (projection_name, schema_version, last_event) VALUES (?, ?, ?) ON CONFLICT (projection_name, schema_version) @@ -65,7 +65,7 @@ export class SqliteEventLocker implements IEventLocker { last_event = excluded.last_event `); - this.#getLastEventQuery = this.#db.prepare(` + this.#getLastEventQuery = db.prepare(` SELECT last_event FROM ${this.#viewLockTableName} @@ -74,7 +74,7 @@ export class SqliteEventLocker implements IEventLocker { AND schema_version =? `); - this.#lockEventQuery = this.#db.prepare(` + this.#lockEventQuery = db.prepare(` INSERT INTO ${this.#eventLockTableName} (projection_name, schema_version, event_id) VALUES (?, ?, ?) ON CONFLICT (projection_name, schema_version, event_id) @@ -85,7 +85,7 @@ export class SqliteEventLocker implements IEventLocker { AND processing_at <= cast(strftime('%f', 'now') * 1000 as INTEGER) - ${this.#eventLockTtl} `); - this.#finalizeEventLockQuery = this.#db.prepare(` + this.#finalizeEventLockQuery = db.prepare(` UPDATE ${this.#eventLockTableName} SET processed_at = (cast(strftime('%f', 'now') * 1000 as INTEGER)) @@ -97,7 +97,9 @@ export class SqliteEventLocker implements IEventLocker { `); } - tryMarkAsProjecting(event: IEvent) { + async tryMarkAsProjecting(event: IEvent) { + await this.assertConnection(); + const eventId = getEventId(event); const r = this.#lockEventQuery.run(this.#projectionName, this.#schemaVersion, eventId); @@ -105,10 +107,12 @@ export class SqliteEventLocker implements IEventLocker { return r.changes !== 0; } - markAsProjected(event: IEvent) { + async markAsProjected(event: IEvent) { + await this.assertConnection(); + const eventId = getEventId(event); - const transaction = this.#db.transaction(() => { + 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`); @@ -119,7 +123,9 @@ export class SqliteEventLocker implements IEventLocker { transaction(); } - getLastEvent(): IEvent | undefined { + async getLastEvent(): Promise | undefined> { + await this.assertConnection(); + const viewInfoRecord = this.#getLastEventQuery.get(this.#projectionName, this.#schemaVersion); if (!viewInfoRecord?.last_event) return undefined; diff --git a/src/sqlite/SqliteObjectStorage.ts b/src/sqlite/SqliteObjectStorage.ts index b780f2c..9babd4f 100644 --- a/src/sqlite/SqliteObjectStorage.ts +++ b/src/sqlite/SqliteObjectStorage.ts @@ -1,47 +1,43 @@ import { Statement, Database } from 'better-sqlite3'; import { guid } from './utils'; -import { IObjectStorage } from '../interfaces'; +import { IContainer, IObjectStorage } from '../interfaces'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; -export class SqliteObjectStorage implements IObjectStorage { +export class SqliteObjectStorage extends AbstractSqliteAccessor implements IObjectStorage { - #db: Database; #tableName: string; - #getQuery: Statement<[Buffer], { data: string, version: number }>; - #insertQuery: Statement<[Buffer, string], void>; - #updateByIdAndVersionQuery: Statement<[string, Buffer, number], void>; - #deleteQuery: Statement<[Buffer], void>; + #getQuery!: Statement<[Buffer], { data: string, version: number }>; + #insertQuery!: Statement<[Buffer, string], void>; + #updateByIdAndVersionQuery!: Statement<[string, Buffer, number], void>; + #deleteQuery!: Statement<[Buffer], void>; - constructor(o: { - viewModelSqliteDb: Database, + constructor(o: Pick & { tableName: string }) { - if (!o.viewModelSqliteDb) - throw new TypeError('viewModelSqliteDb argument required'); - if (!o.tableName) - throw new TypeError('tableName argument required'); + super(o); - this.#db = o.viewModelSqliteDb; this.#tableName = o.tableName; + } - - this.#db.exec(`CREATE TABLE IF NOT EXISTS ${this.#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 = this.#db.prepare(` + this.#getQuery = db.prepare(` SELECT data, version FROM ${this.#tableName} WHERE id = ? `); - this.#insertQuery = this.#db.prepare(` + this.#insertQuery = db.prepare(` INSERT INTO ${this.#tableName} (id, data) VALUES (?, ?) `); - this.#updateByIdAndVersionQuery = this.#db.prepare(` + this.#updateByIdAndVersionQuery = db.prepare(` UPDATE ${this.#tableName} SET data = ?, @@ -51,13 +47,18 @@ export class SqliteObjectStorage implements IObjectStorage { AND version = ? `); - this.#deleteQuery = this.#db.prepare(` + this.#deleteQuery = db.prepare(` DELETE FROM ${this.#tableName} WHERE id = ? `); } - get(id: string): TRecord | undefined { + 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; @@ -65,14 +66,36 @@ export class SqliteObjectStorage implements IObjectStorage { return JSON.parse(r.data); } - create(id: string, data: TRecord) { + 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(); + const r = this.#insertQuery.run(guid(id), JSON.stringify(data)); if (r.changes !== 1) throw new Error(`Record '${id}' could not be created`); - } - update(id: string, update: (r: TRecord) => TRecord) { + 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(); + const gid = guid(id); const record = this.#getQuery.get(gid); if (!record) @@ -90,7 +113,14 @@ export class SqliteObjectStorage implements IObjectStorage { throw new Error(`Record '${id}' could not be updated`); } - updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + 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(); + // Due to better-sqlite3 sync nature, // it's safe to get then modify within this process const record = this.#getQuery.get(guid(id)); @@ -100,7 +130,12 @@ export class SqliteObjectStorage implements IObjectStorage { this.create(id, update()); } - delete(id: string): boolean { + 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 index 31165f4..fee1add 100644 --- a/src/sqlite/SqliteObjectView.ts +++ b/src/sqlite/SqliteObjectView.ts @@ -16,16 +16,14 @@ export class SqliteObjectView extends AbstractSqliteView implements IOb super(options); - this.#sqliteObjectStorage = new SqliteObjectStorage({ + this.#sqliteObjectStorage = new SqliteObjectStorage({ viewModelSqliteDb: options.viewModelSqliteDb, + viewModelSqliteDbFactory: options.viewModelSqliteDbFactory, tableName: `${options.tableNamePrefix}_${options.schemaVersion}` }); } async get(id: string): Promise { - if (typeof id !== 'string' || !id.length) - throw new TypeError('id argument must be a non-empty String'); - if (!this.ready) await this.once('ready'); @@ -33,41 +31,22 @@ export class SqliteObjectView extends AbstractSqliteView implements IOb } getSync(id: string) { - if (typeof id !== 'string' || !id.length) - throw new TypeError('id argument must be a non-empty String'); - - return this.#sqliteObjectStorage.get(id); + return this.#sqliteObjectStorage.getSync(id); } - create(id: string, data: TRecord) { - if (typeof id !== 'string' || !id.length) - throw new TypeError('id argument must be a non-empty String'); - - this.#sqliteObjectStorage.create(id, data); + async create(id: string, data: TRecord) { + await this.#sqliteObjectStorage.create(id, data); } - 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'); - - this.#sqliteObjectStorage.update(id, update); + async update(id: string, update: (r: TRecord) => TRecord) { + await this.#sqliteObjectStorage.update(id, update); } - 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'); - - this.#sqliteObjectStorage.updateEnforcingNew(id, update); + async updateEnforcingNew(id: string, update: (r?: TRecord) => TRecord) { + await this.#sqliteObjectStorage.updateEnforcingNew(id, update); } - delete(id: string): boolean { - if (typeof id !== 'string' || !id.length) - throw new TypeError('id argument must be a non-empty String'); - + async delete(id: string): Promise { return this.#sqliteObjectStorage.delete(id); } } diff --git a/src/sqlite/commonParams.ts b/src/sqlite/SqliteProjectionDataParams.ts similarity index 75% rename from src/sqlite/commonParams.ts rename to src/sqlite/SqliteProjectionDataParams.ts index f26118e..24c721e 100644 --- a/src/sqlite/commonParams.ts +++ b/src/sqlite/SqliteProjectionDataParams.ts @@ -1,11 +1,3 @@ -import { Database } from 'better-sqlite3'; - -export type SqliteDbParams = { - - /** Configured instance of better-sqlite3.Database */ - viewModelSqliteDb: Database; -}; - export type SqliteProjectionDataParams = { /** diff --git a/src/sqlite/SqliteViewLocker.ts b/src/sqlite/SqliteViewLocker.ts index 9907aee..12efe5d 100644 --- a/src/sqlite/SqliteViewLocker.ts +++ b/src/sqlite/SqliteViewLocker.ts @@ -1,12 +1,12 @@ import { Database, Statement } from 'better-sqlite3'; -import { IExtendableLogger, ILogger, IViewLocker } from '../interfaces'; -import { Deferred } from '../in-memory'; +import { IContainer, ILogger, IViewLocker } from '../interfaces'; +import { Deferred } from '../utils'; import { promisify } from 'util'; import { viewLockTableInit } from './queries'; -import { SqliteDbParams, SqliteProjectionDataParams } from './commonParams'; +import { SqliteProjectionDataParams } from './SqliteProjectionDataParams'; const delay = promisify(setTimeout); -export type SqliteViewLockerParams = SqliteDbParams & SqliteProjectionDataParams & { +export type SqliteViewLockerParams = SqliteProjectionDataParams & { /** * (Optional) SQLite table name where event locks along with the latest event are stored @@ -21,13 +21,6 @@ export type SqliteViewLockerParams = SqliteDbParams & SqliteProjectionDataParams * @default 120_000 */ viewLockTtl?: number; - - /** - * (Optional) Logger instance for logging operations, - * can be an IExtendableLogger (Winston) - * or ILogger (Console) - */ - logger?: IExtendableLogger | ILogger; }; export class SqliteViewLocker implements IViewLocker { @@ -45,10 +38,9 @@ export class SqliteViewLocker implements IViewLocker { #removeTableLockQuery: Statement<[string, string], void>; #lockMarker: Deferred | undefined; - // eslint-disable-next-line no-undef #lockProlongationTimeout: NodeJS.Timeout | undefined; - constructor(o: SqliteViewLockerParams) { + constructor(o: Pick & SqliteViewLockerParams) { if (!o.viewModelSqliteDb) throw new TypeError('viewModelSqliteDb argument required'); if (!o.projectionName) diff --git a/src/sqlite/index.ts b/src/sqlite/index.ts index 3eaf404..068463a 100644 --- a/src/sqlite/index.ts +++ b/src/sqlite/index.ts @@ -1,3 +1,5 @@ +export * from './AbstractSqliteAccessor'; +export * from './AbstractSqliteObjectProjection'; export * from './AbstractSqliteView'; export * from './SqliteEventLocker'; export * from './SqliteObjectStorage'; diff --git a/src/in-memory/utils/Deferred.ts b/src/utils/Deferred.ts similarity index 100% rename from src/in-memory/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..de00359 --- /dev/null +++ b/src/utils/Lock.ts @@ -0,0 +1,53 @@ +import { Deferred } from './Deferred'; + +/** + * Provides a simple asynchronous lock mechanism. + * Useful for ensuring that only one asynchronous operation proceeds at a time + * for a specific resource or section of code. + */ +export class Lock { + + #deferred?: Deferred; + + get isLocked(): boolean { + return !(this.#deferred?.settled ?? true); + } + + /** + * Wait until lock is released, then acquire it + */ + async acquire(): Promise { + // the below code cannot be replaced with `await this.unblocked()` + // since check of `isLocked` and `this.#deferred` assignment should happen within 1 callback + while (this.isLocked) + await this.#deferred?.promise; + + this.#deferred = new Deferred(); + } + + /** + * Returns a promise that is resolved once lock is released + */ + async unblocked(): Promise { + while (this.isLocked) + await this.#deferred?.promise; + } + + release(): void { + this.#deferred?.resolve(); + this.#deferred = undefined; + } + + /** + * Execute callback with lock acquired, then release lock + */ + async runLocked(callback: () => Promise) { + try { + await this.acquire(); + await callback(); + } + finally { + this.release(); + } + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 0ec92ea..c9bfc05 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,11 @@ +export * from './Deferred'; export * from './delay'; export * from './getClassName'; export * from './getHandler'; export * from './getMessageHandlerNames'; export * from './isClass'; export * from './iteratorToArray'; +export * from './Lock'; export * from './notEmpty'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; diff --git a/tests/unit/Lock.test.ts b/tests/unit/Lock.test.ts new file mode 100644 index 0000000..d75270a --- /dev/null +++ b/tests/unit/Lock.test.ts @@ -0,0 +1,185 @@ +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.toBeUndefined(); + await expect(l3).resolves.toBeUndefined(); + }); + }); + + describe('isLocked', () => { + + it('returns `false` when lock is not acquired', async () => { + expect(lock).toHaveProperty('isLocked', false); + }); + + it('returns `true` when lock is acquired', async () => { + await lock.acquire(); + expect(lock).toHaveProperty('isLocked', true); + }); + + it('returns `false` when lock is released', async () => { + await lock.acquire(); + await lock.release(); + expect(lock).toHaveProperty('isLocked', false); + }); + }); + + describe('runLocked', () => { + + it('executes callback with lock acquired', async () => { + + let p1status = 'not-started'; + let p2status = 'not-started'; + + const p1 = lock.runLocked(async () => { + p1status = 'started'; + await delay(10); + p1status = 'processed'; + }); + + const p2 = lock.runLocked(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('unblocked', () => { + + it('returns Promise', () => { + expect(lock).toHaveProperty('unblocked'); + expect(lock.unblocked()).toBeInstanceOf(Promise); + }); + + it('returns resolved promise when lock is not acquired', async () => { + await expect(isResolved(lock.unblocked())).resolves.toBe(true); + }); + + it('returns pending promise when lock is acquired', async () => { + await lock.acquire(); + await expect(isResolved(lock.unblocked())).resolves.toBe(false); + }); + + it('returns resolved promise when lock is released', async () => { + await lock.acquire(); + await lock.release(); + await expect(isResolved(lock.unblocked())).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.unblocked(); + const p3 = lock.unblocked(); + const l4 = lock.acquire(); // blocking process (i.e. update_by_query) + const p5 = lock.unblocked(); + 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.toBeUndefined(); + await expect(p3).resolves.toBeUndefined(); + await expect(l4).resolves.toBeUndefined(); + await expect(p5).resolves.toBeUndefined(); + await expect(l6).resolves.toBeUndefined(); + }); + }); +}); diff --git a/tests/unit/SagaEventHandler.test.ts b/tests/unit/SagaEventHandler.test.ts index 0aac4e9..ac0d064 100644 --- a/tests/unit/SagaEventHandler.test.ts +++ b/tests/unit/SagaEventHandler.test.ts @@ -7,9 +7,9 @@ import { CommandBus, AbstractSaga, InMemoryMessageBus, - Deferred, EventDispatcher } from '../../src'; +import { Deferred } from '../../src/utils'; class Saga extends AbstractSaga { static get startsWith() { diff --git a/tests/unit/sqlite/SqliteEventLocker.test.ts b/tests/unit/sqlite/SqliteEventLocker.test.ts index 99bb6e2..243e773 100644 --- a/tests/unit/sqlite/SqliteEventLocker.test.ts +++ b/tests/unit/sqlite/SqliteEventLocker.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import * as createDb from 'better-sqlite3'; import { SqliteEventLocker } from '../../../src/sqlite/SqliteEventLocker'; import { IEvent } from '../../../src/interfaces'; @@ -6,7 +5,7 @@ import { guid } from '../../../src/sqlite'; import { promisify } from 'util'; const delay = promisify(setTimeout); -describe('SqliteEventLocker', function () { +describe('SqliteEventLocker', () => { let db: import('better-sqlite3').Database; let locker: SqliteEventLocker; @@ -22,76 +21,70 @@ describe('SqliteEventLocker', function () { viewLockTableName: 'test_view_lock', eventLockTtl: 50 // ms }); - jest.useFakeTimers(); }); afterEach(() => { db.close(); - jest.useRealTimers(); }); - it('allows marking an event as projecting', function () { - const result = locker.tryMarkAsProjecting(testEvent); - expect(result).to.be.true; + 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', function () { - locker.tryMarkAsProjecting(testEvent); - const result = locker.tryMarkAsProjecting(testEvent); - expect(result).to.be.false; + 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', function () { - locker.tryMarkAsProjecting(testEvent); - locker.markAsProjected(testEvent); + 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).to.exist; - expect(row.processed_at).to.not.be.null; + expect(row).toBeDefined(); + expect(row.processed_at).not.toBeNull(); }); - it('retrieves the last projected event', function () { + it('retrieves the last projected event', async () => { + await locker.tryMarkAsProjecting(testEvent); + await locker.markAsProjected(testEvent); - locker.tryMarkAsProjecting(testEvent); - locker.markAsProjected(testEvent); + const lastEvent = await locker.getLastEvent(); // Assuming getLastEvent might become async - const lastEvent = locker.getLastEvent(); - - expect(lastEvent).to.deep.equal(testEvent); + expect(lastEvent).toEqual(testEvent); }); - it('returns undefined if no event has been projected', function () { - const lastEvent = locker.getLastEvent(); - expect(lastEvent).to.be.undefined; + 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', function () { - expect(() => locker.markAsProjected(testEvent)) - .to.throw(Error, `Event ${testEvent.id} could not be marked as processed`); + 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 function () { - - locker.tryMarkAsProjecting(testEvent); + it('allows re-locking after TTL expires', async () => { + await locker.tryMarkAsProjecting(testEvent); - await delay(51); + await delay(51); // Wait for TTL to expire - const result = locker.tryMarkAsProjecting(testEvent); - expect(result).to.be.true; + const result = await locker.tryMarkAsProjecting(testEvent); + expect(result).toBe(true); }); - it('fails to update an event if its version is modified in DB', function () { - - locker.tryMarkAsProjecting(testEvent); + it('fails to update an event if its version is modified in DB', async () => { + await locker.tryMarkAsProjecting(testEvent); - // Modify the event in DB to simulate an external change db.prepare('UPDATE test_event_lock SET processed_at = ? WHERE event_id = ?') .run(Date.now(), guid(testEvent.id)); - // Attempt to finalize the event processing - expect(() => locker.markAsProjected(testEvent)) - .to.throw(Error, `Event ${testEvent.id} could not be marked as processed`); + 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 index 56bc72f..dd6d9f7 100644 --- a/tests/unit/sqlite/SqliteObjectStorage.test.ts +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai'; import * as createDb from 'better-sqlite3'; import { guid, SqliteObjectStorage } from '../../../src/sqlite'; @@ -6,12 +5,13 @@ describe('SqliteObjectStorage', function () { let db: import('better-sqlite3').Database; let storage: SqliteObjectStorage<{ name: string; value: number }>; - beforeEach(() => { + beforeEach(async () => { db = createDb(':memory:'); storage = new SqliteObjectStorage<{ name: string; value: number }>({ viewModelSqliteDb: db, tableName: 'test_objects' }); + await storage.assertConnection(); }); afterEach(() => { @@ -21,66 +21,66 @@ describe('SqliteObjectStorage', function () { it('stores and retrieves an object', async function () { const obj = { name: 'Test Object', value: 42 }; - storage.create('0001', obj); + await storage.create('0001', obj); - const retrieved = storage.get('0001'); - expect(retrieved).to.deep.equal(obj); + const retrieved = await storage.get('0001'); + expect(retrieved).toEqual(obj); }); it('returns undefined for a non-existent object', async function () { - const retrieved = storage.get('nonexistent'); - expect(retrieved).to.be.undefined; + const retrieved = await storage.get('nonexistent'); + expect(retrieved).not.toBeDefined(); }); it('updates an existing object', async function () { - storage.create('0002', { name: 'Old Data', value: 5 }); + await storage.create('0002', { name: 'Old Data', value: 5 }); - storage.update('0002', r => ({ ...r, value: 99 })); + await storage.update('0002', r => ({ ...r, value: 99 })); - const updated = storage.get('0002'); - expect(updated).to.deep.equal({ name: 'Old Data', 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 () { - expect(() => storage.update('nonexistent', r => ({ ...r, value: 99 }))) - .to.throw(Error, "Record 'nonexistent' does not exist"); + 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).to.be.true; + expect(deleted).toBeTruthy(); const retrieved = storage.get('0003'); - expect(retrieved).to.be.undefined; + expect(retrieved).toBeDefined(); }); it('returns false when deleting a non-existent object', async function () { - const deleted = storage.delete('0000'); - expect(deleted).to.be.false; + const deleted = await storage.delete('0000'); + expect(deleted).toBeFalsy(); }); it('enforces updating or creating a new object', async function () { - storage.updateEnforcingNew('0004', () => ({ name: 'Created', value: 1 })); + await storage.updateEnforcingNew('0004', () => ({ name: 'Created', value: 1 })); - let retrieved = storage.get('0004'); - expect(retrieved).to.deep.equal({ name: 'Created', value: 1 }); + let retrieved = await storage.get('0004'); + expect(retrieved).toEqual({ name: 'Created', value: 1 }); - storage.updateEnforcingNew('0004', r => ({ ...r!, value: 100 })); + await storage.updateEnforcingNew('0004', r => ({ ...r!, value: 100 })); - retrieved = storage.get('0004'); - expect(retrieved).to.deep.equal({ name: 'Created', 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'); - expect(() => storage.get('0005')).to.throw(); + await expect(() => storage.get('0005')).rejects.toThrow(); }); }); From bee4be08a943cc5b80a40e8c5c6d968a458e0ac0 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 11 Apr 2025 01:35:50 +0100 Subject: [PATCH 048/135] Refactor event dispatching to support origin tracking and streamline event processing pipeline --- examples/user-domain/index.js | 3 +- src/CqrsContainerBuilder.ts | 34 ++++---- src/EventDispatcher.ts | 34 +++++--- src/EventStore.ts | 2 +- src/EventValidationProcessor.ts | 33 +++++++ .../EventPersistenceProcessor.ts | 30 ------- .../EventValidationProcessor.ts | 27 ------ .../ExternalEventPublishingProcessor.ts | 29 ------- .../SnapshotPersistenceProcessor.ts | 53 ------------ src/dispatch-pipeline/index.ts | 4 - src/in-memory/InMemoryEventStorage.ts | 31 ++++++- src/in-memory/InMemoryMessageBus.ts | 24 +++++- src/in-memory/InMemorySnapshotStorage.ts | 56 +++++++++++- src/index.ts | 2 +- src/interfaces/IContainer.ts | 4 +- src/interfaces/IDispatchPipelineProcessor.ts | 35 ++++++++ src/interfaces/IEventProcessor.ts | 24 ------ src/interfaces/index.ts | 2 +- src/rabbitmq/IContainer.ts | 6 ++ src/rabbitmq/RabbitMqEventBus.ts | 24 +++++- src/rabbitmq/RabbitMqEventInjector.ts | 2 +- .../rabbitmq/RabbitMqEventInjector.test.ts | 2 +- tests/unit/EventDispatcher.test.ts | 10 +-- tests/unit/EventStore.test.ts | 4 +- tests/unit/dispatch-pipeline.test.ts | 85 +++++++++++++++++++ 25 files changed, 339 insertions(+), 221 deletions(-) create mode 100644 src/EventValidationProcessor.ts delete mode 100644 src/dispatch-pipeline/EventPersistenceProcessor.ts delete mode 100644 src/dispatch-pipeline/EventValidationProcessor.ts delete mode 100644 src/dispatch-pipeline/ExternalEventPublishingProcessor.ts delete mode 100644 src/dispatch-pipeline/SnapshotPersistenceProcessor.ts delete mode 100644 src/dispatch-pipeline/index.ts create mode 100644 src/interfaces/IDispatchPipelineProcessor.ts delete mode 100644 src/interfaces/IEventProcessor.ts create mode 100644 tests/unit/dispatch-pipeline.test.ts diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index 32126eb..b11ef0a 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -9,7 +9,6 @@ const { InMemoryMessageBus, EventDispatcher } = require('../..'); // node-cqrs -const { EventPersistenceProcessor } = require('../../src/dispatch-pipeline'); const UserAggregate = require('./UserAggregate'); const UsersProjection = require('./UsersProjection'); @@ -39,7 +38,7 @@ exports.createBaseInstances = () => { const eventBus = new InMemoryMessageBus(); const storage = new InMemoryEventStorage(); const eventDispatcher = new EventDispatcher({ eventBus }) - eventDispatcher.addPipelineProcessor(new EventPersistenceProcessor({ eventStorageWriter: storage })); + eventDispatcher.addPipelineProcessor(storage); const eventStore = new EventStore({ eventStorageReader: storage, eventBus, eventDispatcher }); const commandBus = new CommandBus(); diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 4ed14fe..95eba03 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -1,21 +1,12 @@ import { ContainerBuilder, TypeConfig, TClassOrFactory } from 'di0'; - import { AggregateCommandHandler } from './AggregateCommandHandler'; import { CommandBus } from './CommandBus'; import { EventStore } from './EventStore'; import { SagaEventHandler } from './SagaEventHandler'; import { EventDispatcher } from './EventDispatcher'; -import { InMemoryMessageBus } from './in-memory'; -import { - EventValidationProcessor, - SnapshotPersistenceProcessor, - EventPersistenceProcessor -} from './dispatch-pipeline'; - -import { - isClass -} from './utils'; - +import { InMemoryEventStorage, InMemoryMessageBus, InMemorySnapshotStorage } from './in-memory'; +import { EventValidationProcessor } from './EventValidationProcessor'; +import { isClass } from './utils'; import { IAggregateConstructor, ICommandHandler, @@ -25,7 +16,6 @@ import { IProjectionConstructor, ISagaConstructor } from './interfaces'; -import { ExternalEventPublishingProcessor } from './dispatch-pipeline/ExternalEventPublishingProcessor'; export class CqrsContainerBuilder extends ContainerBuilder { @@ -38,12 +28,18 @@ export class CqrsContainerBuilder extends ContainerBuilder { super.register(EventStore).as('eventStore'); super.register(CommandBus).as('commandBus'); super.register(EventDispatcher).as('eventDispatcher'); - super.register(container => [ - new EventValidationProcessor(container), - new ExternalEventPublishingProcessor(container), - new EventPersistenceProcessor(container), - new SnapshotPersistenceProcessor(container) - ]).as('eventDispatchProcessors'); + + super.register(InMemoryEventStorage).as('eventStorageWriter'); + super.register(InMemorySnapshotStorage).as('snapshotStorage'); + + // Register default event dispatch pipeline: + // validate events, write to event storage, write to snapshot storage. + // If any of the processors is not defined, it will be skipped. + super.register((container: IContainer) => [ + new EventValidationProcessor(), + container.eventStorageWriter, + container.snapshotStorage + ]).as('eventDispatchPipeline'); } /** Register command handler, which will be subscribed to commandBus upon instance creation */ diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index ceeb6b2..d9568f7 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -1,12 +1,13 @@ import { - EventBatch, + DispatchPipelineBatch, IEvent, IEventDispatcher, - IEventProcessor, + IDispatchPipelineProcessor, IEventSet, IEventBus, isEventSet, - IContainer + IContainer, + isDispatchPipelineProcessor } from './interfaces'; import { parallelPipe } from 'async-parallel-pipe'; import { AsyncIterableBuffer } from 'async-iterable-buffer'; @@ -14,7 +15,7 @@ import { notEmpty } from './utils'; import { InMemoryMessageBus } from './in-memory'; type EventBatchEnvelope = { - data: EventBatch<{ event?: IEvent }>; + data: DispatchPipelineBatch<{ event?: IEvent }>; error?: Error; resolve: (event: IEvent[]) => void; reject: (error: Error) => void; @@ -23,7 +24,7 @@ type EventBatchEnvelope = { export class EventDispatcher implements IEventDispatcher { #pipelineInput = new AsyncIterableBuffer(); - #processors: Array = []; + #processors: Array = []; #pipeline: AsyncIterableIterator | IterableIterator = this.#pipelineInput; /** @@ -38,7 +39,7 @@ export class EventDispatcher implements IEventDispatcher { */ concurrentLimit: number; - constructor(o?: Pick & { + constructor(o?: Pick & { eventDispatcherConfig?: { concurrentLimit?: number } @@ -46,8 +47,16 @@ export class EventDispatcher implements IEventDispatcher { this.eventBus = o?.eventBus ?? new InMemoryMessageBus(); this.concurrentLimit = o?.eventDispatcherConfig?.concurrentLimit ?? 100; - if (o?.eventDispatchProcessors) { - for (const processor of o.eventDispatchProcessors) + if (o?.eventDispatchPipeline) + this.addPipelineProcessors(o.eventDispatchPipeline); + } + + addPipelineProcessors(eventDispatchPipeline: IDispatchPipelineProcessor[]) { + if (!Array.isArray(eventDispatchPipeline)) + throw new TypeError('eventDispatchPipeline argument must be an Array'); + + for (const processor of eventDispatchPipeline) { + if (processor) this.addPipelineProcessor(processor); } } @@ -57,14 +66,15 @@ export class EventDispatcher implements IEventDispatcher { * * Preprocessors run in order they are added but process separate batches in parallel, maintaining FIFO order. */ - addPipelineProcessor(preprocessor: IEventProcessor) { + addPipelineProcessor(preprocessor: IDispatchPipelineProcessor) { + if (!isDispatchPipelineProcessor(preprocessor)) + throw new TypeError('preprocessor must implement IDispatchPipelineProcessor'); if (this.#pipelineProcessing) throw new Error('pipeline processing already started'); this.#processors.push(preprocessor); - // Build a processing pipeline that runs preprocessors concurrently - // while preserving first-in-first-out ordering. + // 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; @@ -119,7 +129,7 @@ export class EventDispatcher implements IEventDispatcher { /** * Revert side effects made by pipeline processors in case of a batch processing failure */ - async #revert(batch: EventBatch) { + async #revert(batch: DispatchPipelineBatch) { for (const processor of this.#processors) await processor.revert?.(batch); } diff --git a/src/EventStore.ts b/src/EventStore.ts index 5a1e1e5..9fc91d1 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -153,7 +153,7 @@ export class EventStore implements IEventStore { const augmentedEvents = await this.#attachSagaIdToSagaStarterEvents(events); - return this.#eventDispatcher.dispatch(augmentedEvents); + return this.#eventDispatcher.dispatch(augmentedEvents, { origin: 'internal' }); } /** diff --git a/src/EventValidationProcessor.ts b/src/EventValidationProcessor.ts new file mode 100644 index 0000000..da5d0b2 --- /dev/null +++ b/src/EventValidationProcessor.ts @@ -0,0 +1,33 @@ +import { DispatchPipelineBatch, IEvent, IDispatchPipelineProcessor } from './interfaces'; +import { validate as defaultValidator } from './Event'; + +export type EventValidator = (event: IEvent) => void; + +/** + * Processor that validates the format of events. + * Rejects the batch if any event fails validation. + */ +export class EventValidationProcessor implements IDispatchPipelineProcessor { + + #validate: EventValidator; + + constructor(o?: { + eventFormatValidator?: EventValidator + }) { + this.#validate = o?.eventFormatValidator ?? defaultValidator; + } + + /** + * Processes a batch of dispatch pipeline items by validating each event within the batch. + * It iterates through the batch and calls the private `#validate` method for each event found. + * + * This method is part of the `IDispatchPipelineProcessor` interface. + */ + async process(batch: DispatchPipelineBatch): Promise { + for (const { event } of batch) { + if (event) + this.#validate(event); + } + return batch; + } +} diff --git a/src/dispatch-pipeline/EventPersistenceProcessor.ts b/src/dispatch-pipeline/EventPersistenceProcessor.ts deleted file mode 100644 index d5ed7b6..0000000 --- a/src/dispatch-pipeline/EventPersistenceProcessor.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { EventBatch, IContainer, IEvent, IEventProcessor, IEventStorageWriter } from '../interfaces'; - -/** - * Processor responsible for persisting events to IEventStoreWriter. - */ -export class EventPersistenceProcessor implements IEventProcessor { - - #storageWriter: IEventStorageWriter | undefined; - - constructor(options: Pick) { - this.#storageWriter = options.eventStorageWriter; - } - - async process(batch: EventBatch): Promise { - if (!this.#storageWriter) - return batch; - - const events: IEvent[] = []; - for (const { event } of batch) { - if (!event) - throw new Error('Event batch does not contain event'); - - events.push(event); - } - - await this.#storageWriter.commitEvents(events); - - return batch; - } -} diff --git a/src/dispatch-pipeline/EventValidationProcessor.ts b/src/dispatch-pipeline/EventValidationProcessor.ts deleted file mode 100644 index 7f24245..0000000 --- a/src/dispatch-pipeline/EventValidationProcessor.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { EventBatch, IEvent, IEventProcessor } from '../interfaces'; -import { validate as defaultValidator } from '../Event'; - -export type EventValidator = (event: IEvent) => void; - -/** - * Processor that validates the format of events. - * Rejects the batch if any event fails validation. - */ -export class EventValidationProcessor implements IEventProcessor { - - #validate: EventValidator; - - constructor(o?: { - eventFormatValidator?: EventValidator - }) { - this.#validate = o?.eventFormatValidator ?? defaultValidator; - } - - async process(batch: EventBatch): Promise { - for (const { event } of batch) { - if (event) - this.#validate(event); - } - return batch; - } -} diff --git a/src/dispatch-pipeline/ExternalEventPublishingProcessor.ts b/src/dispatch-pipeline/ExternalEventPublishingProcessor.ts deleted file mode 100644 index 8cfbad3..0000000 --- a/src/dispatch-pipeline/ExternalEventPublishingProcessor.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IEventProcessor, IEventBus, EventBatch } from '../interfaces'; - -/** - * Event dispatcher processor that publishes events to an external RabbitMQ event bus if provided. - */ -export class ExternalEventPublishingProcessor implements IEventProcessor { - - #externalEventBus?: IEventBus; - - constructor(options: { externalEventBus?: IEventBus }) { - this.#externalEventBus = options.externalEventBus; - } - - async process(batch: EventBatch): Promise { - if (!this.#externalEventBus) - return batch; - - // TODO: ignore external events - - for (const { event } of batch) { - if (!event) - throw new Error('Event batch does not contain `event`'); - - await this.#externalEventBus.publish(event); - } - - return batch; - } -} diff --git a/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts b/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts deleted file mode 100644 index 855a857..0000000 --- a/src/dispatch-pipeline/SnapshotPersistenceProcessor.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - EventBatch, - IAggregateSnapshotStorage, - IEvent, - IEventProcessor, - IExtendableLogger, - ILogger -} from '../interfaces'; -import * as Event from '../Event'; - -const SNAPSHOT_EVENT_TYPE = 'snapshot'; -const isSnapshotEvent = (event?: IEvent): event is IEvent & { type: 'snapshot' } => - (!!event && event.type === SNAPSHOT_EVENT_TYPE); - -export class SnapshotPersistenceProcessor implements IEventProcessor<{ event?: IEvent }> { - - #snapshotStorage?: IAggregateSnapshotStorage; - #logger?: ILogger; - - constructor(options: { - snapshotStorage?: IAggregateSnapshotStorage; - logger?: ILogger | IExtendableLogger; - }) { - this.#snapshotStorage = options.snapshotStorage; - this.#logger = options.logger && 'child' in options.logger ? - options.logger.child({ service: new.target.name }) : - options.logger; - } - - async process(batch: EventBatch): Promise { - if (!this.#snapshotStorage) - return batch; - - const snapshotEvents = batch.map(e => e.event).filter(isSnapshotEvent); - for (const event of snapshotEvents) { - this.#logger?.debug(`Persisting ${Event.describe(event)}`); - await this.#snapshotStorage.saveAggregateSnapshot(event); - } - - return batch.filter(e => !isSnapshotEvent(e.event)); - } - - async revert(batch: EventBatch): Promise { - if (!this.#snapshotStorage) - return; - - const snapshotEvents = batch.map(e => e.event).filter(isSnapshotEvent); - for (const snapshotEvent of snapshotEvents) { - this.#logger?.debug(`Removing ${Event.describe(snapshotEvent)}`); - await this.#snapshotStorage.deleteAggregateSnapshot(snapshotEvent); - } - } -} diff --git a/src/dispatch-pipeline/index.ts b/src/dispatch-pipeline/index.ts deleted file mode 100644 index c536281..0000000 --- a/src/dispatch-pipeline/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './EventPersistenceProcessor'; -export * from './EventValidationProcessor'; -export * from './ExternalEventPublishingProcessor'; -export * from './SnapshotPersistenceProcessor'; diff --git a/src/in-memory/InMemoryEventStorage.ts b/src/in-memory/InMemoryEventStorage.ts index 3b1cb69..d0b8d95 100644 --- a/src/in-memory/InMemoryEventStorage.ts +++ b/src/in-memory/InMemoryEventStorage.ts @@ -6,7 +6,9 @@ import { IEventStorageReader, IEventStream, IEventStorageWriter, - Identifier + Identifier, + IDispatchPipelineProcessor, + DispatchPipelineBatch } from '../interfaces'; import { nextCycle } from './utils'; @@ -14,7 +16,12 @@ import { nextCycle } from './utils'; * A simple event storage implementation intended to use for tests only. * Storage content resets on each app restart. */ -export class InMemoryEventStorage implements IEventStorageReader, IEventStorageWriter, IIdentifierProvider { +export class InMemoryEventStorage implements + IEventStorageReader, + IEventStorageWriter, + IIdentifierProvider, + IDispatchPipelineProcessor { + #nextId: number = 0; #events: IEventSet = []; @@ -78,4 +85,24 @@ export class InMemoryEventStorage implements IEventStorageReader, IEventStorageW 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/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts index 69b8af6..261e1c5 100644 --- a/src/in-memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -1,5 +1,7 @@ import { + DispatchPipelineBatch, ICommand, + IDispatchPipelineProcessor, IEvent, IMessageBus, IMessageHandler, @@ -10,7 +12,7 @@ import { * Default implementation of the message bus. * Keeps all subscriptions and messages in memory. */ -export class InMemoryMessageBus implements IMessageBus { +export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcessor { #handlers: Map> = new Map(); #name: string | undefined; @@ -116,4 +118,24 @@ export class InMemoryMessageBus implements IMessageBus { return Promise.all(handlers.map(handler => handler(event, meta))); } + + /** + * Processes a batch of events and publishes them to the fanout exchange. + * + * This method is part of the `IDispatchPipelineProcessor` interface. + */ + async process(batch: DispatchPipelineBatch): Promise { + for (const { event, origin } of batch) { + // Skip publishing if the event was dispatched from external source + if (origin === 'external') + continue; + + if (!event) + throw new Error('Event batch does not contain `event`'); + + await this.publish(event); + } + + return batch; + } } diff --git a/src/in-memory/InMemorySnapshotStorage.ts b/src/in-memory/InMemorySnapshotStorage.ts index 3fb4947..ae3bbcf 100644 --- a/src/in-memory/InMemorySnapshotStorage.ts +++ b/src/in-memory/InMemorySnapshotStorage.ts @@ -1,12 +1,32 @@ -import { IAggregateSnapshotStorage, Identifier, IEvent } from '../interfaces'; +import { + DispatchPipelineBatch, + IAggregateSnapshotStorage, + IContainer, + Identifier, + IDispatchPipelineProcessor, + IEvent, + ILogger +} from '../interfaces'; +import * as Event from '../Event'; + +const SNAPSHOT_EVENT_TYPE = 'snapshot'; +const isSnapshotEvent = (event?: IEvent): event is IEvent & { type: 'snapshot' } => + (!!event && event.type === SNAPSHOT_EVENT_TYPE); /** * In-memory storage for aggregate snapshots. * Storage content resets on app restart */ -export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { +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 @@ -22,6 +42,8 @@ export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { if (!snapshotEvent.aggregateId) throw new TypeError('event.aggregateId is required'); + this.#logger?.debug(`Persisting ${Event.describe(snapshotEvent)}`); + this.#snapshots.set(snapshotEvent.aggregateId, snapshotEvent); } @@ -32,6 +54,36 @@ export class InMemorySnapshotStorage implements IAggregateSnapshotStorage { 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/index.ts b/src/index.ts index a1e1c55..6db623e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,9 +9,9 @@ export * from './AbstractSaga'; export * from './SagaEventHandler'; export * from './AbstractProjection'; export * from './EventDispatcher'; +export * from './EventValidationProcessor'; export * from './in-memory'; -export * from './dispatch-pipeline'; export * as Event from './Event'; export { diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index 770b8af..6cf5e73 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -3,7 +3,7 @@ import { ICommandBus } from './ICommandBus'; import { IEventDispatcher } from './IEventDispatcher'; import { IEventStore } from './IEventStore'; import { IEventBus } from './IEventBus'; -import { IEventProcessor } from './IEventProcessor'; +import { IDispatchPipelineProcessor } from './IDispatchPipelineProcessor'; import { IEventStorageReader, IEventStorageWriter } from './IEventStorage'; import { IAggregateSnapshotStorage } from './IAggregateSnapshotStorage'; import { IIdentifierProvider } from './IIdentifierProvider'; @@ -19,7 +19,7 @@ export interface IContainer extends Container { commandBus: ICommandBus; eventDispatcher: IEventDispatcher; - eventDispatchProcessors?: IEventProcessor[]; + eventDispatchPipeline?: IDispatchPipelineProcessor[]; logger?: ILogger | IExtendableLogger; diff --git a/src/interfaces/IDispatchPipelineProcessor.ts b/src/interfaces/IDispatchPipelineProcessor.ts new file mode 100644 index 0000000..f263490 --- /dev/null +++ b/src/interfaces/IDispatchPipelineProcessor.ts @@ -0,0 +1,35 @@ +import { IEvent } from './IEvent'; +import { isObject } from './isObject'; + +/** + * Represents a wrapper for an event that can optionally contain additional metadata. + * Used to extend event processing with context-specific data required by processors. + */ +export type DispatchPipelineEnvelope = { + + /** + * Origin of the event. Can be used to distinguish between events coming from different sources. + */ + origin?: 'external' | 'internal'; + + 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/IEventProcessor.ts b/src/interfaces/IEventProcessor.ts deleted file mode 100644 index 7bd448c..0000000 --- a/src/interfaces/IEventProcessor.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { IEvent } from './IEvent'; - -/** - * Represents a wrapper for an event that can optionally contain additional metadata. - * Used to extend event processing with context-specific data required by processors. - */ -type EventEnvelope = { - event?: IEvent; -} - -/** - * A batch of event envelopes. Can contain custom envelope types extending EventEnvelope. - */ -export type EventBatch = Readonly>; - -/** - * Defines a processor that operates on a batch of event envelopes. - * Allows transformations, side-effects, or filtering of events during dispatch. - */ -export interface IEventProcessor { - process(batch: EventBatch): Promise>; - revert?(batch: EventBatch): Promise; -} - diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index bb70131..e736e0a 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -4,11 +4,11 @@ export * from './ICommand'; export * from './ICommandBus'; export * from './IContainer'; export * from './Identifier'; +export * from './IDispatchPipelineProcessor'; export * from './IEvent'; export * from './IEventBus'; export * from './IEventDispatcher'; export * from './IEventLocker'; -export * from './IEventProcessor'; export * from './IEventReceptor'; export * from './IEventSet'; export * from './IEventStorage'; diff --git a/src/rabbitmq/IContainer.ts b/src/rabbitmq/IContainer.ts index 6c43fb0..9a66a29 100644 --- a/src/rabbitmq/IContainer.ts +++ b/src/rabbitmq/IContainer.ts @@ -1,3 +1,4 @@ +import { IEventBus } from '../interfaces'; import { RabbitMqEventInjector } from './RabbitMqEventInjector'; import { RabbitMqGateway } from './RabbitMqGateway'; @@ -6,5 +7,10 @@ declare module '../interfaces/IContainer' { rabbitMqGateway?: RabbitMqGateway; rabbitMqEventInjector?: RabbitMqEventInjector; rabbitMqEventBus?: RabbitMqEventInjector; + + /** + * Optional external event bus for publishing events to an external system. + */ + externalEventBus?: IEventBus; } } diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts index a707779..b47115b 100644 --- a/src/rabbitmq/RabbitMqEventBus.ts +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -1,10 +1,10 @@ -import { IEvent, IEventBus, IMessageHandler, IObservable } from '../interfaces'; +import { IEvent, IEventBus, IDispatchPipelineProcessor, IMessageHandler, IObservable, DispatchPipelineBatch } from '../interfaces'; import { DEFAULT_EXCHANGE } from './constants'; import { RabbitMqGateway } from './RabbitMqGateway'; const ALL_EVENTS_WILDCARD = '*'; -export class RabbitMqEventBus implements IEventBus { +export class RabbitMqEventBus implements IEventBus, IDispatchPipelineProcessor { static get allEventsWildcard(): '*' { return ALL_EVENTS_WILDCARD; @@ -81,4 +81,24 @@ export class RabbitMqEventBus implements IEventBus { } return queue; } + + /** + * Processes a batch of events and publishes them to the fanout exchange. + * + * This method is part of the `IDispatchPipelineProcessor` interface. + */ + async process(batch: DispatchPipelineBatch): Promise { + for (const { event, origin } of batch) { + // Skip publishing if the event was dispatched from external source + if (origin === 'external') + continue; + + if (!event) + throw new Error('Event batch does not contain `event`'); + + await this.publish(event); + } + + return batch; + } } diff --git a/src/rabbitmq/RabbitMqEventInjector.ts b/src/rabbitmq/RabbitMqEventInjector.ts index ddeb713..b68de0c 100644 --- a/src/rabbitmq/RabbitMqEventInjector.ts +++ b/src/rabbitmq/RabbitMqEventInjector.ts @@ -53,7 +53,7 @@ export class RabbitMqEventInjector { async #handleMessage(message: IMessage): Promise { this.#logger?.debug(`"${Event.describe(message)}" received`); try { - await this.#eventDispatcher.dispatch([message]); + await this.#eventDispatcher.dispatch([message], { origin: 'external' }); this.#logger?.debug(`${Event.describe(message)} dispatched successfully`); } diff --git a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts index 1946dad..08eba71 100644 --- a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts +++ b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts @@ -61,6 +61,6 @@ describe('RabbitMqEventInjector', () => { await delay(50); expect(eventDispatcher.dispatch).toHaveBeenCalledTimes(1); - expect(eventDispatcher.dispatch).toHaveBeenCalledWith([testEvent]); + expect(eventDispatcher.dispatch).toHaveBeenCalledWith([testEvent], { origin: 'external' }); }); }); diff --git a/tests/unit/EventDispatcher.test.ts b/tests/unit/EventDispatcher.test.ts index 53f3107..f88048f 100644 --- a/tests/unit/EventDispatcher.test.ts +++ b/tests/unit/EventDispatcher.test.ts @@ -1,4 +1,4 @@ -import { IEvent, IEventBus, IEventProcessor } from '../../src'; +import { IEvent, IEventBus, IDispatchPipelineProcessor } from '../../src'; import { EventDispatcher } from '../../src/EventDispatcher'; describe('EventDispatcher', () => { @@ -15,7 +15,7 @@ describe('EventDispatcher', () => { const event1: IEvent = { type: 'test-event-1' }; const event2: IEvent = { type: 'test-event-2' }; - const processorMock: IEventProcessor = { + const processorMock: IDispatchPipelineProcessor = { process: jest.fn(batch => Promise.resolve(batch)) }; @@ -34,7 +34,7 @@ describe('EventDispatcher', () => { const event: IEvent = { type: 'failing-event' }; const error = new Error('processor error'); - const processorMock: IEventProcessor = { + const processorMock: IDispatchPipelineProcessor = { process: jest.fn().mockRejectedValue(error), revert: jest.fn().mockResolvedValue(undefined) }; @@ -57,7 +57,7 @@ describe('EventDispatcher', () => { const executionOrder: string[] = []; - const processorA: IEventProcessor = { + const processorA: IDispatchPipelineProcessor = { process: jest.fn(async batch => { executionOrder.push(`A-start-${batch[0].event.type}`); await new Promise(res => setTimeout(res, 5)); @@ -66,7 +66,7 @@ describe('EventDispatcher', () => { }) }; - const processorB: IEventProcessor = { + const processorB: IDispatchPipelineProcessor = { process: jest.fn(async batch => { executionOrder.push(`B-start-${batch[0].event.type}`); await new Promise(res => setTimeout(res, 5)); diff --git a/tests/unit/EventStore.test.ts b/tests/unit/EventStore.test.ts index 8cb1bb1..59f65a0 100644 --- a/tests/unit/EventStore.test.ts +++ b/tests/unit/EventStore.test.ts @@ -64,7 +64,7 @@ describe('EventStore', () => { expect(event.sagaId).toBe(mockId); expect(event.sagaVersion).toBe(0); - expect(dispatchSpy).toHaveBeenCalledWith([event]); + expect(dispatchSpy).toHaveBeenCalledWith([event], { origin: 'internal' }); }); it('does not modify non-saga starter events', async () => { @@ -76,7 +76,7 @@ describe('EventStore', () => { expect(event.sagaId).toBeUndefined(); expect(event.sagaVersion).toBeUndefined(); - expect(dispatchSpy).toHaveBeenCalledWith([event]); + expect(dispatchSpy).toHaveBeenCalledWith([event], { origin: 'internal' }); }); }); diff --git a/tests/unit/dispatch-pipeline.test.ts b/tests/unit/dispatch-pipeline.test.ts new file mode 100644 index 0000000..0fc7569 --- /dev/null +++ b/tests/unit/dispatch-pipeline.test.ts @@ -0,0 +1,85 @@ +import { + ContainerBuilder, + EventValidationProcessor, + IContainer, + InMemoryEventStorage, + InMemoryMessageBus +} 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(InMemoryMessageBus).as('externalEventBus'); + builder.register(InMemoryEventStorage).as('eventStorageWriter'); + builder.register((c: IContainer) => [ + new EventValidationProcessor(), + c.externalEventBus, + c.eventStorageWriter, + c.snapshotStorage + ]).as('eventDispatchPipeline'); + + container = builder.container() as IContainer; + }); + + it('delivers locally dispatched events to externalEventBus', async () => { + + const { eventDispatcher, externalEventBus } = container; + + jest.spyOn(externalEventBus, 'publish'); + + await eventDispatcher.dispatch([testEvent], { origin: 'internal' }); + + expect(externalEventBus.publish).toHaveBeenCalledTimes(1); + }); + + it('does not deliver externally dispatched events to externalEventBus', async () => { + + const { eventDispatcher, externalEventBus } = container; + + jest.spyOn(externalEventBus, 'publish'); + + await eventDispatcher.dispatch([testEvent], { origin: 'external' }); + + expect(externalEventBus.publish).toHaveBeenCalledTimes(0); + }); + + 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); + expect(eventBus.publish).toHaveBeenNthCalledWith(2, testEvent); + }); +}); From 85fdf79a19beafedef97d7e2e47dd4b66026747e Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 15:42:25 +0100 Subject: [PATCH 049/135] Enhance error message for invalid preprocessor in EventDispatcher --- src/EventDispatcher.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index d9568f7..1521846 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -11,7 +11,7 @@ import { } from './interfaces'; import { parallelPipe } from 'async-parallel-pipe'; import { AsyncIterableBuffer } from 'async-iterable-buffer'; -import { notEmpty } from './utils'; +import { getClassName, notEmpty } from './utils'; import { InMemoryMessageBus } from './in-memory'; type EventBatchEnvelope = { @@ -68,7 +68,7 @@ export class EventDispatcher implements IEventDispatcher { */ addPipelineProcessor(preprocessor: IDispatchPipelineProcessor) { if (!isDispatchPipelineProcessor(preprocessor)) - throw new TypeError('preprocessor must implement IDispatchPipelineProcessor'); + throw new TypeError(`preprocessor ${getClassName(preprocessor)} does not implement IDispatchPipelineProcessor`); if (this.#pipelineProcessing) throw new Error('pipeline processing already started'); From 04993efe33d14574c708e8605becd4cc2e2350a2 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 15:42:38 +0100 Subject: [PATCH 050/135] Fix import path for Deferred utility in RabbitMqGateway tests --- tests/integration/rabbitmq/RabbitMqGateway.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index c1f15bb..f43c653 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -2,7 +2,7 @@ import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; import { IMessage } from '../../../src/interfaces'; import * as amqplib from 'amqplib'; import { delay } from '../../../src/utils'; -import { Deferred } from '../../../dist/in-memory/utils/Deferred'; +import { Deferred } from '../../../src/utils/Deferred'; import { EventEmitter } from 'stream'; describe('RabbitMqGateway', () => { From 309004c75a0a8f911287196cd827411f591eb4b9 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 15:42:56 +0100 Subject: [PATCH 051/135] Optimize assertConnection method to return early if already initialized --- src/sqlite/AbstractSqliteAccessor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sqlite/AbstractSqliteAccessor.ts b/src/sqlite/AbstractSqliteAccessor.ts index 23c009c..3d07f33 100644 --- a/src/sqlite/AbstractSqliteAccessor.ts +++ b/src/sqlite/AbstractSqliteAccessor.ts @@ -36,6 +36,9 @@ export abstract class AbstractSqliteAccessor { * This method is idempotent and safe to call multiple times. */ async assertConnection() { + if (this.#initialized) + return; + try { this.#initLocker.acquire(); if (this.#initialized) From 5b8aae447a25c0c614fef175373e912dbdb6871d Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 15:43:34 +0100 Subject: [PATCH 052/135] Refactor constructor parameters in AbstractSqliteAccessor and AbstractSqliteView --- src/sqlite/AbstractSqliteAccessor.ts | 2 +- src/sqlite/AbstractSqliteView.ts | 6 +++-- src/sqlite/SqliteEventLocker.ts | 38 +++++++++++++++------------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/sqlite/AbstractSqliteAccessor.ts b/src/sqlite/AbstractSqliteAccessor.ts index 3d07f33..dc5d249 100644 --- a/src/sqlite/AbstractSqliteAccessor.ts +++ b/src/sqlite/AbstractSqliteAccessor.ts @@ -17,7 +17,7 @@ export abstract class AbstractSqliteAccessor { #initLocker = new Lock(); #initialized = false; - constructor(c: Pick) { + constructor(c: Partial>) { if (!c.viewModelSqliteDb && !c.viewModelSqliteDbFactory) throw new TypeError('either viewModelSqliteDb or viewModelSqliteDbFactory argument required'); diff --git a/src/sqlite/AbstractSqliteView.ts b/src/sqlite/AbstractSqliteView.ts index 63b26dd..52aa020 100644 --- a/src/sqlite/AbstractSqliteView.ts +++ b/src/sqlite/AbstractSqliteView.ts @@ -1,8 +1,9 @@ import { IContainer, IEvent, IEventLocker, ILogger, IViewLocker } from '../interfaces'; import { SqliteViewLocker, SqliteViewLockerParams } from './SqliteViewLocker'; import { SqliteEventLocker, SqliteEventLockerParams } from './SqliteEventLocker'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; -export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { +export abstract class AbstractSqliteView extends AbstractSqliteAccessor implements IViewLocker, IEventLocker { protected readonly schemaVersion: string; protected readonly viewLocker: SqliteViewLocker; @@ -13,9 +14,10 @@ export abstract class AbstractSqliteView implements IViewLocker, IEventLocker { return this.viewLocker.ready; } - constructor(options: Pick + constructor(options: Partial> & SqliteEventLockerParams & SqliteViewLockerParams) { + super(options); this.schemaVersion = options.schemaVersion; this.viewLocker = new SqliteViewLocker(options); diff --git a/src/sqlite/SqliteEventLocker.ts b/src/sqlite/SqliteEventLocker.ts index 4322cec..9beedda 100644 --- a/src/sqlite/SqliteEventLocker.ts +++ b/src/sqlite/SqliteEventLocker.ts @@ -6,24 +6,26 @@ import { SqliteViewLockerParams } from './SqliteViewLocker'; import { SqliteProjectionDataParams } from './SqliteProjectionDataParams'; import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; -export type SqliteEventLockerParams = SqliteProjectionDataParams & { - - /** - * (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; -} - & Pick; +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 { From 83198caf9e053d758a9ba10c3e68962c4cd3c417 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 15:43:57 +0100 Subject: [PATCH 053/135] Refactor SqliteViewLocker to extend AbstractSqliteAccessor and streamline database interactions --- src/sqlite/SqliteViewLocker.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/sqlite/SqliteViewLocker.ts b/src/sqlite/SqliteViewLocker.ts index 12efe5d..0c3d113 100644 --- a/src/sqlite/SqliteViewLocker.ts +++ b/src/sqlite/SqliteViewLocker.ts @@ -4,6 +4,7 @@ import { Deferred } from '../utils'; import { promisify } from 'util'; import { viewLockTableInit } from './queries'; import { SqliteProjectionDataParams } from './SqliteProjectionDataParams'; +import { AbstractSqliteAccessor } from './AbstractSqliteAccessor'; const delay = promisify(setTimeout); export type SqliteViewLockerParams = SqliteProjectionDataParams & { @@ -23,9 +24,8 @@ export type SqliteViewLockerParams = SqliteProjectionDataParams & { viewLockTtl?: number; }; -export class SqliteViewLocker implements IViewLocker { +export class SqliteViewLocker extends AbstractSqliteAccessor implements IViewLocker { - #db: Database; #projectionName: string; #schemaVersion: string; @@ -33,22 +33,22 @@ export class SqliteViewLocker implements IViewLocker { #viewLockTtl: number; #logger: ILogger | undefined; - #upsertTableLockQuery: Statement<[string, string, number], void>; - #updateTableLockQuery: Statement<[number, string, string], void>; - #removeTableLockQuery: Statement<[string, string], void>; + #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: Pick & SqliteViewLockerParams) { - if (!o.viewModelSqliteDb) - throw new TypeError('viewModelSqliteDb argument required'); + 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.#db = o.viewModelSqliteDb; this.#projectionName = o.projectionName; this.#schemaVersion = o.schemaVersion; @@ -57,11 +57,12 @@ export class SqliteViewLocker implements IViewLocker { 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.#db.exec(viewLockTableInit(this.#viewLockTableName)); - - this.#upsertTableLockQuery = this.#db.prepare(` + this.#upsertTableLockQuery = db.prepare(` INSERT INTO ${this.#viewLockTableName} (projection_name, schema_version, locked_till) VALUES (?, ?, ?) ON CONFLICT (projection_name, schema_version) @@ -72,7 +73,7 @@ export class SqliteViewLocker implements IViewLocker { OR locked_till < excluded.locked_till `); - this.#updateTableLockQuery = this.#db.prepare(` + this.#updateTableLockQuery = db.prepare(` UPDATE ${this.#viewLockTableName} SET locked_till = ? @@ -82,7 +83,7 @@ export class SqliteViewLocker implements IViewLocker { AND locked_till IS NOT NULL `); - this.#removeTableLockQuery = this.#db.prepare(` + this.#removeTableLockQuery = db.prepare(` UPDATE ${this.#viewLockTableName} SET locked_till = NULL @@ -100,6 +101,8 @@ export class SqliteViewLocker implements IViewLocker { async lock() { this.#lockMarker = new Deferred(); + await this.assertConnection(); + let lockAcquired = false; while (!lockAcquired) { const lockedTill = Date.now() + this.#viewLockTtl; From 6eea5cbca891d7d6c1434551542f76056c6940d4 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 15:44:13 +0100 Subject: [PATCH 054/135] Update type definitions settings in package.json and tsconfig.json --- .gitignore | 1 + package.json | 12 ++++++------ tsconfig.json | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index ea0b7b1..041d411 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ app/tests/coverage/ coverage/ .nyc_output/ dist/ +types/ *.tgz # IDE's diff --git a/package.json b/package.json index dbb6319..3493658 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "url": "https://github.com/snatalenko/node-cqrs.git" }, "main": "./dist/index.js", - "types": "./src/index.ts", + "types": "./types/index.d.ts", "typesVersions": { "*": { "rabbitmq": [ - "src/rabbitmq/index.ts" + "types/rabbitmq/index.d.ts" ], "sqlite": [ - "src/sqlite/index.ts" + "types/sqlite/index.d.ts" ] } }, @@ -26,17 +26,17 @@ ".": { "require": "./dist/index.js", "import": "./dist/index.js", - "types": "./src/index.ts" + "types": "./types/index.d.ts" }, "./rabbitmq": { "require": "./dist/rabbitmq/index.js", "import": "./dist/rabbitmq/index.js", - "types": "./src/rabbitmq/index.ts" + "types": "./types/rabbitmq/index.d.ts" }, "./sqlite": { "require": "./dist/sqlite/index.js", "import": "./dist/sqlite/index.js", - "types": "./src/sqlite/index.ts" + "types": "./types/sqlite/index.d.ts" } }, "directories": { diff --git a/tsconfig.json b/tsconfig.json index b0180a9..28efcd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "alwaysStrict": false, "outDir": "./dist", "target": "ESNext", - "declaration": false, + "declaration": true, + "declarationDir": "./types", "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "strict": true, From 0fd858a60ab149e60580e94836f4fac616eb3437 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 16:05:35 +0100 Subject: [PATCH 055/135] Add missing initialize method in SqliteObjectView --- src/sqlite/SqliteObjectView.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/sqlite/SqliteObjectView.ts b/src/sqlite/SqliteObjectView.ts index fee1add..ac99aed 100644 --- a/src/sqlite/SqliteObjectView.ts +++ b/src/sqlite/SqliteObjectView.ts @@ -1,6 +1,7 @@ import { AbstractSqliteView } from './AbstractSqliteView'; import { IObjectStorage, IEventLocker } from '../interfaces'; import { SqliteObjectStorage } from './SqliteObjectStorage'; +import { Database } from 'better-sqlite3'; export class SqliteObjectView extends AbstractSqliteView implements IObjectStorage, IEventLocker { @@ -23,6 +24,11 @@ export class SqliteObjectView extends AbstractSqliteView implements IOb }); } + // 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'); From 9e187b8ccda78aaf073ad0afb64815b393478820 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 16:56:04 +0100 Subject: [PATCH 056/135] 1.0.0-rc.7 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c2567..a2f74d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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)) + + # [1.0.0-rc.6](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.5...v1.0.0-rc.6) (2025-03-21) diff --git a/package-lock.json b/package-lock.json index b547e2d..8a41ef6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.7", "license": "MIT", "dependencies": { "di0": "^1.0.0" diff --git a/package.json b/package.json index 56f0886..da7d9d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.6", + "version": "1.0.0-rc.7", "description": "Basic ES6 backbone for CQRS app development", "repository": { "type": "git", From 5ae3d2ba78a5d124452f57dfd43a41b493a8ed28 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 16:58:44 +0100 Subject: [PATCH 057/135] Add optional metadata to event publishing methods --- src/EventDispatcher.ts | 7 +++-- src/in-memory/InMemoryMessageBus.ts | 47 ++++++++++++++-------------- src/interfaces/IEventBus.ts | 2 +- src/interfaces/IMessageBus.ts | 2 +- tests/unit/EventDispatcher.test.ts | 4 +-- tests/unit/dispatch-pipeline.test.ts | 4 +-- 6 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 1521846..7efca47 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -115,8 +115,11 @@ export class EventDispatcher implements IEventDispatcher { const events = data.map(e => e.event).filter(notEmpty); try { - for (const event of events) - this.eventBus.publish(event); + for (const batch of data) { + const { event, ...meta } = batch; + if (event) + this.eventBus.publish(event, meta); + } resolve(events); } diff --git a/src/in-memory/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts index 261e1c5..b678096 100644 --- a/src/in-memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -14,18 +14,17 @@ import { */ export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcessor { - #handlers: Map> = new Map(); - #name: string | undefined; - #uniqueEventHandlers: boolean; - // eslint-disable-next-line no-use-before-define - #queues: Map = new Map(); - - constructor({ name, uniqueEventHandlers = !!name }: { - name?: string, + protected handlers: Map> = new Map(); + protected uniqueEventHandlers: boolean; + protected queueName: string | undefined; + protected queues: Map = new Map(); + + constructor({ queueName, uniqueEventHandlers = !!queueName }: { + queueName?: string, uniqueEventHandlers?: boolean } = {}) { - this.#name = name; - this.#uniqueEventHandlers = uniqueEventHandlers; + this.queueName = queueName; + this.uniqueEventHandlers = uniqueEventHandlers; } /** @@ -43,23 +42,23 @@ export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcess // 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`); + 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); + 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; @@ -75,10 +74,10 @@ export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcess 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); } /** @@ -90,7 +89,7 @@ export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcess 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) @@ -111,8 +110,8 @@ export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcess 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 => + ...this.handlers.get(event.type) || [], + ...Array.from(this.queues.values()).map(namedQueue => (e: IEvent, m?: Record) => namedQueue.publish(e, m)) ]; diff --git a/src/interfaces/IEventBus.ts b/src/interfaces/IEventBus.ts index f36c593..0e37e07 100644 --- a/src/interfaces/IEventBus.ts +++ b/src/interfaces/IEventBus.ts @@ -2,7 +2,7 @@ import { IEvent } from './IEvent'; import { IObservable, isIObservable } from './IObservable'; export interface IEventBus extends IObservable { - publish(event: IEvent): Promise; + publish(event: IEvent, meta?: Record): Promise; } export const isIEventBus = (obj: unknown) => diff --git a/src/interfaces/IMessageBus.ts b/src/interfaces/IMessageBus.ts index d986b20..5b626a3 100644 --- a/src/interfaces/IMessageBus.ts +++ b/src/interfaces/IMessageBus.ts @@ -4,5 +4,5 @@ import { IObservable } from './IObservable'; export interface IMessageBus extends IObservable { send(command: ICommand): Promise; - publish(event: IEvent): Promise; + publish(event: IEvent, meta?: Record): Promise; } diff --git a/tests/unit/EventDispatcher.test.ts b/tests/unit/EventDispatcher.test.ts index f88048f..686082c 100644 --- a/tests/unit/EventDispatcher.test.ts +++ b/tests/unit/EventDispatcher.test.ts @@ -24,8 +24,8 @@ describe('EventDispatcher', () => { expect(processorMock.process).toHaveBeenCalledTimes(1); expect(eventBus.publish).toHaveBeenCalledTimes(2); - expect(eventBus.publish).toHaveBeenCalledWith(event1); - expect(eventBus.publish).toHaveBeenCalledWith(event2); + expect(eventBus.publish).toHaveBeenCalledWith(event1, {}); + expect(eventBus.publish).toHaveBeenCalledWith(event2, {}); expect(result).toEqual([event1, event2]); }); diff --git a/tests/unit/dispatch-pipeline.test.ts b/tests/unit/dispatch-pipeline.test.ts index 0fc7569..5797e74 100644 --- a/tests/unit/dispatch-pipeline.test.ts +++ b/tests/unit/dispatch-pipeline.test.ts @@ -79,7 +79,7 @@ describe('eventDispatchPipeline', () => { await eventDispatcher.dispatch([testEvent], { origin: 'external' }); expect(eventBus.publish).toHaveBeenCalledTimes(2); - expect(eventBus.publish).toHaveBeenNthCalledWith(1, testEvent); - expect(eventBus.publish).toHaveBeenNthCalledWith(2, testEvent); + expect(eventBus.publish).toHaveBeenNthCalledWith(1, testEvent, { origin: 'internal' }); + expect(eventBus.publish).toHaveBeenNthCalledWith(2, testEvent, { origin: 'external' }); }); }); From 0a5cfb1c4d2bbddc39eb9ff7b9f1b59fb2552129 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 16:59:32 +0100 Subject: [PATCH 058/135] 1.0.0-rc.8 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2f74d9..c07877c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# [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 + +* 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) diff --git a/package-lock.json b/package-lock.json index e902bed..30f5fd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.7", + "version": "1.0.0-rc.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.7", + "version": "1.0.0-rc.8", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.0.0", diff --git a/package.json b/package.json index 7d30fcf..b0afa83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.7", + "version": "1.0.0-rc.8", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 7299aba3881204e573da95a4e40ea56b5a3bf214 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 21:06:25 +0100 Subject: [PATCH 059/135] Remove integration test step from CI workflow --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2fe7111..4003a5b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,4 +22,3 @@ jobs: env: CI: true - run: npm run test - - run: npm run test:integration From 4549c7eb85f189198bcd38437e5220a5bcc59d19 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 21:07:13 +0100 Subject: [PATCH 060/135] Make code ES2022-compatible --- package.json | 4 ++-- src/EventDispatcher.ts | 9 ++++++++- src/rabbitmq/RabbitMqGateway.ts | 2 +- tsconfig.json | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b0afa83..7f3ad1f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "test": "tests" }, "engines": { - "node": ">=10.3.0" + "node": ">=18.0.0" }, "scripts": { "pretest": "npm run build", @@ -96,4 +96,4 @@ "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } -} +} \ No newline at end of file diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 7efca47..437d97e 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -146,7 +146,14 @@ export class EventDispatcher implements IEventDispatcher { if (!isEventSet(events) || events.length === 0) throw new Error('dispatch requires a non-empty array of events'); - const { promise, resolve, reject } = Promise.withResolvers(); + // const { promise, resolve, reject } = Promise.withResolvers(); + 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, diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 48121b6..f43b6e6 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -144,7 +144,7 @@ export class RabbitMqGateway { async #stopConsuming() { this.#logger?.info(`${this.#appId}: Stopping all consumers...`); - const cancellations = this.#queueConsumers.entries().map(async ([queueName, { channel, consumerTag }]) => { + const cancellations = [...this.#queueConsumers.entries()].map(async ([queueName, { channel, consumerTag }]) => { this.#logger?.debug(`${this.#appId}: Cancelling consumer "${consumerTag}" for queue "${queueName}"`); try { await channel.cancel(consumerTag); diff --git a/tsconfig.json b/tsconfig.json index 28efcd5..5e99f1f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "sourceMap": true, "alwaysStrict": false, "outDir": "./dist", - "target": "ESNext", + "target": "ES2022", "declaration": true, "declarationDir": "./types", "allowSyntheticDefaultImports": true, From b90ccd878b22eb33655b67805e3b53b2bce50079 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 13 Apr 2025 21:07:25 +0100 Subject: [PATCH 061/135] 1.0.0-rc.9 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c07877c..355bdf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) + + + # [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) diff --git a/package-lock.json b/package-lock.json index 30f5fd9..834f5aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.8", + "version": "1.0.0-rc.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.8", + "version": "1.0.0-rc.9", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.0.0", diff --git a/package.json b/package.json index 7f3ad1f..7f91d08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.8", + "version": "1.0.0-rc.9", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", @@ -96,4 +96,4 @@ "better-sqlite3": "^11.3.0", "md5": "^2.3.0" } -} \ No newline at end of file +} From b2724739b3ff483b13c0cfeea30c73c7d8ab8b94 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 14 Apr 2025 00:29:44 +0100 Subject: [PATCH 062/135] Fix: asserting db connection in prolongLock and unlock methods --- src/sqlite/SqliteViewLocker.ts | 8 ++++++-- tests/unit/sqlite/SqliteViewLocker.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sqlite/SqliteViewLocker.ts b/src/sqlite/SqliteViewLocker.ts index 0c3d113..b5f8e49 100644 --- a/src/sqlite/SqliteViewLocker.ts +++ b/src/sqlite/SqliteViewLocker.ts @@ -136,7 +136,9 @@ export class SqliteViewLocker extends AbstractSqliteAccessor implements IViewLoc this.#logger?.debug(`"${this.#projectionName}" lock refresh canceled`); } - private prolongLock() { + 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) @@ -145,12 +147,14 @@ export class SqliteViewLocker extends AbstractSqliteAccessor implements IViewLoc this.#logger?.debug(`"${this.#projectionName}" lock prolonged for ${this.#viewLockTtl}s`); } - unlock() { + 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`); diff --git a/tests/unit/sqlite/SqliteViewLocker.test.ts b/tests/unit/sqlite/SqliteViewLocker.test.ts index 9c868ca..fd0f8c5 100644 --- a/tests/unit/sqlite/SqliteViewLocker.test.ts +++ b/tests/unit/sqlite/SqliteViewLocker.test.ts @@ -96,7 +96,7 @@ describe('SqliteViewLocker', function () { it('should release the lock upon unlock()', async function () { await firstLock.lock(); - firstLock.unlock(); + await firstLock.unlock(); const row = viewModelSqliteDb.prepare('SELECT * FROM tbl_view_lock WHERE projection_name = ? AND schema_version = ?') .get('test', '1.0') as any; @@ -106,7 +106,7 @@ describe('SqliteViewLocker', function () { it('should fail to prolong the lock if already released', async function () { await firstLock.lock(); - firstLock.unlock(); + await firstLock.unlock(); let error; try { From 54348aca4e751a0dd987c9c7e6e7c2bb5bfa2bd0 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 14 Apr 2025 00:29:56 +0100 Subject: [PATCH 063/135] 1.0.0-rc.10 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 355bdf3..79cf36d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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 + +* Asserting db connection in prolongLock and unlock methods ([b272473](https://github.com/snatalenko/node-cqrs/commit/b2724739b3ff483b13c0cfeea30c73c7d8ab8b94)) + + # [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) diff --git a/package-lock.json b/package-lock.json index 834f5aa..151260d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.9", + "version": "1.0.0-rc.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.9", + "version": "1.0.0-rc.10", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.0.0", diff --git a/package.json b/package.json index 7f91d08..a688324 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.9", + "version": "1.0.0-rc.10", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 1d0e827da71c760739588a37ae6afe63a4fa8d34 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 8 May 2025 23:35:26 +0100 Subject: [PATCH 064/135] Chore: use `structuredClone` for snapshot creation --- .eslintrc.json | 799 --------------------------------------- package-lock.json | 24 +- package.json | 1 + src/AbstractAggregate.ts | 13 +- 4 files changed, 23 insertions(+), 814 deletions(-) delete mode 100644 .eslintrc.json 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/package-lock.json b/package-lock.json index 151260d..9218683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "conventional-changelog": "^3.1.25", "eslint": "^9.24.0", "eslint-plugin-jest": "^28.11.0", + "globals": "^16.1.0", "jest": "^29.7.0", "sinon": "^19.0.2", "ts-jest": "^29.2.5", @@ -36,7 +37,7 @@ "typescript-eslint": "^8.29.0" }, "engines": { - "node": ">=10.3.0" + "node": ">=18.0.0" }, "peerDependencies": { "amqplib": "^0.10.5", @@ -562,6 +563,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", @@ -3863,13 +3874,16 @@ } }, "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.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/graceful-fs": { diff --git a/package.json b/package.json index a688324..70bcdec 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "conventional-changelog": "^3.1.25", "eslint": "^9.24.0", "eslint-plugin-jest": "^28.11.0", + "globals": "^16.1.0", "jest": "^29.7.0", "sinon": "^19.0.2", "ts-jest": "^29.2.5", diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index 1c3fdff..57253d0 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -10,13 +10,6 @@ import { import { getClassName, validateHandlers, getHandler, getMessageHandlerNames } from './utils'; -/** - * Deep-clone simple JS object - */ -function clone(obj: T): T { - return JSON.parse(JSON.stringify(obj)); -} - const SNAPSHOT_EVENT_TYPE = 'snapshot'; /** @@ -192,11 +185,11 @@ export abstract class AbstractAggregate Date: Thu, 8 May 2025 23:44:47 +0100 Subject: [PATCH 065/135] Change: Cache immediate aggregates to handle concurrent commands --- src/AbstractAggregate.ts | 10 ++- src/AggregateCommandHandler.ts | 96 +++++++++++++-------- src/CqrsContainerBuilder.ts | 2 +- src/interfaces/IAggregate.ts | 32 +++++-- src/utils/Lock.ts | 96 +++++++++++++++------ src/utils/MapAssertable.ts | 30 +++++++ src/utils/index.ts | 1 + tests/unit/AggregateCommandHandler.test.ts | 99 ++++++++++++++++++++-- tests/unit/Lock.test.ts | 33 ++++---- 9 files changed, 303 insertions(+), 96 deletions(-) create mode 100644 src/utils/MapAssertable.ts diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index 57253d0..12f7eee 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -55,7 +55,10 @@ export abstract class AbstractAggregate(type: string, payload?: TPayload) { if (typeof type !== 'string' || !type.length) diff --git a/src/AggregateCommandHandler.ts b/src/AggregateCommandHandler.ts index e404a52..1ed7ecc 100644 --- a/src/AggregateCommandHandler.ts +++ b/src/AggregateCommandHandler.ts @@ -1,23 +1,19 @@ +import { getClassName, Lock, MapAssertable } from './utils'; import { IAggregate, IAggregateConstructor, IAggregateFactory, ICommand, - ICommandBus, ICommandHandler, IContainer, Identifier, IEventSet, IEventStore, - ILogger + ILogger, + IObservable, + isIObservable } from './interfaces'; -import { - iteratorToArray, - getClassName, - subscribe -} from './utils'; - /** * Aggregate command handler. * @@ -25,14 +21,19 @@ import { * Upon command receiving creates an instance of aggregate, * restores its state, passes command and commits emitted events to event store. */ -export class AggregateCommandHandler implements ICommandHandler { +export class AggregateCommandHandler 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, @@ -40,8 +41,8 @@ export class AggregateCommandHandler implements ICommandHandler { handles, logger }: Pick & { - aggregateType?: IAggregateConstructor, - aggregateFactory?: IAggregateFactory, + aggregateType?: IAggregateConstructor, + aggregateFactory?: IAggregateFactory, handles?: string[] }) { if (!eventStore) @@ -70,30 +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 eventsIterable = this.#eventStore.getAggregateEvents(id); - const events = await iteratorToArray(eventsIterable); + const aggregate = this.#aggregateFactory({ id }); - const aggregate = this.#aggregateFactory({ id, events }); + 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,6 +109,13 @@ 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) @@ -108,24 +123,31 @@ export class AggregateCommandHandler implements ICommandHandler { 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 { + // pass command to aggregate instance + // multiple concurrent calls will execute sequentially + const events = await this.#executionLock.runExclusively(String(aggregate.id), async () => { + await aggregate.handle(cmd); - let events = aggregate.changes; - this.#logger?.info(`${aggregate} "${cmd.type}" command processed, ${events.length} event(s) produced`); - if (!events.length) - return events; + if (aggregate.shouldTakeSnapshot) + aggregate.takeSnapshot?.(); - if (aggregate.shouldTakeSnapshot) { - aggregate.takeSnapshot(); - events = aggregate.changes; - } + return aggregate.popChanges(); + }); + + this.#logger?.info(`${aggregate} "${cmd.type}" command processed, ${events.length} event(s) produced`); - await this.#eventStore.dispatch(events); + if (events.length) + await this.#eventStore.dispatch(events); - return events; + return events; + } + finally { + this.#aggregatesCache.release(aggregate.id); + } } } diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 95eba03..342bd96 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -91,7 +91,7 @@ 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'); diff --git a/src/interfaces/IAggregate.ts b/src/interfaces/IAggregate.ts index 8dcc883..00ff865 100644 --- a/src/interfaces/IAggregate.ts +++ b/src/interfaces/IAggregate.ts @@ -11,17 +11,26 @@ export interface IAggregate { /** Unique aggregate identifier */ readonly id: Identifier; + /** Update aggregate state with event */ + mutate(event: IEvent): void; + /** Main entry point for aggregate commands */ handle(command: ICommand): void | Promise; - /** List of events emitted by Aggregate as a result of handling command(s) */ + /** Get events emitted during command(s) handling and reset the `changes` collection */ + popChanges(): IEventSet; + + /** + * List of events emitted by Aggregate as a result of handling command(s) + * @deprecated use `popChanges()` instead + */ 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; + takeSnapshot?(): void; } export interface IMutableAggregateState { @@ -41,18 +50,25 @@ export type IAggregateConstructorParams { +export interface IAggregateConstructor< + TAggregate extends IAggregate, + TState extends IMutableAggregateState | object | void +> { readonly handles: string[]; - new(options: IAggregateConstructorParams): IAggregate; + new(options: IAggregateConstructorParams): TAggregate; } -export type IAggregateFactory = - (options: IAggregateConstructorParams) => IAggregate; - +export type IAggregateFactory< + TAggregate extends IAggregate, + TState extends IMutableAggregateState | object | void +> = (options: IAggregateConstructorParams) => TAggregate; diff --git a/src/utils/Lock.ts b/src/utils/Lock.ts index de00359..8134643 100644 --- a/src/utils/Lock.ts +++ b/src/utils/Lock.ts @@ -1,53 +1,99 @@ import { Deferred } from './Deferred'; -/** - * Provides a simple asynchronous lock mechanism. - * Useful for ensuring that only one asynchronous operation proceeds at a time - * for a specific resource or section of code. - */ export class Lock { - #deferred?: Deferred; + /** + * 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 + ); + } - get isLocked(): boolean { - return !(this.#deferred?.settled ?? true); + + isLocked(name?: string): boolean { + return !!this.#getAnyBlockingLock(name); } /** - * Wait until lock is released, then acquire it + * Acquire named or global lock + * + * @returns Promise that resolves once lock is acquired */ - async acquire(): Promise { - // the below code cannot be replaced with `await this.unblocked()` + 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 (this.isLocked) - await this.#deferred?.promise; + // 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(); - this.#deferred = new Deferred(); + if (isGlobal) { + this.#globalLockAcquiringLock?.resolve(); + this.#globalLockAcquiringLock = undefined; + } } /** - * Returns a promise that is resolved once lock is released + * @returns Promise that resolves once lock is released */ - async unblocked(): Promise { - while (this.isLocked) - await this.#deferred?.promise; + async waitForUnlock(name?: string): Promise { + while (this.isLocked(name)) + await this.#getAnyBlockingLock(name)?.promise; } - release(): void { - this.#deferred?.resolve(); - this.#deferred = undefined; + /** + * 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 runLocked(callback: () => Promise) { + async runExclusively(name: string | undefined, callback: () => Promise): Promise { try { - await this.acquire(); - await callback(); + await this.acquire(name); + return await callback(); } finally { - this.release(); + 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/index.ts b/src/utils/index.ts index c9bfc05..ada7ab1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,6 +6,7 @@ export * from './getMessageHandlerNames'; export * from './isClass'; export * from './iteratorToArray'; export * from './Lock'; +export * from './MapAssertable'; export * from './notEmpty'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index 837d9b1..d59246c 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -41,6 +41,7 @@ class CommandBus { on(messageType, listener) { this.handlers[messageType] = listener; } + off() { } } describe('AggregateCommandHandler', function () { @@ -48,7 +49,7 @@ describe('AggregateCommandHandler', function () { // this.timeout(500); // this.slow(300); - let eventStorageReader: InMemoryEventStorage; + let eventStorage: InMemoryEventStorage; let snapshotStorage: InMemorySnapshotStorage; let eventStore: IEventStore; let commandBus: ICommandBus; @@ -60,16 +61,21 @@ describe('AggregateCommandHandler', function () { beforeEach(() => { eventBus = new InMemoryMessageBus(); - eventStorageReader = new InMemoryEventStorage(); + eventStorage = new InMemoryEventStorage(); snapshotStorage = new InMemorySnapshotStorage(); - const eventDispatcher = new EventDispatcher({ eventBus }); + const eventDispatcher = new EventDispatcher({ + eventDispatchPipeline: [ + eventStorage + ], + eventBus + }); eventStore = new EventStore({ - eventStorageReader, + eventStorageReader: eventStorage, snapshotStorage, eventBus, eventDispatcher, - identifierProvider: eventStorageReader + identifierProvider: eventStorage }); getNewIdSpy = sinon.spy(eventStore, 'getNewId'); getAggregateEventsSpy = sinon.spy(eventStore, 'getAggregateEvents'); @@ -220,9 +226,84 @@ describe('AggregateCommandHandler', function () { 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('produces events with sequential versions for concurrent commands to the same aggregate', async () => { + + const handler = new AggregateCommandHandler({ eventStore, aggregateType: MyAggregate }); + const aggregateId = 'concurrent-test-id'; + + // Ensure aggregate exists + await handler.execute({ type: 'createAggregate', aggregateId }); + + const command1 = { type: 'doSomething', aggregateId }; + const command2 = { type: 'doSomething', aggregateId }; + + // Execute commands concurrently + await Promise.all([ + handler.execute(command1), + handler.execute(command2) + ]); + + // 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 }); + + // Reset spies/counters before the main test part + getAggregateEventsSpy.resetHistory(); + factoryCallCount = 0; + + const command1 = { type: 'doSomething', aggregateId }; + const command2 = { type: 'doSomething', aggregateId }; + + // Execute commands concurrently + await Promise.all([ + handler.execute(command1), + handler.execute(command2) + ]); + + // 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'); + + + getAggregateEventsSpy.resetHistory(); + factoryCallCount = 0; + + // Execute a third command after the first two completed + const command3 = { type: 'doSomething', aggregateId }; + await handler.execute(command3); + + // 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/Lock.test.ts b/tests/unit/Lock.test.ts index d75270a..1b56315 100644 --- a/tests/unit/Lock.test.ts +++ b/tests/unit/Lock.test.ts @@ -55,35 +55,38 @@ describe('Lock', () => { describe('isLocked', () => { it('returns `false` when lock is not acquired', async () => { - expect(lock).toHaveProperty('isLocked', false); + expect(lock).toHaveProperty('isLocked'); + expect(lock.isLocked()).toBe(false); }); it('returns `true` when lock is acquired', async () => { await lock.acquire(); - expect(lock).toHaveProperty('isLocked', true); + 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', false); + expect(lock).toHaveProperty('isLocked'); + expect(lock.isLocked()).toBe(false); }); }); - describe('runLocked', () => { + describe('runExclusively', () => { it('executes callback with lock acquired', async () => { let p1status = 'not-started'; let p2status = 'not-started'; - const p1 = lock.runLocked(async () => { + const p1 = lock.runExclusively(undefined, async () => { p1status = 'started'; await delay(10); p1status = 'processed'; }); - const p2 = lock.runLocked(async () => { + const p2 = lock.runExclusively(undefined, async () => { p2status = 'started'; await delay(5); p2status = 'processed'; @@ -114,36 +117,36 @@ describe('Lock', () => { }); }); - describe('unblocked', () => { + describe('waitForUnlock', () => { it('returns Promise', () => { - expect(lock).toHaveProperty('unblocked'); - expect(lock.unblocked()).toBeInstanceOf(Promise); + expect(lock).toHaveProperty('waitForUnlock'); + expect(lock.waitForUnlock()).toBeInstanceOf(Promise); }); it('returns resolved promise when lock is not acquired', async () => { - await expect(isResolved(lock.unblocked())).resolves.toBe(true); + await expect(isResolved(lock.waitForUnlock())).resolves.toBe(true); }); it('returns pending promise when lock is acquired', async () => { await lock.acquire(); - await expect(isResolved(lock.unblocked())).resolves.toBe(false); + 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.unblocked())).resolves.toBe(true); + 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.unblocked(); - const p3 = lock.unblocked(); + const p2 = lock.waitForUnlock(); + const p3 = lock.waitForUnlock(); const l4 = lock.acquire(); // blocking process (i.e. update_by_query) - const p5 = lock.unblocked(); + const p5 = lock.waitForUnlock(); const l6 = lock.acquire(); // blocking process (i.e. update_by_query) // Check all are pending initially From ddaf7a8e057e90f2f0a4d1795154a466cab51228 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 8 May 2025 23:47:08 +0100 Subject: [PATCH 066/135] Fix eslint --- eslint.config.mjs | 3 ++- tests/integration/rabbitmq/RabbitMqGateway.test.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index fe2f193..4e2dec9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -7,7 +7,8 @@ import globals from "globals"; export default defineConfig([ globalIgnores([ "coverage/*", - "dist/*" + "dist/*", + "types/*" ]), { files: [ diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index f43c653..f04872f 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -421,7 +421,6 @@ describe('RabbitMqGateway', () => { it('stops receiving messages on SIGINT', async () => { const received: IMessage[] = []; - const process = new EventEmitter(); const handlerBlocker = new Deferred(); const message: IMessage = { type: 'test.sigint', From 3e141fd217c4a094a57fefe8788816d474020ffe Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 9 May 2025 22:50:58 +0100 Subject: [PATCH 067/135] Refactor: Simplify aggregate interface --- src/AbstractAggregate.ts | 56 +++++++++------------- src/AggregateCommandHandler.ts | 19 ++++---- src/interfaces/IAggregate.ts | 38 +++++---------- src/utils/Lock.ts | 2 +- tests/unit/AbstractAggregate.test.ts | 46 +++++++++--------- tests/unit/AggregateCommandHandler.test.ts | 10 ++-- 6 files changed, 74 insertions(+), 97 deletions(-) diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index 12f7eee..1975d24 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -55,14 +55,6 @@ 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: IEvent) { if (event.aggregateVersion !== undefined) @@ -131,8 +107,29 @@ export abstract class AbstractAggregate implements I const aggregate = await this.#getAggregateInstance(cmd.aggregateId); try { - // pass command to aggregate instance - // multiple concurrent calls will execute sequentially - const events = await this.#executionLock.runExclusively(String(aggregate.id), async () => { - await aggregate.handle(cmd); - - if (aggregate.shouldTakeSnapshot) - aggregate.takeSnapshot?.(); + // multiple concurrent commands to a same aggregateId will execute sequentially + if (cmd.aggregateId) + this.#executionLock.acquire(String(cmd.aggregateId)); - return aggregate.popChanges(); - }); + // pass command to aggregate instance + const events = await aggregate.handle(cmd); this.#logger?.info(`${aggregate} "${cmd.type}" command processed, ${events.length} event(s) produced`); @@ -147,7 +143,10 @@ export class AggregateCommandHandler implements I return events; } finally { - this.#aggregatesCache.release(aggregate.id); + if (cmd.aggregateId) { + this.#executionLock.release(String(cmd.aggregateId)); + this.#aggregatesCache.release(cmd.aggregateId); + } } } } diff --git a/src/interfaces/IAggregate.ts b/src/interfaces/IAggregate.ts index 00ff865..633f7cb 100644 --- a/src/interfaces/IAggregate.ts +++ b/src/interfaces/IAggregate.ts @@ -8,43 +8,29 @@ import { IEventSet } from './IEventSet'; */ export interface IAggregate { - /** Unique aggregate identifier */ - readonly id: Identifier; - - /** Update aggregate state with event */ + /** + * Apply a single event to mutate the aggregate's state. + * + * Used by `AggregateCommandHandler` when restoring the aggregate state from the event store. + */ mutate(event: IEvent): void; - /** Main entry point for aggregate commands */ - handle(command: ICommand): void | Promise; - - /** Get events emitted during command(s) handling and reset the `changes` collection */ - popChanges(): IEventSet; - /** - * List of events emitted by Aggregate as a result of handling command(s) - * @deprecated use `popChanges()` instead + * Process a command sent to the aggregate. + * + * This is the main entry point for handling aggregate commands. */ - 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; + handle(command: ICommand): IEventSet | Promise; } export interface IMutableAggregateState { - // schemaVersion?: number; - // constructor: IAggregateStateConstructor; + /** + * Apply a single event to mutate the aggregate's state. + */ mutate(event: IEvent): void; } -// export interface IAggregateStateConstructor extends Function { -// schemaVersion?: number; -// new(): IAggregateState; -// } - export type IAggregateConstructorParams = { /** Unique aggregate identifier */ diff --git a/src/utils/Lock.ts b/src/utils/Lock.ts index 8134643..eafeedc 100644 --- a/src/utils/Lock.ts +++ b/src/utils/Lock.ts @@ -87,7 +87,7 @@ export class Lock { /** * Execute callback with lock acquired, then release lock */ - async runExclusively(name: string | undefined, callback: () => Promise): Promise { + async runExclusively(name: string | undefined, callback: () => Promise | T): Promise { try { await this.acquire(name); return await callback(); diff --git a/tests/unit/AbstractAggregate.test.ts b/tests/unit/AbstractAggregate.test.ts index 7cfcf73..9de2c7b 100644 --- a/tests/unit/AbstractAggregate.test.ts +++ b/tests/unit/AbstractAggregate.test.ts @@ -87,25 +87,21 @@ describe('AbstractAggregate', function () { }); }); - describe('changes', () => { + describe('popChanges', () => { - it('contains an EventStream of changes happened in aggregate', () => { + it('contains an EventStream of changes happened in aggregate', async () => { - const { changes } = agg; + const changes0 = agg.popChanges(); - 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); + expect(changes0).to.be.an('Array'); + expect(changes0).to.be.empty; - return agg.doSomething({}).then(() => { + const changes = 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(changes).to.not.equal(changes0); + expect(changes).to.have.nested.property('[0].type', 'somethingDone'); + expect(changes).to.have.nested.property('[0].aggregateId', 1); + expect(changes).to.have.nested.property('[0].aggregateVersion', 0); }); }); @@ -155,9 +151,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 () => { @@ -186,7 +182,9 @@ describe('AbstractAggregate', function () { it('pushes new event to #changes', () => { (agg as any).emit('eventType', {}); - expect(agg).to.have.nested.property('changes[0].type', 'eventType'); + + const changes = agg.popChanges(); + expect(changes).to.have.nested.property('[0].type', 'eventType'); }); it('increments aggregate #version', () => { @@ -271,19 +269,23 @@ describe('AbstractAggregate', function () { }); }); - describe('takeSnapshot()', () => { + describe('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); diff --git a/tests/unit/AggregateCommandHandler.test.ts b/tests/unit/AggregateCommandHandler.test.ts index d59246c..319247a 100644 --- a/tests/unit/AggregateCommandHandler.test.ts +++ b/tests/unit/AggregateCommandHandler.test.ts @@ -190,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 @@ -201,7 +201,7 @@ describe('AggregateCommandHandler', function () { return this.version !== 0 && this.version % 2 === 0; } }); - sinon.spy(aggregate, 'takeSnapshot'); + sinon.spy(aggregate, 'makeSnapshot'); const handler = new AggregateCommandHandler({ eventStore, @@ -211,17 +211,17 @@ describe('AggregateCommandHandler', function () { // 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; From efc980cc0f7f7fc021a8fcadd913806fb5af2707 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 9 May 2025 22:51:34 +0100 Subject: [PATCH 068/135] 1.0.0-rc.11 --- CHANGELOG.md | 13 +++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79cf36d..2d02243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# [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 + +* Cache immediate aggregates to handle concurrent commands ([e193c4c](https://github.com/snatalenko/node-cqrs/commit/e193c4c8dc7b91de6cbc84e2ac668170ddb48bc0)) +* Use `structuredClone` for snapshot creation ([1d0e827](https://github.com/snatalenko/node-cqrs/commit/1d0e827da71c760739588a37ae6afe63a4fa8d34)) + +### Refactoring + +* Simplify aggregate interface ([3e141fd](https://github.com/snatalenko/node-cqrs/commit/3e141fd217c4a094a57fefe8788816d474020ffe)) + + # [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) diff --git a/package-lock.json b/package-lock.json index 9218683..25100d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.10", + "version": "1.0.0-rc.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.10", + "version": "1.0.0-rc.11", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.0.0", diff --git a/package.json b/package.json index 70bcdec..09fb402 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.10", + "version": "1.0.0-rc.11", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 8e38606fadee78fb93107d6190ebc75a28a7d785 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 11 Aug 2025 16:34:14 +0100 Subject: [PATCH 069/135] Fix missing awaits --- src/AggregateCommandHandler.ts | 2 +- src/EventDispatcher.ts | 10 ++++++---- src/rabbitmq/RabbitMqEventBus.ts | 4 ++-- src/sqlite/AbstractSqliteAccessor.ts | 2 +- src/sqlite/SqliteObjectStorage.ts | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/AggregateCommandHandler.ts b/src/AggregateCommandHandler.ts index c2a107d..9c8c415 100644 --- a/src/AggregateCommandHandler.ts +++ b/src/AggregateCommandHandler.ts @@ -130,7 +130,7 @@ export class AggregateCommandHandler implements I try { // multiple concurrent commands to a same aggregateId will execute sequentially if (cmd.aggregateId) - this.#executionLock.acquire(String(cmd.aggregateId)); + await this.#executionLock.acquire(String(cmd.aggregateId)); // pass command to aggregate instance const events = await aggregate.handle(cmd); diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 437d97e..968415e 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -112,13 +112,15 @@ export class EventDispatcher implements IEventDispatcher { continue; } - const events = data.map(e => e.event).filter(notEmpty); - try { + const events: IEvent[] = []; + for (const batch of data) { const { event, ...meta } = batch; - if (event) - this.eventBus.publish(event, meta); + if (event) { + await this.eventBus.publish(event, meta); + events.push(event); + } } resolve(events); diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts index b47115b..46553b4 100644 --- a/src/rabbitmq/RabbitMqEventBus.ts +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -53,8 +53,8 @@ export class RabbitMqEventBus implements IEventBus, IDispatchPipelineProcessor { /** * Removes a previously registered message handler for a specific event type. */ - off(eventType: string, handler: IMessageHandler): void { - this.#gateway.unsubscribe({ + async off(eventType: string, handler: IMessageHandler): Promise { + await this.#gateway.unsubscribe({ exchange: this.#exchange, queueName: this.#queueName, eventType, diff --git a/src/sqlite/AbstractSqliteAccessor.ts b/src/sqlite/AbstractSqliteAccessor.ts index dc5d249..27d3c08 100644 --- a/src/sqlite/AbstractSqliteAccessor.ts +++ b/src/sqlite/AbstractSqliteAccessor.ts @@ -40,7 +40,7 @@ export abstract class AbstractSqliteAccessor { return; try { - this.#initLocker.acquire(); + await this.#initLocker.acquire(); if (this.#initialized) return; diff --git a/src/sqlite/SqliteObjectStorage.ts b/src/sqlite/SqliteObjectStorage.ts index 9babd4f..fb043f1 100644 --- a/src/sqlite/SqliteObjectStorage.ts +++ b/src/sqlite/SqliteObjectStorage.ts @@ -125,9 +125,9 @@ export class SqliteObjectStorage extends AbstractSqliteAccessor impleme // it's safe to get then modify within this process const record = this.#getQuery.get(guid(id)); if (record) - this.update(id, update); + await this.update(id, update); else - this.create(id, update()); + await this.create(id, update()); } async delete(id: string): Promise { From 79d5cb259baa68b70ee4eeb037d133caba52cdf6 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 11 Aug 2025 16:42:29 +0100 Subject: [PATCH 070/135] Update dependencies --- package-lock.json | 1566 ++++++++++++++++++++++++--------------------- package.json | 28 +- 2 files changed, 862 insertions(+), 732 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25100d5..063550d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,64 +9,42 @@ "version": "1.0.0-rc.11", "license": "MIT", "dependencies": { - "async-iterable-buffer": "^1.0.0", + "async-iterable-buffer": "^1.1.0", "async-parallel-pipe": "^1.0.2", "di0": "^1.0.0" }, "devDependencies": { - "@stylistic/eslint-plugin-ts": "^4.2.0", + "@stylistic/eslint-plugin-ts": "^4.4.1", "@types/amqplib": "^0.10.7", - "@types/better-sqlite3": "^7.6.11", + "@types/better-sqlite3": "^7.6.13", "@types/chai": "^4.3.20", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/md5": "^2.3.5", - "@types/node": "^20.16.9", + "@types/node": "^20.19.10", "@types/sinon": "^17.0.4", "@typescript-eslint/eslint-plugin": "^8.29.0", "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "eslint": "^9.24.0", - "eslint-plugin-jest": "^28.11.0", - "globals": "^16.1.0", + "eslint": "^9.33.0", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.3.0", "jest": "^29.7.0", - "sinon": "^19.0.2", - "ts-jest": "^29.2.5", + "sinon": "^19.0.5", + "ts-jest": "^29.4.1", "ts-node": "^10.9.2", - "typescript": "^5.6.2", - "typescript-eslint": "^8.29.0" + "typescript": "^5.9.2", + "typescript-eslint": "^8.39.0" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "amqplib": "^0.10.5", - "better-sqlite3": "^11.3.0", + "amqplib": "^0.10.8", + "better-sqlite3": "^11.10.0", "md5": "^2.3.0" } }, - "node_modules/@acuminous/bitsyntax": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", - "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "buffer-more-ints": "~1.0.0", - "debug": "^4.3.4", - "safe-buffer": "~5.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/@acuminous/bitsyntax/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "peer": true - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -82,24 +60,24 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -107,22 +85,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -137,17 +115,27 @@ "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.26.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", - "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.10", - "@babel/types": "^7.26.10", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -155,14 +143,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -181,6 +169,16 @@ "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", @@ -188,30 +186,40 @@ "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.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -221,9 +229,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { @@ -231,9 +239,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "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": { @@ -241,9 +249,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -251,9 +259,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "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": { @@ -261,27 +269,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", - "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.10" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -346,13 +354,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -388,13 +396,13 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", - "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -514,13 +522,13 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", - "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -530,58 +538,48 @@ } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", - "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", - "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -619,9 +617,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", - "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { @@ -661,9 +659,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -675,10 +673,34 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -686,9 +708,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -722,12 +744,16 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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": "Python-2.0" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", @@ -742,27 +768,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "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": { - "argparse": "^2.0.1" + "brace-expansion": "^1.1.7" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": "*" } }, "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "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": { @@ -776,32 +815,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.13.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -855,9 +881,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "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": { @@ -895,6 +921,96 @@ "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.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "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", @@ -1198,18 +1314,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "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" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1222,27 +1334,17 @@ "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==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "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.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1326,14 +1428,13 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "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": "^3.0.1", - "lodash.get": "^4.4.2", "type-detect": "^4.1.0" } }, @@ -1345,13 +1446,13 @@ "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@stylistic/eslint-plugin-ts": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-4.2.0.tgz", - "integrity": "sha512-j2o2GvOx9v66x8hmp/HJ+0T+nOppiO5ycGsCkifh7JPGgjxEhpkGmIGx3RWsoxpWbad3VCX8e8/T8n3+7ze1Zg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-4.4.1.tgz", + "integrity": "sha512-2r6cLcmdF6til66lx8esBYvBvsn7xCmLT50gw/n1rGGlTq/OxeNjBIh4c3VEaDGMa/5TybrZTia6sQUHdIWx1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.23.0", + "@typescript-eslint/utils": "^8.32.1", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0" }, @@ -1415,9 +1516,9 @@ } }, "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": { @@ -1436,19 +1537,19 @@ } }, "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.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/better-sqlite3": { - "version": "7.6.12", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", - "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", + "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": { @@ -1463,9 +1564,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "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" }, @@ -1539,13 +1640,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.17.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.25.tgz", - "integrity": "sha512-bT+r2haIlplJUYtlZrEanFHdPIZTeiMeh/fSOEbOOfWf9uTn+lg8g0KU6Q3iMgjd9FLuuMAgfCNSkjUbxL6E3Q==", + "version": "20.19.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", + "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/normalize-package-data": { @@ -1597,21 +1698,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz", - "integrity": "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.29.0", - "@typescript-eslint/type-utils": "8.29.0", - "@typescript-eslint/utils": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1621,22 +1722,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.29.0.tgz", - "integrity": "sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.29.0", - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/typescript-estree": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "engines": { @@ -1648,18 +1749,19 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz", - "integrity": "sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0" + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1667,19 +1769,20 @@ "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.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz", - "integrity": "sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.29.0", - "@typescript-eslint/utils": "8.29.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1687,16 +1790,12 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.29.0.tgz", - "integrity": "sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", "dev": true, "license": "MIT", "engines": { @@ -1705,23 +1804,23 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz", - "integrity": "sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/visitor-keys": "8.29.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1731,59 +1830,64 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@typescript-eslint/types": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "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/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.29.0.tgz", - "integrity": "sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.29.0", - "@typescript-eslint/types": "8.29.0", - "@typescript-eslint/typescript-estree": "8.29.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1794,18 +1898,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz", - "integrity": "sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.29.0", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.39.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1816,9 +1920,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -1876,13 +1980,12 @@ } }, "node_modules/amqplib": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.5.tgz", - "integrity": "sha512-Dx5zmy0Ur+Q7LPPdhz+jx5IzmJBoHd15tOeAfQ8SuvEtyPJ20hBemhOBA4b1WeORCRa0ENM/kHCzmem1w/zHvQ==", + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.8.tgz", + "integrity": "sha512-Tfn1O9sFgAP8DqeMEpt2IacsVTENBpblB3SqLdn0jK2AeX8iyCvbptBc8lyATT9bQ31MsjVwUSQ1g8f4jHOUfw==", "license": "MIT", "peer": true, "dependencies": { - "@acuminous/bitsyntax": "^0.1.2", "buffer-more-ints": "~1.0.0", "url-parse": "~1.5.10" }, @@ -1954,14 +2057,11 @@ "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, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "license": "Python-2.0" }, "node_modules/array-ify": { "version": "1.0.0", @@ -1990,17 +2090,10 @@ "node": "*" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/async-iterable-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-iterable-buffer/-/async-iterable-buffer-1.0.0.tgz", - "integrity": "sha512-pZn6MjtoJFyr+RVy3O0BSRb8ibjSX9BlEh8trEqdtpV4DdnH7oM28Ke14r4QZuzQnSGj3tg1CrEToIxuvsfqsw==", + "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/async-parallel-pipe": { @@ -2065,6 +2158,16 @@ "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", @@ -2082,9 +2185,9 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "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": { @@ -2105,7 +2208,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -2154,9 +2257,9 @@ "peer": true }, "node_modules/better-sqlite3": { - "version": "11.9.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz", - "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", "hasInstallScript": true, "license": "MIT", "peer": true, @@ -2188,14 +2291,13 @@ } }, "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": { @@ -2212,9 +2314,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", "dev": true, "funding": [ { @@ -2232,10 +2334,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -2345,9 +2447,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001706", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz", - "integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==", + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", "dev": true, "funding": [ { @@ -2736,6 +2838,16 @@ "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", @@ -2860,9 +2972,10 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2930,9 +3043,9 @@ } }, "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.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2985,9 +3098,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "peer": true, "engines": { @@ -3043,26 +3156,10 @@ "node": ">=8" } }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.123", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.123.tgz", - "integrity": "sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==", + "version": "1.5.199", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", + "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", "dev": true, "license": "ISC" }, @@ -3087,9 +3184,9 @@ "license": "MIT" }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "peer": true, "dependencies": { @@ -3117,30 +3214,33 @@ } }, "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==", + "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": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3151,9 +3251,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.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", @@ -3188,9 +3288,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.11.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", - "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", + "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": { @@ -3214,9 +3314,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "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": { @@ -3231,9 +3331,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "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": { @@ -3243,78 +3343,50 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/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/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "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", - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 4" } }, - "node_modules/eslint/node_modules/p-locate": { - "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==", + "node_modules/eslint/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": "MIT", + "license": "ISC", "dependencies": { - "p-limit": "^3.0.2" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "*" } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "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.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3534,39 +3606,6 @@ "license": "MIT", "peer": true }, - "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, - "license": "Apache-2.0", - "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, - "license": "MIT", - "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==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3581,17 +3620,20 @@ } }, "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": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { @@ -3821,6 +3863,16 @@ "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", @@ -3873,10 +3925,34 @@ "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": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -4007,9 +4083,9 @@ "peer": true }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -4033,16 +4109,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/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/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -4277,19 +4343,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "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", @@ -4334,25 +4387,6 @@ "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, - "license": "Apache-2.0", - "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", @@ -4873,23 +4907,10 @@ "jest-util": "^29.7.0", "natural-compare": "^1.4.0", "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "semver": "^7.5.3" }, "engines": { - "node": ">=10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-util": { @@ -5001,14 +5022,13 @@ "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.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -5228,16 +5248,19 @@ } }, "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": { @@ -5247,14 +5270,6 @@ "dev": true, "license": "MIT" }, - "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==", - "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", - "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", @@ -5315,19 +5330,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "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", @@ -5396,6 +5398,20 @@ "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", @@ -5403,6 +5419,48 @@ "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", @@ -5558,16 +5616,19 @@ } }, "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": { @@ -5615,6 +5676,7 @@ "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": { @@ -5663,9 +5725,9 @@ } }, "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", "license": "MIT", "peer": true, "dependencies": { @@ -5675,19 +5737,6 @@ "node": ">=10" } }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5718,19 +5767,6 @@ "node": ">=10" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, - "license": "ISC", - "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", @@ -5814,29 +5850,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, - "license": "MIT", - "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" @@ -5995,9 +6018,9 @@ } }, "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": { @@ -6017,6 +6040,62 @@ "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", @@ -6104,9 +6183,9 @@ } }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "peer": true, "dependencies": { @@ -6435,7 +6514,7 @@ "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==", @@ -6445,6 +6524,16 @@ "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.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -6511,13 +6600,15 @@ "license": "MIT" }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/shebang-command": { @@ -6598,9 +6689,9 @@ } }, "node_modules/sinon": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.4.tgz", - "integrity": "sha512-myidFob7fjmYHJb+CHNLtAYScxn3sngGq4t75L2rCGGpE/k4OQVkN3KE5FsN+XkO2+fcDZ65PGvq3KHrlLAm7g==", + "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": { @@ -6694,9 +6785,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "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" }, @@ -6743,6 +6834,16 @@ "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", @@ -6867,9 +6968,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "license": "MIT", "peer": true, "dependencies": { @@ -6911,6 +7012,30 @@ "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", @@ -6982,20 +7107,20 @@ } }, "node_modules/ts-jest": { - "version": "29.2.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", - "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", - "ejs": "^3.1.10", "fast-json-stable-stringify": "^2.1.0", - "jest-util": "^29.0.0", + "handlebars": "^4.7.8", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.1", + "semver": "^7.7.2", + "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -7006,10 +7131,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": { @@ -7027,20 +7153,23 @@ }, "esbuild": { "optional": true + }, + "jest-util": { + "optional": true } } }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "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, - "license": "ISC", - "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": { @@ -7157,9 +7286,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7171,15 +7300,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.29.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.29.0.tgz", - "integrity": "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", + "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.29.0", - "@typescript-eslint/parser": "8.29.0", - "@typescript-eslint/utils": "8.29.0" + "@typescript-eslint/eslint-plugin": "8.39.0", + "@typescript-eslint/parser": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7190,7 +7320,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/uglify-js": { @@ -7208,9 +7338,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "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" }, diff --git a/package.json b/package.json index 09fb402..e99998c 100644 --- a/package.json +++ b/package.json @@ -65,36 +65,36 @@ "license": "MIT", "homepage": "https://github.com/snatalenko/node-cqrs#readme", "dependencies": { - "async-iterable-buffer": "^1.0.0", + "async-iterable-buffer": "^1.1.0", "async-parallel-pipe": "^1.0.2", "di0": "^1.0.0" }, "devDependencies": { - "@stylistic/eslint-plugin-ts": "^4.2.0", + "@stylistic/eslint-plugin-ts": "^4.4.1", "@types/amqplib": "^0.10.7", - "@types/better-sqlite3": "^7.6.11", + "@types/better-sqlite3": "^7.6.13", "@types/chai": "^4.3.20", - "@types/jest": "^29.5.13", + "@types/jest": "^29.5.14", "@types/md5": "^2.3.5", - "@types/node": "^20.16.9", + "@types/node": "^20.19.10", "@types/sinon": "^17.0.4", "@typescript-eslint/eslint-plugin": "^8.29.0", "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "eslint": "^9.24.0", - "eslint-plugin-jest": "^28.11.0", - "globals": "^16.1.0", + "eslint": "^9.33.0", + "eslint-plugin-jest": "^28.14.0", + "globals": "^16.3.0", "jest": "^29.7.0", - "sinon": "^19.0.2", - "ts-jest": "^29.2.5", + "sinon": "^19.0.5", + "ts-jest": "^29.4.1", "ts-node": "^10.9.2", - "typescript": "^5.6.2", - "typescript-eslint": "^8.29.0" + "typescript": "^5.9.2", + "typescript-eslint": "^8.39.0" }, "peerDependencies": { - "amqplib": "^0.10.5", - "better-sqlite3": "^11.3.0", + "amqplib": "^0.10.8", + "better-sqlite3": "^11.10.0", "md5": "^2.3.0" } } From bd0e0f5f04d7d2b7eb259470e66ae8f4c621b283 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 11 Aug 2025 16:43:02 +0100 Subject: [PATCH 071/135] 1.0.0-rc.12 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d02243..e2e6b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 063550d..825f729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.11", + "version": "1.0.0-rc.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.11", + "version": "1.0.0-rc.12", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index e99998c..e01312e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.11", + "version": "1.0.0-rc.12", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 2297da0c8a2eed76d2814ea7f2773022607e1e97 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 15 Aug 2025 00:18:02 +0100 Subject: [PATCH 072/135] Refactor command handling to support synchronous and asynchronous execution --- src/AbstractAggregate.ts | 51 +++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index 1975d24..8325718 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -108,7 +108,7 @@ 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 events emitted during command(s) handling and reset the `changes` collection */ - protected popChanges(): IEventSet { + /** + * 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.emit(SNAPSHOT_EVENT_TYPE, this.makeSnapshot()); + this.takeSnapshot(); - return this.#changes.splice(0); + return this.changes.slice(offset); } /** Format and register aggregate event and mutate aggregate state */ - protected emit(type: string, payload?: TPayload) { + protected emit(type: string, payload?: TPayload): IEvent { if (typeof type !== 'string' || !type.length) throw new TypeError('type argument must be a non-empty string'); const event = this.makeEvent(type, payload, this.command); this.emitRaw(event); + + return event; } /** Format event based on a current aggregate state and a command being executed */ @@ -190,6 +217,12 @@ export abstract class AbstractAggregate) { if (!snapshotEvent) From c896ee6731e184c892708088467949ec64d990fc Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 15 Aug 2025 00:19:50 +0100 Subject: [PATCH 073/135] Refactor event handling to use protected changes array --- src/AbstractAggregate.ts | 11 ++-- src/interfaces/IAggregate.ts | 18 ++++-- tests/unit/AbstractAggregate.test.ts | 91 +++++++++++++++++++++++----- 3 files changed, 95 insertions(+), 25 deletions(-) diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index 8325718..b92715e 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -30,10 +30,12 @@ export abstract class AbstractAggregate 50; */ // eslint-disable-next-line class-methods-use-this protected get shouldTakeSnapshot(): boolean { @@ -206,7 +209,7 @@ export abstract class AbstractAggregate; } diff --git a/tests/unit/AbstractAggregate.test.ts b/tests/unit/AbstractAggregate.test.ts index 9de2c7b..2ccdbe1 100644 --- a/tests/unit/AbstractAggregate.test.ts +++ b/tests/unit/AbstractAggregate.test.ts @@ -87,21 +87,18 @@ describe('AbstractAggregate', function () { }); }); - describe('popChanges', () => { + describe('protected changes', () => { it('contains an EventStream of changes happened in aggregate', async () => { - const changes0 = agg.popChanges(); + expect(agg).to.haveOwnProperty('changes').that.has.length(0); - expect(changes0).to.be.an('Array'); - expect(changes0).to.be.empty; - - const changes = await agg.handle({ type: 'doSomething' }); + await agg.handle({ type: 'doSomething' }); - expect(changes).to.not.equal(changes0); - expect(changes).to.have.nested.property('[0].type', 'somethingDone'); - expect(changes).to.have.nested.property('[0].aggregateId', 1); - expect(changes).to.have.nested.property('[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); }); }); @@ -129,7 +126,7 @@ describe('AbstractAggregate', function () { }); }); - describe('state', () => { + describe('protected state', () => { it('is an inner aggregate state', () => { @@ -175,16 +172,78 @@ 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', {}); - const changes = agg.popChanges(); - expect(changes).to.have.nested.property('[0].type', 'eventType'); + expect(agg).to.have.nested.property('changes[0].type', 'eventType'); }); it('increments aggregate #version', () => { @@ -269,7 +328,7 @@ describe('AbstractAggregate', function () { }); }); - describe('makeSnapshot()', () => { + describe('protected makeSnapshot()', () => { it('exists', () => { expect(agg).to.respondTo('makeSnapshot'); @@ -295,7 +354,7 @@ describe('AbstractAggregate', function () { }); }); - describe('restoreSnapshot(snapshotEvent)', () => { + describe('protected restoreSnapshot(snapshotEvent)', () => { const snapshotEvent = { type: 'snapshot', payload: { somethingDone: 1 } }; From 8ef62c85441a4abea6299cae3ba6d2373cffd66f Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 15 Aug 2025 00:20:13 +0100 Subject: [PATCH 074/135] 1.0.0-rc.13 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2e6b58..3c0b6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 825f729..bfcf54e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.12", + "version": "1.0.0-rc.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.12", + "version": "1.0.0-rc.13", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index e01312e..b445f5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.12", + "version": "1.0.0-rc.13", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 7b000496069a647178632f89e9200027d2f1b781 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 15 Aug 2025 15:02:30 +0100 Subject: [PATCH 075/135] Move jest-deprecated setting to tsconfig --- jest.config.ts | 7 +------ tsconfig.json | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 469c8e1..478bb43 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -37,11 +37,6 @@ export default { // A map from regular expressions to paths to transformers transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - isolatedModules: true - } - ] + '^.+\\.tsx?$': ['ts-jest'] } }; diff --git a/tsconfig.json b/tsconfig.json index 5e99f1f..2ac6347 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, + "isolatedModules": true }, "include": [ "src/**/*" From 7603e25abd8ca5104c8b2e9cf9a4566da7c87c11 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 15 Aug 2025 15:03:28 +0100 Subject: [PATCH 076/135] Remove unused import --- src/EventDispatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 968415e..975cc26 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -11,7 +11,7 @@ import { } from './interfaces'; import { parallelPipe } from 'async-parallel-pipe'; import { AsyncIterableBuffer } from 'async-iterable-buffer'; -import { getClassName, notEmpty } from './utils'; +import { getClassName } from './utils'; import { InMemoryMessageBus } from './in-memory'; type EventBatchEnvelope = { From 10308681f73b63486752b33ff1ce9e8053643658 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 15 Aug 2025 15:04:04 +0100 Subject: [PATCH 077/135] Remove eventStorageWrite from default container as it's implementation-specific --- examples/user-domain/index.js | 2 ++ src/CqrsContainerBuilder.ts | 16 +++++----------- src/interfaces/IContainer.ts | 3 +-- tests/unit/dispatch-pipeline.test.ts | 2 ++ 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index b11ef0a..698160a 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -9,6 +9,7 @@ const { InMemoryMessageBus, EventDispatcher } = require('../..'); // node-cqrs +const { InMemorySnapshotStorage } = require('../../dist/in-memory/InMemorySnapshotStorage'); const UserAggregate = require('./UserAggregate'); const UsersProjection = require('./UsersProjection'); @@ -20,6 +21,7 @@ exports.createContainer = () => { // register infrastructure services builder.register(InMemoryEventStorage).as('eventStorageReader').as('eventStorageWriter'); + builder.register(InMemorySnapshotStorage).as('snapshotStorage'); builder.register(InMemoryMessageBus).as('eventBus'); // register domain entities diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 342bd96..7574f43 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -4,7 +4,7 @@ import { CommandBus } from './CommandBus'; import { EventStore } from './EventStore'; import { SagaEventHandler } from './SagaEventHandler'; import { EventDispatcher } from './EventDispatcher'; -import { InMemoryEventStorage, InMemoryMessageBus, InMemorySnapshotStorage } from './in-memory'; +import { InMemoryMessageBus } from './in-memory'; import { EventValidationProcessor } from './EventValidationProcessor'; import { isClass } from './utils'; import { @@ -29,16 +29,10 @@ export class CqrsContainerBuilder extends ContainerBuilder { super.register(CommandBus).as('commandBus'); super.register(EventDispatcher).as('eventDispatcher'); - super.register(InMemoryEventStorage).as('eventStorageWriter'); - super.register(InMemorySnapshotStorage).as('snapshotStorage'); - - // Register default event dispatch pipeline: - // validate events, write to event storage, write to snapshot storage. - // If any of the processors is not defined, it will be skipped. - super.register((container: IContainer) => [ - new EventValidationProcessor(), - container.eventStorageWriter, - container.snapshotStorage + // Register default event dispatch pipeline with event validation only; + // No storage processors added by default + super.register(() => [ + new EventValidationProcessor() ]).as('eventDispatchPipeline'); } diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index 6cf5e73..b327856 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -4,7 +4,7 @@ import { IEventDispatcher } from './IEventDispatcher'; import { IEventStore } from './IEventStore'; import { IEventBus } from './IEventBus'; import { IDispatchPipelineProcessor } from './IDispatchPipelineProcessor'; -import { IEventStorageReader, IEventStorageWriter } from './IEventStorage'; +import { IEventStorageReader } from './IEventStorage'; import { IAggregateSnapshotStorage } from './IAggregateSnapshotStorage'; import { IIdentifierProvider } from './IIdentifierProvider'; import { IExtendableLogger, ILogger } from './ILogger'; @@ -13,7 +13,6 @@ export interface IContainer extends Container { eventBus: IEventBus; eventStore: IEventStore eventStorageReader: IEventStorageReader; - eventStorageWriter?: IEventStorageWriter; identifierProvider?: IIdentifierProvider; snapshotStorage?: IAggregateSnapshotStorage; diff --git a/tests/unit/dispatch-pipeline.test.ts b/tests/unit/dispatch-pipeline.test.ts index 5797e74..119af15 100644 --- a/tests/unit/dispatch-pipeline.test.ts +++ b/tests/unit/dispatch-pipeline.test.ts @@ -1,3 +1,4 @@ +import { InMemorySnapshotStorage } from '../../dist/in-memory/InMemorySnapshotStorage'; import { ContainerBuilder, EventValidationProcessor, @@ -22,6 +23,7 @@ describe('eventDispatchPipeline', () => { builder.register(InMemoryMessageBus).as('externalEventBus'); builder.register(InMemoryEventStorage).as('eventStorageWriter'); + builder.register(InMemorySnapshotStorage).as('snapshotStorage'); builder.register((c: IContainer) => [ new EventValidationProcessor(), c.externalEventBus, From ea828ff77ee774bc8df9e384e8b9ae8ce66de491 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 15 Aug 2025 15:22:40 +0100 Subject: [PATCH 078/135] 1.0.0-rc.14 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c0b6b8..07e3aab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index bfcf54e..9e04af7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.13", + "version": "1.0.0-rc.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.13", + "version": "1.0.0-rc.14", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index b445f5e..625d7e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.13", + "version": "1.0.0-rc.14", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 233046c597fd13c096fa8a12c48e5b8fafcd9359 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 16 Aug 2025 20:59:51 +0100 Subject: [PATCH 079/135] Add eventStorageWriter and snapshotStorage to event dispatch pipeline if they implement IDispatchPipelineProcessor interface --- examples/user-domain/index.js | 6 ++++-- src/CqrsContainerBuilder.ts | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/examples/user-domain/index.js b/examples/user-domain/index.js index 698160a..e276a54 100644 --- a/examples/user-domain/index.js +++ b/examples/user-domain/index.js @@ -9,7 +9,7 @@ const { InMemoryMessageBus, EventDispatcher } = require('../..'); // node-cqrs -const { InMemorySnapshotStorage } = require('../../dist/in-memory/InMemorySnapshotStorage'); +const { InMemorySnapshotStorage } = require('../..'); const UserAggregate = require('./UserAggregate'); const UsersProjection = require('./UsersProjection'); @@ -19,7 +19,9 @@ const UsersProjection = require('./UsersProjection'); exports.createContainer = () => { const builder = new ContainerBuilder(); - // register infrastructure services + // 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'); diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 7574f43..617dbfa 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -14,7 +14,8 @@ import { IEventReceptor, IProjection, IProjectionConstructor, - ISagaConstructor + ISagaConstructor, + isDispatchPipelineProcessor } from './interfaces'; export class CqrsContainerBuilder extends ContainerBuilder { @@ -29,10 +30,14 @@ export class CqrsContainerBuilder extends ContainerBuilder { super.register(CommandBus).as('commandBus'); super.register(EventDispatcher).as('eventDispatcher'); - // Register default event dispatch pipeline with event validation only; - // No storage processors added by default - super.register(() => [ - new EventValidationProcessor() + // Register default event dispatch pipeline with event validation only + super.register(c => [ + new EventValidationProcessor(), + + // 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'); } From ad51b0aef199d5535ad67cc052dc7653fba9cad2 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 24 Aug 2025 01:41:50 +0100 Subject: [PATCH 080/135] Remove EventValidationProcessor implementation --- src/CqrsContainerBuilder.ts | 4 ---- src/Event.ts | 14 ------------ src/EventValidationProcessor.ts | 33 ---------------------------- src/index.ts | 1 - src/interfaces/IEvent.ts | 3 ++- tests/unit/dispatch-pipeline.test.ts | 2 -- 6 files changed, 2 insertions(+), 55 deletions(-) delete mode 100644 src/EventValidationProcessor.ts diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 617dbfa..072ef23 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -5,7 +5,6 @@ import { EventStore } from './EventStore'; import { SagaEventHandler } from './SagaEventHandler'; import { EventDispatcher } from './EventDispatcher'; import { InMemoryMessageBus } from './in-memory'; -import { EventValidationProcessor } from './EventValidationProcessor'; import { isClass } from './utils'; import { IAggregateConstructor, @@ -30,10 +29,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { super.register(CommandBus).as('commandBus'); super.register(EventDispatcher).as('eventDispatcher'); - // Register default event dispatch pipeline with event validation only super.register(c => [ - new EventValidationProcessor(), - // 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] : [], diff --git a/src/Event.ts b/src/Event.ts index ab2abc8..fecaf16 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -16,17 +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'); -} diff --git a/src/EventValidationProcessor.ts b/src/EventValidationProcessor.ts deleted file mode 100644 index da5d0b2..0000000 --- a/src/EventValidationProcessor.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DispatchPipelineBatch, IEvent, IDispatchPipelineProcessor } from './interfaces'; -import { validate as defaultValidator } from './Event'; - -export type EventValidator = (event: IEvent) => void; - -/** - * Processor that validates the format of events. - * Rejects the batch if any event fails validation. - */ -export class EventValidationProcessor implements IDispatchPipelineProcessor { - - #validate: EventValidator; - - constructor(o?: { - eventFormatValidator?: EventValidator - }) { - this.#validate = o?.eventFormatValidator ?? defaultValidator; - } - - /** - * Processes a batch of dispatch pipeline items by validating each event within the batch. - * It iterates through the batch and calls the private `#validate` method for each event found. - * - * This method is part of the `IDispatchPipelineProcessor` interface. - */ - async process(batch: DispatchPipelineBatch): Promise { - for (const { event } of batch) { - if (event) - this.#validate(event); - } - return batch; - } -} diff --git a/src/index.ts b/src/index.ts index 6db623e..27d5d0b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,6 @@ export * from './AbstractSaga'; export * from './SagaEventHandler'; export * from './AbstractProjection'; export * from './EventDispatcher'; -export * from './EventValidationProcessor'; export * from './in-memory'; diff --git a/src/interfaces/IEvent.ts b/src/interfaces/IEvent.ts index 0afe504..0b007c9 100644 --- a/src/interfaces/IEvent.ts +++ b/src/interfaces/IEvent.ts @@ -10,4 +10,5 @@ export type IEvent = IMessage & { export const isEvent = (event: unknown): event is IEvent => isObject(event) && 'type' in event - && typeof event.type === 'string'; + && typeof event.type === 'string' + && event.type.length > 0; diff --git a/tests/unit/dispatch-pipeline.test.ts b/tests/unit/dispatch-pipeline.test.ts index 119af15..bf1f134 100644 --- a/tests/unit/dispatch-pipeline.test.ts +++ b/tests/unit/dispatch-pipeline.test.ts @@ -1,7 +1,6 @@ import { InMemorySnapshotStorage } from '../../dist/in-memory/InMemorySnapshotStorage'; import { ContainerBuilder, - EventValidationProcessor, IContainer, InMemoryEventStorage, InMemoryMessageBus @@ -25,7 +24,6 @@ describe('eventDispatchPipeline', () => { builder.register(InMemoryEventStorage).as('eventStorageWriter'); builder.register(InMemorySnapshotStorage).as('snapshotStorage'); builder.register((c: IContainer) => [ - new EventValidationProcessor(), c.externalEventBus, c.eventStorageWriter, c.snapshotStorage From 2c574655cb8c46965164d1f08fcbafba55118ff5 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 24 Aug 2025 01:48:38 +0100 Subject: [PATCH 081/135] Make changelog script to regenerate full changelog --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 625d7e7..98f09e1 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "test:coverage": "jest --collect-coverage tests/unit", "pretest:integration": "npm run build", "test:integration": "jest --verbose examples/user-domain-tests tests/integration", - "changelog": "conventional-changelog -n ./scripts/changelog -i CHANGELOG.md -s", + "changelog": "conventional-changelog -n ./scripts/changelog -r 0 > CHANGELOG.md", "clean": "tsc --build --clean", "build": "tsc --build", "prepare": "npm run build", From f7d0c004699cf39f271214a95544dd1d0aec995e Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 24 Aug 2025 01:55:13 +0100 Subject: [PATCH 082/135] Add script of obsolete tags cleanup --- package.json | 2 +- scripts/cleanup_obsolete_tags.sh | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100755 scripts/cleanup_obsolete_tags.sh diff --git a/package.json b/package.json index 98f09e1..3f9bd98 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "build": "tsc --build", "prepare": "npm run build", "preversion": "npm test", - "version": "npm run changelog && git add CHANGELOG.md", + "version": "./scripts/cleanup_obsolete_tags.sh v$npm_package_version && npm run changelog && git add CHANGELOG.md", "lint": "eslint" }, "author": "@snatalenko", 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 From 069ca423afcc675f13e88a16880c7a253db3d363 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 24 Aug 2025 01:56:11 +0100 Subject: [PATCH 083/135] 1.0.0-rc.15 --- CHANGELOG.md | 276 ++++++++++++++++++++++++++++------------------ package-lock.json | 4 +- package.json | 2 +- 3 files changed, 169 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e3aab..f4ec584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) @@ -55,36 +59,21 @@ * Remove `publishAsync` setting, simplify publishing sequence ([79257e5](https://github.com/snatalenko/node-cqrs/commit/79257e59d322df5dd8e41bedf5273c97ae77b609)) -# [1.0.0-rc.6](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.5...v1.0.0-rc.6) (2025-03-21) - - -### Changes - -* Support persistent views; Add SQLite infrastructure ([c235573](https://github.com/snatalenko/node-cqrs/commit/c235573678be349d031d1a696cab3993224979a2)) +# [1.0.0-rc.6](https://github.com/snatalenko/node-cqrs/compare/v0.16.4...v1.0.0-rc.6) (2025-03-21) -# [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) +### Fixes +* Vulnerability in minimist dependency ([07b8c68](https://github.com/snatalenko/node-cqrs/commit/07b8c682fae4278965aa13a06caa994c037934e9)) ### 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)) +### Refactoring -# [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) - - - -# [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) - - - -# [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.1](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.0...v1.0.0-rc.1) (2024-08-03) - +* Migrate to TS and Jest ([6737d55](https://github.com/snatalenko/node-cqrs/commit/6737d5566a9dc6314df0b20a65d32414fc503e54)) ### Build System @@ -92,18 +81,6 @@ * Suppress audit and test for tags ([574a00c](https://github.com/snatalenko/node-cqrs/commit/574a00cc53af009994ca4dd3278cb764743b4ad6)) -# [1.0.0-rc.0](https://github.com/snatalenko/node-cqrs/compare/v0.16.4...v1.0.0-rc.0) (2024-08-02) - - -### Fixes - -* Vulnerability in minimist dependency ([07b8c68](https://github.com/snatalenko/node-cqrs/commit/07b8c682fae4278965aa13a06caa994c037934e9)) - -### Refactoring - -* Migrate to TS and Jest ([6737d55](https://github.com/snatalenko/node-cqrs/commit/6737d5566a9dc6314df0b20a65d32414fc503e54)) - - ## [0.16.4](https://github.com/snatalenko/node-cqrs/compare/v0.16.3...v0.16.4) (2022-08-28) @@ -141,36 +118,24 @@ * Postpone view.get responses to next loop iteration ([950c2e4](https://github.com/snatalenko/node-cqrs/commit/950c2e42f62d7388b0cc668e81fb4f6718656fca)) -# [0.16.0](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-5...v0.16.0) (2020-03-18) - - -### Fixes - -* Moderate security issue in "minimist" dev dependency ([579d523](https://github.com/snatalenko/node-cqrs/commit/579d523745a6d33902a5245bc7e9f3fe843abc2b)) - - -# [0.16.0-5](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-4...v0.16.0-5) (2020-02-19) - - - -# [0.16.0-4](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-3...v0.16.0-4) (2020-02-19) - - - -# [0.16.0-3](https://github.com/snatalenko/node-cqrs/compare/v0.16.0-2...v0.16.0-3) (2020-01-28) +# [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)) ### 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)) ### Changes +* Debug, mocha, sinon ([ac80c27](https://github.com/snatalenko/node-cqrs/commit/ac80c27653828904cf7b80d37b0ecade860b7490)) * Move DI container to a separate package ([350f3f4](https://github.com/snatalenko/node-cqrs/commit/350f3f405a98fea2c7a85ea92f2b0f1aa945c75c)) * Do not bind masterHandler to observer automatically ([d2ec79d](https://github.com/snatalenko/node-cqrs/commit/d2ec79dced5460f619cf9bed5f34df1bbb8e0132)) * Remove deprecated InMemoryView..markAsReady method ([23015ec](https://github.com/snatalenko/node-cqrs/commit/23015ec3f5bc69f843cf6815caa1f4cda9fea27c)) @@ -184,122 +149,213 @@ ### 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/package-lock.json b/package-lock.json index 9e04af7..78c9c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.14", + "version": "1.0.0-rc.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.14", + "version": "1.0.0-rc.15", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 3f9bd98..f2be78d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.14", + "version": "1.0.0-rc.15", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From edd888465c44abe40c6d659fd406ce627a6c2ad5 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 28 Aug 2025 20:37:41 +0100 Subject: [PATCH 084/135] Separate IEventStorage interfaces --- src/interfaces/IContainer.ts | 2 +- .../{IEventStorage.ts => IEventStorageReader.ts} | 9 --------- src/interfaces/IEventStorageWriter.ts | 10 ++++++++++ src/interfaces/IEventStore.ts | 2 +- src/interfaces/index.ts | 3 ++- 5 files changed, 14 insertions(+), 12 deletions(-) rename src/interfaces/{IEventStorage.ts => IEventStorageReader.ts} (83%) create mode 100644 src/interfaces/IEventStorageWriter.ts diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index b327856..7ed1440 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -4,7 +4,7 @@ import { IEventDispatcher } from './IEventDispatcher'; import { IEventStore } from './IEventStore'; import { IEventBus } from './IEventBus'; import { IDispatchPipelineProcessor } from './IDispatchPipelineProcessor'; -import { IEventStorageReader } from './IEventStorage'; +import { IEventStorageReader } from './IEventStorageReader'; import { IAggregateSnapshotStorage } from './IAggregateSnapshotStorage'; import { IIdentifierProvider } from './IIdentifierProvider'; import { IExtendableLogger, ILogger } from './ILogger'; diff --git a/src/interfaces/IEventStorage.ts b/src/interfaces/IEventStorageReader.ts similarity index 83% rename from src/interfaces/IEventStorage.ts rename to src/interfaces/IEventStorageReader.ts index bc7dbdb..ba124b3 100644 --- a/src/interfaces/IEventStorage.ts +++ b/src/interfaces/IEventStorageReader.ts @@ -1,6 +1,5 @@ import { Identifier } from './Identifier'; import { IEvent } from './IEvent'; -import { IEventSet } from './IEventSet'; import { IEventStream } from './IEventStream'; import { isObject } from './isObject'; @@ -34,14 +33,6 @@ export interface IEventStorageReader { getSagaEvents(sagaId: Identifier, options: EventQueryBefore): IEventStream; } -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; -} export const isIEventStorageReader = (storage: unknown): storage is IEventStorageReader => isObject(storage) diff --git a/src/interfaces/IEventStorageWriter.ts b/src/interfaces/IEventStorageWriter.ts new file mode 100644 index 0000000..03521bc --- /dev/null +++ b/src/interfaces/IEventStorageWriter.ts @@ -0,0 +1,10 @@ +import { IEventSet } from './IEventSet'; + +export interface IEventStorageWriter { + + /** + * Persists a set of events to the event store. + * Returns the persisted event set (potentially enriched or normalized). + */ + commitEvents(events: IEventSet): Promise; +} diff --git a/src/interfaces/IEventStore.ts b/src/interfaces/IEventStore.ts index 1320e0d..0ded850 100644 --- a/src/interfaces/IEventStore.ts +++ b/src/interfaces/IEventStore.ts @@ -1,6 +1,6 @@ import { IEventDispatcher } from './IEventDispatcher'; import { IEvent } from './IEvent'; -import { IEventStorageReader } from './IEventStorage'; +import { IEventStorageReader } from './IEventStorageReader'; import { IIdentifierProvider } from './IIdentifierProvider'; import { IMessageHandler, IObservable } from './IObservable'; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index e736e0a..6829a48 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -11,7 +11,8 @@ export * from './IEventDispatcher'; export * from './IEventLocker'; export * from './IEventReceptor'; export * from './IEventSet'; -export * from './IEventStorage'; +export * from './IEventStorageReader'; +export * from './IEventStorageWriter'; export * from './IEventStore'; export * from './IEventStream'; export * from './IIdentifierProvider'; From 5a7803c3da0f335c0e3c90560bf2d8e87f7d602e Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 28 Aug 2025 20:40:45 +0100 Subject: [PATCH 085/135] Remove InMemoryMessageBus implementation of IDispatchPipelineProcessor --- src/in-memory/InMemoryMessageBus.ts | 24 +----------------------- tests/unit/dispatch-pipeline.test.ts | 27 +-------------------------- 2 files changed, 2 insertions(+), 49 deletions(-) diff --git a/src/in-memory/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts index b678096..13c10e3 100644 --- a/src/in-memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -1,7 +1,5 @@ import { - DispatchPipelineBatch, ICommand, - IDispatchPipelineProcessor, IEvent, IMessageBus, IMessageHandler, @@ -12,7 +10,7 @@ import { * Default implementation of the message bus. * Keeps all subscriptions and messages in memory. */ -export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcessor { +export class InMemoryMessageBus implements IMessageBus { protected handlers: Map> = new Map(); protected uniqueEventHandlers: boolean; @@ -117,24 +115,4 @@ export class InMemoryMessageBus implements IMessageBus, IDispatchPipelineProcess return Promise.all(handlers.map(handler => handler(event, meta))); } - - /** - * Processes a batch of events and publishes them to the fanout exchange. - * - * This method is part of the `IDispatchPipelineProcessor` interface. - */ - async process(batch: DispatchPipelineBatch): Promise { - for (const { event, origin } of batch) { - // Skip publishing if the event was dispatched from external source - if (origin === 'external') - continue; - - if (!event) - throw new Error('Event batch does not contain `event`'); - - await this.publish(event); - } - - return batch; - } } diff --git a/tests/unit/dispatch-pipeline.test.ts b/tests/unit/dispatch-pipeline.test.ts index bf1f134..05dc35b 100644 --- a/tests/unit/dispatch-pipeline.test.ts +++ b/tests/unit/dispatch-pipeline.test.ts @@ -2,8 +2,7 @@ import { InMemorySnapshotStorage } from '../../dist/in-memory/InMemorySnapshotSt import { ContainerBuilder, IContainer, - InMemoryEventStorage, - InMemoryMessageBus + InMemoryEventStorage } from '../../src'; describe('eventDispatchPipeline', () => { @@ -20,11 +19,9 @@ describe('eventDispatchPipeline', () => { beforeEach(() => { const builder = new ContainerBuilder(); - builder.register(InMemoryMessageBus).as('externalEventBus'); builder.register(InMemoryEventStorage).as('eventStorageWriter'); builder.register(InMemorySnapshotStorage).as('snapshotStorage'); builder.register((c: IContainer) => [ - c.externalEventBus, c.eventStorageWriter, c.snapshotStorage ]).as('eventDispatchPipeline'); @@ -32,28 +29,6 @@ describe('eventDispatchPipeline', () => { container = builder.container() as IContainer; }); - it('delivers locally dispatched events to externalEventBus', async () => { - - const { eventDispatcher, externalEventBus } = container; - - jest.spyOn(externalEventBus, 'publish'); - - await eventDispatcher.dispatch([testEvent], { origin: 'internal' }); - - expect(externalEventBus.publish).toHaveBeenCalledTimes(1); - }); - - it('does not deliver externally dispatched events to externalEventBus', async () => { - - const { eventDispatcher, externalEventBus } = container; - - jest.spyOn(externalEventBus, 'publish'); - - await eventDispatcher.dispatch([testEvent], { origin: 'external' }); - - expect(externalEventBus.publish).toHaveBeenCalledTimes(0); - }); - it('delivers all events to eventStorageWriter', async () => { const { eventDispatcher, eventStorageWriter } = container; From 023ac6323edc5f7366fd776a08ef83fcec808a52 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 28 Aug 2025 20:43:27 +0100 Subject: [PATCH 086/135] Refactor `publish` method to remove unnecessary arrays --- src/in-memory/InMemoryMessageBus.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/in-memory/InMemoryMessageBus.ts b/src/in-memory/InMemoryMessageBus.ts index 13c10e3..c2b94a6 100644 --- a/src/in-memory/InMemoryMessageBus.ts +++ b/src/in-memory/InMemoryMessageBus.ts @@ -107,12 +107,14 @@ export class InMemoryMessageBus implements IMessageBus { 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, m?: Record) => namedQueue.publish(e, m)) - ]; + const promises: Promise[] = []; - return Promise.all(handlers.map(handler => handler(event, meta))); + 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); } } From 4a9262a4ace6cd5c5de543a79af5dff5f8882e6d Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 29 Aug 2025 22:28:11 +0100 Subject: [PATCH 087/135] 1.0.0-rc.16 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ec584..66de549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 78c9c57..c06f641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.15", + "version": "1.0.0-rc.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.15", + "version": "1.0.0-rc.16", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index f2be78d..a4dd258 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.15", + "version": "1.0.0-rc.16", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 237ab3bdc847f17be6afc4de7d4a3bbe0ff8af2b Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 30 Aug 2025 00:12:24 +0100 Subject: [PATCH 088/135] Implement event dispatch pipelines with routing based on meta.origin --- src/EventDispatchPipeline.ts | 99 ++++++++++++++++ src/EventDispatcher.ts | 182 +++++++++++------------------ src/interfaces/IContainer.ts | 5 + tests/unit/EventDispatcher.test.ts | 91 +++++++++++++++ 4 files changed, 260 insertions(+), 117 deletions(-) create mode 100644 src/EventDispatchPipeline.ts diff --git a/src/EventDispatchPipeline.ts b/src/EventDispatchPipeline.ts new file mode 100644 index 0000000..0d1d95c --- /dev/null +++ b/src/EventDispatchPipeline.ts @@ -0,0 +1,99 @@ +import { + DispatchPipelineBatch, + IEvent, + IDispatchPipelineProcessor, + IEventBus, + isDispatchPipelineProcessor +} from './interfaces'; + +import { parallelPipe } from 'async-parallel-pipe'; +import { AsyncIterableBuffer } from 'async-iterable-buffer'; +import { getClassName } from './utils'; + +export type EventBatchEnvelope = { + data: DispatchPipelineBatch<{ event?: IEvent }>; + error?: Error; + resolve: (event: IEvent[]) => void; + reject: (error: Error) => void; +}; + +export class EventDispatchPipeline { + + #pipelineInput = new AsyncIterableBuffer(); + #processors: Array = []; + #pipeline: AsyncIterableIterator | IterableIterator = this.#pipelineInput; + #processing = false; + + constructor(private readonly eventBus: IEventBus, private readonly concurrentLimit: number) { + } + + addProcessor(preprocessor: IDispatchPipelineProcessor) { + if (!isDispatchPipelineProcessor(preprocessor)) + throw new TypeError(`preprocessor ${getClassName(preprocessor)} does not implement IDispatchPipelineProcessor`); + if (this.#processing) + throw new Error('pipeline processing already started'); + + this.#processors.push(preprocessor); + + // Build a processing pipeline that runs preprocessors concurrently, preserving FIFO ordering + this.#pipeline = parallelPipe(this.#pipeline, this.concurrentLimit, async envelope => { + if (envelope.error) + return envelope; + + try { + return { + ...envelope, + data: await preprocessor.process(envelope.data) + }; + } + catch (error: any) { + return { + ...envelope, + error + }; + } + }); + } + + #ensureProcessingStarted() { + if (this.#processing) + return; + + this.#processing = true; + + (async () => { + for await (const { error, reject, data, resolve } of this.#pipeline) { + try { + if (error) { + await this.revert(data); + reject(error); + continue; + } + + const events: IEvent[] = []; + for (const batch of data) { + const { event, ...meta } = batch as any; + if (event) { + await this.eventBus.publish(event, meta); + events.push(event); + } + } + resolve(events); + } + catch (publishError: any) { + reject(publishError); + } + } + })(); + } + + async revert(batch: DispatchPipelineBatch) { + for (const processor of this.#processors) + await processor.revert?.(batch); + } + + push(envelope: EventBatchEnvelope) { + this.#ensureProcessingStarted(); + this.#pipelineInput.push(envelope); + } +} diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 975cc26..890fa3b 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -1,174 +1,122 @@ import { - DispatchPipelineBatch, - IEvent, IEventDispatcher, IDispatchPipelineProcessor, IEventSet, IEventBus, isEventSet, - IContainer, - isDispatchPipelineProcessor + IContainer } from './interfaces'; -import { parallelPipe } from 'async-parallel-pipe'; -import { AsyncIterableBuffer } from 'async-iterable-buffer'; -import { getClassName } from './utils'; import { InMemoryMessageBus } from './in-memory'; - -type EventBatchEnvelope = { - data: DispatchPipelineBatch<{ event?: IEvent }>; - error?: Error; - resolve: (event: IEvent[]) => void; - reject: (error: Error) => void; -} +import { EventBatchEnvelope, EventDispatchPipeline } from './EventDispatchPipeline'; export class EventDispatcher implements IEventDispatcher { - #pipelineInput = new AsyncIterableBuffer(); - #processors: Array = []; - #pipeline: AsyncIterableIterator | IterableIterator = this.#pipelineInput; + /** 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; /** - * Maximum number of event batches that each pipeline processor can handle in parallel. + * 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 ?? 100; + this.concurrentLimit = o?.eventDispatcherConfig?.concurrentLimit ?? EventDispatcher.DEFAULT_CONCURRENT_LIMIT; + this.eventDispatchRouter = o?.eventDispatchRouter ?? EventDispatcher.DEFAULT_ROUTER; - if (o?.eventDispatchPipeline) - this.addPipelineProcessors(o.eventDispatchPipeline); + 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, []); + } } - addPipelineProcessors(eventDispatchPipeline: IDispatchPipelineProcessor[]) { + /** 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) { - if (processor) - this.addPipelineProcessor(processor); - } + for (const processor of eventDispatchPipeline) + this.addPipelineProcessor(processor, pipelineName); } - /** - * Adds a preprocessor to the event dispatch pipeline. - * - * Preprocessors run in order they are added but process separate batches in parallel, maintaining FIFO order. - */ - addPipelineProcessor(preprocessor: IDispatchPipelineProcessor) { - if (!isDispatchPipelineProcessor(preprocessor)) - throw new TypeError(`preprocessor ${getClassName(preprocessor)} does not implement IDispatchPipelineProcessor`); - if (this.#pipelineProcessing) - 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 - }; - } - }); + /** 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); } - #pipelineProcessing = false; + /** 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`); - /** - * Consume the pipeline, publish events, and resolve/reject each batch - */ - async #startPipelineProcessing() { - if (this.#pipelineProcessing) // should never happen - throw new Error('pipeline processing already started'); - - this.#pipelineProcessing = true; - - for await (const { error, reject, data, resolve } of this.#pipeline) { - if (error) { // some of the preprocessors failed - await this.#revert(data); - reject(error); - continue; - } - - try { - const events: IEvent[] = []; - - for (const batch of data) { - const { event, ...meta } = batch; - if (event) { - await this.eventBus.publish(event, meta); - events.push(event); - } - } - - resolve(events); - } - catch (publishError: any) { - reject(publishError); - } - } - } + const pipeline = new EventDispatchPipeline(this.eventBus, options?.concurrentLimit ?? this.concurrentLimit); + for (const p of processors) + pipeline.addProcessor(p); - /** - * Revert side effects made by pipeline processors in case of a batch processing failure - */ - async #revert(batch: DispatchPipelineBatch) { - for (const processor of this.#processors) - await processor.revert?.(batch); + this.#pipelines.set(name, pipeline); + + return pipeline; } - /** - * Dispatch a set of events through the processing pipeline. - * - * Returns a promise that resolves after all events are processed and published. - */ + /** 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 Error('dispatch requires a non-empty array of events'); - // const { promise, resolve, reject } = Promise.withResolvers(); let resolve!: (value: IEventSet | PromiseLike) => void; let reject!: (reason?: any) => void; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); + const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); const envelope: EventBatchEnvelope = { - data: events.map(event => ({ - event, - ...meta - })), + data: events.map(event => ({ event, ...meta })), resolve, reject }; - if (!this.#pipelineProcessing) - this.#startPipelineProcessing(); + 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`); - this.#pipelineInput.push(envelope); + pipeline.push(envelope); return promise; } diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index 7ed1440..f8a2fb5 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -18,8 +18,13 @@ export interface IContainer extends Container { 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/tests/unit/EventDispatcher.test.ts b/tests/unit/EventDispatcher.test.ts index 686082c..a079000 100644 --- a/tests/unit/EventDispatcher.test.ts +++ b/tests/unit/EventDispatcher.test.ts @@ -97,4 +97,95 @@ describe('EventDispatcher', () => { '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'); + }); }); From 92b2789a47e99657a819acb77418f0f98eaad1ab Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sat, 30 Aug 2025 00:12:37 +0100 Subject: [PATCH 089/135] 1.0.0-rc.17 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66de549..46d5f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) + + + # [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) diff --git a/package-lock.json b/package-lock.json index c06f641..0b58be6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.16", + "version": "1.0.0-rc.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.16", + "version": "1.0.0-rc.17", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index a4dd258..6002c01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.16", + "version": "1.0.0-rc.17", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 4c3f37409d400b199c16c07fcb0d65019020290c Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 11 Sep 2025 14:37:46 +0100 Subject: [PATCH 090/135] Remove unnecessary handler binding --- src/utils/getHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/getHandler.ts b/src/utils/getHandler.ts index 8df01a1..f2de2be 100644 --- a/src/utils/getHandler.ts +++ b/src/utils/getHandler.ts @@ -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; } From a68490aaed2dfc6bd4dcad0e7179a9146779dc29 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 11 Sep 2025 14:41:37 +0100 Subject: [PATCH 091/135] Refactor dispatch argument validations to keep consistent between EventStore and EventDispatcher --- src/EventDispatcher.ts | 7 +++++-- src/EventStore.ts | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/EventDispatcher.ts b/src/EventDispatcher.ts index 890fa3b..ab11cbe 100644 --- a/src/EventDispatcher.ts +++ b/src/EventDispatcher.ts @@ -99,11 +99,14 @@ export class EventDispatcher implements IEventDispatcher { /** 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 Error('dispatch requires a non-empty array of events'); + 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 promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); const envelope: EventBatchEnvelope = { data: events.map(event => ({ event, ...meta })), diff --git a/src/EventStore.ts b/src/EventStore.ts index 9fc91d1..09ba489 100644 --- a/src/EventStore.ts +++ b/src/EventStore.ts @@ -17,7 +17,8 @@ import { IEventBus, isIEventBus, isIEventStorageReader, - IContainer + IContainer, + isEventSet } from './interfaces'; import { getClassName, @@ -148,8 +149,8 @@ export class EventStore implements IEventStore { * @returns Signed and committed events */ async dispatch(events: IEventSet): Promise { - if (!Array.isArray(events)) - throw new TypeError('events argument must be an Array'); + if (!isEventSet(events) || events.length === 0) + throw new TypeError('dispatch requires a non-empty array of events'); const augmentedEvents = await this.#attachSagaIdToSagaStarterEvents(events); From 25012815335ae18d04e1670d25cf0120b1a8f0b3 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 11 Sep 2025 14:44:20 +0100 Subject: [PATCH 092/135] Change "clean" script to cleanup abandoned files --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6002c01..3d8b8e3 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "pretest:integration": "npm run build", "test:integration": "jest --verbose examples/user-domain-tests tests/integration", "changelog": "conventional-changelog -n ./scripts/changelog -r 0 > CHANGELOG.md", - "clean": "tsc --build --clean", + "clean": "rm -rf ./dist ./types ./coverage && tsc --build --clean", "build": "tsc --build", "prepare": "npm run build", "preversion": "npm test", From 09718d87427f338dfa7c1147a8e295896f3c1c7b Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 11 Sep 2025 15:00:52 +0100 Subject: [PATCH 093/135] 1.0.0-rc.18 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d5f57..9d38ce1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 0b58be6..a172e25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.17", + "version": "1.0.0-rc.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.17", + "version": "1.0.0-rc.18", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 3d8b8e3..f30fefb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.17", + "version": "1.0.0-rc.18", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From cb7f22b36c66d8fc16c4ce546906c024531bd6ad Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 24 Sep 2025 20:26:54 +0100 Subject: [PATCH 094/135] Fix handler context binding without masterHandler defined --- src/AbstractProjection.ts | 2 +- src/SagaEventHandler.ts | 2 +- src/utils/subscribe.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index 6a6a625..c7652ed 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -115,7 +115,7 @@ export abstract class AbstractProjection implements IProjection { subscribe(eventStore, this, { - masterHandler: (e: IEvent) => this.project(e) + masterHandler: this.project }); await this.restore(eventStore); diff --git a/src/SagaEventHandler.ts b/src/SagaEventHandler.ts index 5fe163d..6e784ff 100644 --- a/src/SagaEventHandler.ts +++ b/src/SagaEventHandler.ts @@ -83,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 }); } diff --git a/src/utils/subscribe.ts b/src/utils/subscribe.ts index dcb3965..3f5b157 100644 --- a/src/utils/subscribe.ts +++ b/src/utils/subscribe.ts @@ -56,10 +56,10 @@ export function subscribe( if (!observable.queue) throw new TypeError('Observer does not support named queues'); - observable.queue(queueName).on(messageType, handler); + observable.queue(queueName).on(messageType, (event, meta) => handler.call(observer, event, meta)); } else { - observable.on(messageType, handler); + observable.on(messageType, (event, meta) => handler.call(observer, event, meta)); } } } From 2a1478caf62aacc48de67c6700700e461e5976e3 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 24 Sep 2025 20:27:40 +0100 Subject: [PATCH 095/135] Rearrange scripts to separate rabbitmq integration tests --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f30fefb..4f58e7c 100644 --- a/package.json +++ b/package.json @@ -48,13 +48,13 @@ "node": ">=18.0.0" }, "scripts": { + "cleanup": "rm -rf ./dist ./types ./coverage && tsc --build --clean", "pretest": "npm run build", - "test": "jest tests/unit", - "test:coverage": "jest --collect-coverage tests/unit", - "pretest:integration": "npm run build", - "test:integration": "jest --verbose examples/user-domain-tests tests/integration", + "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", - "clean": "rm -rf ./dist ./types ./coverage && tsc --build --clean", "build": "tsc --build", "prepare": "npm run build", "preversion": "npm test", From 9f638c61555ddfff6ab96cf5fb3f9c12c4de8b36 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 24 Sep 2025 20:27:51 +0100 Subject: [PATCH 096/135] 1.0.0-rc.19 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d38ce1..95ea06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index a172e25..3b766eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.18", + "version": "1.0.0-rc.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.18", + "version": "1.0.0-rc.19", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 4f58e7c..134ab2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.18", + "version": "1.0.0-rc.19", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 025765cc31eec5a004142dff5cafd8264af10ea9 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 14 Oct 2025 00:54:52 +0100 Subject: [PATCH 097/135] Change: Enhance type safety in CqrsContainerBuilder with generics --- package-lock.json | 470 +++++++++++++++++------------------ package.json | 16 +- src/CqrsContainerBuilder.ts | 19 +- src/interfaces/IContainer.ts | 2 + 4 files changed, 254 insertions(+), 253 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3b766eb..5546dad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "async-iterable-buffer": "^1.1.0", "async-parallel-pipe": "^1.0.2", - "di0": "^1.0.0" + "di0": "^1.2.0" }, "devDependencies": { "@stylistic/eslint-plugin-ts": "^4.4.1", @@ -20,45 +20,31 @@ "@types/chai": "^4.3.20", "@types/jest": "^29.5.14", "@types/md5": "^2.3.5", - "@types/node": "^20.19.10", + "@types/node": "^20.19.21", "@types/sinon": "^17.0.4", "@typescript-eslint/eslint-plugin": "^8.29.0", "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "eslint": "^9.33.0", + "eslint": "^9.37.0", "eslint-plugin-jest": "^28.14.0", - "globals": "^16.3.0", + "globals": "^16.4.0", "jest": "^29.7.0", "sinon": "^19.0.5", - "ts-jest": "^29.4.1", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", - "typescript": "^5.9.2", - "typescript-eslint": "^8.39.0" + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.1" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "amqplib": "^0.10.8", + "amqplib": "^0.10.9", "better-sqlite3": "^11.10.0", "md5": "^2.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, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -75,9 +61,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "dev": true, "license": "MIT", "engines": { @@ -85,22 +71,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -126,14 +112,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -211,15 +197,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -269,27 +255,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.0" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -553,18 +539,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", + "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -572,9 +558,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -617,9 +603,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -698,19 +684,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -792,9 +781,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "license": "MIT", "engines": { @@ -815,13 +804,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -839,33 +828,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "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.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1314,9 +1289,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "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": { @@ -1324,6 +1299,17 @@ "@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" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1335,16 +1321,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "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.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "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": { @@ -1640,9 +1626,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.10.tgz", - "integrity": "sha512-iAFpG6DokED3roLSP0K+ybeDdIX6Bc0Vd3mLW5uDqThPWtNos3E+EqOM11mPQHKzfWHqEBuLjIlsBQQ8CsISmQ==", + "version": "20.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.21.tgz", + "integrity": "sha512-CsGG2P3I5y48RPMfprQGfy4JPRZ6csfC3ltBZSRItG3ngggmNY/qs2uZKp4p9VbrpqNNSMzUZNFZKzgOGnd/VA==", "dev": true, "license": "MIT", "dependencies": { @@ -1698,17 +1684,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", - "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz", + "integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/type-utils": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/type-utils": "8.46.1", + "@typescript-eslint/utils": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1722,22 +1708,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/parser": "^8.46.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", - "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", + "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4" }, "engines": { @@ -1753,14 +1739,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", - "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz", + "integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.0", - "@typescript-eslint/types": "^8.39.0", + "@typescript-eslint/tsconfig-utils": "^8.46.1", + "@typescript-eslint/types": "^8.46.1", "debug": "^4.3.4" }, "engines": { @@ -1775,14 +1761,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", - "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz", + "integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0" + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1793,9 +1779,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", - "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz", + "integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", "dev": true, "license": "MIT", "engines": { @@ -1810,15 +1796,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", - "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz", + "integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1835,9 +1821,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", - "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", + "integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", "dev": true, "license": "MIT", "engines": { @@ -1849,16 +1835,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", - "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz", + "integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.0", - "@typescript-eslint/tsconfig-utils": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/project-service": "8.46.1", + "@typescript-eslint/tsconfig-utils": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1878,16 +1864,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", + "integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0" + "@typescript-eslint/scope-manager": "8.46.1", + "@typescript-eslint/types": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1902,13 +1888,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", - "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz", + "integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1980,9 +1966,9 @@ } }, "node_modules/amqplib": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.8.tgz", - "integrity": "sha512-Tfn1O9sFgAP8DqeMEpt2IacsVTENBpblB3SqLdn0jK2AeX8iyCvbptBc8lyATT9bQ31MsjVwUSQ1g8f4jHOUfw==", + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.9.tgz", + "integrity": "sha512-jwSftI4QjS3mizvnSnOrPGYiUnm1vI2OP1iXeOUz5pb74Ua0nbf6nPyyTzuiCLEE3fMpaJORXh2K/TQ08H5xGA==", "license": "MIT", "peer": true, "dependencies": { @@ -2256,6 +2242,16 @@ "license": "MIT", "peer": true }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", + "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/better-sqlite3": { "version": "11.10.0", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", @@ -2314,9 +2310,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", "dev": true, "funding": [ { @@ -2334,9 +2330,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -2447,9 +2444,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001734", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", - "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", + "version": "1.0.30001750", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", + "integrity": "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==", "dev": true, "funding": [ { @@ -2972,9 +2969,9 @@ } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -3043,9 +3040,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3098,9 +3095,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "peer": true, "engines": { @@ -3118,9 +3115,9 @@ } }, "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": { @@ -3157,9 +3154,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.199", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", - "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", + "version": "1.5.235", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", + "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", "dev": true, "license": "ISC" }, @@ -3194,9 +3191,9 @@ } }, "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==", + "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": { @@ -3227,20 +3224,20 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3950,9 +3947,9 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -4374,9 +4371,9 @@ } }, "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": { @@ -5725,9 +5722,9 @@ } }, "node_modules/node-abi": { - "version": "3.75.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", - "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", "license": "MIT", "peer": true, "dependencies": { @@ -5745,9 +5742,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", "dev": true, "license": "MIT" }, @@ -5945,13 +5942,14 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "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", - "engines": { - "node": ">=16" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/path-type": { @@ -6600,9 +6598,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -6968,9 +6966,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "peer": true, "dependencies": { @@ -7107,9 +7105,9 @@ } }, "node_modules/ts-jest": { - "version": "29.4.1", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", - "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "version": "29.4.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz", + "integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -7119,7 +7117,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.2", + "semver": "^7.7.3", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -7286,9 +7284,9 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -7300,16 +7298,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", - "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", + "version": "8.46.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz", + "integrity": "sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.39.0", - "@typescript-eslint/parser": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0" + "@typescript-eslint/eslint-plugin": "8.46.1", + "@typescript-eslint/parser": "8.46.1", + "@typescript-eslint/typescript-estree": "8.46.1", + "@typescript-eslint/utils": "8.46.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 134ab2a..ecd1e92 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "dependencies": { "async-iterable-buffer": "^1.1.0", "async-parallel-pipe": "^1.0.2", - "di0": "^1.0.0" + "di0": "^1.2.0" }, "devDependencies": { "@stylistic/eslint-plugin-ts": "^4.4.1", @@ -76,24 +76,24 @@ "@types/chai": "^4.3.20", "@types/jest": "^29.5.14", "@types/md5": "^2.3.5", - "@types/node": "^20.19.10", + "@types/node": "^20.19.21", "@types/sinon": "^17.0.4", "@typescript-eslint/eslint-plugin": "^8.29.0", "@typescript-eslint/parser": "^8.29.0", "chai": "^4.5.0", "conventional-changelog": "^3.1.25", - "eslint": "^9.33.0", + "eslint": "^9.37.0", "eslint-plugin-jest": "^28.14.0", - "globals": "^16.3.0", + "globals": "^16.4.0", "jest": "^29.7.0", "sinon": "^19.0.5", - "ts-jest": "^29.4.1", + "ts-jest": "^29.4.5", "ts-node": "^10.9.2", - "typescript": "^5.9.2", - "typescript-eslint": "^8.39.0" + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.1" }, "peerDependencies": { - "amqplib": "^0.10.8", + "amqplib": "^0.10.9", "better-sqlite3": "^11.10.0", "md5": "^2.3.0" } diff --git a/src/CqrsContainerBuilder.ts b/src/CqrsContainerBuilder.ts index 072ef23..8d7f0c7 100644 --- a/src/CqrsContainerBuilder.ts +++ b/src/CqrsContainerBuilder.ts @@ -17,7 +17,8 @@ import { isDispatchPipelineProcessor } from './interfaces'; -export class CqrsContainerBuilder extends ContainerBuilder { +export class CqrsContainerBuilder + extends ContainerBuilder { constructor(options?: { types: Readonly[]>, @@ -38,9 +39,9 @@ export class CqrsContainerBuilder extends ContainerBuilder { } /** Register command handler, which will be subscribed to commandBus upon instance creation */ - registerCommandHandler(typeOrFactory: TClassOrFactory) { + registerCommandHandler(typeOrFactory: TClassOrFactory) { return super.register( - (container: IContainer) => { + (container: TContainerInterface) => { const handler = container.createInstance(typeOrFactory); handler.subscribe(container.commandBus); return handler; @@ -49,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: IContainer) => { + (container: TContainerInterface) => { const receptor = container.createInstance(typeOrFactory); receptor.subscribe(container.eventStore); return receptor; @@ -63,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: IContainer): IProjection => { + const projectionFactory = (container: TContainerInterface): IProjection => { const projection = container.createInstance(ProjectionType); projection.subscribe(container.eventStore); @@ -90,7 +91,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { if (!isClass(AggregateType)) throw new TypeError('AggregateType argument must be a constructor function'); - const commandHandlerFactory = (container: IContainer): ICommandHandler => + const commandHandlerFactory = (container: TContainerInterface): ICommandHandler => container.createInstance(AggregateCommandHandler, { aggregateFactory: (options: any) => container.createInstance(AggregateType, options), @@ -106,7 +107,7 @@ export class CqrsContainerBuilder extends ContainerBuilder { if (!isClass(SagaType)) throw new TypeError('SagaType argument must be a constructor function'); - const eventReceptorFactory = (container: IContainer): IEventReceptor => + const eventReceptorFactory = (container: TContainerInterface): IEventReceptor => container.createInstance(SagaEventHandler, { sagaFactory: (options: any) => container.createInstance(SagaType, options), handles: SagaType.handles, diff --git a/src/interfaces/IContainer.ts b/src/interfaces/IContainer.ts index f8a2fb5..1d6bebb 100644 --- a/src/interfaces/IContainer.ts +++ b/src/interfaces/IContainer.ts @@ -8,11 +8,13 @@ import { IEventStorageReader } from './IEventStorageReader'; import { IAggregateSnapshotStorage } from './IAggregateSnapshotStorage'; import { IIdentifierProvider } from './IIdentifierProvider'; import { IExtendableLogger, ILogger } from './ILogger'; +import { IEventStorageWriter } from './IEventStorageWriter'; export interface IContainer extends Container { eventBus: IEventBus; eventStore: IEventStore eventStorageReader: IEventStorageReader; + eventStorageWriter?: IEventStorageWriter; identifierProvider?: IIdentifierProvider; snapshotStorage?: IAggregateSnapshotStorage; From 9e0edd38fac9156df0fea87fde3f7e8a8eb5f471 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 14 Oct 2025 00:56:03 +0100 Subject: [PATCH 098/135] Expose MQ defaults as static properties instead of inner constants --- src/rabbitmq/RabbitMqEventBus.ts | 5 +++-- src/rabbitmq/RabbitMqEventInjector.ts | 6 +++--- src/rabbitmq/RabbitMqGateway.ts | 5 +++-- src/rabbitmq/constants.ts | 2 -- 4 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 src/rabbitmq/constants.ts diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts index 46553b4..894a672 100644 --- a/src/rabbitmq/RabbitMqEventBus.ts +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -1,5 +1,4 @@ import { IEvent, IEventBus, IDispatchPipelineProcessor, IMessageHandler, IObservable, DispatchPipelineBatch } from '../interfaces'; -import { DEFAULT_EXCHANGE } from './constants'; import { RabbitMqGateway } from './RabbitMqGateway'; const ALL_EVENTS_WILDCARD = '*'; @@ -10,6 +9,8 @@ export class RabbitMqEventBus implements IEventBus, IDispatchPipelineProcessor { return ALL_EVENTS_WILDCARD; } + static DEFAULT_EXCHANGE = 'node-cqrs.events'; + #gateway: RabbitMqGateway; #queues = new Map(); #exchange: string; @@ -21,7 +22,7 @@ export class RabbitMqEventBus implements IEventBus, IDispatchPipelineProcessor { queueName?: string }) { this.#gateway = o.rabbitMqGateway; - this.#exchange = o.exchange ?? DEFAULT_EXCHANGE; + this.#exchange = o.exchange ?? RabbitMqEventBus.DEFAULT_EXCHANGE; this.#queueName = o.queueName; } diff --git a/src/rabbitmq/RabbitMqEventInjector.ts b/src/rabbitmq/RabbitMqEventInjector.ts index b68de0c..c973177 100644 --- a/src/rabbitmq/RabbitMqEventInjector.ts +++ b/src/rabbitmq/RabbitMqEventInjector.ts @@ -3,7 +3,7 @@ import { IMessage } from '../interfaces/IMessage'; import { RabbitMqGateway } from './RabbitMqGateway'; import { IEventDispatcher } from '../interfaces'; import * as Event from '../Event'; -import { DEFAULT_EXCHANGE } from './constants'; +import { RabbitMqEventBus } from './RabbitMqEventBus'; /** * Injects events received from a RabbitMQ exchange into the local event dispatcher. @@ -31,7 +31,7 @@ export class RabbitMqEventInjector { container.logger; } - async start(exchange: string = DEFAULT_EXCHANGE): Promise { + async start(exchange: string = RabbitMqEventBus.DEFAULT_EXCHANGE): Promise { this.#logger?.debug(`Subscribing to messages from exchange "${exchange}"...`); await this.#rabbitMqGateway.subscribeToFanout(exchange, this.#messageHandler); @@ -39,7 +39,7 @@ export class RabbitMqEventInjector { this.#logger?.debug(`Listening to messages from exchange "${exchange}"`); } - async stop(exchange: string = DEFAULT_EXCHANGE): Promise { + async stop(exchange: string = RabbitMqEventBus.DEFAULT_EXCHANGE): Promise { this.#logger?.debug(`Unsubscribing from messages from exchange "${exchange}"...`); await this.#rabbitMqGateway.unsubscribe({ diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index f43b6e6..72317a7 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -2,7 +2,6 @@ import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; import { IContainer, ILogger, IMessage, isMessage } from '../interfaces'; import * as Event from '../Event'; import { delay } from '../utils'; -import { HANDLER_PROCESS_TIMEOUT } from './constants'; import { TerminationHandler } from './TerminationHandler'; /** Generate a short pseudo-unique identifier using a truncated timestamp and random component */ @@ -47,6 +46,8 @@ const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); */ export class RabbitMqGateway { + static HANDLER_PROCESS_TIMEOUT = 60 * 60 * 1000; // 1 hour + #connectionFactory: () => Promise; #appId: string; #logger: ILogger | undefined; @@ -360,7 +361,7 @@ export class RabbitMqGateway { messageId }); channel.nack(msg, false, false); - }, HANDLER_PROCESS_TIMEOUT); + }, RabbitMqGateway.HANDLER_PROCESS_TIMEOUT); try { diff --git a/src/rabbitmq/constants.ts b/src/rabbitmq/constants.ts deleted file mode 100644 index 2f3a455..0000000 --- a/src/rabbitmq/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DEFAULT_EXCHANGE = 'node-cqrs.events'; -export const HANDLER_PROCESS_TIMEOUT = 60 * 60 * 1000; // 1 hour From 3dd57a100766b164a8dfebbb946a62e22e5d7dce Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 14 Oct 2025 00:56:22 +0100 Subject: [PATCH 099/135] 1.0.0-rc.20 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ea06c..53aa750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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) + + +### Changes + +* Enhance type safety in CqrsContainerBuilder with generics ([025765c](https://github.com/snatalenko/node-cqrs/commit/025765cc31eec5a004142dff5cafd8264af10ea9)) + + # [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) diff --git a/package-lock.json b/package-lock.json index 5546dad..79de12f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.19", + "version": "1.0.0-rc.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.19", + "version": "1.0.0-rc.20", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index ecd1e92..b95dfe4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.19", + "version": "1.0.0-rc.20", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From ca4016a486a7b2a010f86174140bd21e0a1c0d08 Mon Sep 17 00:00:00 2001 From: Anton Korotkov Date: Tue, 14 Oct 2025 11:22:46 +0200 Subject: [PATCH 100/135] fix: Proper milliseconds calculation for Event Locker --- src/sqlite/SqliteEventLocker.ts | 6 +++--- src/sqlite/queries/eventLockTableInit.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sqlite/SqliteEventLocker.ts b/src/sqlite/SqliteEventLocker.ts index 9beedda..316f028 100644 --- a/src/sqlite/SqliteEventLocker.ts +++ b/src/sqlite/SqliteEventLocker.ts @@ -81,16 +81,16 @@ export class SqliteEventLocker extends AbstractSqliteAccessor implements IEventL VALUES (?, ?, ?) ON CONFLICT (projection_name, schema_version, event_id) DO UPDATE SET - processing_at = cast(strftime('%f', 'now') * 1000 as INTEGER) + processing_at = cast(unixepoch('subsec') * 1000 as INTEGER) WHERE processed_at IS NULL - AND processing_at <= cast(strftime('%f', 'now') * 1000 as INTEGER) - ${this.#eventLockTtl} + AND processing_at <= cast(unixepoch('subsec') * 1000 as INTEGER) - ${this.#eventLockTtl} `); this.#finalizeEventLockQuery = db.prepare(` UPDATE ${this.#eventLockTableName} SET - processed_at = (cast(strftime('%f', 'now') * 1000 as INTEGER)) + processed_at = cast(unixepoch('subsec') * 1000 as INTEGER) WHERE projection_name = ? AND schema_version = ? diff --git a/src/sqlite/queries/eventLockTableInit.ts b/src/sqlite/queries/eventLockTableInit.ts index 31a6b95..5480654 100644 --- a/src/sqlite/queries/eventLockTableInit.ts +++ b/src/sqlite/queries/eventLockTableInit.ts @@ -3,7 +3,7 @@ export const eventLockTableInit = (eventLockTableName: string) => ` projection_name TEXT NOT NULL, schema_version TEXT NOT NULL, event_id BLOB NOT NULL, - processing_at INTEGER NOT NULL DEFAULT (cast(strftime('%f', 'now') * 1000 as INTEGER)), + processing_at INTEGER NOT NULL DEFAULT (cast(unixepoch('subsec') * 1000 as INTEGER)), processed_at INTEGER, PRIMARY KEY (projection_name, schema_version, event_id) ); From f3850315cdc07528db14abe123781ea7f24c0937 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 14 Oct 2025 12:50:57 +0100 Subject: [PATCH 101/135] Improve rabbitmq error logging --- src/rabbitmq/RabbitMqGateway.ts | 35 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 72317a7..54a9d4a 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -36,6 +36,19 @@ type Subscription = { const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); +function extractErrorMessage(err: Error | AggregateError | unknown): string { + if (!err || typeof err !== 'object') + return String(err); + + if (err instanceof AggregateError) + return err.errors?.map(e => (e && 'message' in e ? e.message : String(e))).join('; '); + + if (err instanceof Error && err.message) + return err.message; + + return String(err); +} + /** * RabbitMqGateway implements the IObservable interface using RabbitMQ. * @@ -113,8 +126,8 @@ export class RabbitMqGateway { for (const subscription of subscriptionsToRestore) await this.subscribe(subscription); } - catch (err: any) { - this.#logger?.warn(`${this.#appId}: Connection attempt failed: ${err.message}`); + catch (err: unknown) { + this.#logger?.warn(`${this.#appId}: Connection attempt failed: ${extractErrorMessage(err)}`); await delay(5_000); } } @@ -135,9 +148,9 @@ export class RabbitMqGateway { this.#logger?.debug(`${this.#appId}: Disconnected from RabbitMQ`); } - catch (err: any) { - this.#logger?.error(`${this.#appId}: Failed to disconnect from RabbitMQ: ${err.message}`, { - stack: err.stack + catch (err: unknown) { + this.#logger?.error(`${this.#appId}: Failed to disconnect from RabbitMQ: ${extractErrorMessage(err)}`, { + stack: (err as Error)?.stack }); } } @@ -152,8 +165,8 @@ export class RabbitMqGateway { this.#logger?.debug(`${this.#appId}: Consumer "${consumerTag}" on queue "${queueName}" cancelled successfully`); this.#queueConsumers.delete(queueName); } - catch (err: any) { - this.#logger?.error(`${this.#appId}: Failed to cancel consumer "${consumerTag}" for queue "${queueName}": ${err.message}`); + catch (err: unknown) { + this.#logger?.error(`${this.#appId}: Failed to cancel consumer "${consumerTag}" for queue "${queueName}": ${extractErrorMessage(err)}`); } }); @@ -161,8 +174,8 @@ export class RabbitMqGateway { this.#logger?.info(`${this.#appId}: All consumers stopped.`); } - #onConnectionError(err: Error) { - this.#logger?.warn(`${this.#appId}: Connection error: ${err.message}`); + #onConnectionError(err: unknown) { + this.#logger?.warn(`${this.#appId}: Connection error: ${extractErrorMessage(err)}`); } #onConnectionClosed() { @@ -390,8 +403,8 @@ export class RabbitMqGateway { channel?.ack(msg); } - catch (err: any) { - this.#logger?.error(`${this.#appId}: Message processing failed: ${err.message}`); + catch (err: unknown) { + this.#logger?.error(`${this.#appId}: Message processing failed: ${extractErrorMessage(err)}`); // Redirect message to dead letter queue, if `{ noAck: true }` was not set on consumption channel?.nack(msg, false, false); From 984cbd515578b40a8d05738ece8adc8d06f3f6ca Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 14 Oct 2025 12:51:33 +0100 Subject: [PATCH 102/135] 1.0.0-rc.21 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53aa750..3a5c16e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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 + +* Proper milliseconds calculation for Event Locker ([ca4016a](https://github.com/snatalenko/node-cqrs/commit/ca4016a486a7b2a010f86174140bd21e0a1c0d08)) + + # [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) diff --git a/package-lock.json b/package-lock.json index 79de12f..5565b22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.20", + "version": "1.0.0-rc.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.20", + "version": "1.0.0-rc.21", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index b95dfe4..228082a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.20", + "version": "1.0.0-rc.21", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From b38cfb0489173d463565eeb353cf2994ed72a79e Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 24 Oct 2025 00:40:12 +0100 Subject: [PATCH 103/135] Remove RabbitMqEventInjector and related tests; simplify RabbitMqGateway and EventBus --- src/rabbitmq/IContainer.ts | 9 --- src/rabbitmq/RabbitMqEventBus.ts | 30 ++------- src/rabbitmq/RabbitMqEventInjector.ts | 66 ------------------- src/rabbitmq/RabbitMqGateway.ts | 7 +- src/rabbitmq/index.ts | 1 - .../rabbitmq/RabbitMqEventInjector.test.ts | 66 ------------------- 6 files changed, 9 insertions(+), 170 deletions(-) delete mode 100644 src/rabbitmq/RabbitMqEventInjector.ts delete mode 100644 tests/integration/rabbitmq/RabbitMqEventInjector.test.ts diff --git a/src/rabbitmq/IContainer.ts b/src/rabbitmq/IContainer.ts index 9a66a29..ceac5c4 100644 --- a/src/rabbitmq/IContainer.ts +++ b/src/rabbitmq/IContainer.ts @@ -1,16 +1,7 @@ -import { IEventBus } from '../interfaces'; -import { RabbitMqEventInjector } from './RabbitMqEventInjector'; import { RabbitMqGateway } from './RabbitMqGateway'; declare module '../interfaces/IContainer' { interface IContainer { rabbitMqGateway?: RabbitMqGateway; - rabbitMqEventInjector?: RabbitMqEventInjector; - rabbitMqEventBus?: RabbitMqEventInjector; - - /** - * Optional external event bus for publishing events to an external system. - */ - externalEventBus?: IEventBus; } } diff --git a/src/rabbitmq/RabbitMqEventBus.ts b/src/rabbitmq/RabbitMqEventBus.ts index 894a672..50e2e01 100644 --- a/src/rabbitmq/RabbitMqEventBus.ts +++ b/src/rabbitmq/RabbitMqEventBus.ts @@ -1,12 +1,10 @@ -import { IEvent, IEventBus, IDispatchPipelineProcessor, IMessageHandler, IObservable, DispatchPipelineBatch } from '../interfaces'; +import { IEvent, IEventBus, IMessageHandler, IObservable } from '../interfaces'; import { RabbitMqGateway } from './RabbitMqGateway'; -const ALL_EVENTS_WILDCARD = '*'; +export class RabbitMqEventBus implements IEventBus { -export class RabbitMqEventBus implements IEventBus, IDispatchPipelineProcessor { - - static get allEventsWildcard(): '*' { - return ALL_EVENTS_WILDCARD; + static get allEventsWildcard(): string { + return RabbitMqGateway.ALL_EVENTS_WILDCARD; } static DEFAULT_EXCHANGE = 'node-cqrs.events'; @@ -82,24 +80,4 @@ export class RabbitMqEventBus implements IEventBus, IDispatchPipelineProcessor { } return queue; } - - /** - * Processes a batch of events and publishes them to the fanout exchange. - * - * This method is part of the `IDispatchPipelineProcessor` interface. - */ - async process(batch: DispatchPipelineBatch): Promise { - for (const { event, origin } of batch) { - // Skip publishing if the event was dispatched from external source - if (origin === 'external') - continue; - - if (!event) - throw new Error('Event batch does not contain `event`'); - - await this.publish(event); - } - - return batch; - } } diff --git a/src/rabbitmq/RabbitMqEventInjector.ts b/src/rabbitmq/RabbitMqEventInjector.ts deleted file mode 100644 index c973177..0000000 --- a/src/rabbitmq/RabbitMqEventInjector.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { IContainer } from '../interfaces/IContainer'; -import { IMessage } from '../interfaces/IMessage'; -import { RabbitMqGateway } from './RabbitMqGateway'; -import { IEventDispatcher } from '../interfaces'; -import * as Event from '../Event'; -import { RabbitMqEventBus } from './RabbitMqEventBus'; - -/** - * Injects events received from a RabbitMQ exchange into the local event dispatcher. - * - * It subscribes to a specified fanout exchange on RabbitMQ and dispatches - * any received messages as events using the provided event dispatcher. - */ -export class RabbitMqEventInjector { - #rabbitMqGateway: RabbitMqGateway; - #messageHandler: (message: IMessage) => Promise; - #eventDispatcher: IEventDispatcher; - #logger: IContainer['logger']; - - constructor(container: Partial>) { - if (!container.eventDispatcher) - throw new Error('eventDispatcher is required in the container.'); - if (!container.rabbitMqGateway) - throw new Error('rabbitMqGateway is required in the container.'); - - this.#rabbitMqGateway = container.rabbitMqGateway; - this.#messageHandler = (msg: IMessage) => this.#handleMessage(msg); - this.#eventDispatcher = container.eventDispatcher; - this.#logger = container.logger && 'child' in container.logger ? - container.logger.child({ service: new.target.name }) : - container.logger; - } - - async start(exchange: string = RabbitMqEventBus.DEFAULT_EXCHANGE): Promise { - this.#logger?.debug(`Subscribing to messages from exchange "${exchange}"...`); - - await this.#rabbitMqGateway.subscribeToFanout(exchange, this.#messageHandler); - - this.#logger?.debug(`Listening to messages from exchange "${exchange}"`); - } - - async stop(exchange: string = RabbitMqEventBus.DEFAULT_EXCHANGE): Promise { - this.#logger?.debug(`Unsubscribing from messages from exchange "${exchange}"...`); - - await this.#rabbitMqGateway.unsubscribe({ - exchange, - handler: this.#messageHandler - }); - - this.#logger?.debug(`Stopped listening to messages from exchange "${exchange}"`); - } - - async #handleMessage(message: IMessage): Promise { - this.#logger?.debug(`"${Event.describe(message)}" received`); - try { - await this.#eventDispatcher.dispatch([message], { origin: 'external' }); - - this.#logger?.debug(`${Event.describe(message)} dispatched successfully`); - } - catch (error: any) { - this.#logger?.error(`Failed to dispatch event ${message.type}: ${error.message}`, { stack: error.stack }); - - throw error; // Re-throw to ensure message is nack'd by the gateway - } - } -} diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 54a9d4a..84e0d58 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -60,6 +60,7 @@ function extractErrorMessage(err: Error | AggregateError | unknown): string { export class RabbitMqGateway { static HANDLER_PROCESS_TIMEOUT = 60 * 60 * 1000; // 1 hour + static ALL_EVENTS_WILDCARD = '*'; #connectionFactory: () => Promise; #appId: string; @@ -175,7 +176,9 @@ export class RabbitMqGateway { } #onConnectionError(err: unknown) { - this.#logger?.warn(`${this.#appId}: Connection error: ${extractErrorMessage(err)}`); + this.#logger?.error(`${this.#appId}: Connection error: ${extractErrorMessage(err)}`, { + stack: err instanceof Error ? err.stack : undefined + }); } #onConnectionClosed() { @@ -192,7 +195,7 @@ export class RabbitMqGateway { s.queueGivenName === queueGivenName && ( !s.eventType - || s.eventType === '*' + || s.eventType === RabbitMqGateway.ALL_EVENTS_WILDCARD || s.eventType === eventType ) ); diff --git a/src/rabbitmq/index.ts b/src/rabbitmq/index.ts index 26b4d04..79404df 100644 --- a/src/rabbitmq/index.ts +++ b/src/rabbitmq/index.ts @@ -1,3 +1,2 @@ export * from './RabbitMqEventBus'; -export * from './RabbitMqEventInjector'; export * from './RabbitMqGateway'; diff --git a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts b/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts deleted file mode 100644 index 08eba71..0000000 --- a/tests/integration/rabbitmq/RabbitMqEventInjector.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as amqplib from 'amqplib'; -import { RabbitMqGateway } from '../../../src/rabbitmq/RabbitMqGateway'; -import { RabbitMqEventInjector } from '../../../src/rabbitmq/RabbitMqEventInjector'; -import { IEvent, IEventDispatcher } from '../../../src/interfaces'; -import { jest } from '@jest/globals'; -import { delay } from '../../../src/utils'; - -describe('RabbitMqEventInjector', () => { - let rabbitMqGateway: RabbitMqGateway; - let rabbitMqGateway2: RabbitMqGateway; - let eventDispatcher: jest.Mocked; - - const exchange = 'node-cqrs.events'; - const eventType = 'test-injector-event'; - - beforeEach(async () => { - const rabbitMqConnectionFactory = () => amqplib.connect('amqp://localhost'); - rabbitMqGateway = new RabbitMqGateway({ rabbitMqConnectionFactory }); - rabbitMqGateway2 = new RabbitMqGateway({ rabbitMqConnectionFactory }); - - eventDispatcher = { - dispatch: jest.fn().mockResolvedValue(undefined) - } as unknown as jest.Mocked; - - const injector = new RabbitMqEventInjector({ rabbitMqGateway, eventDispatcher }); - - await injector.start(exchange); - }); - - afterEach(async () => { - const ch = await rabbitMqGateway.connection?.createChannel(); - await ch.deleteExchange(exchange); - await ch.close(); - await rabbitMqGateway.disconnect(); - await rabbitMqGateway2.disconnect(); - }); - - it('does not receive messages published to own gateway', async () => { - const testEvent: IEvent = { - type: eventType, - payload: { data: 'test-payload' }, - id: 'test-id-123' - }; - - await rabbitMqGateway.publish(exchange, testEvent); - - await delay(50); - - expect(eventDispatcher.dispatch).not.toHaveBeenCalled(); - }); - - it('receives messages published to other gateway, dispatches to eventDispatcher', async () => { - const testEvent: IEvent = { - type: eventType, - payload: { data: 'test-payload' }, - id: 'test-id-123' - }; - - await rabbitMqGateway2.publish(exchange, testEvent); - - await delay(50); - - expect(eventDispatcher.dispatch).toHaveBeenCalledTimes(1); - expect(eventDispatcher.dispatch).toHaveBeenCalledWith([testEvent], { origin: 'external' }); - }); -}); From 149414cc0292a76a11cc61dd911cf0c2ab967bc7 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 24 Oct 2025 00:40:27 +0100 Subject: [PATCH 104/135] 1.0.0-rc.22 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a5c16e..2751a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 5565b22..253c333 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.21", + "version": "1.0.0-rc.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.21", + "version": "1.0.0-rc.22", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 228082a..1cc0050 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.21", + "version": "1.0.0-rc.22", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 1d6767a391ad2cd755e0cf0557fee028215521c0 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 24 Oct 2025 16:37:15 +0100 Subject: [PATCH 105/135] Refactor event type handling in RabbitMqGateway to use constant for wildcard --- src/rabbitmq/RabbitMqGateway.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 84e0d58..b704e9c 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -190,7 +190,7 @@ export class RabbitMqGateway { this.#queueConsumers.clear(); } - #getHandlers(queueGivenName: string = '', eventType: string = '*') { + #getHandlers(queueGivenName: string = '', eventType: string = RabbitMqGateway.ALL_EVENTS_WILDCARD) { return this.#subscriptions.filter(s => s.queueGivenName === queueGivenName && ( @@ -346,7 +346,7 @@ export class RabbitMqGateway { } async #assertBinding(channel: Channel, exchange: string, queueGivenName: string, eventType?: string) { - if (!eventType || eventType === '*') + if (!eventType || eventType === RabbitMqGateway.ALL_EVENTS_WILDCARD) eventType = '#'; await channel.bindQueue(queueGivenName, exchange, eventType); From 5c1166a0388062f3b23dbdc5f3d0bdf811f3ddfc Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 26 Oct 2025 23:21:33 +0000 Subject: [PATCH 106/135] Update DispatchPipelineEnvelope to allow flexible origin types --- src/interfaces/IDispatchPipelineProcessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/IDispatchPipelineProcessor.ts b/src/interfaces/IDispatchPipelineProcessor.ts index f263490..958412f 100644 --- a/src/interfaces/IDispatchPipelineProcessor.ts +++ b/src/interfaces/IDispatchPipelineProcessor.ts @@ -10,7 +10,7 @@ export type DispatchPipelineEnvelope = { /** * Origin of the event. Can be used to distinguish between events coming from different sources. */ - origin?: 'external' | 'internal'; + origin?: string; event?: IEvent; } From 5799d18398600da9d988b8d8eb8c16a794be46ab Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 26 Oct 2025 23:21:44 +0000 Subject: [PATCH 107/135] 1.0.0-rc.23 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2751a68..a36db8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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.22](https://github.com/snatalenko/node-cqrs/compare/v1.0.0-rc.21...v1.0.0-rc.22) (2025-10-23) diff --git a/package-lock.json b/package-lock.json index 253c333..a0d6283 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.22", + "version": "1.0.0-rc.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.22", + "version": "1.0.0-rc.23", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 1cc0050..870f407 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.22", + "version": "1.0.0-rc.23", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 0425c9d6335e6c1d685713ef7beca307b0e81726 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 30 Oct 2025 12:34:51 +0000 Subject: [PATCH 108/135] Enhance RabbitMqGateway subscription options with `noAck` and `handlerProcessTimeout`; expose all options on `subscribeToQueue` and `subscribeToFanout` --- src/rabbitmq/RabbitMqGateway.ts | 47 ++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index b704e9c..1280629 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -32,6 +32,22 @@ type Subscription = { /** 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; }; const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); @@ -201,8 +217,9 @@ export class RabbitMqGateway { ); } - async subscribeToQueue(exchange: string, queueName: string, handler: MessageHandler) { - return this.subscribe({ exchange, queueName, handler }); + async subscribeToQueue(exchange: string, queueName: string, handler: MessageHandler, + options?: Omit) { + return this.subscribe({ exchange, queueName, handler, ...options }); } /** @@ -211,8 +228,9 @@ export class RabbitMqGateway { * Messages are considered "delivered" upon receipt. * Failed message processing does not result in redelivery or dead-lettering. */ - async subscribeToFanout(exchange: string, handler: MessageHandler) { - return this.subscribe({ exchange, handler, ignoreOwn: true }); + async subscribeToFanout(exchange: string, handler: MessageHandler, + options?: Omit) { + return this.subscribe({ exchange, handler, ignoreOwn: true, ...options }); } /** @@ -238,8 +256,7 @@ export class RabbitMqGateway { const { exchange, queueName, - eventType, - concurrentLimit + eventType } = subscription; const channel = await this.#assertChannel(queueName); @@ -271,7 +288,7 @@ export class RabbitMqGateway { await this.#assetQueue(channel, exchange, queueGivenName, eventType, { deadLetterExchangeName }); } - await this.#assertConsumer(queueGivenName, channel, concurrentLimit); + await this.#assertConsumer(queueGivenName, channel, subscription); this.#subscriptions.push({ ...subscription, queueGivenName }); } @@ -354,12 +371,16 @@ export class RabbitMqGateway { this.#logger?.debug(`${this.#appId}: Queue "${queueGivenName}" bound to exchange "${exchange}" with pattern "${eventType}"`); } - async #assertConsumer(queueGivenName: string, channel: Channel, concurrentLimit?: number) { + async #assertConsumer( + queueGivenName: string, + channel: Channel, + options?: Pick + ) { if (this.#queueConsumers.has(queueGivenName)) return; - if (concurrentLimit) - await channel.prefetch(concurrentLimit); + if (options?.concurrentLimit !== undefined) + await channel.prefetch(options.concurrentLimit); const c = await channel.consume(queueGivenName, async (msg: ConsumeMessage | null) => { if (!msg) @@ -369,6 +390,7 @@ export class RabbitMqGateway { const { messageId, correlationId, appId } = msg.properties ?? {}; // Keep the process alive while waiting for the handler to finish + const handlerProcessTimeout = options?.handlerProcessTimeout ?? RabbitMqGateway.HANDLER_PROCESS_TIMEOUT; const keepAliveTimeout = setTimeout(() => { this.#logger?.warn(`${this.#appId}: Message processing timed out`, { queueName: queueGivenName, @@ -377,10 +399,9 @@ export class RabbitMqGateway { messageId }); channel.nack(msg, false, false); - }, RabbitMqGateway.HANDLER_PROCESS_TIMEOUT); + }, handlerProcessTimeout); try { - this.#logger?.debug(`${this.#appId}: Message received`, { queueName: queueGivenName, consumerTag, @@ -415,6 +436,8 @@ export class RabbitMqGateway { finally { clearTimeout(keepAliveTimeout); } + }, { + noAck: options?.noAck }); this.#logger?.debug(`${this.#appId}: Consumer "${c.consumerTag}" registered on queue "${queueGivenName}"`); From d779c7e37659c043b17c7c9146b4057c1f4b2f56 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 30 Oct 2025 15:03:57 +0000 Subject: [PATCH 109/135] 1.0.0-rc.24 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a36db8d..9608d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index a0d6283..3942654 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.23", + "version": "1.0.0-rc.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.23", + "version": "1.0.0-rc.24", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 870f407..b65ada9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.23", + "version": "1.0.0-rc.24", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From d54c54370c0e2110eec7bad27e9a2b44c2690261 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 31 Oct 2025 18:01:17 +0000 Subject: [PATCH 110/135] Avoid publishing "snapshot" events to eventBus --- src/AbstractAggregate.ts | 5 ++--- src/EventDispatchPipeline.ts | 15 ++++++++++----- src/in-memory/InMemorySnapshotStorage.ts | 7 ++----- src/interfaces/ISnapshotEvent.ts | 10 ++++++++++ src/interfaces/index.ts | 1 + 5 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 src/interfaces/ISnapshotEvent.ts diff --git a/src/AbstractAggregate.ts b/src/AbstractAggregate.ts index b92715e..4243173 100644 --- a/src/AbstractAggregate.ts +++ b/src/AbstractAggregate.ts @@ -5,13 +5,12 @@ import { Identifier, IEvent, IEventSet, - IAggregateConstructorParams + IAggregateConstructorParams, + SNAPSHOT_EVENT_TYPE } from './interfaces'; import { getClassName, validateHandlers, getHandler, getMessageHandlerNames } from './utils'; -const SNAPSHOT_EVENT_TYPE = 'snapshot'; - /** * Base class for Aggregate definition */ diff --git a/src/EventDispatchPipeline.ts b/src/EventDispatchPipeline.ts index 0d1d95c..340612d 100644 --- a/src/EventDispatchPipeline.ts +++ b/src/EventDispatchPipeline.ts @@ -3,7 +3,8 @@ import { IEvent, IDispatchPipelineProcessor, IEventBus, - isDispatchPipelineProcessor + isDispatchPipelineProcessor, + isSnapshotEvent } from './interfaces'; import { parallelPipe } from 'async-parallel-pipe'; @@ -73,10 +74,14 @@ export class EventDispatchPipeline { const events: IEvent[] = []; for (const batch of data) { const { event, ...meta } = batch as any; - if (event) { - await this.eventBus.publish(event, meta); - events.push(event); - } + if (!event) + continue; + if (isSnapshotEvent(event)) + continue; + + await this.eventBus.publish(event, meta); + + events.push(event); } resolve(events); } diff --git a/src/in-memory/InMemorySnapshotStorage.ts b/src/in-memory/InMemorySnapshotStorage.ts index ae3bbcf..879dcb2 100644 --- a/src/in-memory/InMemorySnapshotStorage.ts +++ b/src/in-memory/InMemorySnapshotStorage.ts @@ -5,14 +5,11 @@ import { Identifier, IDispatchPipelineProcessor, IEvent, - ILogger + ILogger, + isSnapshotEvent } from '../interfaces'; import * as Event from '../Event'; -const SNAPSHOT_EVENT_TYPE = 'snapshot'; -const isSnapshotEvent = (event?: IEvent): event is IEvent & { type: 'snapshot' } => - (!!event && event.type === SNAPSHOT_EVENT_TYPE); - /** * In-memory storage for aggregate snapshots. * Storage content resets on app restart diff --git a/src/interfaces/ISnapshotEvent.ts b/src/interfaces/ISnapshotEvent.ts new file mode 100644 index 0000000..81b61e6 --- /dev/null +++ b/src/interfaces/ISnapshotEvent.ts @@ -0,0 +1,10 @@ +import { IEvent, isEvent } from './IEvent'; + +export const SNAPSHOT_EVENT_TYPE: 'snapshot' = 'snapshot'; + +export interface ISnapshotEvent extends IEvent { + type: typeof SNAPSHOT_EVENT_TYPE +} + +export const isSnapshotEvent = (event?: unknown): event is ISnapshotEvent => + isEvent(event) && event.type === SNAPSHOT_EVENT_TYPE; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 6829a48..9b9539f 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -24,4 +24,5 @@ export * from './IObservable'; export * from './IObserver'; export * from './IProjection'; export * from './ISaga'; +export * from './ISnapshotEvent'; export * from './IViewLocker'; From 4697db1348b57a8d59062a4a5ee6ad62fba066ce Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 31 Oct 2025 18:01:26 +0000 Subject: [PATCH 111/135] 1.0.0-rc.25 --- CHANGELOG.md | 4 ++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9608d2e..da34cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 3942654..6687ebc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.24", + "version": "1.0.0-rc.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.24", + "version": "1.0.0-rc.25", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index b65ada9..4834a21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.24", + "version": "1.0.0-rc.25", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From f6171498db544d820e876d550421eef75c66088f Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Tue, 2 Dec 2025 20:35:02 +0000 Subject: [PATCH 112/135] Internal Fix: Use "quorum" type for durable queues --- src/rabbitmq/RabbitMqGateway.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 1280629..e129685 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -350,9 +350,13 @@ export class RabbitMqGateway { const { queue: queueGivenName } = await channel.assertQueue(queueName, { exclusive, durable, - ...deadLetterExchangeName && { - arguments: { + 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' } } }); From 0e9b25edd0a81581fb084256638c9ab56afb4115 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 3 Dec 2025 00:49:43 +0000 Subject: [PATCH 113/135] Internal Fix: Vulnerability in js-yaml dev dependency --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6687ebc..d61ce55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -921,9 +921,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/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": "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": { @@ -5019,9 +5019,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "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": { From 506acc2dde02dd4d83cb8e8d6079dc63fa992651 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 5 Dec 2025 00:44:36 +0000 Subject: [PATCH 114/135] Internal Fix: Ensure proper subscription management in TerminationHandler --- src/rabbitmq/TerminationHandler.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/rabbitmq/TerminationHandler.ts b/src/rabbitmq/TerminationHandler.ts index f5e63f3..693fe5a 100644 --- a/src/rabbitmq/TerminationHandler.ts +++ b/src/rabbitmq/TerminationHandler.ts @@ -7,6 +7,7 @@ export class TerminationHandler { #process: NodeJS.Process; #cleanupHandler: () => Promise; #terminationHandler: () => Promise; + #subscribed = false; constructor(process: NodeJS.Process, cleanupHandler: () => Promise) { this.#process = process; @@ -15,13 +16,18 @@ export class TerminationHandler { } on() { + if (this.#subscribed) + return; + this.#process.once('SIGINT', this.#terminationHandler); this.#process.once('SIGTERM', this.#terminationHandler); + this.#subscribed = true; } off() { this.#process.off('SIGINT', this.#terminationHandler); this.#process.off('SIGTERM', this.#terminationHandler); + this.#subscribed = false; } async #onProcessTermination() { From a42d138fc93bc767ae5d7fac75f5582cb3936103 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 5 Dec 2025 00:46:12 +0000 Subject: [PATCH 115/135] Change: Move reconnect logic to rabbitMqConnectionFactory; re-establish subscriptions on reconnect --- src/rabbitmq/RabbitMqGateway.ts | 96 ++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index e129685..79142ce 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -65,6 +65,8 @@ function extractErrorMessage(err: Error | AggregateError | unknown): string { return String(err); } +const describeSub = (s: Subscription) => `to${s.queueName ? ` queue "${s.queueName}" of` : ''} exchange "${s.exchange}"`; + /** * RabbitMqGateway implements the IObservable interface using RabbitMQ. * @@ -89,11 +91,19 @@ export class RabbitMqGateway { #queueChannels = new Map(); #queueConsumers = new Map(); - #subscriptions: Array = []; - #handlers: Map>> = new Map(); + #subscriptions: Array = []; /** Handles termination signals for graceful shutdown */ #terminationHandler: TerminationHandler | undefined; + #restoringSubscriptions: boolean = false; get connection() { return this.#connection; @@ -127,31 +137,53 @@ export class RabbitMqGateway { */ async connect(): Promise { while (this.#connecting) - await delay(1_000); + await delay(100); + + if (this.#connection) { + this.#logger?.debug(`${this.#appId}: Connection is already established`); + return this.#connection; + } this.#connecting = true; - while (!this.#connection) { - try { - this.#connection = await this.#connectionFactory(); - this.#connection.on('error', err => this.#onConnectionError(err)); - this.#connection.on('close', () => this.#onConnectionClosed()); - this.#logger?.info(`${this.#appId}: Connection established`); - - this.#handlers.clear(); - const subscriptionsToRestore = this.#subscriptions.splice(0); - for (const subscription of subscriptionsToRestore) - await this.subscribe(subscription); - } - catch (err: unknown) { - this.#logger?.warn(`${this.#appId}: Connection attempt failed: ${extractErrorMessage(err)}`); - await delay(5_000); - } + try { + this.#connection = await this.#connectionFactory(); + this.#connection.on('error', err => this.#onConnectionError(err)); + this.#connection.on('close', () => this.#onConnectionClosed()); + + this.#logger?.info(`${this.#appId}: Connection established`); + + await this.#restoreSubscriptions(); + + return this.#connection; } + catch (err: unknown) { + this.#logger?.error(`${this.#appId}: Connection attempt failed: ${extractErrorMessage(err)}`, { + stack: (err as Error)?.stack + }); + throw err; + } + finally { + this.#connecting = false; + } + } - this.#connecting = false; + async #restoreSubscriptions() { + this.#restoringSubscriptions = true; + try { + 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; - return this.#connection; + await this.#subscribe(subscriptionToRestore); + } + + this.#logger?.debug(`${this.#appId}: ${this.#subscriptions.length} subscription(s) restored`); + } + finally { + this.#restoringSubscriptions = false; + } } async disconnect() { @@ -249,9 +281,20 @@ export class RabbitMqGateway { * @returns A promise that resolves when the subscription is successfully set up. */ async subscribe(subscription: Subscription) { + while (this.#restoringSubscriptions) + await delay(100); + + return this.#subscribe(subscription); + } + + async #subscribe(subscription: Subscription) { const subscriptionExists = !!this.#findSubscription(subscription); if (subscriptionExists) - throw new Error('Subscription already exists'); + 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(`${this.#appId}: Subscription ${describeSub(subscription)} recorded`); const { exchange, @@ -290,7 +333,11 @@ export class RabbitMqGateway { await this.#assertConsumer(queueGivenName, channel, subscription); - this.#subscriptions.push({ ...subscription, queueGivenName }); + const subscriptionRecord = this.#findSubscription(subscription); + if (subscriptionRecord) + subscriptionRecord.queueGivenName = queueGivenName; + + this.#logger?.debug(`${this.#appId}: Subscription ${describeSub(subscription)} established`); } #findSubscription(subscription: Pick) { @@ -308,7 +355,8 @@ export class RabbitMqGateway { this.#subscriptions = this.#subscriptions.filter(s => s !== subscriptionToRemove); - await this.#tryDropConsumer(subscriptionToRemove.queueGivenName); + if (subscriptionToRemove.queueGivenName) + await this.#tryDropConsumer(subscriptionToRemove.queueGivenName); } async #assertConnection() { From 8c6ead0a9b4f3feba7bbfba539082eeb0b09b9f9 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 5 Dec 2025 00:53:50 +0000 Subject: [PATCH 116/135] Build: Update changelog titles and commit message prefixes --- scripts/changelog/index.js | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/scripts/changelog/index.js b/scripts/changelog/index.js index 1028fe2..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]) - 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' From 96a0bff08556d10e902dfa41267ac90c88b6ecf1 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 5 Dec 2025 00:54:01 +0000 Subject: [PATCH 117/135] 1.0.0-rc.26 --- CHANGELOG.md | 60 ++++++++++++++++++++++++++++++----------------- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da34cfb..3f1d452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# [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 + +* 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) @@ -68,10 +86,10 @@ ### Changes * Cache immediate aggregates to handle concurrent commands ([e193c4c](https://github.com/snatalenko/node-cqrs/commit/e193c4c8dc7b91de6cbc84e2ac668170ddb48bc0)) -* Use `structuredClone` for snapshot creation ([1d0e827](https://github.com/snatalenko/node-cqrs/commit/1d0e827da71c760739588a37ae6afe63a4fa8d34)) -### Refactoring +### 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)) @@ -110,29 +128,30 @@ # [1.0.0-rc.6](https://github.com/snatalenko/node-cqrs/compare/v0.16.4...v1.0.0-rc.6) (2025-03-21) -### Fixes - -* Vulnerability in minimist dependency ([07b8c68](https://github.com/snatalenko/node-cqrs/commit/07b8c682fae4278965aa13a06caa994c037934e9)) - ### 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)) -### Refactoring +### Fixes -* Migrate to TS and Jest ([6737d55](https://github.com/snatalenko/node-cqrs/commit/6737d5566a9dc6314df0b20a65d32414fc503e54)) +* Vulnerability in minimist dependency ([07b8c68](https://github.com/snatalenko/node-cqrs/commit/07b8c682fae4278965aa13a06caa994c037934e9)) ### 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)) +### 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.4](https://github.com/snatalenko/node-cqrs/compare/v0.16.3...v0.16.4) (2022-08-28) -### Refactoring +### Internal Fixes * Use di package from npm ([0e8db91](https://github.com/snatalenko/node-cqrs/commit/0e8db91636541e95f804e2c266e2d8bbf0f49a8b)) @@ -157,14 +176,14 @@ ## [0.16.1](https://github.com/snatalenko/node-cqrs/compare/v0.16.0...v0.16.1) (2021-05-28) -### Fixes - -* Mark aggregateId optional on command send ([f496ecf](https://github.com/snatalenko/node-cqrs/commit/f496ecfbd5413e8e2a4c69af7848ecc3f1a5365a)) - ### 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) @@ -174,16 +193,8 @@ * 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)) -### 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)) - ### Changes -* Debug, mocha, sinon ([ac80c27](https://github.com/snatalenko/node-cqrs/commit/ac80c27653828904cf7b80d37b0ecade860b7490)) * Move DI container to a separate package ([350f3f4](https://github.com/snatalenko/node-cqrs/commit/350f3f405a98fea2c7a85ea92f2b0f1aa945c75c)) * Do not bind masterHandler to observer automatically ([d2ec79d](https://github.com/snatalenko/node-cqrs/commit/d2ec79dced5460f619cf9bed5f34df1bbb8e0132)) * Remove deprecated InMemoryView..markAsReady method ([23015ec](https://github.com/snatalenko/node-cqrs/commit/23015ec3f5bc69f843cf6815caa1f4cda9fea27c)) @@ -191,6 +202,13 @@ * 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)) diff --git a/package-lock.json b/package-lock.json index d61ce55..c5944b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.25", + "version": "1.0.0-rc.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.25", + "version": "1.0.0-rc.26", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 4834a21..efca09e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.25", + "version": "1.0.0-rc.26", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 21686bebb6a0ca5901263f3d382ffe369d62ef85 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 17 Dec 2025 00:14:07 +0000 Subject: [PATCH 118/135] Internal Fix: close rabbitmq connection on SIGINT/SIGTERM --- src/rabbitmq/RabbitMqGateway.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 79142ce..5835b77 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -122,7 +122,7 @@ export class RabbitMqGateway { o.logger; if (o.process) - this.#terminationHandler = new TerminationHandler(o.process, () => this.#stopConsuming()); + this.#terminationHandler = new TerminationHandler(o.process, () => this.disconnect()); } /** @@ -193,7 +193,7 @@ export class RabbitMqGateway { await this.#stopConsuming(); await this.#connection?.close(); if (this.#connection) // clean up in case 'close' event was not triggered - this.#onConnectionClosed(); + this.#cleanup(); this.#logger?.debug(`${this.#appId}: Disconnected from RabbitMQ`); } @@ -230,7 +230,11 @@ export class RabbitMqGateway { } #onConnectionClosed() { - this.#logger?.warn('Connection closed'); + this.#logger?.warn(`${this.#appId}: Connection closed`); + this.#cleanup(); + } + + #cleanup() { this.#connection = undefined; this.#pubChannel = undefined; this.#exclusiveQueueName = undefined; From 78b563783d5d236f77a578ac733f2207a215e7cc Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 17 Dec 2025 00:15:34 +0000 Subject: [PATCH 119/135] 1.0.0-rc.27 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f1d452..1ded456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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) + + +### 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) diff --git a/package-lock.json b/package-lock.json index c5944b8..63f2d90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.26", + "version": "1.0.0-rc.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.26", + "version": "1.0.0-rc.27", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index efca09e..5c6ca21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.26", + "version": "1.0.0-rc.27", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 72c537092c435fe68e343c33ad46d99a1f474b06 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 17 Dec 2025 03:30:21 +0000 Subject: [PATCH 120/135] Chore: Refactor subscription handling, improve logging on subscription removing --- src/rabbitmq/RabbitMqGateway.ts | 54 +++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 5835b77..dc6914f 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -50,6 +50,16 @@ type Subscription = { 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.'); function extractErrorMessage(err: Error | AggregateError | unknown): string { @@ -65,7 +75,8 @@ function extractErrorMessage(err: Error | AggregateError | unknown): string { return String(err); } -const describeSub = (s: Subscription) => `to${s.queueName ? ` queue "${s.queueName}" of` : ''} exchange "${s.exchange}"`; +const describeSub = (s: EstablishedSubscription) => + `to${s.queueName ?? s.queueGivenName ? ` queue "${s.queueName ?? s.queueGivenName}" of` : ''} exchange "${s.exchange}"`; /** * RabbitMqGateway implements the IObservable interface using RabbitMQ. @@ -91,15 +102,7 @@ export class RabbitMqGateway { #queueChannels = new Map(); #queueConsumers = new Map(); - #subscriptions: Array = []; + #subscriptions: Array = []; /** Handles termination signals for graceful shutdown */ #terminationHandler: TerminationHandler | undefined; @@ -359,8 +362,9 @@ export class RabbitMqGateway { this.#subscriptions = this.#subscriptions.filter(s => s !== subscriptionToRemove); - if (subscriptionToRemove.queueGivenName) - await this.#tryDropConsumer(subscriptionToRemove.queueGivenName); + await this.#tryDropConsumer(subscriptionToRemove); + + this.#logger?.debug(`${this.#appId}: Subscription ${describeSub(subscriptionToRemove)} removed`); } async #assertConnection() { @@ -502,25 +506,29 @@ export class RabbitMqGateway { channel, consumerTag: c.consumerTag }); - - this.#terminationHandler?.on(); } - async #tryDropConsumer(queueGivenName: string) { - const queueStillUsed = this.#subscriptions.some(s => s.queueGivenName === queueGivenName); - if (queueStillUsed) + async #tryDropConsumer(subscription: EstablishedSubscription) { + if (!subscription.queueGivenName) { + this.#logger?.warn(`${this.#appId}: 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(`${this.#appId}: Queue "${subscription.queueGivenName}" has other consumers in this process`); return; + } - const consumer = this.#queueConsumers.get(queueGivenName); - if (!consumer) + const consumer = this.#queueConsumers.get(subscription.queueGivenName); + if (!consumer) { + this.#logger?.warn(`${this.#appId}: Queue "${subscription.queueGivenName}" does not have consumers`); return; + } - this.#queueConsumers.delete(queueGivenName); + this.#queueConsumers.delete(subscription.queueGivenName); await consumer.channel.cancel(consumer.consumerTag); - // If no consumers are active anymore, disable the termination handler - if (!this.#queueConsumers.size) - this.#terminationHandler?.off(); + this.#logger?.warn(`${this.#appId}: Consumer "${consumer.consumerTag}" removed from subscription ${describeSub(subscription)}`); } /** From 8c5dc0c5976d1f3538e81ff284f2bde9ece1fe4d Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Wed, 17 Dec 2025 03:30:43 +0000 Subject: [PATCH 121/135] 1.0.0-rc.28 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ded456..f960127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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) + + +### Internal Fixes + +* Refactor subscription handling, improve logging on subscription removing ([72c5370](https://github.com/snatalenko/node-cqrs/commit/72c537092c435fe68e343c33ad46d99a1f474b06)) + + # [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) diff --git a/package-lock.json b/package-lock.json index 63f2d90..177c11f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.27", + "version": "1.0.0-rc.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.27", + "version": "1.0.0-rc.28", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 5c6ca21..fee5a31 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.27", + "version": "1.0.0-rc.28", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 57d3f3099cc52c19963279a2b4a66c79e5fbd3ee Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 21 Dec 2025 01:24:02 +0000 Subject: [PATCH 122/135] Chore: Enhance logging in RabbitMqGateway and AbstractProjection for better traceability --- src/AbstractProjection.ts | 3 +- src/rabbitmq/RabbitMqGateway.ts | 130 ++++++++++++++++--------------- src/utils/extractErrorDetails.ts | 48 ++++++++++++ src/utils/index.ts | 1 + 4 files changed, 118 insertions(+), 64 deletions(-) create mode 100644 src/utils/extractErrorDetails.ts diff --git a/src/AbstractProjection.ts b/src/AbstractProjection.ts index c7652ed..8b106c4 100644 --- a/src/AbstractProjection.ts +++ b/src/AbstractProjection.ts @@ -124,8 +124,9 @@ export abstract class AbstractProjection implements IProjection { if (this._viewLocker && !this._viewLocker?.ready) { - this._logger?.debug('view is locked, awaiting until it is 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); diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index dc6914f..934b8de 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -1,7 +1,7 @@ import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; import { IContainer, ILogger, IMessage, isMessage } from '../interfaces'; import * as Event from '../Event'; -import { delay } from '../utils'; +import { delay, extractErrorDetails } from '../utils'; import { TerminationHandler } from './TerminationHandler'; /** Generate a short pseudo-unique identifier using a truncated timestamp and random component */ @@ -62,22 +62,17 @@ type EstablishedSubscription = Subscription & { const isSystemQueue = (queueName: string) => queueName.startsWith('amq.'); -function extractErrorMessage(err: Error | AggregateError | unknown): string { - if (!err || typeof err !== 'object') - return String(err); - - if (err instanceof AggregateError) - return err.errors?.map(e => (e && 'message' in e ? e.message : String(e))).join('; '); - - if (err instanceof Error && err.message) - return err.message; - - return String(err); -} - 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 +}); + /** * RabbitMqGateway implements the IObservable interface using RabbitMQ. * @@ -121,7 +116,7 @@ export class RabbitMqGateway { this.#connectionFactory = o.rabbitMqConnectionFactory; this.#appId = getRandomAppId(); this.#logger = o.logger && 'child' in o.logger ? - o.logger.child({ service: new.target.name }) : + o.logger.child({ service: new.target.name, appId: this.#appId }) : o.logger; if (o.process) @@ -143,7 +138,7 @@ export class RabbitMqGateway { await delay(100); if (this.#connection) { - this.#logger?.debug(`${this.#appId}: Connection is already established`); + this.#logger?.debug('Connection is already established'); return this.#connection; } @@ -154,15 +149,15 @@ export class RabbitMqGateway { this.#connection.on('error', err => this.#onConnectionError(err)); this.#connection.on('close', () => this.#onConnectionClosed()); - this.#logger?.info(`${this.#appId}: Connection established`); + this.#logger?.info('Connection established'); await this.#restoreSubscriptions(); return this.#connection; } catch (err: unknown) { - this.#logger?.error(`${this.#appId}: Connection attempt failed: ${extractErrorMessage(err)}`, { - stack: (err as Error)?.stack + this.#logger?.error('Connection attempt failed', { + error: extractErrorDetails(err) }); throw err; } @@ -182,7 +177,7 @@ export class RabbitMqGateway { await this.#subscribe(subscriptionToRestore); } - this.#logger?.debug(`${this.#appId}: ${this.#subscriptions.length} subscription(s) restored`); + this.#logger?.debug(`${this.#subscriptions.length} subscription(s) restored`); } finally { this.#restoringSubscriptions = false; @@ -191,49 +186,51 @@ export class RabbitMqGateway { async disconnect() { try { - this.#logger?.debug(`${this.#appId}: Disconnecting from RabbitMQ...`); + 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(`${this.#appId}: Disconnected from RabbitMQ`); + this.#logger?.debug('Disconnected from RabbitMQ'); } catch (err: unknown) { - this.#logger?.error(`${this.#appId}: Failed to disconnect from RabbitMQ: ${extractErrorMessage(err)}`, { - stack: (err as Error)?.stack + this.#logger?.error('Failed to disconnect from RabbitMQ', { + error: extractErrorDetails(err) }); } } async #stopConsuming() { - this.#logger?.info(`${this.#appId}: Stopping all consumers...`); + this.#logger?.info('Stopping all consumers...'); const cancellations = [...this.#queueConsumers.entries()].map(async ([queueName, { channel, consumerTag }]) => { - this.#logger?.debug(`${this.#appId}: Cancelling consumer "${consumerTag}" for queue "${queueName}"`); + this.#logger?.debug(`Cancelling consumer "${consumerTag}" for queue "${queueName}"`); try { await channel.cancel(consumerTag); - this.#logger?.debug(`${this.#appId}: Consumer "${consumerTag}" on queue "${queueName}" cancelled successfully`); + this.#logger?.debug(`Consumer "${consumerTag}" on queue "${queueName}" cancelled successfully`); this.#queueConsumers.delete(queueName); } catch (err: unknown) { - this.#logger?.error(`${this.#appId}: Failed to cancel consumer "${consumerTag}" for queue "${queueName}": ${extractErrorMessage(err)}`); + this.#logger?.error(`Failed to cancel consumer "${consumerTag}" for queue "${queueName}"`, { + error: extractErrorDetails(err) + }); } }); await Promise.all(cancellations); - this.#logger?.info(`${this.#appId}: All consumers stopped.`); + this.#logger?.info('All consumers stopped'); } #onConnectionError(err: unknown) { - this.#logger?.error(`${this.#appId}: Connection error: ${extractErrorMessage(err)}`, { - stack: err instanceof Error ? err.stack : undefined + this.#logger?.error('Connection error', { + error: extractErrorDetails(err) }); } #onConnectionClosed() { - this.#logger?.warn(`${this.#appId}: Connection closed`); + this.#logger?.warn('Connection closed'); this.#cleanup(); } @@ -301,7 +298,7 @@ export class RabbitMqGateway { // record subscription details to restore it if connection attempt fails or on reconnect this.#subscriptions.push(subscription); - this.#logger?.debug(`${this.#appId}: Subscription ${describeSub(subscription)} recorded`); + this.#logger?.debug(`Subscription ${describeSub(subscription)} recorded`); const { exchange, @@ -344,7 +341,7 @@ export class RabbitMqGateway { if (subscriptionRecord) subscriptionRecord.queueGivenName = queueGivenName; - this.#logger?.debug(`${this.#appId}: Subscription ${describeSub(subscription)} established`); + this.#logger?.debug(`Subscription ${describeSub(subscription)} established`); } #findSubscription(subscription: Pick) { @@ -364,7 +361,7 @@ export class RabbitMqGateway { await this.#tryDropConsumer(subscriptionToRemove); - this.#logger?.debug(`${this.#appId}: Subscription ${describeSub(subscriptionToRemove)} removed`); + this.#logger?.debug(`Subscription ${describeSub(subscriptionToRemove)} removed`); } async #assertConnection() { @@ -428,7 +425,7 @@ export class RabbitMqGateway { await channel.bindQueue(queueGivenName, exchange, eventType); - this.#logger?.debug(`${this.#appId}: Queue "${queueGivenName}" bound to exchange "${exchange}" with pattern "${eventType}"`); + this.#logger?.debug(`Queue "${queueGivenName}" bound to exchange "${exchange}" with pattern "${eventType}"`); } async #assertConsumer( @@ -446,29 +443,20 @@ export class RabbitMqGateway { if (!msg) return; - const { consumerTag, routingKey } = msg.fields ?? {}; - const { messageId, correlationId, appId } = msg.properties ?? {}; - // Keep the process alive while waiting for the handler to finish const handlerProcessTimeout = options?.handlerProcessTimeout ?? RabbitMqGateway.HANDLER_PROCESS_TIMEOUT; const keepAliveTimeout = setTimeout(() => { - this.#logger?.warn(`${this.#appId}: Message processing timed out`, { + this.#logger?.error('Message processing timed out', { queueName: queueGivenName, - consumerTag, - routingKey, - messageId + msg: extractMessageMeta(msg) }); - channel.nack(msg, false, false); + this.#rejectMessage(channel, msg); }, handlerProcessTimeout); try { - this.#logger?.debug(`${this.#appId}: Message received`, { + this.#logger?.debug('Message received', { queueName: queueGivenName, - consumerTag, - routingKey, - messageId, - correlationId, - appId + msg: extractMessageMeta(msg) }); const jsonContent = msg.content.toString(); @@ -479,19 +467,22 @@ export class RabbitMqGateway { 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 && appId === this.#appId) + if (ignoreOwn && msg.properties.appId === this.#appId) continue; await handler(message); } - channel?.ack(msg); + channel.ack(msg); } catch (err: unknown) { - this.#logger?.error(`${this.#appId}: Message processing failed: ${extractErrorMessage(err)}`); + this.#logger?.error('Message processing failed', { + error: extractErrorDetails(err), + queueName: queueGivenName, + msg: extractMessageMeta(msg) + }); - // Redirect message to dead letter queue, if `{ noAck: true }` was not set on consumption - channel?.nack(msg, false, false); + this.#rejectMessage(channel, msg); } finally { clearTimeout(keepAliveTimeout); @@ -500,7 +491,7 @@ export class RabbitMqGateway { noAck: options?.noAck }); - this.#logger?.debug(`${this.#appId}: Consumer "${c.consumerTag}" registered on queue "${queueGivenName}"`); + this.#logger?.debug(`Consumer "${c.consumerTag}" registered on queue "${queueGivenName}"`); this.#queueConsumers.set(queueGivenName, { channel, @@ -508,27 +499,40 @@ export class RabbitMqGateway { }); } + /** 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(`${this.#appId}: Subscription ${describeSub(subscription)} is not bound to a queue`); + 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(`${this.#appId}: Queue "${subscription.queueGivenName}" has other consumers in this process`); + 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(`${this.#appId}: Queue "${subscription.queueGivenName}" does not have consumers`); + this.#logger?.warn(`Queue "${subscription.queueGivenName}" does not have consumers`); return; } this.#queueConsumers.delete(subscription.queueGivenName); await consumer.channel.cancel(consumer.consumerTag); - this.#logger?.warn(`${this.#appId}: Consumer "${consumer.consumerTag}" removed from subscription ${describeSub(subscription)}`); + this.#logger?.info(`Consumer "${consumer.consumerTag}" removed from subscription ${describeSub(subscription)}`); } /** @@ -564,14 +568,14 @@ export class RabbitMqGateway { return new Promise((resolve, reject) => { if (!this.#pubChannel) - throw new Error(`${this.#appId}: No channel available for publishing`); + throw new Error('No channel available for publishing'); - this.#logger?.debug(`${this.#appId}: Publishing message "${Event.describe(message)}" to exchange "${exchange}"`); + 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(`${this.#appId}: Failed to send event ${Event.describe(message)}, channel buffer is full`); + throw new Error(`Failed to send event ${Event.describe(message)}, channel buffer is full`); }); } } 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/index.ts b/src/utils/index.ts index ada7ab1..b987ba6 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,3 +11,4 @@ export * from './notEmpty'; export * from './setupOneTimeEmitterSubscription'; export * from './subscribe'; export * from './validateHandlers'; +export * from './extractErrorDetails'; From 1fd59923a820d0f1f0b111b335c021ebf3e0c0fe Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 21 Dec 2025 01:24:14 +0000 Subject: [PATCH 123/135] 1.0.0-rc.29 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f960127..6010574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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 + +* 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) diff --git a/package-lock.json b/package-lock.json index 177c11f..aa73592 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.28", + "version": "1.0.0-rc.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.28", + "version": "1.0.0-rc.29", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index fee5a31..525163b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.28", + "version": "1.0.0-rc.29", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 35a974b15ab650728768d1efd655b45a6df052fb Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Sun, 21 Dec 2025 23:12:26 +0000 Subject: [PATCH 124/135] Internal Fix: MQ consumption starts before handler is properly recorded --- src/rabbitmq/RabbitMqGateway.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 934b8de..ed32aa7 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -335,12 +335,12 @@ export class RabbitMqGateway { await this.#assetQueue(channel, exchange, queueGivenName, eventType, { deadLetterExchangeName }); } - await this.#assertConsumer(queueGivenName, channel, subscription); - const subscriptionRecord = this.#findSubscription(subscription); if (subscriptionRecord) subscriptionRecord.queueGivenName = queueGivenName; + await this.#assertConsumer(queueGivenName, channel, subscription); + this.#logger?.debug(`Subscription ${describeSub(subscription)} established`); } From 63b4f48f1abc6936472db66e821de2543dbc874b Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 22 Dec 2025 00:01:19 +0000 Subject: [PATCH 125/135] Internal Fix: RabbitMQ connection not auto-closing on SIGTERM --- src/rabbitmq/RabbitMqGateway.ts | 7 ++--- src/rabbitmq/TerminationHandler.ts | 37 ----------------------- src/rabbitmq/utils/index.ts | 1 + src/rabbitmq/utils/registerExitCleanup.ts | 29 ++++++++++++++++++ 4 files changed, 32 insertions(+), 42 deletions(-) delete mode 100644 src/rabbitmq/TerminationHandler.ts create mode 100644 src/rabbitmq/utils/index.ts create mode 100644 src/rabbitmq/utils/registerExitCleanup.ts diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index ed32aa7..614e246 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -2,7 +2,7 @@ import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; import { IContainer, ILogger, IMessage, isMessage } from '../interfaces'; import * as Event from '../Event'; import { delay, extractErrorDetails } from '../utils'; -import { TerminationHandler } from './TerminationHandler'; +import { registerExitCleanup } from './utils'; /** Generate a short pseudo-unique identifier using a truncated timestamp and random component */ const getRandomAppId = () => @@ -99,8 +99,6 @@ export class RabbitMqGateway { #subscriptions: Array = []; - /** Handles termination signals for graceful shutdown */ - #terminationHandler: TerminationHandler | undefined; #restoringSubscriptions: boolean = false; get connection() { @@ -119,8 +117,7 @@ export class RabbitMqGateway { o.logger.child({ service: new.target.name, appId: this.#appId }) : o.logger; - if (o.process) - this.#terminationHandler = new TerminationHandler(o.process, () => this.disconnect()); + registerExitCleanup(o.process, () => this.disconnect()); } /** diff --git a/src/rabbitmq/TerminationHandler.ts b/src/rabbitmq/TerminationHandler.ts deleted file mode 100644 index 693fe5a..0000000 --- a/src/rabbitmq/TerminationHandler.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Handles graceful termination of a Node.js process. - * Listens for SIGINT and executes a cleanup routine before allowing the process to exit. - */ -export class TerminationHandler { - - #process: NodeJS.Process; - #cleanupHandler: () => Promise; - #terminationHandler: () => Promise; - #subscribed = false; - - constructor(process: NodeJS.Process, cleanupHandler: () => Promise) { - this.#process = process; - this.#cleanupHandler = cleanupHandler; - this.#terminationHandler = this.#onProcessTermination.bind(this); - } - - on() { - if (this.#subscribed) - return; - - this.#process.once('SIGINT', this.#terminationHandler); - this.#process.once('SIGTERM', this.#terminationHandler); - this.#subscribed = true; - } - - off() { - this.#process.off('SIGINT', this.#terminationHandler); - this.#process.off('SIGTERM', this.#terminationHandler); - this.#subscribed = false; - } - - async #onProcessTermination() { - this.off(); - await this.#cleanupHandler(); - } -} diff --git a/src/rabbitmq/utils/index.ts b/src/rabbitmq/utils/index.ts new file mode 100644 index 0000000..807ea15 --- /dev/null +++ b/src/rabbitmq/utils/index.ts @@ -0,0 +1 @@ +export * from './registerExitCleanup'; diff --git a/src/rabbitmq/utils/registerExitCleanup.ts b/src/rabbitmq/utils/registerExitCleanup.ts new file mode 100644 index 0000000..7f3982d --- /dev/null +++ b/src/rabbitmq/utils/registerExitCleanup.ts @@ -0,0 +1,29 @@ +/** + * Registers cleanup handlers for SIGINT and SIGTERM signals on a Node.js process. + * Executes the provided cleanup procedure when one of these signals is received, + * then removes the listeners to allow the process to exit gracefully. + * + * @returns An object with a `dispose` method to manually remove the registered signal handlers. + */ +export const registerExitCleanup = ( + process: NodeJS.Process | undefined, + cleanupProcedure: () => Promise | unknown +) => { + const handler = async () => { + // remove listeners to allow the process to exit + process?.off('SIGINT', handler); + process?.off('SIGTERM', handler); + + await cleanupProcedure(); + }; + + process?.once('SIGINT', handler); + process?.once('SIGTERM', handler); + + return { + dispose: () => { + process?.off('SIGINT', handler); + process?.off('SIGTERM', handler); + } + }; +}; From 1e3a548278670d0ae7e20bf6460f66ba082f4aee Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 22 Dec 2025 00:01:33 +0000 Subject: [PATCH 126/135] 1.0.0-rc.30 --- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6010574..9c309ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index aa73592..01a10eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.29", + "version": "1.0.0-rc.30", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.29", + "version": "1.0.0-rc.30", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 525163b..a3d9a14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.29", + "version": "1.0.0-rc.30", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From ba8053697fb271a57fde7fc236d0f15c7d497c8e Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 22 Dec 2025 22:31:56 +0000 Subject: [PATCH 127/135] Change: Auto-reconnect to RabbitMQ --- src/rabbitmq/RabbitMqGateway.ts | 72 +++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 614e246..512d63f 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -73,6 +73,10 @@ const extractMessageMeta = (msg: ConsumeMessage) => ({ appId: msg.properties?.appId }); +interface RabbitMqGatewayConnected { + get connection(): ChannelModel; +} + /** * RabbitMqGateway implements the IObservable interface using RabbitMQ. * @@ -85,12 +89,19 @@ 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; #connecting = false; + #desiredState: 'connected' | 'disconnected' = 'disconnected'; + + /** + * Established connection to RabbitMQ. + * If empty, the gateway is not connected. + */ #connection: ChannelModel | undefined; #pubChannel: ConfirmChannel | undefined; #exclusiveQueueName: string | undefined; @@ -101,10 +112,14 @@ export class RabbitMqGateway { #restoringSubscriptions: boolean = false; - get connection() { + get connection(): ChannelModel | undefined { return this.#connection; } + isConnected(): this is this & RabbitMqGatewayConnected { + return !!this.#connection; + } + constructor(o: Partial> & { rabbitMqConnectionFactory?: () => Promise }) { @@ -131,6 +146,9 @@ export class RabbitMqGateway { * @returns A promise that resolves with the ChannelModel representing the established connection. */ async connect(): Promise { + this.#desiredState = 'connected'; + this.#clearReconnectTimer(); + while (this.#connecting) await delay(100); @@ -156,6 +174,10 @@ export class RabbitMqGateway { this.#logger?.error('Connection attempt failed', { error: extractErrorDetails(err) }); + + if (this.#desiredState === 'connected') + this.#scheduleReconnect(); + throw err; } finally { @@ -163,6 +185,33 @@ export class RabbitMqGateway { } } + #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() || this.#connecting) + return; + + this.connect().catch(() => undefined); + } + async #restoreSubscriptions() { this.#restoringSubscriptions = true; try { @@ -182,11 +231,23 @@ export class RabbitMqGateway { } async disconnect() { + this.#desiredState = 'disconnected'; + this.#clearReconnectTimer(); + + while (this.#connecting) + await delay(100); + + if (!this.#connection) { + this.#logger?.debug('Connection is not established'); + return; + } + try { this.#logger?.debug('Disconnecting from RabbitMQ...'); await this.#stopConsuming(); - await this.#connection?.close(); + await this.#connection.close(); + if (this.#connection) // clean up in case 'close' event was not triggered this.#cleanup(); @@ -200,7 +261,7 @@ export class RabbitMqGateway { } async #stopConsuming() { - this.#logger?.info('Stopping all consumers...'); + 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}"`); @@ -227,8 +288,11 @@ export class RabbitMqGateway { } #onConnectionClosed() { - this.#logger?.warn('Connection closed'); + this.#logger?.info('Connection closed'); this.#cleanup(); + + if (this.#desiredState === 'connected') + this.#reconnect(); } #cleanup() { From d591e565d27c70a74ae401b10bb84742b13a922b Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 22 Dec 2025 22:32:09 +0000 Subject: [PATCH 128/135] 1.0.0-rc.31 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c309ca..0c6ec6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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 + +* 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) diff --git a/package-lock.json b/package-lock.json index 01a10eb..28f2ed5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.30", + "version": "1.0.0-rc.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.30", + "version": "1.0.0-rc.31", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index a3d9a14..5a5aea6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.30", + "version": "1.0.0-rc.31", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From 196332e1f382880161e0f7192966e2fb4f222be7 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 1 Jan 2026 17:16:20 +0000 Subject: [PATCH 129/135] Chore: Update Lock interface to support resource management with `using` keyword --- src/utils/Lock.ts | 19 ++++++++++++++++++- tests/unit/Lock.test.ts | 14 +++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/utils/Lock.ts b/src/utils/Lock.ts index eafeedc..2db3489 100644 --- a/src/utils/Lock.ts +++ b/src/utils/Lock.ts @@ -1,5 +1,20 @@ import { Deferred } from './Deferred'; +export class LockLease { + constructor( + readonly lock: Lock, + readonly name?: string + ) { } + + release() { + this.lock.release(this.name); + } + + [Symbol.dispose]() { + this.release(); + } +} + export class Lock { /** @@ -36,7 +51,7 @@ export class Lock { * * @returns Promise that resolves once lock is acquired */ - async acquire(name?: string): Promise { + async acquire(name?: string): Promise { while (this.#globalLockAcquiringLock) await this.#globalLockAcquiringLock.promise; @@ -60,6 +75,8 @@ export class Lock { this.#globalLockAcquiringLock?.resolve(); this.#globalLockAcquiringLock = undefined; } + + return new LockLease(this, name); } /** diff --git a/tests/unit/Lock.test.ts b/tests/unit/Lock.test.ts index 1b56315..0efeb91 100644 --- a/tests/unit/Lock.test.ts +++ b/tests/unit/Lock.test.ts @@ -47,8 +47,8 @@ describe('Lock', () => { await l3; // Wait for l3 to fully complete // Ensure both promises associated with acquire calls are resolved - await expect(l2).resolves.toBeUndefined(); - await expect(l3).resolves.toBeUndefined(); + await expect(l2).resolves.not.toThrow(); + await expect(l3).resolves.not.toThrow(); }); }); @@ -178,11 +178,11 @@ describe('Lock', () => { await lock.release(); // Ensure all original promises eventually resolve - await expect(p2).resolves.toBeUndefined(); - await expect(p3).resolves.toBeUndefined(); - await expect(l4).resolves.toBeUndefined(); - await expect(p5).resolves.toBeUndefined(); - await expect(l6).resolves.toBeUndefined(); + 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(); }); }); }); From cbfd0bf39c12419f5b15a6c0e15554ae70f52609 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Thu, 1 Jan 2026 17:16:43 +0000 Subject: [PATCH 130/135] Fix rabbitmq consumer cancellation test --- .../rabbitmq/RabbitMqGateway.test.ts | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/integration/rabbitmq/RabbitMqGateway.test.ts b/tests/integration/rabbitmq/RabbitMqGateway.test.ts index f04872f..bc717b2 100644 --- a/tests/integration/rabbitmq/RabbitMqGateway.test.ts +++ b/tests/integration/rabbitmq/RabbitMqGateway.test.ts @@ -311,8 +311,20 @@ describe('RabbitMqGateway', () => { it('cancels consumer when unsubscribing the last subscription on a queue', async () => { - const processOnSpy = jest.spyOn(process, 'once'); - const processOffSpy = jest.spyOn(process, 'off'); + 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[] = []; @@ -326,37 +338,40 @@ describe('RabbitMqGateway', () => { const event1 = { type: 'test.unsubscribe', - payload: { info: 'event for handler2' }, + payload: { info: 'event for handlers' }, context: { ts: Date.now() } }; - // Subscribe both handlers to the same durable queue await gateway1.subscribe({ exchange, + queueName, eventType: event1.type, handler: handler1 }); await gateway1.subscribe({ exchange, + queueName, eventType: event1.type, handler: handler2 }); - expect(processOnSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); - expect(processOnSpy).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); - expect(processOnSpy).toHaveBeenCalledTimes(2); - expect(processOffSpy).not.toHaveBeenCalled(); + await gateway1.publish(exchange, event1); + await delay(50); + + expect(received1).toEqual([event1]); + expect(received2).toEqual([event1]); - // Unsubscribe one handler; the queue should still be active await gateway1.unsubscribe({ exchange, + queueName, eventType: event1.type, handler: handler1 }); - expect(processOffSpy).not.toHaveBeenCalled(); + expect(cancelledConsumerTags).toHaveLength(0); - // Publish an event; only handler2 should receive it + received1.length = 0; + received2.length = 0; await gateway1.publish(exchange, event1); await delay(50); @@ -364,22 +379,23 @@ describe('RabbitMqGateway', () => { expect(received1).toEqual([]); expect(received2).toEqual([event1]); - // Now unsubscribe the last handler; this should cancel the consumer for the queue await gateway1.unsubscribe({ exchange, + queueName, eventType: event1.type, handler: handler2 }); - expect(processOffSpy).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + expect(cancelledConsumerTags).toHaveLength(1); - // Publish a second event; no handler should receive it because the consumer is cancelled + received1.length = 0; received2.length = 0; await gateway1.publish(exchange, event1); await delay(50); expect(received1).toEqual([]); + expect(received2).toEqual([]); }); }); From 42fe3497ce886bc4e20efa6008b97104380a8ba5 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 9 Jan 2026 02:36:01 +0000 Subject: [PATCH 131/135] Chore: Expose connection state events on RabbitMqGateway --- src/rabbitmq/RabbitMqGateway.ts | 137 +++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 48 deletions(-) diff --git a/src/rabbitmq/RabbitMqGateway.ts b/src/rabbitmq/RabbitMqGateway.ts index 512d63f..78b49ce 100644 --- a/src/rabbitmq/RabbitMqGateway.ts +++ b/src/rabbitmq/RabbitMqGateway.ts @@ -1,8 +1,9 @@ import { Channel, ChannelModel, ConfirmChannel, ConsumeMessage } from 'amqplib'; import { IContainer, ILogger, IMessage, isMessage } from '../interfaces'; import * as Event from '../Event'; -import { delay, extractErrorDetails } from '../utils'; +import { extractErrorDetails, Lock } from '../utils'; import { registerExitCleanup } from './utils'; +import { EventEmitter } from 'events'; /** Generate a short pseudo-unique identifier using a truncated timestamp and random component */ const getRandomAppId = () => @@ -77,6 +78,11 @@ interface RabbitMqGatewayConnected { get connection(): ChannelModel; } +type GatewayEvents = { + connected: []; + disconnected: [reason?: string]; +}; + /** * RabbitMqGateway implements the IObservable interface using RabbitMQ. * @@ -94,9 +100,10 @@ export class RabbitMqGateway { #connectionFactory: () => Promise; #appId: string; #logger: ILogger | undefined; + #emitter: EventEmitter; - #connecting = false; #desiredState: 'connected' | 'disconnected' = 'disconnected'; + #stateChangeLock = new Lock(); /** * Established connection to RabbitMQ. @@ -110,8 +117,6 @@ export class RabbitMqGateway { #subscriptions: Array = []; - #restoringSubscriptions: boolean = false; - get connection(): ChannelModel | undefined { return this.#connection; } @@ -121,11 +126,13 @@ export class RabbitMqGateway { } constructor(o: Partial> & { - rabbitMqConnectionFactory?: () => Promise + 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 ? @@ -149,25 +156,34 @@ export class RabbitMqGateway { this.#desiredState = 'connected'; this.#clearReconnectTimer(); - while (this.#connecting) - await delay(100); + const lease = await this.#stateChangeLock.acquire(); + try { + if (this.#connection) { + this.#logger?.debug('Connection is already established'); + return this.#connection; + } - if (this.#connection) { - this.#logger?.debug('Connection is already established'); - return this.#connection; + return await this.#connect(); } + finally { + lease.release(); + } + } - this.#connecting = true; - + async #connect(): Promise { try { - this.#connection = await this.#connectionFactory(); - this.#connection.on('error', err => this.#onConnectionError(err)); - this.#connection.on('close', () => this.#onConnectionClosed()); + 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) { @@ -180,9 +196,6 @@ export class RabbitMqGateway { throw err; } - finally { - this.#connecting = false; - } } #reconnectTimeout: NodeJS.Timeout | undefined; @@ -206,43 +219,36 @@ export class RabbitMqGateway { } #reconnect() { - if (this.#desiredState !== 'connected' || this.isConnected() || this.#connecting) + if (this.#desiredState !== 'connected' || this.isConnected()) return; this.connect().catch(() => undefined); } async #restoreSubscriptions() { - this.#restoringSubscriptions = true; - try { - 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; + 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`); - } - finally { - this.#restoringSubscriptions = false; + await this.#subscribe(subscriptionToRestore); } + + this.#logger?.debug(`${this.#subscriptions.length} subscription(s) restored`); } async disconnect() { this.#desiredState = 'disconnected'; this.#clearReconnectTimer(); - while (this.#connecting) - await delay(100); - - if (!this.#connection) { - this.#logger?.debug('Connection is not established'); - return; - } + 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(); @@ -252,12 +258,16 @@ export class RabbitMqGateway { 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() { @@ -287,12 +297,17 @@ export class RabbitMqGateway { }); } - #onConnectionClosed() { + #onConnectionClosed(connection: ChannelModel) { + if (connection !== this.#connection) + return; + this.#logger?.info('Connection closed'); this.#cleanup(); - if (this.#desiredState === 'connected') + if (this.#desiredState === 'connected') { + this.#emitter.emit('disconnected', 'Connection closed'); this.#reconnect(); + } } #cleanup() { @@ -346,10 +361,19 @@ export class RabbitMqGateway { * @returns A promise that resolves when the subscription is successfully set up. */ async subscribe(subscription: Subscription) { - while (this.#restoringSubscriptions) - await delay(100); + this.#desiredState = 'connected'; + this.#clearReconnectTimer(); + + const lease = await this.#stateChangeLock.acquire(); + try { + if (!this.#connection) + await this.#connect(); - return this.#subscribe(subscription); + await this.#subscribe(subscription); + } + finally { + lease.release(); + } } async #subscribe(subscription: Subscription) { @@ -367,7 +391,7 @@ export class RabbitMqGateway { eventType } = subscription; - const channel = await this.#assertChannel(queueName); + const channel = await this.#assertQueueChannel(queueName); let queueGivenName = queueName; if (!queueGivenName) { @@ -430,11 +454,13 @@ export class RabbitMqGateway { } /** Get existing or open a new channel for a given queue name */ - async #assertChannel(queueName: string = ''): Promise { - const connection = await this.#assertConnection(); + async #assertQueueChannel(queueName: string = ''): Promise { let channel = this.#queueChannels.get(queueName); if (!channel) { - channel = await connection.createChannel(); + if (!this.#connection) + throw new Error('No connection established to create channel'); + + channel = await this.#connection.createChannel(); this.#queueChannels.set(queueName, channel); } return channel; @@ -639,4 +665,19 @@ export class RabbitMqGateway { 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; + } } From 1e2e992fb9462dfd50b0b5188380106d21609a0a Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Fri, 9 Jan 2026 05:07:59 +0000 Subject: [PATCH 132/135] 1.0.0-rc.32 --- CHANGELOG.md | 9 +++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c6ec6c..6bcebb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# [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) + + +### 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) diff --git a/package-lock.json b/package-lock.json index 28f2ed5..12fb1ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.31", + "version": "1.0.0-rc.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.31", + "version": "1.0.0-rc.32", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 5a5aea6..87fc2a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.31", + "version": "1.0.0-rc.32", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From bab78078de52bd88bb86c293adb87eeb974241d5 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 26 Jan 2026 21:08:29 +0000 Subject: [PATCH 133/135] Fix: Concurrent operations handling in SqliteObjectStorage updateEnforcingNew --- src/sqlite/SqliteObjectStorage.ts | 19 ++++++++++--- tests/integration/sqlite/SqliteView.test.ts | 30 ++++++++++++++++++++- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/sqlite/SqliteObjectStorage.ts b/src/sqlite/SqliteObjectStorage.ts index fb043f1..8f16142 100644 --- a/src/sqlite/SqliteObjectStorage.ts +++ b/src/sqlite/SqliteObjectStorage.ts @@ -83,6 +83,10 @@ export class SqliteObjectStorage extends AbstractSqliteAccessor impleme 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`); @@ -96,11 +100,20 @@ export class SqliteObjectStorage extends AbstractSqliteAccessor impleme 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); @@ -121,13 +134,11 @@ export class SqliteObjectStorage extends AbstractSqliteAccessor impleme await this.assertConnection(); - // Due to better-sqlite3 sync nature, - // it's safe to get then modify within this process const record = this.#getQuery.get(guid(id)); if (record) - await this.update(id, update); + this.#updateExistingSync(id, record, update as (r: TRecord) => TRecord); else - await this.create(id, update()); + this.#createSync(id, update()); } async delete(id: string): Promise { diff --git a/tests/integration/sqlite/SqliteView.test.ts b/tests/integration/sqlite/SqliteView.test.ts index eaf116c..6cdc644 100644 --- a/tests/integration/sqlite/SqliteView.test.ts +++ b/tests/integration/sqlite/SqliteView.test.ts @@ -1,6 +1,6 @@ import { existsSync, unlinkSync } from 'fs'; import { AbstractProjection, IEvent } from '../../../src'; -import { SqliteObjectView } from '../../../src/sqlite'; +import { SqliteObjectStorage, SqliteObjectView, guid } from '../../../src/sqlite'; import * as createDb from 'better-sqlite3'; type UserPayload = { @@ -116,4 +116,32 @@ describe('SqliteView', () => { // tbl_test_1: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1 LIMIT 3`).all() // }); }); + + it('updateEnforcingNew is safe under same-process concurrency', async () => { + const storage = new SqliteObjectStorage<{ n: number }>({ + viewModelSqliteDb, + tableName: 'tbl_concurrency' + }); + + const id = '00000000000000000000000000000001'; + + await storage.assertConnection(); + + + const p1 = storage.updateEnforcingNew(id, r => ({ n: (r?.n ?? 0) + 1 })); + const p2 = storage.updateEnforcingNew(id, r => ({ n: (r?.n ?? 0) + 1 })); + + await Promise.all([p1, p2]); + + const record = await storage.get(id); + expect(record).toEqual({ n: 2 }); + + const row = viewModelSqliteDb.prepare(` + SELECT version + FROM tbl_concurrency + WHERE id = ? + `).get(guid(id)); + + expect(row?.version).toBe(2); + }); }); From 165654b85da0861f7faf3046ba865c4a9aff4138 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 26 Jan 2026 21:08:47 +0000 Subject: [PATCH 134/135] 1.0.0-rc.33 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bcebb1..26db4ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# [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) diff --git a/package-lock.json b/package-lock.json index 12fb1ea..6a9bade 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.32", + "version": "1.0.0-rc.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "node-cqrs", - "version": "1.0.0-rc.32", + "version": "1.0.0-rc.33", "license": "MIT", "dependencies": { "async-iterable-buffer": "^1.1.0", diff --git a/package.json b/package.json index 87fc2a1..8e4a29d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-cqrs", - "version": "1.0.0-rc.32", + "version": "1.0.0-rc.33", "description": "Basic ES6 backbone for CQRS app development", "keywords": [ "cqrs", From d953bfca17cabc00a3c6d422cb7f5d318733f6d3 Mon Sep 17 00:00:00 2001 From: Stanislav Natalenko Date: Mon, 26 Jan 2026 21:47:28 +0000 Subject: [PATCH 135/135] Move SqliteObjectStorage test to correct location --- tests/integration/sqlite/SqliteView.test.ts | 30 +------------------ tests/unit/sqlite/SqliteObjectStorage.test.ts | 26 ++++++++++++++++ 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/tests/integration/sqlite/SqliteView.test.ts b/tests/integration/sqlite/SqliteView.test.ts index 6cdc644..eaf116c 100644 --- a/tests/integration/sqlite/SqliteView.test.ts +++ b/tests/integration/sqlite/SqliteView.test.ts @@ -1,6 +1,6 @@ import { existsSync, unlinkSync } from 'fs'; import { AbstractProjection, IEvent } from '../../../src'; -import { SqliteObjectStorage, SqliteObjectView, guid } from '../../../src/sqlite'; +import { SqliteObjectView } from '../../../src/sqlite'; import * as createDb from 'better-sqlite3'; type UserPayload = { @@ -116,32 +116,4 @@ describe('SqliteView', () => { // tbl_test_1: viewModelSqliteDb.prepare(`SELECT * FROM tbl_test_1 LIMIT 3`).all() // }); }); - - it('updateEnforcingNew is safe under same-process concurrency', async () => { - const storage = new SqliteObjectStorage<{ n: number }>({ - viewModelSqliteDb, - tableName: 'tbl_concurrency' - }); - - const id = '00000000000000000000000000000001'; - - await storage.assertConnection(); - - - const p1 = storage.updateEnforcingNew(id, r => ({ n: (r?.n ?? 0) + 1 })); - const p2 = storage.updateEnforcingNew(id, r => ({ n: (r?.n ?? 0) + 1 })); - - await Promise.all([p1, p2]); - - const record = await storage.get(id); - expect(record).toEqual({ n: 2 }); - - const row = viewModelSqliteDb.prepare(` - SELECT version - FROM tbl_concurrency - WHERE id = ? - `).get(guid(id)); - - expect(row?.version).toBe(2); - }); }); diff --git a/tests/unit/sqlite/SqliteObjectStorage.test.ts b/tests/unit/sqlite/SqliteObjectStorage.test.ts index dd6d9f7..cd4bcff 100644 --- a/tests/unit/sqlite/SqliteObjectStorage.test.ts +++ b/tests/unit/sqlite/SqliteObjectStorage.test.ts @@ -83,4 +83,30 @@ describe('SqliteObjectStorage', function () { 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); + }); });