diff --git a/.codebeatignore b/.codebeatignore
deleted file mode 100644
index 3669b72..0000000
--- a/.codebeatignore
+++ /dev/null
@@ -1 +0,0 @@
-benchmark/**
diff --git a/.codebeatsettings b/.codebeatsettings
deleted file mode 100644
index d808262..0000000
--- a/.codebeatsettings
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "TYPESCRIPT": {
- "TOTAL_LOC": [500, 1000, 1500, 2000],
- "TOO_MANY_FUNCTIONS": [30, 40, 50, 60]
- }
-}
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 934f1aa..2001e82 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,17 +1,60 @@
-name: Node.js CI
+name: Build
on:
push:
+ branches: [master]
+ tags: ['*']
pull_request:
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
- build:
+ quality:
+ name: Lint & format
runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24.x
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Check formatting
+ run: npm run format:check
+ - name: Lint
+ run: npm run lint
+
+ test:
+ name: Test (Node ${{ matrix.node-version }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
strategy:
+ fail-fast: false
matrix:
- node-version: [lts/*]
-
+ node-version: [22.x, 24.x]
+ services:
+ redis:
+ image: redis:7
+ ports:
+ - 6379:6379
+ options: >-
+ --health-cmd "redis-cli ping"
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -20,9 +63,10 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
+ cache: npm
- name: Install dependencies
- run: npm install
+ run: npm ci
- name: Run tests
run: npm test
diff --git a/.npmignore b/.npmignore
index 6a52e79..5168a36 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,8 +1,6 @@
.gitignore
.travis.yml
.dockerignore
-.codebeatignore
-.codebeatsettings
.ssh/
dist/
diff --git a/.oxfmtrc.json b/.oxfmtrc.json
new file mode 100644
index 0000000..16d1869
--- /dev/null
+++ b/.oxfmtrc.json
@@ -0,0 +1,9 @@
+{
+ "singleQuote": true,
+ "tabWidth": 4,
+ "printWidth": 80,
+ "semi": true,
+ "trailingComma": "all",
+ "arrowParens": "avoid",
+ "bracketSpacing": true
+}
diff --git a/.oxlintrc.json b/.oxlintrc.json
new file mode 100644
index 0000000..0b8ff7a
--- /dev/null
+++ b/.oxlintrc.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
+ "categories": {
+ "correctness": "error"
+ },
+ "rules": {
+ "no-debugger": "error",
+ "no-console": ["warn", { "allow": ["warn", "error", "info"] }],
+ "no-unused-vars": "warn",
+ "no-unused-expressions": [
+ "error",
+ { "allowShortCircuit": true, "allowTernary": true }
+ ]
+ },
+ "overrides": [
+ {
+ "files": ["test/**"],
+ "rules": {
+ "no-unused-expressions": "off",
+ "no-async-promise-executor": "off",
+ "no-useless-catch": "off",
+ "unicorn/no-new-array": "off",
+ "unicorn/prefer-string-starts-ends-with": "off",
+ "unicorn/no-unnecessary-await": "off",
+ "no-unused-vars": "off"
+ }
+ }
+ ],
+ "ignorePatterns": [
+ "**/*.js",
+ "**/*.d.ts",
+ "docs/",
+ "coverage/",
+ ".nyc_output/",
+ "node_modules/"
+ ]
+}
diff --git a/README.md b/README.md
index e44ea6b..31f6d18 100644
--- a/README.md
+++ b/README.md
@@ -179,6 +179,74 @@ import { hello } from './clients/Hello';
In this case all complex types defined within service implementation
will be available under imported namespace of the client.
+## Complex Types
+
+To expose complex (object) types as service method arguments or return values,
+annotate the class with `@classType()` and its fields with `@property()`:
+
+~~~typescript
+import { classType, property, expose, IMQService } from '@imqueue/rpc';
+
+@classType()
+class Address {
+ @property('string')
+ country: string;
+
+ @property('string', true)
+ zipCode?: string; // optional
+}
+
+@classType()
+class User {
+ @property('string')
+ firstName: string;
+
+ @property('Array
', true)
+ addresses?: Address[];
+}
+
+class UserService extends IMQService {
+ /**
+ * Persists the given user
+ *
+ * @param {User} user - user to save
+ * @returns {Promise}
+ */
+ @expose()
+ public async save(user: User): Promise {
+ // User and Address are now exposed to generated clients
+ return true;
+ }
+}
+~~~
+
+The `@classType()` class decorator is **required** on every class that uses
+`@property()` — without it the type will not be registered and will not appear
+in generated clients. (Indexed types use `@indexed()`, which registers `@property`
+fields as well.)
+
+## Requirements
+
+This package uses **standard (TC39) decorators**. Consuming projects must set,
+in their `tsconfig.json`:
+
+~~~json
+{
+ "compilerOptions": {
+ "experimentalDecorators": false,
+ "removeComments": false,
+ "lib": ["es2023", "esnext.decorators"]
+ }
+}
+~~~
+
+Because standard decorators provide no runtime type reflection (there is no
+`emitDecoratorMetadata`), the RPC layer derives argument and return types from
+**JSDoc**. Therefore **every exposed method must be documented with JSDoc**
+`@param`/`@returns` tags carrying the types (as shown in the examples above),
+and `removeComments` must remain `false` so those comments survive compilation.
+Undocumented parameters fall back to `any` in generated clients.
+
## Notes
For image containers builds assign machine UUID in `/etc/machine-id` and
diff --git a/package-lock.json b/package-lock.json
index 6c8c493..105e50e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,574 +9,776 @@
"version": "2.1.0",
"license": "GPL-3.0-only",
"dependencies": {
- "@imqueue/core": "^2.0.26",
- "@types/node": "^24.9.1",
- "acorn": "^8.15.0",
- "farmhash": "^5.0.1",
- "node-machine-id": "^1.1.12",
- "reflect-metadata": "^0.2.2",
- "typescript": "^5.9.3"
+ "@imqueue/core": "^3.0.0",
+ "typescript": "^6.0.3"
},
"devDependencies": {
- "@types/chai": "^5.2.2",
- "@types/mocha": "^10.0.10",
"@types/mock-require": "^3.0.0",
- "@types/sinon": "^17.0.4",
- "chai": "^6.0.1",
- "coveralls-next": "^5.0.0",
- "minimist": "^1.2.8",
- "mocha": "^11.7.2",
- "mocha-lcov-reporter": "^1.3.0",
+ "@types/node": "^24.9.1",
"mock-require": "^3.0.3",
- "nyc": "^17.1.0",
- "open": "^10.1.2",
- "sinon": "^21.0.0",
- "source-map-support": "^0.5.21",
- "ts-node": "^10.9.2",
- "typedoc": "^0.28.12"
+ "oxfmt": "0.57.0",
+ "oxlint": "1.72.0",
+ "typedoc": "^0.28.20"
}
},
- "node_modules/@babel/code-frame": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
- "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "node_modules/@gerrit0/mini-shiki": {
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz",
+ "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.27.1",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
+ "@shikijs/engine-oniguruma": "^3.23.0",
+ "@shikijs/langs": "^3.23.0",
+ "@shikijs/themes": "^3.23.0",
+ "@shikijs/types": "^3.23.0",
+ "@shikijs/vscode-textmate": "^10.0.2"
+ }
+ },
+ "node_modules/@imqueue/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-D1B78GfWVXeHmbRR3t97YGyYt5Anpi8Cl5xsU0mYGrMfqxLhnacUKAMYKcHU0STaEkdmf9wmiT7N0X8QkKqWHg==",
+ "license": "GPL-3.0-only",
+ "dependencies": {
+ "ioredis": "^5.11.1"
}
},
- "node_modules/@babel/compat-data": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
- "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "node_modules/@ioredis/commands": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz",
+ "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==",
+ "license": "MIT"
+ },
+ "node_modules/@oxfmt/binding-android-arm-eabi": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.57.0.tgz",
+ "integrity": "sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/core": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
- "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "node_modules/@oxfmt/binding-android-arm64": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.57.0.tgz",
+ "integrity": "sha512-mp6PibWbao3aizijcheOeHQaYEhcUAt8pwLniYbtLfHxL/psFF0BykAwCj+s3c6qIpa8yN8keZICWrqtZ70w8g==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.5",
- "@babel/helper-compilation-targets": "^7.27.2",
- "@babel/helper-module-transforms": "^7.28.3",
- "@babel/helpers": "^7.28.4",
- "@babel/parser": "^7.28.5",
- "@babel/template": "^7.27.2",
- "@babel/traverse": "^7.28.5",
- "@babel/types": "^7.28.5",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
+ "optional": true,
+ "os": [
+ "android"
+ ],
"engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/core/node_modules/convert-source-map": {
- "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==",
+ "node_modules/@oxfmt/binding-darwin-arm64": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.57.0.tgz",
+ "integrity": "sha512-T+0stuCBqmUVY+aMIvrgXhzGhHO3sD5tNiiEcYqgSdPsnukskQqn2u5qOVD0sv1l7RLdFS5Z/f5Wi9Ktyjr3Eg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "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==",
+ "node_modules/@oxfmt/binding-darwin-x64": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.57.0.tgz",
+ "integrity": "sha512-O+3JbqWs/mCI2oi4xfhRO2IVPFJNDDEBV8Odo+ZpmsUOeKJfjXoNH7nDmBEQcDgK7NfjDIyE7kRgYSZcTLDO0A==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/generator": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
- "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "node_modules/@oxfmt/binding-freebsd-x64": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.57.0.tgz",
+ "integrity": "sha512-pxwhxVC+JkLX9twOQ/8C/vbuOQcMZyKIDmiRDZfO7yITuVcIdZCiLRqqf4QOxb2+8FWrRXzQpm+1DBKcMpHSSQ==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.28.5",
- "@babel/types": "^7.28.5",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "jsesc": "^3.0.2"
- },
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
- "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.57.0.tgz",
+ "integrity": "sha512-pxBU4zH2imB/MDBfth2rOMeVxXUMjRQLCazagwLARIFH3hVlxZJBlM4nSnHXaIHJK4/qezoFCIORN6AY8Mra4A==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@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"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxfmt/binding-linux-arm-musleabihf": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.57.0.tgz",
+ "integrity": "sha512-JAprOzt8tycYou36ZgEw14DlRHTiN8qdtKANdV3VZIRIvTI/lh/cX13c9pJ/EnDk2GT3FASH7KvCgQ2AufAifQ==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxfmt/binding-linux-arm64-gnu": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.57.0.tgz",
+ "integrity": "sha512-ajtjaxSaj9xl4BW7REt+Cef/ttzbAq00Bq4z7JUDZEfgFXdwSjH8K9bF+IcIJzZB9lKqMfQ4eHuSFOvvlvtqOg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/helper-module-imports": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
- "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "node_modules/@oxfmt/binding-linux-arm64-musl": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.57.0.tgz",
+ "integrity": "sha512-p4Y/+RYk9Bk5WO+zHSUXAClRmZ2fbJCejMuCAsU2HhyME4jqf6Ftt/mJYEwIah1wGCBDYOB7wEGV1x5bCEZ6hA==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.27.1",
- "@babel/types": "^7.27.1"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.28.3",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
- "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "node_modules/@oxfmt/binding-linux-ppc64-gnu": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.57.0.tgz",
+ "integrity": "sha512-By6tRALAZsno0F4zedmtG+wdMvJiJmJoXM4d3+A9zHE4HRXLqXITwRH8mgrlcXc5yJM2g2W3riRPwTYdgemZLQ==",
+ "cpu": [
+ "ppc64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.27.1",
- "@babel/traverse": "^7.28.3"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/helper-string-parser": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
- "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "node_modules/@oxfmt/binding-linux-riscv64-gnu": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.57.0.tgz",
+ "integrity": "sha512-skYeG+RgvyzspqVEBsEprL90OYYZfoVNqB3HcCNR6QDJyXKOzfDRT3zncnHmUaFluIlBHuY23mU1b5WGgR98hA==",
+ "cpu": [
+ "riscv64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
- "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "node_modules/@oxfmt/binding-linux-riscv64-musl": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.57.0.tgz",
+ "integrity": "sha512-FFgACrZOXAXUh5KQh2mt1CDOVOZmn+QzHP71wM9QobNwyQvoFfyAeefVUltW83g3sm7LTiH3yfFqLLVUpA5ZFQ==",
+ "cpu": [
+ "riscv64"
+ ],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/helper-validator-option": {
- "version": "7.27.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
- "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "node_modules/@oxfmt/binding-linux-s390x-gnu": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.57.0.tgz",
+ "integrity": "sha512-Nm/BAOfQeFiiKd502mZn/GAVKJwtd0RdCg17G3Wz/WSOIQmDi3+7/SZH4BHn1Ye5KvTVH3ua8WvfwLLycNIuvA==",
+ "cpu": [
+ "s390x"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/helpers": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
- "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "node_modules/@oxfmt/binding-linux-x64-gnu": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.57.0.tgz",
+ "integrity": "sha512-BiSy5Ku3mQqyxS6YIqAJgd403wEUWvI7kerfzPxc2l/txZVmZM0pSj7oDM+4bGBExowxOi7o73jEam1W0EDTZg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "@babel/template": "^7.27.2",
- "@babel/types": "^7.28.4"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/parser": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
- "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "node_modules/@oxfmt/binding-linux-x64-musl": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.57.0.tgz",
+ "integrity": "sha512-BCRkJiotz5s9afLYD2LuMvzAoDYx9H17E/YbDyu4xK7l4zHDPeny9ErSXL//i/nJyaOwRk08x4b8cgJC00+JDg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "@babel/types": "^7.28.5"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/template": {
- "version": "7.27.2",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
- "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "node_modules/@oxfmt/binding-openharmony-arm64": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.57.0.tgz",
+ "integrity": "sha512-4Oaxe1qrGgXfpCJ1C/ERJ2iCtV2rN1R79ga9fsfyVHfSQRu/hVW780u2KDqZWFZ/iGTHODJji0JemxqFZ63eIQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.27.1",
- "@babel/parser": "^7.27.2",
- "@babel/types": "^7.27.1"
- },
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/traverse": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
- "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "node_modules/@oxfmt/binding-win32-arm64-msvc": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.57.0.tgz",
+ "integrity": "sha512-MYLAsDnhdNsSGheLYhWgbk0vfIrlS84iQYun/y21fX6u0jj8iBtYtbpZMdiqYeuf8U12eVPUjVY2xE2NrCfJ0g==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.5",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.5",
- "@babel/template": "^7.27.2",
- "@babel/types": "^7.28.5",
- "debug": "^4.3.1"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@babel/types": {
- "version": "7.28.5",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
- "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "node_modules/@oxfmt/binding-win32-ia32-msvc": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.57.0.tgz",
+ "integrity": "sha512-PBwdzZALJY/jcCx2E6is0yu+cuVXeySTDmwuseD+9j0mHqlRNxwlKgsyRTBed/woPeqfVfuXfWjoq4Cx2Zt3Eg==",
+ "cpu": [
+ "ia32"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">=6.9.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxfmt/binding-win32-x64-msvc": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.57.0.tgz",
+ "integrity": "sha512-bQJdH9i4RRfw55jm7+8/xS7GzHLLTbHx4huhrrDxQJaJtbSDbsyOnODvP1ftT7EG0KFKAYO2S+q6AcioXODx8w==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "0.3.9"
- },
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
- "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "node_modules/@oxlint/binding-android-arm-eabi": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.72.0.tgz",
+ "integrity": "sha512-zhCmvn+1Mj3UchAc/90i99S0t7jJUsHmFVSPg4UWrjO8b8eaSGwscgO6QAUtvHBstkjQwBttQNswEnAF1mIQdA==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.0.3",
- "@jridgewell/sourcemap-codec": "^1.4.10"
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@gerrit0/mini-shiki": {
- "version": "3.14.0",
- "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.14.0.tgz",
- "integrity": "sha512-c5X8fwPLOtUS8TVdqhynz9iV0GlOtFUT1ppXYzUUlEXe4kbZ/mvMT8wXoT8kCwUka+zsiloq7sD3pZ3+QVTuNQ==",
+ "node_modules/@oxlint/binding-android-arm64": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.72.0.tgz",
+ "integrity": "sha512-mtH+aY/ozv1eZoCUC2owjFAtyNBKHpJHygKeEu9zXXnQGW1Q2/qOpvx+I+Lf23+TvTz66F4iiXUbl2cGvoLPCQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@shikijs/engine-oniguruma": "^3.14.0",
- "@shikijs/langs": "^3.14.0",
- "@shikijs/themes": "^3.14.0",
- "@shikijs/types": "^3.14.0",
- "@shikijs/vscode-textmate": "^10.0.2"
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@imqueue/core": {
- "version": "2.0.26",
- "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-2.0.26.tgz",
- "integrity": "sha512-4Ecy/23+mVcSkeNYJGbcj1XaU4Hicnu9MQTLejOhuhVrgspOWrYr/GY7dDlF7amWKw1zFN8FehRFdszgz1YxPQ==",
- "license": "GPL-3.0-only",
- "dependencies": {
- "ioredis": "^5.8.2"
+ "node_modules/@oxlint/binding-darwin-arm64": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.72.0.tgz",
+ "integrity": "sha512-EvnajNPDtfknB3ZieeOOyDTwJn9QXDiwfnF4ZDQqART6RG6hjY4WigQcZdGoK2dkB3e1vrmEzN9aYbQCUkh/gQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@ioredis/commands": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
- "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==",
- "license": "MIT"
+ "node_modules/@oxlint/binding-darwin-x64": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.72.0.tgz",
+ "integrity": "sha512-ZkCdEa/G80A7vEHfeCDz/+L3m33DE73v32mDKhgOIgz8Uwf0DFcK7+uu6qC+7LEhmz5fpOe1osWKyjSNMydFIQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "node_modules/@oxlint/binding-freebsd-x64": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.72.0.tgz",
+ "integrity": "sha512-NroXv2vh+sxVY1uya/rM5pjhx1hm8BzlYpx9q67QP0Xhw5MH2bf5GJylpvLEC+781p1Xli/317EoV9AlGwViag==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@istanbuljs/load-nyc-config": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
- "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "node_modules/@oxlint/binding-linux-arm-gnueabihf": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.72.0.tgz",
+ "integrity": "sha512-0NDywYgfj279Ou/BcQuCYSj7NJwBfmWn5qc5uGO/Ny7fUWmXyIpvawqX/8acQlWG6IXelJsJhj+JAy6sjsKj0A==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
- "license": "ISC",
- "dependencies": {
- "camelcase": "^5.3.1",
- "find-up": "^4.1.0",
- "get-package-type": "^0.1.0",
- "js-yaml": "^3.13.1",
- "resolve-from": "^5.0.0"
- },
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=8"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxlint/binding-linux-arm-musleabihf": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.72.0.tgz",
+ "integrity": "sha512-4vpXB06h65Ezsy4hRyrGjGrfa1SkVPii09yaajiYhmVpgsFiLD+KNxIx/BNAY+XiO+i1yqp9HHdwqM8VTqa5XQ==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "sprintf-js": "~1.0.2"
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxlint/binding-linux-arm64-gnu": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.72.0.tgz",
+ "integrity": "sha512-immaN4g2ZGFiOkKrvRX9LvzZdd2GkQM5wR+UyzYyUuyhUTXGQ4HKUJH18xp4G8OfhCVaVAJfKZxwE1r8+4hhaQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "locate-path": "^5.0.0",
- "path-exists": "^4.0.0"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=8"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxlint/binding-linux-arm64-musl": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.72.0.tgz",
+ "integrity": "sha512-JGHS9Mnr7iWyyLDxgCv1MhzVpAckgptg00F2gnxt/GD7lQ2SW1BRcxHqhSTaSdDpjWRrBkBxMMh4+Hn3aVtExg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxlint/binding-linux-ppc64-gnu": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.72.0.tgz",
+ "integrity": "sha512-AOYgBZqxNshrg83P9v0RYv+m8s10Cqkj4/PxXFDhcS3k7FqsIG5+CxErshZCIN7G8iy4Y+VGfAsuEdar8AcbBg==",
+ "cpu": [
+ "ppc64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "p-locate": "^4.1.0"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=8"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxlint/binding-linux-riscv64-gnu": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.72.0.tgz",
+ "integrity": "sha512-QMybPS5ij3/vrKG67mqzHwW++91sYxK/PPUVi6SBtNCEzW4niS52fVBdXbQ6nou0wWbUPEpx8Sl/ZjtgE3clXA==",
+ "cpu": [
+ "riscv64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "p-try": "^2.0.0"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=6"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxlint/binding-linux-riscv64-musl": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.72.0.tgz",
+ "integrity": "sha512-gOc3W7JV0PXRpIL7stUlLe3Wa9Gp0Kdlup87IT3gHDvPKck2xNgMIl/Gs2lldYY2lyXZDC4rWi3hmoLUobkgbQ==",
+ "cpu": [
+ "riscv64"
+ ],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "p-limit": "^2.2.0"
- },
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=8"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@istanbuljs/schema": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
- "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "node_modules/@oxlint/binding-linux-s390x-gnu": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.72.0.tgz",
+ "integrity": "sha512-rpGxph+FjjHcYI5q6uxB3Az+tnfmEnDbSA8+PK9ZE/VzyUAkvBOMeuY7ZQMhu5mpZH7YQDsTdW6Cx4kV/msc6w==",
+ "cpu": [
+ "s390x"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">=8"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@jridgewell/gen-mapping": {
- "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==",
+ "node_modules/@oxlint/binding-linux-x64-gnu": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.72.0.tgz",
+ "integrity": "sha512-WND+uhf/Ko13SLqQMWQUgsZuLvYYEvL0ZKgg0tgGYfLqxG7l8Ju123fHDMJyYSDl5E3bUbpFUuii/OvMreFQzw==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
+ "libc": [
+ "glibc"
+ ],
"license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "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==",
+ "node_modules/@oxlint/binding-linux-x64-musl": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.72.0.tgz",
+ "integrity": "sha512-SrpbrUL70nG9vh6zP4/oKHWgLuHquwsr7MW9XOn0olBVgh10Uqr8qscKhQoBGEn6olK/IUpn5GSKcdQ5AjUhGA==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
+ "libc": [
+ "musl"
+ ],
"license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "node_modules/@oxlint/binding-openharmony-arm64": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.72.0.tgz",
+ "integrity": "sha512-qkrsEn6NmgFKr7U/QnezQMb+q/vzAy0Dd9Y95gQGQTyjzDLN+HRZMuM5u70iyH4nBLCfKBzhjMsYCehKay2jyg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
"engines": {
- "node": ">=6.0.0"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "node_modules/@oxlint/binding-win32-arm64-msvc": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.72.0.tgz",
+ "integrity": "sha512-LWR6ZlFZph+KPjXv8opgZsXRDCdrdQe8VL8Cg9zxCoBS73h6znzZpydVgmdnwj8mB9AuSM5jxEgDJDpQkjboeg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "node_modules/@oxlint/binding-win32-ia32-msvc": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.72.0.tgz",
+ "integrity": "sha512-yt6HEh7IsHvtjRWtmeZRX134eaXKHq5Gnqlf1xBJdJl1JtdoRUEJw3nAxpZoUDS860cX/foKbztO441anVBtVQ==",
+ "cpu": [
+ "ia32"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "node_modules/@oxlint/binding-win32-x64-msvc": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.72.0.tgz",
+ "integrity": "sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
"optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": ">=14"
+ "node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@shikijs/engine-oniguruma": {
- "version": "3.14.0",
- "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz",
- "integrity": "sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==",
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz",
+ "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@shikijs/types": "3.14.0",
+ "@shikijs/types": "3.23.0",
"@shikijs/vscode-textmate": "^10.0.2"
}
},
"node_modules/@shikijs/langs": {
- "version": "3.14.0",
- "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.14.0.tgz",
- "integrity": "sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==",
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz",
+ "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@shikijs/types": "3.14.0"
+ "@shikijs/types": "3.23.0"
}
},
"node_modules/@shikijs/themes": {
- "version": "3.14.0",
- "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.14.0.tgz",
- "integrity": "sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==",
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz",
+ "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@shikijs/types": "3.14.0"
+ "@shikijs/types": "3.23.0"
}
},
"node_modules/@shikijs/types": {
- "version": "3.14.0",
- "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.14.0.tgz",
- "integrity": "sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==",
+ "version": "3.23.0",
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz",
+ "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -591,2916 +793,425 @@
"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==",
+ "node_modules/@types/hast": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"dev": true,
- "license": "BSD-3-Clause",
+ "license": "MIT",
"dependencies": {
- "type-detect": "4.0.8"
+ "@types/unist": "*"
}
},
- "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==",
+ "node_modules/@types/mock-require": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mock-require/-/mock-require-3.0.0.tgz",
+ "integrity": "sha512-TZ/s3ufaF+kEFPx9PqXKNd7OsPJoVsgOd59EA8XQLTRjJCsOdx5sEAOGhyrZ3Os78iaDKyGJWdQWyYWRg1Iyvg==",
"dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@sinonjs/commons": "^3.0.1"
- }
+ "license": "MIT"
},
- "node_modules/@sinonjs/samsam": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz",
- "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==",
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
"dev": true,
- "license": "BSD-3-Clause",
+ "license": "MIT",
"dependencies": {
- "@sinonjs/commons": "^3.0.1",
- "type-detect": "^4.1.0"
+ "undici-types": "~7.18.0"
}
},
- "node_modules/@sinonjs/samsam/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"
- }
- },
- "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,
- "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,
- "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,
- "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,
- "license": "MIT"
- },
- "node_modules/@types/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
- }
- },
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"dev": true,
"license": "MIT"
},
- "node_modules/@types/hast": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
- "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/unist": "*"
- }
- },
- "node_modules/@types/mocha": {
- "version": "10.0.10",
- "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
- "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
+ "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": "MIT"
+ "license": "Python-2.0"
},
- "node_modules/@types/mock-require": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@types/mock-require/-/mock-require-3.0.0.tgz",
- "integrity": "sha512-TZ/s3ufaF+kEFPx9PqXKNd7OsPJoVsgOd59EA8XQLTRjJCsOdx5sEAOGhyrZ3Os78iaDKyGJWdQWyYWRg1Iyvg==",
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "24.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
- "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"license": "MIT",
- "dependencies": {
- "undici-types": "~7.16.0"
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
- "node_modules/@types/sinon": {
- "version": "17.0.4",
- "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz",
- "integrity": "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==",
+ "node_modules/brace-expansion": {
+ "version": "5.0.7",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+ "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/sinonjs__fake-timers": "*"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
}
},
- "node_modules/@types/sinonjs__fake-timers": {
- "version": "15.0.1",
- "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz",
- "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/unist": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
- "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
+ "node_modules/cluster-key-slot": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz",
+ "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==",
+ "license": "Apache-2.0",
"engines": {
- "node": ">=0.4.0"
+ "node": ">=0.10.0"
}
},
- "node_modules/acorn-walk": {
- "version": "8.3.4",
- "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
- "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
- "dev": true,
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
- "acorn": "^8.11.0"
+ "ms": "^2.1.3"
},
"engines": {
- "node": ">=0.4.0"
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
}
},
- "node_modules/aggregate-error": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
- "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "clean-stack": "^2.0.0",
- "indent-string": "^4.0.0"
- },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
"engines": {
- "node": ">=8"
+ "node": ">=0.10"
}
},
- "node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"dev": true,
- "license": "MIT",
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">=12"
+ "node": ">=0.12"
},
"funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ "url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "node_modules/get-caller-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
+ "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
"dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ioredis": {
+ "version": "5.11.1",
+ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz",
+ "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==",
"license": "MIT",
"dependencies": {
- "color-convert": "^2.0.1"
+ "@ioredis/commands": "1.10.0",
+ "cluster-key-slot": "1.1.1",
+ "debug": "4.4.3",
+ "denque": "2.1.0",
+ "redis-errors": "1.2.0",
+ "redis-parser": "3.0.0",
+ "standard-as-callback": "2.1.0"
},
"engines": {
- "node": ">=8"
+ "node": ">=12.22.0"
},
"funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ "type": "opencollective",
+ "url": "https://opencollective.com/ioredis"
}
},
- "node_modules/append-transform": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
- "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==",
+ "node_modules/linkify-it": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
+ "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
"license": "MIT",
"dependencies": {
- "default-require-extensions": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/archy": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
- "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/arg": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
- "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
- "dev": true,
- "license": "MIT"
- },
- "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/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
+ "uc.micro": "^2.0.0"
}
},
- "node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "node_modules/lunr": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
+ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true,
"license": "MIT"
},
- "node_modules/baseline-browser-mapping": {
- "version": "2.8.20",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz",
- "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.js"
- }
- },
- "node_modules/brace-expansion": {
- "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"
- }
- },
- "node_modules/browser-stdout": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
- "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/browserslist": {
- "version": "4.27.0",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
- "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
+ "node_modules/markdown-it": {
+ "version": "14.3.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
+ "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
"dev": true,
"funding": [
{
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
- "url": "https://github.com/sponsors/ai"
+ "url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.8.19",
- "caniuse-lite": "^1.0.30001751",
- "electron-to-chromium": "^1.5.238",
- "node-releases": "^2.0.26",
- "update-browserslist-db": "^1.1.4"
+ "argparse": "^2.0.1",
+ "entities": "^4.5.0",
+ "linkify-it": "^5.0.2",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
},
"bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ "markdown-it": "bin/markdown-it.mjs"
}
},
- "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==",
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"dev": true,
"license": "MIT"
},
- "node_modules/bundle-name": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
- "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
- "license": "MIT",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "run-applescript": "^7.0.0"
+ "brace-expansion": "^5.0.5"
},
"engines": {
- "node": ">=18"
+ "node": "18 || 20 || >=22"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/caching-transform": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
- "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==",
+ "node_modules/mock-require": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz",
+ "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "hasha": "^5.0.0",
- "make-dir": "^3.0.0",
- "package-hash": "^4.0.0",
- "write-file-atomic": "^3.0.0"
+ "get-caller-file": "^1.0.2",
+ "normalize-path": "^2.1.1"
},
"engines": {
- "node": ">=8"
- }
- },
- "node_modules/camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
+ "node": ">=4.3.0"
}
},
- "node_modules/caniuse-lite": {
- "version": "1.0.30001751",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
- "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
},
- "node_modules/chai": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz",
- "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==",
+ "node_modules/normalize-path": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
+ "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "remove-trailing-separator": "^1.0.1"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=0.10.0"
}
},
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "node_modules/oxfmt": {
+ "version": "0.57.0",
+ "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.57.0.tgz",
+ "integrity": "sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
+ "tinypool": "2.1.0"
+ },
+ "bin": {
+ "oxfmt": "bin/oxfmt"
},
"engines": {
- "node": ">=10"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/chalk/node_modules/supports-color": {
- "version": "7.2.0",
- "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"
+ "url": "https://github.com/sponsors/Boshen"
},
- "engines": {
- "node": ">=8"
+ "optionalDependencies": {
+ "@oxfmt/binding-android-arm-eabi": "0.57.0",
+ "@oxfmt/binding-android-arm64": "0.57.0",
+ "@oxfmt/binding-darwin-arm64": "0.57.0",
+ "@oxfmt/binding-darwin-x64": "0.57.0",
+ "@oxfmt/binding-freebsd-x64": "0.57.0",
+ "@oxfmt/binding-linux-arm-gnueabihf": "0.57.0",
+ "@oxfmt/binding-linux-arm-musleabihf": "0.57.0",
+ "@oxfmt/binding-linux-arm64-gnu": "0.57.0",
+ "@oxfmt/binding-linux-arm64-musl": "0.57.0",
+ "@oxfmt/binding-linux-ppc64-gnu": "0.57.0",
+ "@oxfmt/binding-linux-riscv64-gnu": "0.57.0",
+ "@oxfmt/binding-linux-riscv64-musl": "0.57.0",
+ "@oxfmt/binding-linux-s390x-gnu": "0.57.0",
+ "@oxfmt/binding-linux-x64-gnu": "0.57.0",
+ "@oxfmt/binding-linux-x64-musl": "0.57.0",
+ "@oxfmt/binding-openharmony-arm64": "0.57.0",
+ "@oxfmt/binding-win32-arm64-msvc": "0.57.0",
+ "@oxfmt/binding-win32-ia32-msvc": "0.57.0",
+ "@oxfmt/binding-win32-x64-msvc": "0.57.0"
+ },
+ "peerDependencies": {
+ "svelte": "^5.0.0",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "svelte": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
}
},
- "node_modules/chokidar": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
- "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "node_modules/oxlint": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.72.0.tgz",
+ "integrity": "sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "readdirp": "^4.0.1"
+ "bin": {
+ "oxlint": "bin/oxlint"
},
"engines": {
- "node": ">= 14.16.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
- "url": "https://paulmillr.com/funding/"
+ "url": "https://github.com/sponsors/Boshen"
+ },
+ "optionalDependencies": {
+ "@oxlint/binding-android-arm-eabi": "1.72.0",
+ "@oxlint/binding-android-arm64": "1.72.0",
+ "@oxlint/binding-darwin-arm64": "1.72.0",
+ "@oxlint/binding-darwin-x64": "1.72.0",
+ "@oxlint/binding-freebsd-x64": "1.72.0",
+ "@oxlint/binding-linux-arm-gnueabihf": "1.72.0",
+ "@oxlint/binding-linux-arm-musleabihf": "1.72.0",
+ "@oxlint/binding-linux-arm64-gnu": "1.72.0",
+ "@oxlint/binding-linux-arm64-musl": "1.72.0",
+ "@oxlint/binding-linux-ppc64-gnu": "1.72.0",
+ "@oxlint/binding-linux-riscv64-gnu": "1.72.0",
+ "@oxlint/binding-linux-riscv64-musl": "1.72.0",
+ "@oxlint/binding-linux-s390x-gnu": "1.72.0",
+ "@oxlint/binding-linux-x64-gnu": "1.72.0",
+ "@oxlint/binding-linux-x64-musl": "1.72.0",
+ "@oxlint/binding-openharmony-arm64": "1.72.0",
+ "@oxlint/binding-win32-arm64-msvc": "1.72.0",
+ "@oxlint/binding-win32-ia32-msvc": "1.72.0",
+ "@oxlint/binding-win32-x64-msvc": "1.72.0"
+ },
+ "peerDependencies": {
+ "oxlint-tsgolint": ">=0.22.1",
+ "vite-plus": "*"
+ },
+ "peerDependenciesMeta": {
+ "oxlint-tsgolint": {
+ "optional": true
+ },
+ "vite-plus": {
+ "optional": true
+ }
}
},
- "node_modules/clean-stack": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
- "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
- "node_modules/cliui": {
- "version": "8.0.1",
- "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",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/cliui/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cliui/node_modules/string-width": {
- "version": "4.2.3",
- "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",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/node_modules/strip-ansi": {
- "version": "6.0.1",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/cliui/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",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/cluster-key-slot": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
- "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "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"
- },
- "engines": {
- "node": ">=7.0.0"
- }
- },
- "node_modules/color-name": {
- "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,
- "license": "MIT"
- },
- "node_modules/commondir": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
- "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/convert-source-map": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/coveralls-next": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/coveralls-next/-/coveralls-next-5.0.0.tgz",
- "integrity": "sha512-RCj6Oflf6iQtN3Q5b0SSemEbQBzeBjQlLUrc3bfNECTy83hMJA9krdNZ5GTRm7Jpbyo92yKUbQDP5FYlWcL5sA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "js-yaml": "4.1.0",
- "lcov-parse": "1.0.0",
- "log-driver": "1.2.7",
- "minimist": "1.2.8"
- },
- "bin": {
- "coveralls": "bin/coveralls.js"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/create-require": {
- "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,
- "license": "MIT"
- },
- "node_modules/cross-spawn": {
- "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",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/decamelize": {
- "version": "1.2.0",
- "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"
- }
- },
- "node_modules/default-browser": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
- "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "bundle-name": "^4.1.0",
- "default-browser-id": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-browser-id": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
- "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/default-require-extensions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
- "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "strip-bom": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/define-lazy-prop": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
- "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/denque": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
- "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/diff": {
- "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"
- }
- },
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.241",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz",
- "integrity": "sha512-ILMvKX/ZV5WIJzzdtuHg8xquk2y0BOGlFOxBVwTpbiXqWIH0hamG45ddU4R3PQ0gYu+xgo0vdHXHli9sHIGb4w==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/entities": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
- "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
- "node_modules/es6-error": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
- "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/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"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/farmhash": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/farmhash/-/farmhash-5.0.1.tgz",
- "integrity": "sha512-YSrAI+lNwDsvBsHfhYO6ihkFjObVsI/4DckifTcfGrks6MR4WxZoOtNWlHXDfzmFo4B7x0rOlxAxgxghu8qMtw==",
- "license": "Apache-2.0",
- "dependencies": {
- "detect-libc": "^2.1.1"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- }
- },
- "node_modules/find-cache-dir": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
- "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "commondir": "^1.0.1",
- "make-dir": "^3.0.2",
- "pkg-dir": "^4.1.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
- }
- },
- "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/flat": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
- "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "bin": {
- "flat": "cli.js"
- }
- },
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/fromentries": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz",
- "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==",
- "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/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,
- "license": "ISC"
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "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"
- }
- },
- "node_modules/get-caller-file": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
- "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/get-package-type": {
- "version": "0.1.0",
- "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"
- }
- },
- "node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
- },
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/graceful-fs": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
- "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "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"
- }
- },
- "node_modules/hasha": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
- "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-stream": "^2.0.0",
- "type-fest": "^0.8.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/he": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
- "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "he": "bin/he"
- }
- },
- "node_modules/html-escaper": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
- "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/indent-string": {
- "version": "4.0.0",
- "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"
- }
- },
- "node_modules/inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "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"
- }
- },
- "node_modules/inherits": {
- "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/ioredis": {
- "version": "5.8.2",
- "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
- "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==",
- "license": "MIT",
- "dependencies": {
- "@ioredis/commands": "1.4.0",
- "cluster-key-slot": "^1.1.0",
- "debug": "^4.3.4",
- "denque": "^2.1.0",
- "lodash.defaults": "^4.2.0",
- "lodash.isarguments": "^3.1.0",
- "redis-errors": "^1.2.0",
- "redis-parser": "^3.0.0",
- "standard-as-callback": "^2.1.0"
- },
- "engines": {
- "node": ">=12.22.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/ioredis"
- }
- },
- "node_modules/is-docker": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
- "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "is-docker": "cli.js"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-inside-container": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
- "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-docker": "^3.0.0"
- },
- "bin": {
- "is-inside-container": "cli.js"
- },
- "engines": {
- "node": ">=14.16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-path-inside": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
- "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-plain-obj": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
- "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/is-stream": {
- "version": "2.0.1",
- "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"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "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,
- "license": "MIT"
- },
- "node_modules/is-unicode-supported": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
- "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/is-windows": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
- "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-wsl": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
- "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-inside-container": "^1.0.0"
- },
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "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"
- }
- },
- "node_modules/istanbul-lib-hook": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz",
- "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "append-transform": "^2.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/istanbul-lib-instrument": {
- "version": "6.0.3",
- "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",
- "@istanbuljs/schema": "^0.1.3",
- "istanbul-lib-coverage": "^3.2.0",
- "semver": "^7.5.4"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-lib-processinfo": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz",
- "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "archy": "^1.0.0",
- "cross-spawn": "^7.0.3",
- "istanbul-lib-coverage": "^3.2.0",
- "p-map": "^3.0.0",
- "rimraf": "^3.0.0",
- "uuid": "^8.3.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/istanbul-lib-report": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
- "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "istanbul-lib-coverage": "^3.0.0",
- "make-dir": "^4.0.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-lib-report/node_modules/make-dir": {
- "version": "4.0.0",
- "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"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/istanbul-lib-report/node_modules/supports-color": {
- "version": "7.2.0",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/istanbul-lib-source-maps": {
- "version": "4.0.1",
- "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",
- "source-map": "^0.6.1"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/istanbul-reports": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
- "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "html-escaper": "^2.0.0",
- "istanbul-lib-report": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
- "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==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1"
- },
- "bin": {
- "js-yaml": "bin/js-yaml.js"
- }
- },
- "node_modules/jsesc": {
- "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": ">=6"
- }
- },
- "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"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/lcov-parse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz",
- "integrity": "sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "bin": {
- "lcov-parse": "bin/cli.js"
- }
- },
- "node_modules/linkify-it": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
- "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "uc.micro": "^2.0.0"
- }
- },
- "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/lodash.defaults": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
- "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
- "license": "MIT"
- },
- "node_modules/lodash.flattendeep": {
- "version": "4.4.0",
- "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
- "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lodash.isarguments": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
- "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
- "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"
- }
- },
- "node_modules/log-symbols": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
- "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.1.0",
- "is-unicode-supported": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/lunr": {
- "version": "2.3.9",
- "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
- "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/make-dir": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
- "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "semver": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/make-dir/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/make-error": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
- "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/markdown-it": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
- "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "argparse": "^2.0.1",
- "entities": "^4.4.0",
- "linkify-it": "^5.0.0",
- "mdurl": "^2.0.0",
- "punycode.js": "^2.3.1",
- "uc.micro": "^2.1.0"
- },
- "bin": {
- "markdown-it": "bin/markdown-it.mjs"
- }
- },
- "node_modules/mdurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
- "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
- "dev": true,
- "license": "MIT"
- },
- "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/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/minipass": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
- "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/mocha": {
- "version": "11.7.4",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.4.tgz",
- "integrity": "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "browser-stdout": "^1.3.1",
- "chokidar": "^4.0.1",
- "debug": "^4.3.5",
- "diff": "^7.0.0",
- "escape-string-regexp": "^4.0.0",
- "find-up": "^5.0.0",
- "glob": "^10.4.5",
- "he": "^1.2.0",
- "is-path-inside": "^3.0.3",
- "js-yaml": "^4.1.0",
- "log-symbols": "^4.1.0",
- "minimatch": "^9.0.5",
- "ms": "^2.1.3",
- "picocolors": "^1.1.1",
- "serialize-javascript": "^6.0.2",
- "strip-json-comments": "^3.1.1",
- "supports-color": "^8.1.1",
- "workerpool": "^9.2.0",
- "yargs": "^17.7.2",
- "yargs-parser": "^21.1.1",
- "yargs-unparser": "^2.0.0"
- },
- "bin": {
- "_mocha": "bin/_mocha",
- "mocha": "bin/mocha.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/mocha-lcov-reporter": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz",
- "integrity": "sha512-/5zI2tW4lq/ft8MGpYQ1nIH6yePPtIzdGeUEwFMKfMRdLfAQ1QW2c68eEJop32tNdN5srHa/E2TzB+erm3YMYA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/mock-require": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz",
- "integrity": "sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-caller-file": "^1.0.2",
- "normalize-path": "^2.1.1"
- },
- "engines": {
- "node": ">=4.3.0"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/node-machine-id": {
- "version": "1.1.12",
- "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz",
- "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==",
- "license": "MIT"
- },
- "node_modules/node-preload": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
- "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "process-on-spawn": "^1.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.26",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz",
- "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/normalize-path": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
- "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "remove-trailing-separator": "^1.0.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/nyc": {
- "version": "17.1.0",
- "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz",
- "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "@istanbuljs/load-nyc-config": "^1.0.0",
- "@istanbuljs/schema": "^0.1.2",
- "caching-transform": "^4.0.0",
- "convert-source-map": "^1.7.0",
- "decamelize": "^1.2.0",
- "find-cache-dir": "^3.2.0",
- "find-up": "^4.1.0",
- "foreground-child": "^3.3.0",
- "get-package-type": "^0.1.0",
- "glob": "^7.1.6",
- "istanbul-lib-coverage": "^3.0.0",
- "istanbul-lib-hook": "^3.0.0",
- "istanbul-lib-instrument": "^6.0.2",
- "istanbul-lib-processinfo": "^2.0.2",
- "istanbul-lib-report": "^3.0.0",
- "istanbul-lib-source-maps": "^4.0.0",
- "istanbul-reports": "^3.0.2",
- "make-dir": "^3.0.0",
- "node-preload": "^0.2.1",
- "p-map": "^3.0.0",
- "process-on-spawn": "^1.0.0",
- "resolve-from": "^5.0.0",
- "rimraf": "^3.0.0",
- "signal-exit": "^3.0.2",
- "spawn-wrap": "^2.0.0",
- "test-exclude": "^6.0.0",
- "yargs": "^15.0.2"
- },
- "bin": {
- "nyc": "bin/nyc.js"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/nyc/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/nyc/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/nyc/node_modules/cliui": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
- "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^4.2.0",
- "strip-ansi": "^6.0.0",
- "wrap-ansi": "^6.2.0"
- }
- },
- "node_modules/nyc/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nyc/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/nyc/node_modules/get-caller-file": {
- "version": "2.0.5",
- "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.*"
- }
- },
- "node_modules/nyc/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",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/nyc/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/nyc/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/nyc/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/nyc/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/nyc/node_modules/signal-exit": {
- "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,
- "license": "ISC"
- },
- "node_modules/nyc/node_modules/string-width": {
- "version": "4.2.3",
- "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",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/nyc/node_modules/strip-ansi": {
- "version": "6.0.1",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/nyc/node_modules/wrap-ansi": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
- "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/nyc/node_modules/y18n": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
- "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/nyc/node_modules/yargs": {
- "version": "15.4.1",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
- "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cliui": "^6.0.0",
- "decamelize": "^1.2.0",
- "find-up": "^4.1.0",
- "get-caller-file": "^2.0.1",
- "require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
- "string-width": "^4.2.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^18.1.2"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/nyc/node_modules/yargs-parser": {
- "version": "18.1.3",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
- "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/open": {
- "version": "10.2.0",
- "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
- "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "default-browser": "^5.2.1",
- "define-lazy-prop": "^3.0.0",
- "is-inside-container": "^1.0.0",
- "wsl-utils": "^0.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "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/p-map": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
- "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "aggregate-error": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/package-hash": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz",
- "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "graceful-fs": "^4.1.15",
- "hasha": "^5.0.0",
- "lodash.flattendeep": "^4.4.0",
- "release-zalgo": "^1.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "dev": true,
- "license": "BlueOak-1.0.0"
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "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"
- }
- },
- "node_modules/path-is-absolute": {
- "version": "1.0.1",
- "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"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "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"
- }
- },
- "node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
- },
- "engines": {
- "node": ">=16 || 14 >=14.18"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/path-scurry/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/picocolors": {
- "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/pkg-dir": {
- "version": "4.2.0",
- "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"
- },
- "engines": {
- "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/process-on-spawn": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz",
- "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fromentries": "^1.2.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/punycode.js": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
- "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
- "node_modules/readdirp": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
- "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 14.18.0"
- },
- "funding": {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- },
- "node_modules/redis-errors": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
- "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/redis-parser": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
- "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
- "license": "MIT",
- "dependencies": {
- "redis-errors": "^1.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/reflect-metadata": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
- "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
- "license": "Apache-2.0"
- },
- "node_modules/release-zalgo": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
- "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "es6-error": "^4.0.1"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/remove-trailing-separator": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
- "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/require-main-filename": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
- "dev": true,
- "license": "ISC"
- },
- "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/rimraf": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
- "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
- "deprecated": "Rimraf versions prior to v4 are no longer supported",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "glob": "^7.1.3"
- },
- "bin": {
- "rimraf": "bin.js"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rimraf/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/rimraf/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",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/rimraf/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/run-applescript": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
- "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/safe-buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "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/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/serialize-javascript": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
- "node_modules/set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "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"
- }
- },
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/sinon": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz",
- "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "@sinonjs/commons": "^3.0.1",
- "@sinonjs/fake-timers": "^13.0.5",
- "@sinonjs/samsam": "^8.0.1",
- "diff": "^7.0.0",
- "supports-color": "^7.2.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/sinon"
- }
- },
- "node_modules/sinon/node_modules/supports-color": {
- "version": "7.2.0",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/source-map": {
- "version": "0.6.1",
- "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"
- }
- },
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "node_modules/spawn-wrap": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
- "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "foreground-child": "^2.0.0",
- "is-windows": "^1.0.2",
- "make-dir": "^3.0.0",
- "rimraf": "^3.0.0",
- "signal-exit": "^3.0.2",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/spawn-wrap/node_modules/foreground-child": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
- "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.0",
- "signal-exit": "^3.0.2"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/spawn-wrap/node_modules/signal-exit": {
- "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,
- "license": "ISC"
- },
- "node_modules/sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
- "dev": true,
- "license": "BSD-3-Clause"
- },
- "node_modules/standard-as-callback": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
- "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
- "license": "MIT"
- },
- "node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "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",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/string-width-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-bom": {
- "version": "4.0.0",
- "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"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "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"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/supports-color": {
- "version": "8.1.1",
- "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"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "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",
- "minimatch": "^3.0.4"
- },
- "engines": {
- "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,
+ "node_modules/redis-errors": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
+ "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
"license": "MIT",
- "dependencies": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "node_modules/test-exclude/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",
- "inherits": "2",
- "minimatch": "^3.1.1",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- },
- "engines": {
- "node": "*"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "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": ">=4"
}
},
- "node_modules/ts-node": {
- "version": "10.9.2",
- "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
- "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
- "dev": true,
+ "node_modules/redis-parser": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
+ "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
"license": "MIT",
"dependencies": {
- "@cspotcode/source-map-support": "^0.8.0",
- "@tsconfig/node10": "^1.0.7",
- "@tsconfig/node12": "^1.0.7",
- "@tsconfig/node14": "^1.0.0",
- "@tsconfig/node16": "^1.0.2",
- "acorn": "^8.4.1",
- "acorn-walk": "^8.1.1",
- "arg": "^4.1.0",
- "create-require": "^1.1.0",
- "diff": "^4.0.1",
- "make-error": "^1.1.1",
- "v8-compile-cache-lib": "^3.0.1",
- "yn": "3.1.1"
- },
- "bin": {
- "ts-node": "dist/bin.js",
- "ts-node-cwd": "dist/bin-cwd.js",
- "ts-node-esm": "dist/bin-esm.js",
- "ts-node-script": "dist/bin-script.js",
- "ts-node-transpile-only": "dist/bin-transpile.js",
- "ts-script": "dist/bin-script-deprecated.js"
- },
- "peerDependencies": {
- "@swc/core": ">=1.2.50",
- "@swc/wasm": ">=1.2.50",
- "@types/node": "*",
- "typescript": ">=2.7"
+ "redis-errors": "^1.0.0"
},
- "peerDependenciesMeta": {
- "@swc/core": {
- "optional": true
- },
- "@swc/wasm": {
- "optional": true
- }
- }
- },
- "node_modules/ts-node/node_modules/diff": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
- "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.3.1"
- }
- },
- "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,
- "license": "MIT",
"engines": {
"node": ">=4"
}
},
- "node_modules/type-fest": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
- "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "node_modules/remove-trailing-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
+ "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==",
"dev": true,
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=8"
- }
+ "license": "ISC"
+ },
+ "node_modules/standard-as-callback": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
+ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
+ "license": "MIT"
},
- "node_modules/typedarray-to-buffer": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
- "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "node_modules/tinypool": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz",
+ "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "is-typedarray": "^1.0.0"
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/typedoc": {
- "version": "0.28.14",
- "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.14.tgz",
- "integrity": "sha512-ftJYPvpVfQvFzpkoSfHLkJybdA/geDJ8BGQt/ZnkkhnBYoYW6lBgPQXu6vqLxO4X75dA55hX8Af847H5KXlEFA==",
+ "version": "0.28.20",
+ "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz",
+ "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@gerrit0/mini-shiki": "^3.12.0",
+ "@gerrit0/mini-shiki": "^3.23.0",
"lunr": "^2.3.9",
- "markdown-it": "^14.1.0",
- "minimatch": "^9.0.5",
- "yaml": "^2.8.1"
+ "markdown-it": "^14.3.0",
+ "minimatch": "^10.2.5",
+ "yaml": "^2.9.0"
},
"bin": {
"typedoc": "bin/typedoc"
@@ -3510,13 +1221,13 @@
"pnpm": ">= 10"
},
"peerDependencies": {
- "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x"
+ "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x"
}
},
"node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -3534,248 +1245,16 @@
"license": "MIT"
},
"node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
- "license": "MIT"
- },
- "node_modules/update-browserslist-db": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
- "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/uuid": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
- "dev": true,
- "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",
- "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/which": {
- "version": "2.0.2",
- "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"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/which-module": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
- "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/workerpool": {
- "version": "9.3.4",
- "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz",
- "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs": {
- "name": "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",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "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",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/wrappy": {
- "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": "3.0.3",
- "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
- "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "imurmurhash": "^0.1.4",
- "is-typedarray": "^1.0.0",
- "signal-exit": "^3.0.2",
- "typedarray-to-buffer": "^3.1.5"
- }
- },
- "node_modules/write-file-atomic/node_modules/signal-exit": {
- "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,
- "license": "ISC"
- },
- "node_modules/wsl-utils": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
- "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-wsl": "^3.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/y18n": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/yaml": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
- "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
+ "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -3783,155 +1262,9 @@
},
"engines": {
"node": ">= 14.6"
- }
- },
- "node_modules/yargs": {
- "version": "17.7.2",
- "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",
- "get-caller-file": "^2.0.5",
- "require-directory": "^2.1.1",
- "string-width": "^4.2.3",
- "y18n": "^5.0.5",
- "yargs-parser": "^21.1.1"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/yargs-parser": {
- "version": "21.1.1",
- "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"
- }
- },
- "node_modules/yargs-unparser": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
- "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "camelcase": "^6.0.0",
- "decamelize": "^4.0.0",
- "flat": "^5.0.2",
- "is-plain-obj": "^2.1.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/yargs-unparser/node_modules/camelcase": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
- "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/yargs-unparser/node_modules/decamelize": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
- "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/yargs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/yargs/node_modules/get-caller-file": {
- "version": "2.0.5",
- "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.*"
- }
- },
- "node_modules/yargs/node_modules/string-width": {
- "version": "4.2.3",
- "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",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yargs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "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"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/yn": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
- "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "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"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://github.com/sponsors/eemeli"
}
}
}
diff --git a/package.json b/package.json
index fa8ff74..1d64de0 100644
--- a/package.json
+++ b/package.json
@@ -13,10 +13,18 @@
"message-queue"
],
"scripts": {
- "prepublishOnly": "./node_modules/.bin/tsc",
- "test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov",
- "test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"",
- "test-local": "export COVERALLS_REPO_TOKEN=$IMQ_RPC_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"import('open').then(open => open.default('https://coveralls.io/github/imqueue/imq-rpc', { wait: false }));\"",
+ "clean-compiled": "npm run clean-js && npm run clean-typedefs && npm run clean-maps",
+ "build:deps": "if [ -f ../core/package.json ]; then npm run build --prefix ../core; fi",
+ "build": "npm run build:deps && npm run clean-compiled && tsc",
+ "prepublishOnly": "npm run build",
+ "lint": "oxlint",
+ "format": "oxfmt \"index.ts\" \"debug.ts\" \"src/**/*.ts\" \"test/**/*.ts\"",
+ "format:check": "oxfmt --check \"index.ts\" \"debug.ts\" \"src/**/*.ts\" \"test/**/*.ts\"",
+ "test": "npm run build && node --test --test-timeout=15000 $(find test -name '*.spec.js')",
+ "test-coverage": "npm run build && node --enable-source-maps --test --experimental-test-coverage --test-timeout=15000 $(find test -name '*.spec.js')",
+ "test-lcov": "npm run build && mkdir -p coverage && node --enable-source-maps --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-timeout=15000 $(find test -name '*.spec.js'); node scripts/strip-comment-coverage.mjs coverage/lcov.info",
+ "test-coverage-html": "npm run test-lcov; genhtml coverage/lcov.info --output-directory coverage/html --ignore-errors inconsistent,corrupt,format,mismatch && echo \"Coverage report: file://$(pwd)/coverage/html/index.html\"",
+ "test-dev": "npm run test && npm run clean-js && npm run clean-typedefs && npm run clean-maps",
"clean-typedefs": "find . -name '*.d.ts' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete",
"clean-maps": "find . -name '*.js.map' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete",
"clean-js": "find . -name '*.js' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete",
@@ -24,7 +32,7 @@
"clean-doc": "rm -rf docs",
"clean-benchmark": "rm -rf benchmark-result",
"clean": "npm run clean-tests && npm run clean-typedefs && npm run clean-maps && npm run clean-js && npm run clean-doc && npm run clean-benchmark",
- "doc": "rm -rf docs && typedoc --excludePrivate --excludeExternals --hideGenerator --exclude \"**/+(debug|test|node_modules|docs|coverage|benchmark|.nyc_output)/**/*\" --out ./docs . && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/docs/index.html', { wait: false }));\""
+ "doc": "typedoc && echo \"Docs generated: file://$(pwd)/docs/index.html\""
},
"repository": {
"type": "git",
@@ -37,62 +45,17 @@
"author": "imqueue.com (https://imqueue.com)",
"license": "GPL-3.0-only",
"dependencies": {
- "@imqueue/core": "^2.0.26",
- "@types/node": "^24.9.1",
- "acorn": "^8.15.0",
- "farmhash": "^5.0.1",
- "node-machine-id": "^1.1.12",
- "reflect-metadata": "^0.2.2",
- "typescript": "^5.9.3"
+ "@imqueue/core": "^3.0.0",
+ "typescript": "^6.0.3"
},
"devDependencies": {
- "@types/chai": "^5.2.2",
- "@types/mocha": "^10.0.10",
"@types/mock-require": "^3.0.0",
- "@types/sinon": "^17.0.4",
- "chai": "^6.0.1",
- "coveralls-next": "^5.0.0",
- "minimist": "^1.2.8",
- "mocha": "^11.7.2",
- "mocha-lcov-reporter": "^1.3.0",
+ "@types/node": "^24.9.1",
"mock-require": "^3.0.3",
- "nyc": "^17.1.0",
- "open": "^10.1.2",
- "sinon": "^21.0.0",
- "source-map-support": "^0.5.21",
- "ts-node": "^10.9.2",
- "typedoc": "^0.28.12"
+ "oxfmt": "0.57.0",
+ "oxlint": "1.72.0",
+ "typedoc": "^0.28.20"
},
"main": "index.js",
- "typescript": {
- "definitions": "index.d.ts"
- },
- "mocha": {
- "require": [
- "ts-node/register",
- "source-map-support/register"
- ],
- "recursive": true,
- "bail": true,
- "full-trace": true
- },
- "nyc": {
- "check-coverage": false,
- "extension": [
- ".ts"
- ],
- "exclude": [
- "**/*.d.ts",
- "**/test/**"
- ],
- "require": [
- "ts-node/register"
- ],
- "reporter": [
- "html",
- "text",
- "text-summary",
- "lcovonly"
- ]
- }
+ "types": "index.d.ts"
}
diff --git a/scripts/strip-comment-coverage.mjs b/scripts/strip-comment-coverage.mjs
new file mode 100644
index 0000000..8aa52ec
--- /dev/null
+++ b/scripts/strip-comment-coverage.mjs
@@ -0,0 +1,464 @@
+/*!
+ * Coverage post-processor
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+
+/**
+ * Node's built-in coverage (`--experimental-test-coverage --enable-source-maps`)
+ * reports every non-code line of a `.ts` source as uncovered, because comment
+ * and blank lines have no source-map mappings and therefore fall outside every
+ * covered V8 byte-range. That paints the license header and each JSDoc block
+ * red in the HTML report and pushes per-file line% far below reality.
+ *
+ * Node's reporter also emits records that the (strict) genhtml LCOV 2.x tool
+ * rejects — function entries with `undefined` line numbers or synthetic,
+ * duplicated names (``, and decorated methods all
+ * mis-named `has`/`get`), plus `BRDA:undefined,...` branch records. Left in,
+ * they produce a wall of warnings and a fatal "unexpected category" error.
+ *
+ * This script rewrites an lcov file in place: it drops the `DA`/`BRDA` records
+ * for lines that contain no executable code (comments and blank lines), drops
+ * all function records and malformed branch records, then recomputes the
+ * `LF`/`LH`/`BRF`/`BRH` totals. It uses the TypeScript scanner (already a dev
+ * dependency) to locate comments reliably — comment markers inside strings,
+ * template literals and regexes are not misdetected. The result renders in
+ * genhtml with accurate line + branch coverage and no warnings or errors.
+ */
+import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import ts from 'typescript';
+
+const lcovPath = process.argv[2] || 'coverage/lcov.info';
+
+if (!existsSync(lcovPath)) {
+ console.warn(`strip-comment-coverage: ${lcovPath} not found, skipping`);
+ process.exit(0);
+}
+
+/**
+ * Returns the set of 1-based line numbers in the given source that hold no
+ * executable code — i.e. blank lines and lines whose only content is a comment.
+ *
+ * @param {string} text
+ * @returns {Set}
+ */
+function nonCodeLines(text) {
+ const commentChars = new Uint8Array(text.length);
+ // Collect every comment range from the parsed tree. A standalone
+ // ts.createScanner is unreliable here — without parser context it can
+ // mis-scan a stray `/` as a regex literal and swallow the comments inside
+ // it — so walk all tokens and take their leading/trailing comment trivia,
+ // which every comment in the file belongs to.
+ const sf = ts.createSourceFile('x.ts', text, ts.ScriptTarget.Latest, true);
+ const seen = new Set();
+ const mark = pos => {
+ for (const get of [
+ ts.getLeadingCommentRanges,
+ ts.getTrailingCommentRanges,
+ ]) {
+ for (const range of get(text, pos) || []) {
+ const key = `${range.pos}:${range.end}`;
+
+ if (seen.has(key)) {
+ continue;
+ }
+
+ seen.add(key);
+
+ for (let i = range.pos; i < range.end; i++) {
+ commentChars[i] = 1;
+ }
+ }
+ }
+ };
+ const visit = node => {
+ mark(node.getFullStart());
+ node.getChildren(sf).forEach(visit);
+ };
+
+ visit(sf);
+
+ const lines = text.split('\n');
+ const result = new Set();
+ let pos = 0;
+
+ for (let ln = 0; ln < lines.length; ln++) {
+ const line = lines[ln];
+ let hasCode = false;
+
+ for (let i = 0; i < line.length; i++) {
+ const ch = line[i];
+
+ if (ch === ' ' || ch === '\t' || ch === '\r') {
+ continue;
+ }
+
+ if (!commentChars[pos + i]) {
+ hasCode = true;
+ break;
+ }
+ }
+
+ if (!hasCode) {
+ result.add(ln + 1);
+ }
+
+ pos += line.length + 1; // account for the split '\n'
+ }
+
+ return result;
+}
+
+/**
+ * Returns the set of 1-based lines that carry no coverable executable code of
+ * their own yet Node's source-mapped coverage mis-reports as uncovered
+ * (the mapping lands outside the covered byte-range, especially under
+ * `nodenext` output). These are:
+ * - imports (compile to a `require` that always runs on load);
+ * - module-scope variable declarations (`const`/`let`/`var`) — the whole
+ * statement if its initializer has no function body, otherwise just the
+ * declaration line so real bodies keep their coverage;
+ * - class declaration headers and type-only property fields (no initializer).
+ * Method/accessor/constructor bodies are never touched, so genuinely unreached
+ * code still shows up.
+ *
+ * @param {string} text
+ * @returns {Set}
+ */
+function importLines(text) {
+ const sf = ts.createSourceFile(
+ 'x.ts',
+ text,
+ ts.ScriptTarget.Latest,
+ false,
+ );
+ const result = new Set();
+ const lineAt = pos => sf.getLineAndCharacterOfPosition(pos).line;
+ const addRange = (start, end) => {
+ for (let line = lineAt(start); line <= lineAt(end); line++) {
+ result.add(line + 1);
+ }
+ };
+ const hasFunction = node => {
+ let found = false;
+ const walk = n => {
+ if (found) {
+ return;
+ }
+
+ if (
+ ts.isFunctionExpression(n) ||
+ ts.isArrowFunction(n) ||
+ ts.isFunctionDeclaration(n) ||
+ ts.isMethodDeclaration(n)
+ ) {
+ found = true;
+
+ return;
+ }
+
+ n.forEachChild(walk);
+ };
+
+ walk(node);
+
+ return found;
+ };
+
+ for (const stmt of sf.statements) {
+ if (
+ ts.isImportDeclaration(stmt) ||
+ ts.isImportEqualsDeclaration(stmt)
+ ) {
+ addRange(stmt.getStart(sf), stmt.getEnd());
+ } else if (ts.isVariableStatement(stmt)) {
+ if (hasFunction(stmt)) {
+ result.add(lineAt(stmt.getStart(sf)) + 1);
+ } else {
+ addRange(stmt.getStart(sf), stmt.getEnd());
+ }
+ } else if (ts.isClassDeclaration(stmt)) {
+ // class header (`class X extends Y {`): everything up to the first
+ // member, or the whole thing for an empty class
+ const headerEnd = stmt.members.length
+ ? stmt.members[0].getStart(sf)
+ : stmt.getEnd();
+
+ addRange(stmt.getStart(sf), headerEnd);
+ // the closing brace line emits no coverable code either
+ result.add(lineAt(stmt.getEnd()) + 1);
+
+ // type-only fields (definite-assignment / uninitialized) emit no JS
+ for (const member of stmt.members) {
+ if (
+ ts.isPropertyDeclaration(member) &&
+ !member.initializer
+ ) {
+ addRange(member.getStart(sf), member.getEnd());
+ }
+ }
+ }
+ }
+
+ return result;
+}
+
+const B64 =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+const B64MAP = new Map([...B64].map((c, i) => [c, i]));
+
+/**
+ * Decodes one base64 VLQ source-map segment into its integer fields.
+ *
+ * @param {string} seg
+ * @returns {number[]}
+ */
+function decodeVLQ(seg) {
+ const out = [];
+ let shift = 0;
+ let value = 0;
+
+ for (const ch of seg) {
+ const digit = B64MAP.get(ch);
+
+ if (digit === undefined) {
+ break;
+ }
+
+ value += (digit & 31) << shift;
+
+ if (digit & 32) {
+ shift += 5;
+ } else {
+ out.push(value & 1 ? -(value >> 1) : value >> 1);
+ value = 0;
+ shift = 0;
+ }
+ }
+
+ return out;
+}
+
+/**
+ * Returns the set of 1-based source lines that produced generated JavaScript,
+ * read from the file's `.js.map`. These are exactly the coverable lines — every
+ * other line (comments, blanks, type-only declarations, interfaces) emits no
+ * code and can never be executed. Returns null when no source map is present.
+ *
+ * @param {string} sourceFile - path of the .ts source (e.g. src/RedisQueue.ts)
+ * @returns {Set | null}
+ */
+function emittedLines(sourceFile) {
+ const mapPath = sourceFile.replace(/\.ts$/, '.js.map');
+
+ if (!existsSync(mapPath)) {
+ return null;
+ }
+
+ let map;
+
+ try {
+ map = JSON.parse(readFileSync(mapPath, 'utf8'));
+ } catch {
+ return null;
+ }
+
+ if (typeof map.mappings !== 'string') {
+ return null;
+ }
+
+ const lines = new Set();
+ let srcIndex = 0;
+ let srcLine = 0;
+
+ for (const group of map.mappings.split(';')) {
+ for (const seg of group.split(',')) {
+ if (!seg) {
+ continue;
+ }
+
+ const fields = decodeVLQ(seg);
+
+ // [genCol, srcIndex, srcLine, srcCol, nameIndex]; <4 fields = no
+ // source position for this segment
+ if (fields.length >= 4) {
+ srcIndex += fields[1];
+ srcLine += fields[2];
+
+ if (srcIndex === 0) {
+ lines.add(srcLine + 1); // map lines are 0-based
+ }
+ }
+ }
+ }
+
+ return lines;
+}
+
+const coverableCache = new Map();
+
+/**
+ * Resolves how to filter a source file's line records:
+ * - `{ mode: 'keep', set }` — keep only lines in `set` (source-map driven);
+ * - `{ mode: 'drop', set }` — drop lines in `set` (comment-scanner fallback).
+ *
+ * @param {string} sourceFile
+ * @returns {{ mode: 'keep' | 'drop', set: Set }}
+ */
+function coverableInfo(sourceFile) {
+ if (coverableCache.has(sourceFile)) {
+ return coverableCache.get(sourceFile);
+ }
+
+ const emitted = sourceFile ? emittedLines(sourceFile) : null;
+ let info;
+
+ if (emitted && existsSync(sourceFile)) {
+ // A line is coverable only if it emitted JS (has a source-map mapping)
+ // AND is not a comment/blank AND is not an import statement. The comment
+ // check is required because `removeComments: false` keeps license/JSDoc
+ // comments in the output, so they carry mappings too; the mapping check
+ // drops type-only lines (interfaces, type aliases) which emit nothing;
+ // the import check drops the mis-mapped import artifact.
+ const text = readFileSync(sourceFile, 'utf8');
+ const nonCode = nonCodeLines(text);
+ const imports = importLines(text);
+ const keep = new Set(
+ [...emitted].filter(
+ line => !nonCode.has(line) && !imports.has(line),
+ ),
+ );
+ info = { mode: 'keep', set: keep };
+ } else if (sourceFile && existsSync(sourceFile)) {
+ const text = readFileSync(sourceFile, 'utf8');
+ const skip = nonCodeLines(text);
+
+ for (const line of importLines(text)) {
+ skip.add(line);
+ }
+
+ info = { mode: 'drop', set: skip };
+ } else {
+ info = { mode: 'drop', set: new Set() };
+ }
+
+ coverableCache.set(sourceFile, info);
+
+ return info;
+}
+
+let strippedLines = 0;
+
+/**
+ * Rewrites a single lcov record so that genhtml accepts it without warnings or
+ * errors. It:
+ * - drops DA/BRDA entries on non-code (comment/blank) lines;
+ * - drops all function records (FN/FNDA/FNF/FNH) — Node's source-mapped
+ * function detection is unreliable here (undefined line numbers, mis-named
+ * and duplicated entries), which genhtml rejects, so line + branch coverage
+ * is kept instead;
+ * - drops malformed branch records such as `BRDA:undefined,...`;
+ * - recomputes the LF/LH/BRF/BRH totals.
+ *
+ * @param {string[]} recordLines - record body, excluding the end_of_record line
+ * @returns {string}
+ */
+function processRecord(recordLines) {
+ const sfLine = recordLines.find(line => line.startsWith('SF:'));
+ const sourceFile = sfLine ? sfLine.slice(3).trim() : '';
+ const info = coverableInfo(sourceFile);
+
+ // true when a source line produces no executable code and must be dropped
+ const drop = n => (info.mode === 'keep' ? !info.set.has(n) : info.set.has(n));
+
+ let lf = 0;
+ let lh = 0;
+ let brf = 0;
+ let brh = 0;
+
+ const kept = recordLines.filter(line => {
+ // drop all function records — unreliable under source maps
+ if (/^(FN:|FNDA:|FNF:|FNH:)/.test(line)) {
+ return false;
+ }
+
+ const da = line.match(/^DA:(\d+),(\d+)/);
+
+ if (da) {
+ if (drop(+da[1])) {
+ strippedLines++;
+
+ return false;
+ }
+
+ lf++;
+
+ if (+da[2] > 0) {
+ lh++;
+ }
+
+ return true;
+ }
+
+ if (line.startsWith('BRDA:')) {
+ const brda = line.match(/^BRDA:(\d+),\d+,\d+,(\d+|-)$/);
+
+ // drop malformed (e.g. BRDA:undefined,...) or non-code branches
+ if (!brda || drop(+brda[1])) {
+ return false;
+ }
+
+ brf++;
+
+ if (brda[2] !== '-' && +brda[2] > 0) {
+ brh++;
+ }
+
+ return true;
+ }
+
+ // drop the old totals — they are recomputed below
+ return !/^(LF|LH|BRF|BRH):/.test(line);
+ });
+
+ return (
+ `${kept.join('\n')}\n` +
+ `LF:${lf}\nLH:${lh}\nBRF:${brf}\nBRH:${brh}\nend_of_record\n`
+ );
+}
+
+const output = [];
+let record = [];
+
+for (const line of readFileSync(lcovPath, 'utf8').split('\n')) {
+ if (line === 'end_of_record') {
+ output.push(processRecord(record));
+ record = [];
+ } else if (line !== '') {
+ record.push(line);
+ }
+}
+
+writeFileSync(lcovPath, output.join(''));
+console.info(
+ `strip-comment-coverage: removed ${strippedLines} comment/blank line ` +
+ `entries from ${lcovPath}`,
+);
diff --git a/src/IMQCache.ts b/src/IMQCache.ts
index 72a5cbd..792b1ad 100644
--- a/src/IMQCache.ts
+++ b/src/IMQCache.ts
@@ -27,38 +27,35 @@ import { ICache, ICacheAdapter, ICacheConstructor } from '.';
* Generic cache registry
*/
export class IMQCache {
-
private static options: { [name: string]: any } = {};
public static adapters: { [name: string]: ICache } = {};
/**
- * Registers given cache adapter
+ * Registers the given cache adapter.
*
- * @param { ICache | string} adapter - adapter name or class or instance
- * @param {any} options - adapter specific options
+ * @param {ICacheAdapter | string} adapter - adapter name, class or
+ * instance
+ * @param {any} [options] - adapter-specific options
* @returns {IMQCache}
*/
- public static register(adapter: ICacheAdapter | string, options?: any) {
+ public static register(
+ adapter: ICacheAdapter | string,
+ options?: any,
+ ): typeof IMQCache {
const self = IMQCache;
if (typeof adapter === 'string') {
- // istanbul ignore else
if (!self.adapters[adapter]) {
- self.adapters[adapter] = new (require(
- `${__dirname}/cache/${adapter}.js`
- )[adapter])();
+ self.adapters[adapter] = (
+ new (require(`${__dirname}/cache/${adapter}.js`)[adapter])()
+ );
}
- }
-
- else {
- // istanbul ignore else
+ } else {
if (!self.adapters[(adapter as ICacheConstructor).name]) {
if (typeof adapter === 'function') {
self.adapters[(adapter as ICacheConstructor).name] =
- new (adapter)();
- }
-
- else {
+ new (adapter as ICacheConstructor)();
+ } else {
self.adapters[(adapter as ICache).name] =
adapter as any as ICache;
}
@@ -71,22 +68,24 @@ export class IMQCache {
}
/**
- * Overrides existing adapter options with the given
+ * Overrides existing adapter options with the given ones.
*
- * @param {ICache | string} adapter - adapter to apply options to
- * @param {any} options - adapter specific options
+ * @param {ICacheAdapter | string} adapter - adapter to apply options to
+ * @param {any} options - adapter-specific options
* @returns {IMQCache}
*/
- public static apply(adapter: ICacheAdapter | string, options: any) {
+ public static apply(
+ adapter: ICacheAdapter | string,
+ options: any,
+ ): typeof IMQCache {
const self = IMQCache;
if (!options) {
return self;
}
- const name = typeof adapter === 'string'
- ? adapter
- : (adapter as ICache).name;
+ const name =
+ typeof adapter === 'string' ? adapter : (adapter as ICache).name;
let opts = self.options[name] || {};
@@ -96,21 +95,18 @@ export class IMQCache {
}
/**
- * Initializes all registered cache adapters
+ * Initializes all registered cache adapters.
*
* @returns {Promise}
*/
- public static async init() {
- const self: any = IMQCache;
+ public static async init(): Promise {
+ const self = IMQCache;
const promises = [];
for (let adapter of Object.keys(self.adapters)) {
- // istanbul ignore else
if (!self.adapters[adapter].ready) {
promises.push(
- self.adapters[adapter].init(
- self.options[adapter]
- )
+ self.adapters[adapter].init(self.options[adapter]),
);
}
}
@@ -121,17 +117,14 @@ export class IMQCache {
}
/**
- * Returns registered cache adapter by its given name or class
+ * Returns a registered cache adapter by its given name or class.
*
- * @param { ICache | string} adapter - adapter name or class
+ * @param {ICacheAdapter} adapter - adapter name, class or instance
* @returns {ICache} - adapter instance
*/
public static get(adapter: ICacheAdapter): ICache {
return IMQCache.adapters[
- typeof adapter === 'string'
- ? adapter
- : (adapter as ICache).name
+ typeof adapter === 'string' ? adapter : (adapter as ICache).name
];
}
-
}
diff --git a/src/IMQClient.ts b/src/IMQClient.ts
index c93d64e..095900e 100644
--- a/src/IMQClient.ts
+++ b/src/IMQClient.ts
@@ -29,40 +29,42 @@ import IMQ, {
IMQ_SHUTDOWN_TIMEOUT,
} from '@imqueue/core';
import {
- pid,
- forgetPid,
- osUuid,
DEFAULT_IMQ_CLIENT_OPTIONS,
IMQClientOptions,
IMQRPCResponse,
IMQRPCRequest,
IMQDelay,
+ IMQError,
remote,
Description,
- fileExists,
- mkdir,
- writeFile,
IMQMetadata,
BEFORE_HOOK_ERROR,
AFTER_HOOK_ERROR,
- SIGNALS,
} from '.';
-import * as ts from 'typescript';
-import { EventEmitter } from 'events';
-import * as vm from 'vm';
-import { CompilerOptions } from 'typescript';
+import {
+ pid,
+ forgetPid,
+ osUuid,
+ fileExists,
+ mkdir,
+ writeFile,
+ SIGNALS,
+} from './helpers';
+import { transpile, type CompilerOptions } from 'typescript';
+import { EventEmitter } from 'node:events';
+import { Script } from 'node:vm';
import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions';
-process.setMaxListeners(10000);
-
-const tsOptions = require('../tsconfig.json').compilerOptions;
+// Loaded via require (not a static import) so that consumers which type-check
+// this source through a file: link do not need `resolveJsonModule` enabled.
+const tsOptions = require('../tsconfig.json')
+ .compilerOptions as CompilerOptions;
const RX_SEMICOLON: RegExp = /;+$/g;
/**
* Class IMQClient - base abstract class for service clients.
*/
export abstract class IMQClient extends EventEmitter {
-
public readonly options: IMQClientOptions;
public readonly id: number;
public readonly name: string;
@@ -73,18 +75,23 @@ export abstract class IMQClient extends EventEmitter {
private readonly baseName: string;
private readonly imq: IMessageQueue;
private readonly subscriptionImq: IMessageQueue;
- private static singleImq: IMessageQueue & { name?: string};
+ private static singleImq?: IMessageQueue & { name?: string };
+ private static singleImqRefs: number = 0;
+ private static maxListenersBumped: boolean = false;
+ private destroyed: boolean = false;
+ private readonly signalHandlers: Array<[string, (...args: any[]) => void]> =
+ [];
private readonly logger: ILogger;
- private resolvers: { [id: string]: [
- (data: AnyJson, res: IMQRPCResponse) => void,
- (err: any, res: IMQRPCResponse) => void],
+ private resolvers: {
+ [id: string]: [
+ (data: AnyJson, res: IMQRPCResponse) => void,
+ (err: any, res: IMQRPCResponse) => void,
+ ];
} = {};
- // noinspection TypeScriptAbstractClassConstructorCanBeMadeProtected
/**
* Class constructor
*
- * @constructor
* @param {Partial} options
* @param {string} serviceName
* @param {string} name
@@ -92,7 +99,7 @@ export abstract class IMQClient extends EventEmitter {
public constructor(
options?: Partial,
serviceName?: string,
- name?: string
+ name?: string,
) {
super();
@@ -101,26 +108,40 @@ export abstract class IMQClient extends EventEmitter {
this.baseName = baseName;
if (this.constructor.name === 'IMQClient') {
- throw new TypeError('IMQClient class is abstract and cannot ' +
- 'be instantiated directly!');
+ throw new TypeError(
+ 'IMQClient class is abstract and cannot ' +
+ 'be instantiated directly!',
+ );
}
this.options = { ...DEFAULT_IMQ_CLIENT_OPTIONS, ...options };
this.id = pid(baseName);
this.logger = this.options.logger || /* istanbul ignore next */ console;
- this.hostName = IMQClient.singleImq?.name ||
- `${osUuid()}-${this.id}:client`;
+ this.hostName =
+ IMQClient.singleImq?.name || `${osUuid()}-${this.id}:client`;
this.name = `${baseName}-${this.hostName}`;
this.serviceName = serviceName || baseName.replace(/Client$/, '');
this.queueName = this.options.singleQueue ? this.hostName : this.name;
this.imq = this.createImq();
this.subscriptionImq = this.createSubscriptionImq();
- SIGNALS.forEach((signal: any) => process.on(signal, async () => {
- this.destroy().catch(this.logger.error);
- // istanbul ignore next
- setTimeout(() => process.exit(0), IMQ_SHUTDOWN_TIMEOUT);
- }));
+ // raise the process listener limit on first use (many clients may
+ // coexist, each registering its own signal handlers)
+ if (!IMQClient.maxListenersBumped) {
+ IMQClient.maxListenersBumped = true;
+ process.setMaxListeners(10000);
+ }
+
+ SIGNALS.forEach((signal: string) => {
+ const handler = (): void => {
+ this.destroy().catch(this.logger.error);
+ setTimeout(() => process.exit(0), IMQ_SHUTDOWN_TIMEOUT);
+ };
+
+ // tracked so destroy() can unregister them (see below)
+ this.signalHandlers.push([signal, handler]);
+ process.on(signal, handler);
+ });
}
private createImq(): IMessageQueue {
@@ -132,6 +153,10 @@ export abstract class IMQClient extends EventEmitter {
IMQClient.singleImq = IMQ.create(this.queueName, this.options);
}
+ // the shared queue is reference-counted so that destroying one client
+ // does not tear the transport down under the others
+ IMQClient.singleImqRefs++;
+
return IMQClient.singleImq;
}
@@ -146,7 +171,6 @@ export abstract class IMQClient extends EventEmitter {
/**
* Sends call to remote service method
*
- * @access protected
* @param {...any[]} args
* @template T
* @returns {Promise}
@@ -160,10 +184,8 @@ export abstract class IMQClient extends EventEmitter {
let metadata: IMQMetadata | undefined;
if (args[args.length - 1] instanceof IMQDelay) {
- // noinspection TypeScriptUnresolvedVariable
delay = args.pop().ms;
- // istanbul ignore if
if (!isFinite(delay) || isNaN(delay) || delay < 0) {
delay = 0;
}
@@ -192,24 +214,66 @@ export abstract class IMQClient extends EventEmitter {
}
}
- return new Promise(async (resolve, reject) => {
- try {
- const id = await this.imq.send(to, request, delay, reject);
-
- this.resolvers[id] = [
- imqCallResolver(resolve, request, this),
- imqCallRejector(reject, request, this),
- ];
- }
-
- catch (err) {
- // istanbul ignore next
- imqCallRejector(reject, request, this)(err);
- }
+ const callTimeout = this.options.callTimeout;
+
+ return new Promise((resolve, reject) => {
+ let timer: NodeJS.Timeout | null = null;
+ const clearTimer = (): void => {
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+ const doResolve = (data: T): void => {
+ clearTimer();
+ resolve(data);
+ };
+ const doReject = (err: any): void => {
+ clearTimer();
+ reject(err);
+ };
+
+ void (async () => {
+ try {
+ const id = await this.imq.send(
+ to,
+ request,
+ delay,
+ doReject,
+ );
+
+ this.resolvers[id] = [
+ imqCallResolver(doResolve, request, this),
+ imqCallRejector(doReject, request, this),
+ ];
+
+ if (callTimeout && callTimeout > 0) {
+ // reject and release the pending resolver if no
+ // response arrives in time; a requested delivery delay
+ // extends the budget accordingly
+ timer = setTimeout(() => {
+ delete this.resolvers[id];
+ doReject(
+ IMQError(
+ 'IMQ_RPC_CALL_TIMEOUT',
+ `Call to ${to}.${method}() timed out after ${
+ callTimeout
+ } ms.`,
+ new Error().stack,
+ method,
+ args,
+ ),
+ );
+ }, callTimeout + delay);
+ timer.unref?.();
+ }
+ } catch (err) {
+ imqCallRejector(doReject, request, this)(err);
+ }
+ })();
}) as Promise;
}
- // noinspection JSUnusedGlobalSymbols
/**
* Adds subscription to service event channel
*
@@ -220,7 +284,6 @@ export abstract class IMQClient extends EventEmitter {
return this.subscriptionImq.subscribe(this.serviceName, handler);
}
- // noinspection JSUnusedGlobalSymbols
/**
* Destroys subscription channel to service
*
@@ -230,7 +293,6 @@ export abstract class IMQClient extends EventEmitter {
return this.subscriptionImq.unsubscribe();
}
- // noinspection JSUnusedGlobalSymbols
/**
* Broadcasts given payload to all other service clients subscribed.
* So this is like client-to-clients publishing.
@@ -248,11 +310,10 @@ export abstract class IMQClient extends EventEmitter {
* @returns {Promise}
*/
public async start(): Promise {
- this.imq.on('message', (message: IMQRPCResponse) => {
+ this.imq.on('message', (message: any) => {
// the following condition below is hard to test with the
// current redis mock, BTW it was tested manually on real
// redis run
- // istanbul ignore if
if (!this.resolvers[message.to]) {
// when there is no resolvers it means
// we have message in queue which was initiated
@@ -262,7 +323,7 @@ export abstract class IMQClient extends EventEmitter {
this.emit(message.request.method, message);
}
- const [ resolve, reject ] = this.resolvers[message.to] || [];
+ const [resolve, reject] = this.resolvers[message.to] || [];
// make sure no memory leaking
delete this.resolvers[message.to];
@@ -279,7 +340,6 @@ export abstract class IMQClient extends EventEmitter {
}
}
- // noinspection JSUnusedGlobalSymbols
/**
* Stops client work
*
@@ -295,20 +355,56 @@ export abstract class IMQClient extends EventEmitter {
* @returns {Promise}
*/
public async destroy(): Promise {
- await this.imq.unsubscribe();
+ if (this.destroyed) {
+ return;
+ }
+
+ this.destroyed = true;
+
+ // unregister this instance's process signal handlers so destroyed
+ // clients do not leak process-level listeners
+ for (const [signal, handler] of this.signalHandlers) {
+ process.removeListener(signal, handler);
+ }
+ this.signalHandlers.length = 0;
+
+ await this.subscriptionImq.unsubscribe();
forgetPid(this.baseName, this.id, this.logger);
this.removeAllListeners();
- await this.imq.destroy();
+
+ // in singleQueue mode the subscription queue is a separate,
+ // per-client instance and must be torn down with the client
+ if (this.subscriptionImq !== this.imq) {
+ await this.subscriptionImq.destroy();
+ }
+
+ if (!this.options.singleQueue) {
+ await this.imq.destroy();
+
+ return;
+ }
+
+ // shared queue: only the last client tears it down
+ if (--IMQClient.singleImqRefs <= 0) {
+ IMQClient.singleImqRefs = 0;
+
+ const singleImq = IMQClient.singleImq;
+
+ IMQClient.singleImq = undefined;
+
+ await singleImq?.destroy();
+ }
}
/**
* Returns service description metadata.
*
- * @param {IMQDelay} delay
+ * @param {IMQDelay} [_delay] - optional delivery delay; forwarded to the
+ * service through `arguments` by the `@remote` decorator
* @returns {Promise}
*/
@remote()
- public async describe(delay?: IMQDelay): Promise {
+ public async describe(_delay?: IMQDelay): Promise {
return await this.remoteCall(...arguments);
}
@@ -321,7 +417,7 @@ export abstract class IMQClient extends EventEmitter {
*/
public static async create(
name: string,
- options?: Partial
+ options?: Partial,
): Promise {
const clientOptions: IMQClientOptions = {
...DEFAULT_IMQ_CLIENT_OPTIONS,
@@ -330,16 +426,16 @@ export abstract class IMQClient extends EventEmitter {
return await generator(name, clientOptions);
}
-
}
/**
- * Builds and returns call resolver, which supports after call optional hook
+ * Builds a call resolver that resolves the pending promise and then runs the
+ * optional after-call hook.
*
- * @param {(...args: any[]) => void} resolve - source promise like resolver
- * @param {IMQRPCRequest} req - request message
- * @param {IMQClient} client - imq client
- * @return {(data: any, res: IMQRPCResponse) => void} - hook-supported resolve
+ * @param {(data: any) => void} resolve - the underlying promise resolver
+ * @param {IMQRPCRequest} req - the originating request message
+ * @param {IMQClient} client - the client the call belongs to
+ * @return {(data: any, res: IMQRPCResponse) => void} - a hook-aware resolver
*/
export function imqCallResolver(
resolve: (data: any) => void,
@@ -357,28 +453,29 @@ export function imqCallResolver(
).bind(client);
try {
- await afterCall(req, res);
+ await afterCall(req, res as IMQRPCResponse);
} catch (err) {
logger.warn(AFTER_HOOK_ERROR, err);
}
}
- }
+ };
}
/**
- * Builds and returns call rejector, which supports after call optional hook
+ * Builds a call rejector that rejects the pending promise and then runs the
+ * optional after-call hook.
*
- * @param {(err: any) => void} reject - source promise like rejector
- * @param {IMQRPCRequest} req - call request
- * @param {IMQClient} client - imq client
- * @return {(err: any) => void} - hook-supported reject
+ * @param {(err: any) => void} reject - the underlying promise rejector
+ * @param {IMQRPCRequest} req - the originating request message
+ * @param {IMQClient} client - the client the call belongs to
+ * @return {(err: any, res?: IMQRPCResponse) => void} - a hook-aware rejector
*/
export function imqCallRejector(
reject: (err: any) => void,
req: IMQRPCRequest,
client: IMQClient,
): (err: any, res?: IMQRPCResponse) => void {
- return async (err: any, res: IMQRPCResponse) => {
+ return async (err: any, res?: IMQRPCResponse) => {
const logger = client.options.logger || console;
reject(err);
@@ -394,12 +491,11 @@ export function imqCallRejector(
logger.warn(AFTER_HOOK_ERROR, err);
}
}
- }
+ };
}
/**
* Class GeneratorClient - generator helper class implementation
- * @access private
*/
class GeneratorClient extends IMQClient {}
@@ -407,52 +503,58 @@ class GeneratorClient extends IMQClient {}
* Fetches and returns service description using the timeout (to handle
* situations when the service is not started)
*
- * @access private
* @param {string} name
* @param {IMQClientOptions} options
* @returns {Promise}
*/
async function getDescription(
name: string,
- options: IMQClientOptions
+ options: IMQClientOptions,
): Promise {
- return new Promise(async (resolve, reject) => {
- const client: any = new GeneratorClient(options, name, `${name}Client`);
- await client.start();
- const timeout = setTimeout(async () => {
- await client.destroy();
+ return new Promise((resolve, reject) => {
+ void (async () => {
+ const client: any = new GeneratorClient(
+ options,
+ name,
+ `${name}Client`,
+ );
+ await client.start();
+ const timeout = setTimeout(async () => {
+ await client.destroy();
+ timeout && clearTimeout(timeout);
+ reject(
+ new EvalError(
+ 'Generate client error: service remote ' +
+ `call timed-out! Is service "${name}" running?`,
+ ),
+ );
+ }, options.timeout);
+ const description = await client.describe();
timeout && clearTimeout(timeout);
- reject(new EvalError('Generate client error: service remote ' +
- `call timed-out! Is service "${name}" running?`));
- }, options.timeout);
- const description = await client.describe();
- timeout && clearTimeout(timeout);
- await client.destroy();
-
- resolve(description);
- }) as Promise;
+ await client.destroy();
+ resolve(description);
+ })();
+ }) as Promise;
}
-// codebeat:disable[LOC,ABC]
/**
* Client generator helper function
*
- * @access private
* @param {string} name
* @param {IMQClientOptions} options
* @returns {Promise}
*/
async function generator(
name: string,
- options: IMQClientOptions
+ options: IMQClientOptions,
): Promise {
const description: Description = await getDescription(name, options);
const serviceName = description.service.name;
const clientName = serviceName.replace(/Service$|$/, 'Client');
- const namespaceName = serviceName.charAt(0).toLowerCase() +
- serviceName.substr(1);
+ const namespaceName =
+ serviceName.charAt(0).toLowerCase() + serviceName.substr(1);
let src = `/*!
* IMQ-RPC Service Client: ${description.service.name}
@@ -527,21 +629,26 @@ export namespace ${namespaceName} {\n`;
const args = methods[methodName].arguments;
const description = methods[methodName].description;
const ret = methods[methodName].returns;
- const addArgs = [{
- description: 'if passed, will deliver given metadata to ' +
- 'service, and will initiate trace handler calls',
- name: 'imqMetadata',
- type: 'IMQMetadata',
- tsType: 'IMQMetadata',
- isOptional: true,
- }, {
- description: 'if passed the method will be called with ' +
- 'the specified delay over message queue',
- name: 'imqDelay',
- type: 'IMQDelay',
- tsType: 'IMQDelay',
- isOptional: true
- }];
+ const addArgs = [
+ {
+ description:
+ 'if passed, will deliver given metadata to ' +
+ 'service, and will initiate trace handler calls',
+ name: 'imqMetadata',
+ type: 'IMQMetadata',
+ tsType: 'IMQMetadata',
+ isOptional: true,
+ },
+ {
+ description:
+ 'if passed the method will be called with ' +
+ 'the specified delay over message queue',
+ name: 'imqDelay',
+ type: 'IMQDelay',
+ tsType: 'IMQDelay',
+ isOptional: true,
+ },
+ ];
let retType = ret.tsType.replace(/\r?\n/g, ' ').replace(/\s{2,}/g, ' ');
for (let i = 1; i <= 2; i++) {
@@ -554,21 +661,21 @@ export namespace ${namespaceName} {\n`;
args.push(...addArgs); // make sure client expect them
- // istanbul ignore if
if (retType === 'Promise') {
retType = 'Promise';
}
src += ' /**\n';
- // istanbul ignore next
- src += description ? description.split(/\r?\n/)
- .map(line => ` * ${line}`)
- .join('\n') + '\n *\n' : '';
+ src += description
+ ? description
+ .split(/\r?\n/)
+ .map(line => ` * ${line}`)
+ .join('\n') + '\n *\n'
+ : '';
for (let i = 0, s = args.length; i < s; i++) {
const arg = args[i];
- src += ` * @param {${
- toComment(arg.tsType)}} `;
+ src += ` * @param {${toComment(arg.tsType)}} `;
src += arg.isOptional ? `[${arg.name}]` : arg.name;
src += arg.description ? ' - ' + arg.description : '';
src += '\n';
@@ -582,15 +689,19 @@ export namespace ${namespaceName} {\n`;
for (let i = 0, s = args.length; i < s; i++) {
const arg = args[i];
- src += arg.name + (arg.isOptional ? '?' : '') +
- ': ' + arg.tsType.replace(/\s{2,}/g, ' ') +
- (i === s - 1 ? '': ', ');
+ src +=
+ arg.name +
+ (arg.isOptional ? '?' : '') +
+ ': ' +
+ arg.tsType.replace(/\s{2,}/g, ' ') +
+ (i === s - 1 ? '' : ', ');
}
src += `): ${promisedType(retType)} {\n`;
src += ' '.repeat(12);
- src += `return await this.remoteCall<${
- cleanType(retType)}>(...arguments);`;
+ src += `return await this.remoteCall<${cleanType(
+ retType,
+ )}>(...arguments);`;
src += '\n }\n\n';
}
@@ -600,18 +711,15 @@ export namespace ${namespaceName} {\n`;
return module ? module[namespaceName] : /* istanbul ignore next */ null;
}
-// codebeat:enable[LOC,ABC]
/**
- * Return promised typedef of a given type if its missing
+ * Return the promised typedef of a given type if its missing
*
- *c @access private
* @param {string} typedef
* @returns {string}
*/
-function promisedType(typedef: string) {
- // istanbul ignore next
- if (!/^Promise`;
}
@@ -621,18 +729,16 @@ function promisedType(typedef: string) {
/**
* Removes Promise from type definition if any
*
- * @access private
* @param {string} typedef
* @returns {string}
*/
-function cleanType(typedef: string) {
+function cleanType(typedef: string): string {
return typedef.replace(/^Promise<([\s\S]+?)>$/, '$1');
}
/**
* Type to comment
*
- * @access private
* @param {string} typedef
* @param {boolean} [promised]
* @returns {string}
@@ -642,8 +748,8 @@ function toComment(typedef: string, promised: boolean = false): string {
typedef = promisedType(typedef);
}
- // istanbul ignore next
- return typedef.split(/\r?\n/)
+ return typedef
+ .split(/\r?\n/)
.map((line, lineNum) => (lineNum ? ' * ' : '') + line)
.join('\n');
}
@@ -651,7 +757,6 @@ function toComment(typedef: string, promised: boolean = false): string {
/**
* Compiles client source code and returns loaded module
*
- * @access private
* @param {string} name
* @param {string} src
* @param {IMQClientOptions} options
@@ -665,31 +770,24 @@ async function compile(
const path = options.path;
const srcFile = `${path}/${name}.ts`;
const jsFile = `${path}/${name}.js`;
- const js = ts.transpile(src, tsOptions as CompilerOptions | undefined);
+ const js = transpile(src, tsOptions);
if (options.write) {
- // istanbul ignore else
- if (!await fileExists(path)) {
+ if (!(await fileExists(path))) {
await mkdir(path);
}
- await Promise.all([
- writeFile(srcFile, src),
- writeFile(jsFile, js),
- ]);
+ await Promise.all([writeFile(srcFile, src), writeFile(jsFile, js)]);
}
- // istanbul ignore else
if (options.compile) {
- const script = new vm.Script(js);
+ const script = new Script(js);
const context = { exports: {}, require };
script.runInNewContext(context, { filename: jsFile });
- return context.exports;
+ return context.exports;
}
- // istanbul ignore next
return null;
}
-
diff --git a/src/IMQDelay.ts b/src/IMQDelay.ts
index c5529d8..2c58f50 100644
--- a/src/IMQDelay.ts
+++ b/src/IMQDelay.ts
@@ -21,19 +21,38 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
+/**
+ * Represents a delay expressed as a numeric timer value in a given time unit.
+ * Used to defer IMQ request processing.
+ */
export class IMQDelay {
- public get ms() {
+ /**
+ * Returns this delay converted to milliseconds.
+ *
+ * @return {number}
+ */
+ public get ms(): number {
switch (this.unit) {
- case 'ms': return this.timer;
- case 's': return this.timer * 1000;
- case 'm': return this.timer * 60000;
- case 'h': return this.timer * 3600000;
- case 'd': return this.timer * 86400000;
+ case 'ms':
+ return this.timer;
+ case 's':
+ return this.timer * 1000;
+ case 'm':
+ return this.timer * 60000;
+ case 'h':
+ return this.timer * 3600000;
+ case 'd':
+ return this.timer * 86400000;
}
}
+ /**
+ * @param {number} timer - delay value expressed in the given unit
+ * @param {'ms' | 's' | 'm' | 'h' | 'd'} unit - time unit of the timer
+ * value, defaults to 'ms'
+ */
constructor(
public timer: number,
- public unit: 'ms' | 's' | 'm' | 'h' | 'd' = 'ms'
+ public unit: 'ms' | 's' | 'm' | 'h' | 'd' = 'ms',
) {}
}
diff --git a/src/IMQLock.ts b/src/IMQLock.ts
index 2a3d7f3..693b372 100644
--- a/src/IMQLock.ts
+++ b/src/IMQLock.ts
@@ -25,7 +25,7 @@ import { ILogger } from '@imqueue/core';
export type AcquiredLock = T | boolean;
export type IMQLockTask = [(...args: any[]) => any, (...args: any[]) => any];
-export type IMQLockQueue = Array;
+export type IMQLockQueue = Array;
export interface IMQLockMetadataItem {
className: string;
methodName: string | symbol;
@@ -47,11 +47,11 @@ export interface IMQLockMetadata {
* const lock: AcquiredLock = await IMQLock.acquire('doSomething');
*
* if (IMQLock.locked('doSomething')) {
- * // avoiding err handling in this way can cause ded-locks
- * // so it is good always try catch locked calls!
+ * // skipping error handling this way can cause dead-locks,
+ * // so it is always good to wrap locked calls in try/catch!
* // BTW, IMQLock uses timeouts to avoid dead-locks
* try {
- * // this code will be called only once per multiple async calls
+ * // this code is called only once across multiple async calls,
* // so all promises will be resolved with the same value
* const res = Math.random();
* IMQLock.release('doSomething', res);
@@ -77,7 +77,6 @@ export interface IMQLockMetadata {
* ~~~
*/
export class IMQLock {
-
private static acquiredLocks: { [key: string]: boolean } = {};
private static queues: { [key: string]: IMQLockQueue } = {};
private static metadata: IMQLockMetadata = {};
@@ -90,19 +89,20 @@ export class IMQLock {
public static deadlockTimeout: number = 10000;
/**
- * Logger used to log errors which appears during locked calls
+ * Logger used to log errors that appear during locked calls
*
* @type {ILogger}
*/
public static logger: ILogger = console;
/**
- * Acquires a lock for a given key
+ * Acquires a lock for a given key.
*
- * @param {string} key
- * @param {(...args: any[]) => any} [callback]
- * @param {IMQLockMetadataItem} [metadata]
- * @returns {AcquiredLock}
+ * @param {string} key - key to acquire the lock for
+ * @param {(...args: any[]) => any} [callback] - callback invoked on
+ * lock resolution
+ * @param {IMQLockMetadataItem} [metadata] - metadata for the locked call
+ * @returns {Promise>}
*/
public static async acquire(
key: string,
@@ -117,25 +117,26 @@ export class IMQLock {
if (IMQLock.locked(key)) {
return new Promise((resolve, reject) => {
- let timer: any = null;
+ let timer: NodeJS.Timeout | null = null;
- // istanbul ignore else
if (IMQLock.deadlockTimeout) {
// avoid dead-locks using timeouts
timer = setTimeout(() => {
let dumpStr = '';
try {
- dumpStr = JSON.stringify(IMQLock.metadata[key]);
- } catch (err) {
+ dumpStr = JSON.stringify(IMQLock.metadata[key]);
+ } catch {
dumpStr = 'Unable to stringify metadata';
}
- const err = new Error(`Lock timeout, "${
- key
- }" call rejected, metadata: ${ dumpStr }`);
+ const err = new Error(
+ `Lock timeout, "${
+ key
+ }" call rejected, metadata: ${dumpStr}`,
+ );
- clearTimeout(timer);
+ timer && clearTimeout(timer);
timer = null;
IMQLock.release(key, null, err);
@@ -143,30 +144,30 @@ export class IMQLock {
}
IMQLock.queues[key].push([
- // istanbul ignore next
- (result: any) => { // lock resolve
+ (result: any) => {
+ // lock resolve
try {
timer && clearTimeout(timer);
timer = null;
callback && callback(null, result);
+ } catch (err) {
+ IMQLock.logger.error(err);
}
- catch (err) { IMQLock.logger.error(err) }
-
resolve(result);
},
- // istanbul ignore next
- (err: any) => { // lock reject
+ (err: any) => {
+ // lock reject
try {
timer && clearTimeout(timer);
timer = null;
callback && callback(err);
+ } catch (e) {
+ err = e;
}
- catch (e) { err = e }
-
reject(err);
- }
+ },
]);
});
}
@@ -177,13 +178,14 @@ export class IMQLock {
}
/**
- * Releases previously acquired lock for a given key
+ * Releases a previously acquired lock for a given key.
*
- * @param {string} key
- * @param {T} value
- * @param {E} err
+ * @param {string} key - key to release the lock for
+ * @param {T} [value] - value to resolve pending calls with
+ * @param {E} [err] - error to reject pending calls with
+ * @returns {void}
*/
- public static release(key: string, value?: T, err?: E) {
+ public static release(key: string, value?: T, err?: E): void {
const queue: IMQLockQueue = IMQLock.queues[key];
IMQLock.queues[key] = [];
@@ -200,13 +202,12 @@ export class IMQLock {
}
/**
- * Returns true if given key is locked, false otherwise
+ * Returns true if the given key is locked, false otherwise.
*
- * @param {string} key
+ * @param {string} key - key to check the lock state for
* @returns {boolean}
*/
public static locked(key: string): boolean {
- // noinspection PointlessBooleanExpressionJS
return !!IMQLock.acquiredLocks[key];
}
}
diff --git a/src/IMQMetadata.ts b/src/IMQMetadata.ts
index ae90722..caa2c04 100644
--- a/src/IMQMetadata.ts
+++ b/src/IMQMetadata.ts
@@ -23,9 +23,17 @@
*/
import { AnyJson, JsonObject } from '@imqueue/core';
+/**
+ * Arbitrary, JSON-serializable metadata bag carried alongside an IMQ request.
+ * Each property value must be a valid JSON value.
+ */
export class IMQMetadata {
[property: string]: AnyJson;
+ /**
+ * @param {JsonObject} metadata - source object whose own enumerable
+ * properties are copied into this instance
+ */
constructor(metadata: JsonObject) {
for (const property of Object.keys(metadata)) {
this[property] = metadata[property];
diff --git a/src/IMQRPCDescription.ts b/src/IMQRPCDescription.ts
index 8bccaf8..52e532a 100644
--- a/src/IMQRPCDescription.ts
+++ b/src/IMQRPCDescription.ts
@@ -73,15 +73,15 @@ export interface ServiceDescription {
}
/**
- * Service type description
+ * Type property description
*/
export interface PropertyDescription {
type: string;
- isOptional: boolean
+ isOptional: boolean;
}
/**
- * Service types description
+ * Type properties collection description
*/
export interface TypeDescription {
[propertyName: string]: PropertyDescription;
@@ -92,15 +92,13 @@ export interface TypeDescription {
*/
export interface TypesDescription {
[typeName: string]: {
- properties: TypeDescription,
- inherits: string,
- indexType?: string,
+ properties: TypeDescription;
+ inherits: string;
+ indexType?: string;
};
}
export class IMQRPCDescription {
-
public static serviceDescription: ServiceDescription = {};
public static typesDescription: TypesDescription = {};
-
}
diff --git a/src/IMQRPCError.ts b/src/IMQRPCError.ts
index 68e40eb..db4d16e 100644
--- a/src/IMQRPCError.ts
+++ b/src/IMQRPCError.ts
@@ -39,16 +39,15 @@ export interface IMQRPCError extends JsonObject {
original?: any;
}
-// istanbul ignore next
/**
- * Builds JSON representation of IMQ Error
+ * Builds a JSON representation of an IMQ error.
*
- * @param {string} code - error code
+ * @param {string} code - error code
* @param {string} message - error message
- * @param {string} stack - error stack
- * @param {string} method - IMQ service method called, which produced an error
- * @param {any} args - IMQ service method call args passed
- * @param {any} [original] - Original error thrown (JSON'ified) if any
+ * @param {any} stack - error stack
+ * @param {any} method - IMQ service method that produced the error
+ * @param {unknown} args - arguments passed to the service method call
+ * @param {unknown} [original] - original error thrown (JSON-serialized), if any
* @return {IMQRPCError}
*/
export function IMQError(
@@ -56,8 +55,8 @@ export function IMQError(
message: string,
stack: any,
method: any,
- args: any,
- original?: any,
+ args: unknown,
+ original?: unknown,
): IMQRPCError {
return {
code,
@@ -68,7 +67,7 @@ export function IMQError(
original: (() => {
try {
return JSON.stringify(original);
- } catch (err) {
+ } catch {
return undefined;
}
})(),
diff --git a/src/IMQRPCOptions.ts b/src/IMQRPCOptions.ts
index 425f020..082f132 100644
--- a/src/IMQRPCOptions.ts
+++ b/src/IMQRPCOptions.ts
@@ -27,20 +27,40 @@ import { IMQRPCResponse } from './IMQRPCResponse';
import { IMQService } from './IMQService';
import { IMQClient } from './IMQClient';
-export interface IMQBeforeCall {
+/**
+ * Hook invoked before a service method call is dispatched.
+ *
+ * @param {IMQRPCRequest} [req] - the incoming request
+ * @param {IMQRPCResponse} [res] - the response being prepared
+ * @return {Promise}
+ */
+export interface IMQBeforeCall<_T> {
(req?: IMQRPCRequest, res?: IMQRPCResponse): Promise;
}
-export interface IMQAfterCall {
+/**
+ * Hook invoked after a service method call has been handled.
+ *
+ * @param {IMQRPCRequest} req - the handled request
+ * @param {IMQRPCResponse} [res] - the produced response
+ * @return {Promise}
+ */
+export interface IMQAfterCall<_T> {
(req: IMQRPCRequest, res?: IMQRPCResponse): Promise;
}
+/**
+ * Options for the built-in metrics server.
+ */
export interface IMQMetricsServerOptions {
enabled?: boolean;
port?: number;
- queueLengthFormatter?: (length: number, metricName: string) => string,
+ queueLengthFormatter?: (length: number, metricName: string) => string;
}
+/**
+ * Options accepted by an IMQ service.
+ */
export interface IMQServiceOptions extends IMQOptions {
multiProcess: boolean;
childrenPerCore: number;
@@ -49,11 +69,21 @@ export interface IMQServiceOptions extends IMQOptions {
afterCall?: IMQAfterCall;
}
+/**
+ * Options accepted by a generated IMQ client.
+ */
export interface IMQClientOptions extends IMQOptions {
path: string;
compile: boolean;
timeout: number;
write: boolean;
+ // Per-call timeout in milliseconds. When set to a positive number, every
+ // remote call that has not received a response within the given time (plus
+ // any requested IMQDelay) is rejected with an IMQ_RPC_CALL_TIMEOUT error
+ // and its pending resolver is released. When 0 or unset, calls wait
+ // indefinitely (a hung service keeps the caller's promise pending
+ // forever), so enabling it is recommended for production use.
+ callTimeout?: number;
beforeCall?: IMQBeforeCall;
afterCall?: IMQAfterCall;
singleQueue?: boolean;
@@ -73,18 +103,17 @@ export const DEFAULT_IMQ_SERVICE_OPTIONS: IMQServiceOptions = {
};
/**
- * Default metric server options
+ * Default metrics server options
*
* @type {NonNullable}
*/
-export const DEFAULT_IMQ_METRICS_SERVER_OPTIONS: NonNullable<
- IMQMetricsServerOptions
-> = {
- enabled: false,
- port: 9090,
- queueLengthFormatter: (length, metricName) =>
- `${ metricName }{} ${ length }`,
-};
+export const DEFAULT_IMQ_METRICS_SERVER_OPTIONS: NonNullable =
+ {
+ enabled: false,
+ port: 9090,
+ queueLengthFormatter: (length, metricName) =>
+ `${metricName}{} ${length}`,
+ };
/**
* Default client options
diff --git a/src/IMQRPCRequest.ts b/src/IMQRPCRequest.ts
index 36a64a8..9562fee 100644
--- a/src/IMQRPCRequest.ts
+++ b/src/IMQRPCRequest.ts
@@ -25,7 +25,7 @@ import { JsonObject } from '@imqueue/core';
import { IMQMetadata } from './IMQMetadata';
/**
- * Request message data structure expected to be handled by a service
+ * Request message data structure to be handled by a service.
*/
export interface IMQRPCRequest extends JsonObject {
from: string;
diff --git a/src/IMQRPCResponse.ts b/src/IMQRPCResponse.ts
index f24c493..3ab28e5 100644
--- a/src/IMQRPCResponse.ts
+++ b/src/IMQRPCResponse.ts
@@ -25,12 +25,12 @@ import { JsonObject } from '@imqueue/core';
import { IMQRPCError, IMQRPCRequest } from '.';
/**
- * Response message data structure, which service replies to handled
+ * Response message data structure that a service replies with to handled
* requests.
*/
export interface IMQRPCResponse extends JsonObject {
to: string;
data: JsonObject | null;
error: IMQRPCError | null;
- request: IMQRPCRequest
+ request: IMQRPCRequest;
}
diff --git a/src/IMQService.ts b/src/IMQService.ts
index 11d6c81..f8d8602 100644
--- a/src/IMQService.ts
+++ b/src/IMQService.ts
@@ -42,25 +42,28 @@ import {
DEFAULT_IMQ_SERVICE_OPTIONS,
AFTER_HOOK_ERROR,
BEFORE_HOOK_ERROR,
- SIGNALS, DEFAULT_IMQ_METRICS_SERVER_OPTIONS,
+ DEFAULT_IMQ_METRICS_SERVER_OPTIONS,
} from '.';
-import * as os from 'os';
+import { SIGNALS } from './helpers';
+import { cpus } from 'node:os';
+import cluster, { type Worker } from 'node:cluster';
import { ArgDescription } from './IMQRPCDescription';
import { IMQBeforeCall, IMQAfterCall } from './IMQRPCOptions';
import { runWithRequest } from './IMQRequestContext';
-import * as http from 'node:http';
-
-const cluster: any = require('cluster');
+import { createServer, type Server } from 'node:http';
export class Description {
- service: {
- name: string,
- methods: MethodsCollectionDescription
+ service!: {
+ name: string;
+ methods: MethodsCollectionDescription;
};
- types: TypesDescription;
+ types!: TypesDescription;
}
-const serviceDescriptions: Map = new Map();
+const serviceDescriptions: Map = new Map<
+ string,
+ Description
+>();
/**
* Returns collection of class methods metadata even those are inherited
@@ -94,11 +97,9 @@ function getClassMethods(className: string): MethodsCollectionDescription {
* @returns {boolean}
*/
function isValidArgsCount(argsInfo: ArgDescription[], args: any[]): boolean {
- // istanbul ignore next
- return (argsInfo.some(argInfo => argInfo.isOptional)
+ return argsInfo.some(argInfo => argInfo.isOptional)
? argsInfo.length >= args.length
- : argsInfo.length === args.length
- );
+ : argsInfo.length === args.length;
}
/**
@@ -106,22 +107,21 @@ function isValidArgsCount(argsInfo: ArgDescription[], args: any[]): boolean {
* Basic abstract service (server-side) implementation
*/
export abstract class IMQService {
-
[property: string]: any;
protected imq: IMessageQueue;
protected logger: ILogger;
- protected cache: ICache;
- protected metricsServer?: http.Server;
+ protected cache!: ICache;
+ protected metricsServer?: Server;
+ private readonly signalHandlers: Array<[string, (...args: any[]) => void]> =
+ [];
public name: string;
public options: IMQServiceOptions;
- // noinspection TypeScriptAbstractClassConstructorCanBeMadeProtected
/**
* Class constructor
*
- * @constructor
* @param {Partial} options
* @param {string} [name]
*/
@@ -129,8 +129,10 @@ export abstract class IMQService {
this.name = name || this.constructor.name;
if (this.name === 'IMQService') {
- throw new TypeError('IMQService class is abstract and cannot ' +
- 'be instantiated directly!');
+ throw new TypeError(
+ 'IMQService class is abstract and cannot ' +
+ 'be instantiated directly!',
+ );
}
this.options = {
@@ -138,7 +140,7 @@ export abstract class IMQService {
...options,
metricsServer: {
...DEFAULT_IMQ_METRICS_SERVER_OPTIONS,
- ...(options?.metricsServer || {}),
+ ...options?.metricsServer,
},
};
this.logger = this.options.logger || /* istanbul ignore next */ console;
@@ -146,24 +148,40 @@ export abstract class IMQService {
this.handleRequest = this.handleRequest.bind(this);
- SIGNALS.forEach((signal: any) => process.on(signal, async () => {
- this.destroy().catch(this.logger.error);
+ SIGNALS.forEach((signal: string) => {
+ const handler = (): void => {
+ this.destroy().catch(this.logger.error);
- if (this.metricsServer) {
- this.metricsServer.close();
- }
+ if (this.metricsServer) {
+ this.metricsServer.close();
+ }
- // istanbul ignore next
- setTimeout(() => process.exit(0), IMQ_SHUTDOWN_TIMEOUT);
- }));
+ setTimeout(() => process.exit(0), IMQ_SHUTDOWN_TIMEOUT);
+ };
- this.imq.on('message', this.handleRequest);
+ // tracked so destroy() can unregister them (see below)
+ this.signalHandlers.push([signal, handler]);
+ process.on(signal, handler);
+ });
+
+ // guard the async handler: a failure while processing or publishing
+ // the response (e.g. the broker went away mid-reply) must be logged,
+ // not surface as an unhandled rejection and crash the process
+ this.imq.on(
+ 'message',
+ ((request: IMQRPCRequest, id: string): Promise =>
+ this.handleRequest(request, id).catch(err =>
+ this.logger.error(
+ `${this.name}: error handling request:`,
+ err,
+ ),
+ )) as any,
+ );
}
/**
* Handles incoming request and produces corresponding response
*
- * @access private
* @param {IMQRPCRequest} request - request message
* @param {string} id - message unique identifier
* @return {Promise}
@@ -197,7 +215,7 @@ export abstract class IMQService {
try {
await beforeCall(request, response);
- } catch (err) {
+ } catch (err: any) {
logger.warn(BEFORE_HOOK_ERROR, err);
}
}
@@ -206,10 +224,11 @@ export abstract class IMQService {
response.error = IMQError(
'IMQ_RPC_NO_METHOD',
`Method ${this.name}.${method}() does not exist.`,
- new Error().stack, method, args);
- }
-
- else if (!description.service.methods[method]) {
+ new Error().stack,
+ method,
+ args,
+ );
+ } else if (!description.service.methods[method]) {
// Allow calling runtime-attached methods (own props) even if
// they are not present in the exposed service description.
// Deny access for prototype (class) methods not decorated with @expose.
@@ -217,24 +236,31 @@ export abstract class IMQService {
const value: any = (this as any)[method];
const proto = Object.getPrototypeOf(this);
const protoValue = proto && proto[method];
- const isSameAsProto = typeof protoValue === 'function' && value === protoValue;
+ const isSameAsProto =
+ typeof protoValue === 'function' && value === protoValue;
// Allow only truly dynamic own-instance functions (not the same as prototype)
if (!(isOwn && typeof value === 'function' && !isSameAsProto)) {
response.error = IMQError(
'IMQ_RPC_NO_ACCESS',
`Access to ${this.name}.${method}() denied!`,
- new Error().stack, method, args);
+ new Error().stack,
+ method,
+ args,
+ );
}
- }
-
- else if (!isValidArgsCount(
- description.service.methods[method].arguments,
- args
- )) {
+ } else if (
+ !isValidArgsCount(
+ description.service.methods[method].arguments,
+ args,
+ )
+ ) {
response.error = IMQError(
'IMQ_RPC_INVALID_ARGS_COUNT',
`Invalid args count for ${this.name}.${method}().`,
- new Error().stack, method, args);
+ new Error().stack,
+ method,
+ args,
+ );
}
if (response.error) {
@@ -246,15 +272,18 @@ export abstract class IMQService {
try {
response.data = this[method].apply(this, args);
- // istanbul ignore next
if (response.data && response.data.then) {
response.data = await response.data;
}
- }
-
- catch (err) {
- response.error = IMQError(err.code || 'IMQ_RPC_CALL_ERROR',
- err.message, err.stack, method, args, err);
+ } catch (err: any) {
+ response.error = IMQError(
+ err.code || 'IMQ_RPC_CALL_ERROR',
+ err.message,
+ err.stack,
+ method,
+ args,
+ err,
+ );
}
return await send(request, response, this);
@@ -271,7 +300,8 @@ export abstract class IMQService {
if (!this.options.multiProcess) {
this.logger.info(
'%s: starting single-worker, pid %s',
- this.name, process.pid
+ this.name,
+ process.pid,
);
this.describe();
@@ -280,7 +310,7 @@ export abstract class IMQService {
}
if (cluster.isMaster) {
- const numCpus = os.cpus().length;
+ const numCpus = cpus().length;
const numWorkers = numCpus * this.options.childrenPerCore;
for (let i = 0; i < numWorkers; i++) {
@@ -288,18 +318,19 @@ export abstract class IMQService {
cluster.fork({ workerId: i });
}
- // istanbul ignore next
- cluster.on('exit', (worker: any) => {
+ cluster.on('exit', (worker: Worker) => {
+ /* node:coverage disable */
+ // exercised by tests, but V8 will not record coverage for an
+ // inline listener body that terminates via process.exit()
this.logger.info(
'%s: worker pid %s died, exiting',
this.name,
worker.process.pid,
);
process.exit(1);
+ /* node:coverage enable */
});
- }
-
- else {
+ } else {
this.logger.info(
'%s: worker #%s started, pid %s',
this.name,
@@ -315,7 +346,6 @@ export abstract class IMQService {
return this.startWithMetricsServer();
}
- // noinspection JSUnusedGlobalSymbols
/**
* Sends given data to service subscription channel
*
@@ -342,6 +372,13 @@ export abstract class IMQService {
*/
@profile()
public async destroy(): Promise {
+ // unregister this instance's process signal handlers so destroyed
+ // services do not leak process-level listeners
+ for (const [signal, handler] of this.signalHandlers) {
+ process.removeListener(signal, handler);
+ }
+ this.signalHandlers.length = 0;
+
await this.imq.unsubscribe();
await this.imq.destroy();
}
@@ -359,9 +396,9 @@ export abstract class IMQService {
description = {
service: {
name: this.name,
- methods: getClassMethods(this.constructor.name)
+ methods: getClassMethods(this.constructor.name),
},
- types: IMQRPCDescription.typesDescription
+ types: IMQRPCDescription.typesDescription,
};
serviceDescriptions.set(this.name, description);
@@ -380,12 +417,14 @@ export abstract class IMQService {
this.logger.log('Starting metrics server...');
- this.metricsServer = http.createServer(async (req, res) => {
+ this.metricsServer = createServer(async (req, res) => {
if (req.url === '/metrics') {
const length = await this.imq.queueLength();
- const content = metricServerOptions.queueLengthFormatter?.(
- length, 'queue_length',
- ) || String(length);
+ const content =
+ metricServerOptions.queueLengthFormatter?.(
+ length,
+ 'queue_length',
+ ) || String(length);
res.setHeader('Content-Type', 'plain/text');
res.setHeader('Content-Length', Buffer.byteLength(content));
@@ -398,17 +437,13 @@ export abstract class IMQService {
res.writeHead(404);
res.end();
});
- this.metricsServer.listen(
- metricServerOptions.port,
- '0.0.0.0',
- () => {
- this.logger.info(
- '%s: metrics server started on port %s',
- this.name,
- metricServerOptions.port,
- );
- },
- );
+ this.metricsServer.listen(metricServerOptions.port, '0.0.0.0', () => {
+ this.logger.info(
+ '%s: metrics server started on port %s',
+ this.name,
+ metricServerOptions.port,
+ );
+ });
return service;
}
@@ -437,7 +472,7 @@ export async function send(
try {
await afterCall(request, response);
- } catch (err) {
+ } catch (err: any) {
logger.warn(AFTER_HOOK_ERROR, err);
}
}
diff --git a/src/cache/ICache.ts b/src/cache/ICache.ts
index 2599a1b..357da07 100644
--- a/src/cache/ICache.ts
+++ b/src/cache/ICache.ts
@@ -21,21 +21,77 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
+/**
+ * Constructor signature for cache adapter implementations.
+ */
export interface ICacheConstructor {
new (name?: string): ICache;
}
+/**
+ * Generic cache adapter interface. Any cache engine implementation must
+ * conform to this contract to be usable within IMQ.
+ */
export interface ICache {
-
+ /**
+ * Adapter (cache) name.
+ *
+ * @type {string}
+ */
name: string;
+
+ /**
+ * Whether the cache adapter is initialized and ready to use.
+ *
+ * @type {boolean}
+ */
ready: boolean;
+ /**
+ * Initializes the cache adapter with the given adapter-specific options.
+ *
+ * @param {any} [options] - adapter-specific options
+ * @returns {void}
+ */
init(options?: any): void;
+
+ /**
+ * Returns the value stored in the cache under the given key.
+ *
+ * @param {string} key - key to read the value for
+ * @returns {Promise} - stored value, or undefined if not found
+ */
get(key: string): Promise;
+
+ /**
+ * Stores the given value in the cache under the given key.
+ *
+ * @param {string} key - key to store the value under
+ * @param {any} value - value to store
+ * @param {number} [ttl] - time-to-live in milliseconds
+ * @returns {Promise}
+ */
set(key: string, value: any, ttl?: number): Promise;
+
+ /**
+ * Removes the value stored in the cache under the given key.
+ *
+ * @param {string} key - key to remove
+ * @returns {Promise}
+ */
del(key: string): Promise;
- purge(keyMask: string): Promise;
+ /**
+ * Purges all keys from the cache matching the given wildcard mask.
+ *
+ * @param {string} keyMask - wildcard mask to match keys against
+ * @returns {Promise}
+ */
+ purge(keyMask: string): Promise;
}
+/**
+ * Accepted cache adapter references: a constructor, an instance, or an
+ * adapter name.
+ */
export type ICacheAdapter = ICacheConstructor | ICache | string;
diff --git a/src/cache/RedisCache.ts b/src/cache/RedisCache.ts
index 29788f1..51a7815 100644
--- a/src/cache/RedisCache.ts
+++ b/src/cache/RedisCache.ts
@@ -21,7 +21,6 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import { ICache } from '.';
import {
ILogger,
DEFAULT_IMQ_OPTIONS,
@@ -29,7 +28,8 @@ import {
IMQOptions,
Redis,
} from '@imqueue/core';
-import * as os from 'os';
+import { hostname } from 'node:os';
+import { ICache } from '.';
export interface IRedisCacheOptions extends Partial {
conn?: IRedisClient;
@@ -43,19 +43,24 @@ export const DEFAULT_REDIS_CACHE_OPTIONS = {
export const REDIS_CLIENT_INIT_ERROR = 'Redis client is not initialized!';
/**
- * Class RedisCache. Implements cache engine over redis.
+ * Class RedisCache. Implements a cache engine on top of Redis.
*/
export class RedisCache implements ICache {
private static redis?: IRedisClient;
- private logger: ILogger;
- public options: IRedisCacheOptions;
+ // pending shared connection attempt; concurrent init() calls await this
+ // single promise instead of opening one connection each
+ private static initPromise?: Promise;
+ private logger!: ILogger;
+ public options!: IRedisCacheOptions;
public name: string = RedisCache.name;
public ready: boolean = false;
/**
- * Initializes cache instance
+ * Initializes the cache instance. The underlying Redis connection is
+ * shared between all instances; concurrent initializations share a single
+ * connection attempt.
*
- * @param {IRedisCacheOptions} [options]
+ * @param {IRedisCacheOptions} [options] - Redis cache options
* @returns {Promise}
*/
public async init(options?: IRedisCacheOptions): Promise {
@@ -64,69 +69,80 @@ export class RedisCache implements ICache {
...options,
};
- this.logger = this.options.logger ||
- // istanbul ignore next
- console;
+ this.logger = this.options.logger || console;
+
+ if (RedisCache.redis && !RedisCache.initPromise) {
+ this.ready = true;
- if (RedisCache.redis) {
return this;
}
- if (this.options.conn) {
+ if (this.options.conn && !RedisCache.initPromise) {
this.logger.info('Re-using given connection for cache.');
RedisCache.redis = this.options.conn;
+ this.ready = true;
return this;
}
- return new Promise((resolve, reject) => {
- const connectionName = `${
- this.options.prefix}:${
- this.name }:pid:${
- process.pid }:host:${
- os.hostname()
- }`;
-
- RedisCache.redis = new Redis({
- port: Number(this.options.port),
- host: String(this.options.host),
- username: this.options.username,
- password: this.options.password,
- connectionName,
+ if (!RedisCache.initPromise) {
+ RedisCache.initPromise = new Promise((resolve, reject) => {
+ const connectionName = `${this.options.prefix}:${
+ this.name
+ }:pid:${process.pid}:host:${hostname()}`;
+
+ RedisCache.redis = new Redis({
+ port: Number(this.options.port),
+ host: String(this.options.host),
+ username: this.options.username,
+ password: this.options.password,
+ connectionName,
+ });
+
+ RedisCache.redis.on('ready', () => {
+ this.logger.info(
+ '%s: redis cache connected, host %s:%s, pid %s',
+ this.name,
+ this.options.host,
+ this.options.port,
+ process.pid,
+ );
+
+ resolve();
+ });
+
+ RedisCache.redis.on('error', (err: Error) => {
+ this.logger.error(
+ `${this.name}: error connecting redis, pid ${
+ process.pid
+ }:`,
+ err,
+ );
+
+ reject(err);
+ });
});
+ }
- RedisCache.redis.on('ready', async () => {
- this.logger.info(
- '%s: redis cache connected, host %s:%s, pid %s',
- this.name,
- this.options.host,
- this.options.port,
- process.pid,
- );
-
- this.ready = true;
-
- resolve(this);
- });
+ try {
+ await RedisCache.initPromise;
+ } finally {
+ // the promise only guards the pending connection attempt; once it
+ // settles (either way), clear it so later init() calls observe
+ // the current RedisCache.redis state (or retry after a failure)
+ RedisCache.initPromise = undefined;
+ }
- // istanbul ignore next
- RedisCache.redis.on('error', (err: Error) => {
- this.logger.error(
- `${this.name}: error connecting redis, pid ${process.pid}:`,
- err
- );
+ this.ready = true;
- reject(err);
- });
- });
+ return this;
}
/**
- * Returns fully qualified key name for a given generic key
+ * Returns the fully qualified key name for a given generic key.
*
- * @access private
- * @param {string} key
+ * @param {string} key - generic key to qualify
* @returns {string}
*/
private key(key: string): string {
@@ -134,18 +150,17 @@ export class RedisCache implements ICache {
}
/**
- * Returns value stored in cache by a given key
+ * Returns the value stored in the cache under a given key.
*
- * @param {string} key
- * @returns {Promise}
+ * @param {string} key - key to read the value for
+ * @returns {Promise} - stored value, or undefined if not found
*/
public async get(key: string): Promise {
if (!RedisCache.redis) {
throw new TypeError(REDIS_CLIENT_INIT_ERROR);
}
- // noinspection ES6RedundantAwait
- const data = await RedisCache.redis.get(this.key(key));
+ const data = await RedisCache.redis.get(this.key(key));
if (data) {
return JSON.parse(data);
@@ -155,31 +170,31 @@ export class RedisCache implements ICache {
}
/**
- * Stores in cache given value under given key. If TTL is specified,
- * cached value will expire in a given number of milliseconds. If NX
- * argument set to true will create key:value in cache only if it does
- * not exist yet. Given value could be any JSON-compatible object and
- * will be serialized automatically.
+ * Stores the given value in the cache under the given key. If TTL is
+ * specified, the cached value will expire after the given number of
+ * milliseconds. If the NX argument is set to true, the key:value pair
+ * is created only if it does not exist yet. The given value can be any
+ * JSON-compatible object and will be serialized automatically.
*
- * @param {string} key
- * @param {any} value
- * @param {number} ttl
- * @param {boolean} nx
+ * @param {string} key - key to store the value under
+ * @param {any} value - value to store
+ * @param {number} [ttl] - time-to-live in milliseconds
+ * @param {boolean} [nx] - store only if the key does not exist yet
* @returns {Promise}
*/
public async set(
key: string,
value: any,
ttl?: number,
- nx: boolean = false
+ nx: boolean = false,
): Promise {
if (!RedisCache.redis) {
throw new TypeError(REDIS_CLIENT_INIT_ERROR);
}
- const args: any[] = [
+ const args: (string | number)[] = [
this.key(key),
- JSON.stringify(value && value.then ? await value : value)
+ JSON.stringify(value && value.then ? await value : value),
];
if (ttl && ttl > 0) {
@@ -190,13 +205,16 @@ export class RedisCache implements ICache {
args.push('NX');
}
- return await RedisCache.redis.set.apply(RedisCache.redis, args);
+ return await (RedisCache.redis.set as any).apply(
+ RedisCache.redis,
+ args,
+ );
}
/**
- * Removes stored in cache value under given key
+ * Removes the value stored in the cache under the given key.
*
- * @param {string} key
+ * @param {string} key - key to remove
* @returns {Promise}
*/
public async del(key: string): Promise {
@@ -204,14 +222,14 @@ export class RedisCache implements ICache {
throw new TypeError(REDIS_CLIENT_INIT_ERROR);
}
- return !!await RedisCache.redis.del(this.key(key));
+ return !!(await RedisCache.redis.del(this.key(key)));
}
/**
- * Purges all keys from cache by a given wildcard mask
+ * Purges all keys from the cache matching a given wildcard mask.
*
- * @param {string} keyMask
- * @return {boolean}
+ * @param {string} keyMask - wildcard mask to match keys against
+ * @return {Promise}
*/
public async purge(keyMask: string): Promise {
if (!RedisCache.redis) {
@@ -219,7 +237,6 @@ export class RedisCache implements ICache {
}
try {
- // noinspection SpellCheckingInspection
await RedisCache.redis.eval(
`for _,k in ipairs(redis.call('keys','${
keyMask
@@ -236,22 +253,20 @@ export class RedisCache implements ICache {
}
/**
- * Safely destroys redis connection
+ * Safely destroys the Redis connection.
*
* @returns {Promise}
*/
public static async destroy(): Promise {
+ RedisCache.initPromise = undefined;
+
try {
- // istanbul ignore else
if (RedisCache.redis) {
RedisCache.redis.removeAllListeners();
RedisCache.redis.disconnect(false);
RedisCache.redis.quit();
delete RedisCache.redis;
}
- }
-
- catch (err) {}
+ } catch {}
}
-
}
diff --git a/src/decorators/cache.ts b/src/decorators/cache.ts
index 7228c13..7b2cfb1 100644
--- a/src/decorators/cache.ts
+++ b/src/decorators/cache.ts
@@ -21,12 +21,13 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import { IMQCache, ICache, RedisCache, ICacheConstructor, signature } from '..';
+import { IMQCache, ICache, RedisCache, ICacheConstructor } from '..';
+import { signature } from '../helpers';
export interface CacheDecoratorOptions {
adapter?: string | ICache | ICacheConstructor;
- ttl?: number; // milliseconds
- nx?: boolean; // rewrite only if not exists in cache
+ ttl?: number; // time-to-live, in milliseconds
+ nx?: boolean; // write only if the key does not already exist in the cache
}
export interface CacheDecorator {
@@ -34,42 +35,47 @@ export interface CacheDecorator {
globalOptions?: CacheDecoratorOptions;
}
-// codebeat:disable[BLOCK_NESTING]
-export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) {
+/**
+ * Creates a `@cache()` method decorator that memoizes the decorated method's
+ * result in a cache adapter (RedisCache by default). On each call the cache is
+ * checked first; on a miss the method runs, and its result is stored under a
+ * key derived from the class name, method name, and arguments. The returned
+ * decorator is dual-mode: it works both as a standard (TC39) and as a legacy
+ * method decorator.
+ *
+ * @param {CacheDecoratorOptions} [options] - per-method cache options (adapter,
+ * ttl, nx); merged over `cache.globalOptions`
+ * @return {Function} - a dual-mode method decorator
+ */
+export const cache: CacheDecorator = function (
+ options?: CacheDecoratorOptions,
+) {
const cacheOptions: CacheDecoratorOptions = {
...cache.globalOptions,
...options,
};
let Adapter: any = cacheOptions.adapter || RedisCache;
- return function(
- target: any,
+ const wrap = (
+ original: (...args: any[]) => any,
methodName: string | symbol,
- descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
- ) {
- const original: (...args: any[]) => any = descriptor.value as any;
-
- descriptor.value = async function(...args: any[]) {
- const context: any = this;
+ ) =>
+ async function (this: any, ...args: any[]) {
const className = this.constructor.name;
- if (!context.cache) {
+ if (!this.cache) {
let cache = IMQCache.get(Adapter);
- // istanbul ignore next
if (cache && cache.ready) {
- context.cache = cache;
- }
-
- else {
+ this.cache = cache;
+ } else {
let opts: any = undefined;
- if (context.imq && context.imq.writer) {
- opts = { conn: (context.imq).writer };
+ if (this.imq && this.imq.writer) {
+ opts = { conn: (this.imq).writer };
}
- const logger = context.logger ||
- (context.imq && context.imq.logger);
+ const logger = this.logger || (this.imq && this.imq.logger);
if (logger) {
opts = { ...opts, logger };
@@ -77,19 +83,19 @@ export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) {
await IMQCache.register(Adapter, opts).init();
- context.cache = IMQCache.get(Adapter);
+ this.cache = IMQCache.get(Adapter);
}
}
try {
const key = signature(className, methodName, args);
- let result = await context.cache.get(key);
+ let result = await this.cache.get(key);
if (result === undefined) {
result = original.apply(this, args);
- await context.cache.set(
+ await this.cache.set(
key,
result,
cacheOptions.ttl,
@@ -98,23 +104,32 @@ export const cache: CacheDecorator = function(options?: CacheDecoratorOptions) {
}
return result;
- }
-
- catch (err) {
- // istanbul ignore next
- (this.logger || context.cache.logger).warn(
+ } catch (err) {
+ (this.logger || this.cache.logger).warn(
'cache: Error fetching cached value for %s.%s(), args: %s!',
- className, methodName, JSON.stringify(args), err,
+ className,
+ methodName,
+ JSON.stringify(args),
+ err,
);
- // istanbul ignore next
return original.apply(this, args);
}
};
- }
+
+ // Dual-mode: standard (TC39) invocations pass a context object with a
+ // `kind` property; legacy ones pass (target, propertyKey, descriptor).
+ return function (target: any, context: any, descriptor?: any): any {
+ if (context && typeof context === 'object' && 'kind' in context) {
+ return wrap(target, context.name);
+ }
+
+ descriptor.value = wrap(descriptor.value, context);
+
+ return descriptor;
+ };
};
-// codebeat:enable[BLOCK_NESTING]
cache.globalOptions = {
- adapter: RedisCache
+ adapter: RedisCache,
};
diff --git a/src/decorators/classType.ts b/src/decorators/classType.ts
new file mode 100644
index 0000000..7be7328
--- /dev/null
+++ b/src/decorators/classType.ts
@@ -0,0 +1,82 @@
+/*!
+ * IMQ-RPC Decorators: classType
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+import { registerType } from './property';
+
+/**
+ * Implements the '@classType' decorator factory.
+ *
+ * Applied to a complex-type class, it registers the class's '@property' field
+ * definitions into the RPC type description so the type can be exposed to
+ * service clients. It is required on any class that uses '@property':
+ * standard (TC39) field decorators cannot see the class they belong to, so a
+ * class-level decorator is needed to flush the collected properties under the
+ * class name.
+ *
+ * @example
+ * ~~~typescript
+ * import { classType, property, expose, IMQService } from '@imqueue/rpc';
+ *
+ * @classType()
+ * class Address {
+ * @property('string')
+ * country: string;
+ *
+ * @property('string', true)
+ * zipCode?: string; // optional
+ * }
+ *
+ * @classType()
+ * class User {
+ * @property('string')
+ * firstName: string;
+ *
+ * @property('Array', true)
+ * addresses?: Address[];
+ * }
+ *
+ * class UserService extends IMQService {
+ * @expose()
+ * public save(user: User) {
+ * // now User (and Address) are properly exposed to clients
+ * }
+ * }
+ * ~~~
+ *
+ * @return {(value: Function, context: ClassDecoratorContext) => void}
+ */
+export function classType(): any {
+ // Dual-mode: standard (TC39) class decorators pass a context object with a
+ // `kind` property; legacy ones pass only the constructor. In legacy mode
+ // @property already registers each field directly, so there is nothing to
+ // flush and the class is returned unchanged.
+ return function (value: any, context?: any): any {
+ if (context && typeof context === 'object' && 'kind' in context) {
+ registerType(value, context.metadata);
+
+ return;
+ }
+
+ return value;
+ };
+}
diff --git a/src/decorators/expose.ts b/src/decorators/expose.ts
index 44ab39e..b5cd9c1 100644
--- a/src/decorators/expose.ts
+++ b/src/decorators/expose.ts
@@ -21,22 +21,17 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import 'reflect-metadata';
-import * as acorn from 'acorn';
import {
- ArgDescription,
- ReturnValueDescription,
- IMQRPCDescription,
-} from '..';
-
-const TS_TYPES = [
- 'object',
- 'string',
- 'number',
- 'boolean',
- 'null',
- 'undefined',
-];
+ createSourceFile,
+ getLeadingCommentRanges,
+ isClassDeclaration,
+ isIdentifier,
+ isMethodDeclaration,
+ ScriptKind,
+ ScriptTarget,
+ SyntaxKind,
+} from 'typescript';
+import { ArgDescription, ReturnValueDescription, IMQRPCDescription } from '..';
type CommentMetadata = {
[key: string]: string | ArgDescription[] | ReturnValueDescription;
@@ -54,7 +49,7 @@ type ClassDescriptions = {
[key: string]: string | CommentBlockMetadata;
inherits: string;
};
-}
+};
const descriptions: ClassDescriptions = {};
@@ -63,11 +58,9 @@ const RX_COMMA_SPLIT = /\s*,\s*/;
const RX_MULTILINE_CLEANUP = /\*?\n +\* ?/g;
const RX_DESCRIPTION = /^([\s\S]*?)@/;
const RX_TAG = /(@[^@]+)/g;
-// noinspection RegExpRedundantEscape
const RX_TYPE = /\{([\s\S]+)\}/;
const RX_LI_CLEANUP = /^\s*-\s*/;
const RX_SPACE_SPLIT = / /;
-// noinspection RegExpRedundantEscape
const RX_OPTIONAL = /^\[(.*?)\]$/;
/**
@@ -78,11 +71,11 @@ const RX_OPTIONAL = /^\[(.*?)\]$/;
*/
function argumentNames(fn: (...args: any[]) => any): string[] {
let src: string = fn.toString();
- return src.slice(
- src.indexOf('(') + 1, src.indexOf(')')
- ).split(RX_COMMA_SPLIT).map(arg =>
- arg.trim().replace(RX_ARG_NAMES, '$1')
- ).filter(arg => arg);
+ return src
+ .slice(src.indexOf('(') + 1, src.indexOf(')'))
+ .split(RX_COMMA_SPLIT)
+ .map(arg => arg.trim().replace(RX_ARG_NAMES, '$1'))
+ .filter(arg => arg);
}
/**
@@ -100,13 +93,13 @@ function parseComment(src: string): CommentMetadata {
description: '',
type: '',
tsType: '',
- }
+ },
};
- let match, tags = [];
+ let match,
+ tags = [];
- // istanbul ignore next
data.description = String(
- (cleanSrc.match(RX_DESCRIPTION) || [])[1] || ''
+ (cleanSrc.match(RX_DESCRIPTION) || [])[1] || '',
).trim();
while ((match = RX_TAG.exec(cleanSrc))) {
@@ -118,7 +111,10 @@ function parseComment(src: string): CommentMetadata {
let tagName = parts.shift();
let tagDef = parts.join(' ');
let typeMatch = tagDef.match(RX_TYPE);
- let tsType = '', name = '', description = '', type = '';
+ let tsType = '',
+ name = '',
+ description = '',
+ type = '';
let isOptional = false;
if (typeMatch) {
@@ -162,7 +158,6 @@ function parseComment(src: string): CommentMetadata {
return data;
}
-// codebeat:disable[LOC,ABC]
/**
* Finds and parses methods and their comment blocks for a given class
*
@@ -170,97 +165,62 @@ function parseComment(src: string): CommentMetadata {
* @param {string} src - class source code
*/
function parseDescriptions(name: string, src: string) {
- const comments: acorn.Comment[] = [];
- const options: acorn.Options = {
- ecmaVersion: 8,
- locations: true,
- ranges: true,
- allowReserved: true,
- onComment: comments,
- };
- const nodes = (acorn.parse(src, options) as any).body;
-
+ // parsed with the TypeScript compiler, which is already a runtime
+ // dependency (used for client generation) — no parent pointers needed,
+ // only class members and their leading comment trivia are read
+ const sourceFile = createSourceFile(
+ `${name}.js`,
+ src,
+ ScriptTarget.Latest,
+ false,
+ ScriptKind.JS,
+ );
+
+ // the local `inherits` is a placeholder only: the public description's
+ // `inherits` is derived from the runtime prototype chain in
+ // buildMethodDescription(), never from the parsed source
descriptions[name] = {
- inherits: 'Function'
+ inherits: 'Function',
};
- for (let node of nodes) {
- // istanbul ignore if
- if (!(
- node && node.type === 'ClassDeclaration' &&
- node.id && node.id.name === name &&
- node.body.type === 'ClassBody'
- )) {
+ for (const statement of sourceFile.statements) {
+ if (
+ !isClassDeclaration(statement) ||
+ !statement.name ||
+ statement.name.text !== name
+ ) {
continue;
}
- if (node.superClass) {
- // istanbul ignore next
- if (node.superClass.type === 'MemberExpression') {
- descriptions[name].inherits =
- (node.superClass.property).name;
- }
-
- else if (node.superClass.type === 'Identifier') {
- descriptions[name].inherits = node.superClass.name;
- }
- }
-
- const methods = node.body.body.filter((f: any) =>
- f.type === 'MethodDefinition');
-
- for (let method of node.body.body) {
- // istanbul ignore if
- if (method.type !== 'MethodDefinition') {
+ for (const member of statement.members) {
+ if (
+ !isMethodDeclaration(member) ||
+ !member.name ||
+ !isIdentifier(member.name)
+ ) {
continue;
}
- const methodName: string = (method.key).name;
- const blocks: acorn.Comment[] = comments.filter(comment =>
- comment.type === 'Block');
+ // the method's doc is the last block comment in its leading
+ // trivia; member.pos starts right after the previous token, so
+ // comments belonging to earlier members can never match
+ const blocks = (
+ getLeadingCommentRanges(src, member.pos) || []
+ ).filter(range => range.kind === SyntaxKind.MultiLineCommentTrivia);
if (!blocks.length) {
continue;
}
- const methodStart: number = (method).start;
- let lastDif: number = methodStart - blocks[0].end;
- let foundBlock: acorn.Comment = blocks[0];
-
- for (let comment of blocks) {
- const dif: number = methodStart - comment.end;
+ const block = blocks[blocks.length - 1];
- if (dif < 0) {
- break;
- }
-
- if (dif >= lastDif) {
- continue;
- }
-
- lastDif = dif;
- foundBlock = comment;
- }
-
- if (!method.range || foundBlock.start > method.range[1]) {
- continue;
- }
-
- const index: number = methods.indexOf(method);
- const prev: any = index && methods[index - 1];
- const prevBeforeComment = !prev
- || (prev && prev.range && prev.range[1] <= foundBlock.start);
-
- if (prevBeforeComment) {
- // it's a method comment block!!!!
- descriptions[name][methodName] = {
- comment: parseComment(foundBlock.value),
- };
- }
+ descriptions[name][member.name.text] = {
+ // comment body without the enclosing /* and */ delimiters
+ comment: parseComment(src.slice(block.pos + 2, block.end - 2)),
+ };
}
}
}
-// codebeat:enable[LOC,ABC]
/**
* Helper function to make easy descriptions parts extractions
@@ -275,14 +235,12 @@ function get(
prop: string,
className: string,
method: string,
- defaults: T
+ defaults: T,
): T {
if (descriptions[className] && descriptions[className][method]) {
- let comment = (
- descriptions[className][method]
- ).comment;
+ let comment = (descriptions[className][method])
+ .comment;
- // istanbul ignore else
if (comment[prop]) {
return comment[prop];
}
@@ -292,100 +250,139 @@ function get(
}
/**
- * Converts JavaScript type to most close possible TypeScript type
+ * Builds and registers the RPC description for a single exposed method.
*
- * @param type
- * @returns {string}
+ * Types are taken from the method's JSDoc (parsed from the class source,
+ * preserved by `removeComments: false`); standard decorators provide no
+ * runtime `design:type` metadata, so JSDoc is the sole type source.
+ *
+ * @param {Function} ctor - class that declares the method
+ * @param {string} methodName - exposed method name
+ * @param {(...args: any[]) => any} fn - the method implementation
*/
-function cast(type: string) {
- let tsType = String(type).toLowerCase();
-
- if (tsType === 'undefined') {
- tsType = 'void';
+function buildMethodDescription(
+ ctor: Function,
+ methodName: string,
+ fn: (...args: any[]) => any,
+): void {
+ const className: string = ctor.name;
+ const argNames: string[] = argumentNames(fn);
+
+ if (!descriptions[className]) {
+ parseDescriptions(className, ctor.toString());
}
- else if (tsType === 'array') {
- tsType = 'any[]';
+ const args: ArgDescription[] = get(
+ 'params',
+ className,
+ methodName,
+ [],
+ );
+ const ret: ReturnValueDescription = get(
+ 'returns',
+ className,
+ methodName,
+ {
+ description: '',
+ type: '',
+ tsType: '',
+ },
+ );
+
+ if (!args || !args.length) {
+ for (let i = 0, s = argNames.length; i < s; i++) {
+ args[i] = {
+ description: '',
+ name: argNames[i],
+ type: '',
+ tsType: '',
+ isOptional: false,
+ };
+ }
}
- else if (!~TS_TYPES.indexOf(tsType)) {
- tsType = type;
+ // JSDoc is the sole type source; mirror the parsed tsType into `type`
+ // (the only consumer of `type` is the client generator's
+ // IMQDelay/IMQMetadata stripping) and default any undocumented type to
+ // 'any' so generated clients are always valid TypeScript
+ for (const arg of args) {
+ arg.tsType = arg.tsType || 'any';
+ arg.type = arg.type || arg.tsType;
}
- return tsType;
+ ret.tsType = ret.tsType || 'any';
+ ret.type = ret.type || ret.tsType;
+
+ // derive the parent from the runtime prototype chain rather than the
+ // parsed source: standard-decorator output aliases the `extends` clause
+ // to a helper (e.g. `extends _classSuper`), so the source name is unusable
+ const parent: any = Object.getPrototypeOf(ctor);
+ const inherits: string = parent && parent.name ? parent.name : 'Function';
+
+ IMQRPCDescription.serviceDescription[className] = IMQRPCDescription
+ .serviceDescription[className] || {
+ inherits,
+ methods: {},
+ };
+ IMQRPCDescription.serviceDescription[className].methods[methodName] = {
+ description: get('description', className, methodName, ''),
+ arguments: args,
+ returns: ret,
+ };
}
/**
- * Expose decorator factory
+ * Expose decorator factory. Applied to a service method, it registers that
+ * method in the RPC service description. (Complex argument/return types are
+ * registered separately via the '@classType' decorator on those classes.)
*
- * @return {(
- * target: object,
- * methodName: (string),
- * descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
- * ) => void} - decorator function
+ * @return {(value: any, context: ClassMethodDecoratorContext) => void}
*/
-export function expose(): (...args: any[]) => any {
+export function expose(): any {
+ // Dual-mode: standard (TC39) invocations pass a context object with a
+ // `kind` property; legacy ones pass (target, propertyKey, descriptor).
return function exposeDecorator(
target: any,
- methodName: string,
- descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
- ) {
- let className: string = target.constructor.name;
- let argNames: string[] = argumentNames(
- <(...args: any[]) => any>descriptor.value,
- );
- let retType = Reflect.getMetadata(
- 'design:returntype',
- target,
- methodName,
- );
- let retTypeName: string = retType ? retType.name : String(retType);
+ context: any,
+ descriptor?: any,
+ ): any {
+ if (context && typeof context === 'object' && 'kind' in context) {
+ const value: any = target;
+ const methodName: string = String(context.name);
+
+ // the class is not available at decoration time, so defer to an
+ // initializer that runs at construction, where `this` is known
+ context.addInitializer(function (this: any): void {
+ // register the method under the class that actually declares
+ // it (inherited methods are described under the base class,
+ // matching decoration-time behavior)
+ let proto: any = this.constructor.prototype;
+
+ while (
+ proto &&
+ !Object.prototype.hasOwnProperty.call(proto, methodName)
+ ) {
+ proto = Object.getPrototypeOf(proto);
+ }
- if (!descriptions[className]) {
- parseDescriptions(className, target.constructor.toString());
- }
+ const ctor: Function = proto
+ ? proto.constructor
+ : this.constructor;
- let args: ArgDescription[] = get(
- 'params', className, methodName, []);
- let ret: ReturnValueDescription = get(
- 'returns', className, methodName, {
- description: '',
- type: retTypeName,
- tsType: cast(retTypeName),
+ buildMethodDescription(ctor, methodName, value);
});
- ret.type = retTypeName;
-
- if (!args || !args.length) {
- for (let i = 0, s = argNames.length; i < s; i++) {
- args[i] = {
- description: '',
- name: argNames[i],
- type: '',
- tsType: '',
- isOptional : false,
- };
- }
+ return;
}
- Reflect.getMetadata(
- 'design:paramtypes',
- target,
- methodName,
- ).forEach((typeConstructor: any, i: number) => {
- args[i].type = args[i].type || typeConstructor.name;
- args[i].tsType = args[i].tsType || cast(args[i].type);
- });
-
- IMQRPCDescription.serviceDescription[className] =
- IMQRPCDescription.serviceDescription[className] || {
- inherits: descriptions[className].inherits,
- methods: {},
- };
- IMQRPCDescription.serviceDescription[className].methods[methodName] = {
- description: get('description', className, methodName, ''),
- arguments: args,
- returns: ret,
- };
- }
+ // legacy: `target` is the prototype, so its constructor is the class
+ // that declares the method — available immediately at decoration time
+ buildMethodDescription(
+ target.constructor,
+ String(context),
+ descriptor.value,
+ );
+
+ return descriptor;
+ };
}
diff --git a/src/decorators/index.ts b/src/decorators/index.ts
index cb7772e..88eaa4a 100644
--- a/src/decorators/index.ts
+++ b/src/decorators/index.ts
@@ -21,7 +21,12 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
+// must load first: installs the Symbol.metadata polyfill required by the
+// standard-decorator metadata used across the decorators below
+import './metadata';
+
export * from './expose';
+export * from './classType';
export * from './remote';
export * from './lock';
export * from './cache';
diff --git a/src/decorators/indexed.ts b/src/decorators/indexed.ts
index 5a00387..d386b23 100644
--- a/src/decorators/indexed.ts
+++ b/src/decorators/indexed.ts
@@ -21,8 +21,8 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import { IMQRPCDescription } from '..';
-import { Thunk } from '..';
+import { Thunk, IMQRPCDescription } from '..';
+import { registerType } from './property';
/**
* Implements '@indexed' decorator factory
@@ -42,31 +42,43 @@ import { Thunk } from '..';
* @return {(constructor: Function) => void}
*/
export function indexed(indexTypedef: string | Thunk): any {
- return function (constructor: Function): any {
- // istanbul ignore if
+ // Dual-mode: standard (TC39) class decorators pass a context object with a
+ // `kind` property; legacy ones pass only the constructor.
+ return function (value: any, context?: any): any {
+ const isStandard =
+ context && typeof context === 'object' && 'kind' in context;
+
if (!indexTypedef) {
- return ; // nothing to do here
+ return isStandard ? undefined : value; // nothing to do here
}
if (typeof indexTypedef === 'function') {
indexTypedef = indexTypedef();
}
- // istanbul ignore if
if (typeof indexTypedef !== 'string') {
indexTypedef = String(indexTypedef);
}
- const typeName = constructor.name;
+ if (isStandard) {
+ // registers any @property fields on this class plus the index type
+ registerType(value, context.metadata, indexTypedef as string);
+
+ return;
+ }
+
+ // legacy: @property already registered the fields directly, so only
+ // the index type needs to be attached to the existing description
+ const typeName = value.name;
- IMQRPCDescription.typesDescription[typeName] =
- IMQRPCDescription.typesDescription[typeName] || {
- indexType: indexTypedef as string,
+ IMQRPCDescription.typesDescription[typeName] = IMQRPCDescription
+ .typesDescription[typeName] || {
properties: {},
- inherits: Object.getPrototypeOf(constructor).name,
+ inherits: Object.getPrototypeOf(value).name,
};
+ IMQRPCDescription.typesDescription[typeName].indexType =
+ indexTypedef as string;
- IMQRPCDescription.typesDescription[typeName]
- .indexType = indexTypedef as string;
+ return value;
};
}
diff --git a/src/decorators/lock.ts b/src/decorators/lock.ts
index 8a56ed8..2b99bcc 100644
--- a/src/decorators/lock.ts
+++ b/src/decorators/lock.ts
@@ -21,7 +21,8 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import { IMQLock, AcquiredLock, signature } from '..';
+import { IMQLock, AcquiredLock } from '..';
+import { signature } from '../helpers';
export interface LockOptions {
disabled?: boolean;
@@ -29,39 +30,38 @@ export interface LockOptions {
}
/**
- * \@lock() decorator implementation
- * Will make all simultaneous similar method calls locked to be resolved with
- * the first obtained values. Similarity is identified by a bypassed method
- * argument values.
+ * Creates a `@lock()` method decorator. Concurrent calls to the decorated
+ * method that share the same arguments are coalesced: only the first call is
+ * executed, and all the others resolve with its result. Call similarity is
+ * determined by the method's argument values. The returned decorator is
+ * dual-mode: it works both as a standard (TC39) and as a legacy method
+ * decorator.
*
- * @param {boolean|LockOptions} enabledOrOptions - whether to enable locks or not
- * @return {(
- * target: any,
- * methodName: (string),
- * descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
- * ) => void}
+ * @param {boolean | LockOptions} enabledOrOptions - whether locking is enabled,
+ * or the lock options
+ * @return {Function} - a dual-mode method decorator
*/
-export function lock(enabledOrOptions: boolean | LockOptions = true) {
- return function(
- target: any,
- methodName: string | symbol,
- descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
- ) {
- const enabled = typeof enabledOrOptions === 'boolean'
+export function lock(enabledOrOptions: boolean | LockOptions = true): any {
+ const enabled =
+ typeof enabledOrOptions === 'boolean'
? enabledOrOptions
: !enabledOrOptions.disabled;
- const skipArgs = typeof enabledOrOptions === 'boolean'
+ const skipArgs =
+ typeof enabledOrOptions === 'boolean'
? undefined
: enabledOrOptions.skipArgs;
- const original = descriptor.value;
- const className: string = typeof target === 'function'
- ? target.name // static
- : target.constructor.name; // dynamic
- descriptor.value = async function(...args: any[]): Promise {
- const withLocks = !parseInt(
- process.env['DISABLE_LOCKS'] + ''
- ) && enabled;
+ const wrap = (
+ original: (...args: any[]) => any,
+ methodName: string | symbol,
+ isStatic: boolean,
+ ) =>
+ async function (this: any, ...args: any[]): Promise {
+ const className: string = isStatic
+ ? this.name // static: `this` is the class
+ : this.constructor.name; // dynamic: `this` is the instance
+ const withLocks =
+ !parseInt(process.env['DISABLE_LOCKS'] + '') && enabled;
let lock: AcquiredLock;
let sig: string = '';
@@ -69,9 +69,9 @@ export function lock(enabledOrOptions: boolean | LockOptions = true) {
sig = signature(
className,
methodName,
- skipArgs ? args.filter((_, index) =>
- !~skipArgs.indexOf(index)
- ) : args,
+ skipArgs
+ ? args.filter((_, index) => !~skipArgs.indexOf(index))
+ : args,
);
lock = await IMQLock.acquire(sig, undefined, {
className,
@@ -85,12 +85,13 @@ export function lock(enabledOrOptions: boolean | LockOptions = true) {
}
try {
- // istanbul ignore next
let result: any = original
? original.apply(this, args)
: undefined;
- if (result && result.then &&
+ if (
+ result &&
+ result.then &&
typeof result.then === 'function'
) {
result = await result;
@@ -102,14 +103,25 @@ export function lock(enabledOrOptions: boolean | LockOptions = true) {
return result;
} catch (err) {
- // istanbul ignore next
if (withLocks) {
IMQLock.release(sig, null, err);
}
- // istanbul ignore next
throw err;
}
};
- }
+
+ // Dual-mode: standard (TC39) invocations pass a context object with a
+ // `kind` property; legacy ones pass (target, propertyKey, descriptor).
+ return function (target: any, context: any, descriptor?: any): any {
+ if (context && typeof context === 'object' && 'kind' in context) {
+ return wrap(target, context.name, context.static);
+ }
+
+ const isStatic = typeof target === 'function';
+
+ descriptor.value = wrap(descriptor.value, context, isStatic);
+
+ return descriptor;
+ };
}
diff --git a/src/decorators/logged.ts b/src/decorators/logged.ts
index 26f91de..9134607 100644
--- a/src/decorators/logged.ts
+++ b/src/decorators/logged.ts
@@ -31,53 +31,46 @@ export interface LoggedDecoratorOptions {
doNotThrow?: boolean;
}
-// noinspection JSUnusedGlobalSymbols
/**
- * Logger decorator factory for class methods. Will try, catch and log errors
- * during method calls. If logger is bypassed, will use given logger, otherwise
- * will try to use logger defined on class dynamically or statically or will
- * fallback to console.
+ * Creates a `@logged()` method decorator that wraps the decorated method in a
+ * try/catch and logs any error it throws. The logger is resolved in this
+ * order: an explicitly passed logger, then a `logger` defined on the instance
+ * or on the class, and finally the global `console`. By default the error is
+ * re-thrown after being logged; pass `{ doNotThrow: true }` to swallow it. The
+ * returned decorator is dual-mode: it works both as a standard (TC39) and as a
+ * legacy method decorator.
*
- * @param {ILogger | LoggedDecoratorOptions} [options] - custom logger or
- * logged decorator
- * options
- * @return {Function} - decorator function
+ * @param {ILogger | LoggedDecoratorOptions} [options] - a logger to use, or the
+ * logged-decorator options
+ * @return {Function} - a dual-mode method decorator
*/
-export function logged(options?: ILogger | LoggedDecoratorOptions) {
- return (
- target: any,
- methodName: string | symbol,
- descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
- ) => {
- const original = descriptor.value;
- const level: LoggedLogLevel = (
- options &&
- (options as LoggedDecoratorOptions).level
- )
- ? (options as LoggedDecoratorOptions).level as LoggedLogLevel
+export function logged(options?: ILogger | LoggedDecoratorOptions): any {
+ const level: LoggedLogLevel =
+ options && (options as LoggedDecoratorOptions).level
+ ? ((options as LoggedDecoratorOptions).level as LoggedLogLevel)
: 'error';
- const doThrow = !options ||
- !(options as LoggedDecoratorOptions).doNotThrow;
+ const doThrow = !options || !(options as LoggedDecoratorOptions).doNotThrow;
- descriptor.value = async function(...args: any[]): Promise {
+ const wrap = (original: (...args: any[]) => any) =>
+ async function (this: any, ...args: any[]): Promise {
try {
- const ctx = (typeof this !== 'undefined' && this !== null)
- ? this
- : target;
-
if (original) {
- return await original.apply(ctx, args);
+ return await original.apply(this, args);
}
} catch (err) {
- const logger: ILogger = (options && (options as LoggedDecoratorOptions).logger)
- ? (options as LoggedDecoratorOptions).logger as ILogger
- : (options && (options as any).error)
- ? options as ILogger
- : (this && (this as any).logger)
- ? (this as any).logger as ILogger
- : (target && (target as any).logger)
- ? (target as any).logger as ILogger
- : console;
+ const logger: ILogger =
+ options && (options as LoggedDecoratorOptions).logger
+ ? ((options as LoggedDecoratorOptions)
+ .logger as ILogger)
+ : options && (options as any).error
+ ? (options as ILogger)
+ : this && (this as any).logger
+ ? ((this as any).logger as ILogger)
+ : this &&
+ this.constructor &&
+ (this.constructor as any).logger
+ ? ((this.constructor as any).logger as ILogger)
+ : console;
(logger as any)[level](err);
@@ -86,5 +79,18 @@ export function logged(options?: ILogger | LoggedDecoratorOptions) {
}
}
};
+
+ // Dual-mode: works as both a standard (TC39) and a legacy
+ // (experimentalDecorators) method decorator. Standard invocations pass a
+ // context object with a `kind` property; legacy ones pass
+ // (target, propertyKey, descriptor).
+ return function (target: any, context: any, descriptor?: any): any {
+ if (context && typeof context === 'object' && 'kind' in context) {
+ return wrap(target);
+ }
+
+ descriptor.value = wrap(descriptor.value);
+
+ return descriptor;
};
}
diff --git a/test/helpers/os-uuid.ts b/src/decorators/metadata.ts
similarity index 55%
rename from test/helpers/os-uuid.ts
rename to src/decorators/metadata.ts
index 0d375c6..d2df7ce 100644
--- a/test/helpers/os-uuid.ts
+++ b/src/decorators/metadata.ts
@@ -1,5 +1,5 @@
/*!
- * osUuid() Function Unit Tests
+ * IMQ-RPC Decorators: Symbol.metadata polyfill
*
* I'm Queue Software Project
* Copyright (C) 2025 imqueue.com
@@ -21,16 +21,23 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import '../mocks';
-import { expect } from 'chai';
-import { osUuid } from '../..';
-describe('helpers/osUuid()', () => {
- it('should be a function', () => {
- expect(typeof osUuid).to.equal('function');
- });
-
- it('should return same id for each call', () => {
- expect(osUuid()).to.equal(osUuid());
+/**
+ * Standard (TC39) decorator metadata relies on the well-known
+ * `Symbol.metadata`. Node.js does not implement it natively yet, so without
+ * this polyfill the compiler emits `context.metadata` as `undefined` and
+ * decorator metadata (used by @property/@expose/@indexed to collect complex
+ * type definitions) is unavailable.
+ *
+ * This module must be imported before any decorated class is defined; it is
+ * imported first by the decorator barrel, which every decorator consumer
+ * loads transitively.
+ */
+if (!(Symbol as { metadata?: symbol }).metadata) {
+ Object.defineProperty(Symbol, 'metadata', {
+ value: Symbol.for('Symbol.metadata'),
+ writable: false,
+ enumerable: false,
+ configurable: true,
});
-});
+}
diff --git a/src/decorators/property.ts b/src/decorators/property.ts
index 37a1c7a..051b9d7 100644
--- a/src/decorators/property.ts
+++ b/src/decorators/property.ts
@@ -87,53 +87,167 @@ export interface Thunk {
* descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
* ) => void}
*/
+interface CollectedProperty {
+ rawType: string | Thunk | any;
+ isOptional: boolean;
+}
+
+/**
+ * Per-class store of collected @property definitions, keyed off the shared
+ * decorator metadata object. Standard field decorators cannot see their
+ * class at decoration time, so properties are stashed here and flushed to
+ * the RPC type description by a class-level decorator (@expose on a type,
+ * or @indexed) once the class name is available.
+ */
+const PROPERTIES = Symbol('@imqueue/rpc:properties');
+
+/**
+ * Resolves a @property type argument (string, constructor, thunk, or array
+ * form) to its RPC type-definition string.
+ *
+ * @param {string | Thunk | any} input
+ * @return {string}
+ */
+function resolveTypeDef(input: string | Thunk | any): string {
+ let type: any = input;
+
+ if (typeof type === 'function' && !(type as Function).name) {
+ type = (type as () => any)();
+ }
+
+ let typeDef: any = type;
+
+ if (Array.isArray(typeDef)) {
+ typeDef = typeDef[0];
+ }
+
+ if (typeDef && typeof typeDef !== 'string') {
+ typeDef = typeDef.name;
+ }
+
+ if (Array.isArray(type)) {
+ typeDef += '[]';
+ }
+
+ if (!typeDef) {
+ typeDef = String(type);
+ }
+
+ return typeDef as string;
+}
+
+/**
+ * Flushes @property definitions collected on a class into the RPC type
+ * description. Invoked by class-level decorators once the class (and hence
+ * its name) is available.
+ *
+ * @param {Function} ctor - the decorated class constructor
+ * @param {DecoratorMetadata | undefined} metadata - shared decorator metadata
+ * @param {string} [indexType] - optional index signature definition
+ */
+export function registerType(
+ ctor: Function,
+ metadata: DecoratorMetadata | undefined,
+ indexType?: string,
+): void {
+ const typeName = ctor.name;
+ const collected: Record =
+ metadata && Object.prototype.hasOwnProperty.call(metadata, PROPERTIES)
+ ? (metadata as any)[PROPERTIES]
+ : {};
+
+ IMQRPCDescription.typesDescription[typeName] = IMQRPCDescription
+ .typesDescription[typeName] || {
+ properties: {},
+ inherits: Object.getPrototypeOf(ctor).name,
+ };
+
+ const description = IMQRPCDescription.typesDescription[typeName];
+
+ description.inherits = Object.getPrototypeOf(ctor).name;
+
+ for (const key of Object.keys(collected)) {
+ const { rawType, isOptional } = collected[key];
+ let resolved: string | undefined;
+
+ // resolve the type lazily on first read: standard decorators run
+ // before class bindings are initialized, so a thunk referencing the
+ // (self- or forward-referenced) type cannot be called during
+ // decoration — only once the description is actually consumed
+ Object.defineProperty(description.properties, key, {
+ enumerable: true,
+ configurable: true,
+ value: {
+ isOptional,
+ get type(): string {
+ if (resolved === undefined) {
+ resolved = resolveTypeDef(rawType);
+ }
+
+ return resolved;
+ },
+ },
+ });
+ }
+
+ if (indexType !== undefined) {
+ description.indexType = indexType;
+ }
+}
+
export function property(
type: string | Thunk | any,
isOptional: boolean = false,
): any {
- // istanbul ignore if
if (!type) {
- return ;
+ return;
}
- return function (
- target: any,
- propertyKey: string,
- ): any {
- const typeName = target.constructor.name;
- let typeDef: any;
+ // Dual-mode: standard (TC39) field decorators pass a context object with a
+ // `kind` property; legacy ones pass (prototype, propertyKey).
+ return function (target: any, context: any): any {
+ if (context && typeof context === 'object' && 'kind' in context) {
+ const metadata = context.metadata as any;
- if (typeof type === 'function' && !(type as Function).name) {
- type = (type as () => any)();
- }
+ // each class keeps its OWN property bag (metadata prototype-
+ // inherits from a base class, so we must not mutate the inherited
+ // one)
+ if (!Object.prototype.hasOwnProperty.call(metadata, PROPERTIES)) {
+ Object.defineProperty(metadata, PROPERTIES, {
+ value: {},
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ });
+ }
- typeDef = type;
+ // store the raw type; resolution is deferred to first read (see
+ // registerType) to avoid touching not-yet-initialized bindings
+ (metadata[PROPERTIES] as Record)[
+ String(context.name)
+ ] = {
+ rawType: type,
+ isOptional,
+ };
- if (Array.isArray(typeDef)) {
- typeDef = typeDef[0];
+ return;
}
- if (typeDef && typeof typeDef !== 'string') {
- typeDef = typeDef.name;
- }
-
- if (Array.isArray(type)) {
- typeDef += '[]';
- }
-
- // istanbul ignore if
- if (!typeDef) {
- typeDef = String(type);
- }
+ // legacy: the class is available at decoration time, so write the
+ // property straight into the RPC type description (no @classType flush
+ // is required in this mode)
+ const typeName = target.constructor.name;
- IMQRPCDescription.typesDescription[typeName] =
- IMQRPCDescription.typesDescription[typeName] || {
+ IMQRPCDescription.typesDescription[typeName] = IMQRPCDescription
+ .typesDescription[typeName] || {
properties: {},
inherits: Object.getPrototypeOf(target.constructor).name,
};
- IMQRPCDescription.typesDescription[typeName].properties[propertyKey] = {
- type: typeDef as string,
+ IMQRPCDescription.typesDescription[typeName].properties[
+ String(context)
+ ] = {
+ type: resolveTypeDef(type),
isOptional,
};
};
diff --git a/src/decorators/remote.ts b/src/decorators/remote.ts
index 8d75c1e..5efde1d 100644
--- a/src/decorators/remote.ts
+++ b/src/decorators/remote.ts
@@ -22,27 +22,32 @@
* to get commercial licensing options.
*/
/**
- * Implements '@remote' decorator factory
+ * Creates a `@remote()` method decorator for client classes. The decorated
+ * method has the remote method name appended to its arguments and is then
+ * forwarded to `remoteCall()`. The returned decorator is dual-mode: it works
+ * both as a standard (TC39) and as a legacy (experimentalDecorators) method
+ * decorator.
*
- * @return {(
- * target: any,
- * methodName: (string),
- * descriptor: TypedPropertyDescriptor<(...args: any[]) => any>
- * ) => void}
+ * @return {Function} - a dual-mode method decorator
*/
-export function remote() {
- return function(
- target: any,
- methodName: string,
- descriptor: TypedPropertyDescriptor<(...args: any[]) => any>,
- ) {
- const original = descriptor.value;
-
- descriptor.value = function(...args: any[]) {
+export function remote(): any {
+ const wrap = (original: (...args: any[]) => any, methodName: string) =>
+ function (this: any, ...args: any[]) {
args.push(methodName);
// istanbul ignore next
- return (original ? original.apply(this, args) : undefined);
+ return original ? original.apply(this, args) : undefined;
};
- }
+
+ // Dual-mode: standard (TC39) invocations pass a context object with a
+ // `kind` property; legacy ones pass (target, propertyKey, descriptor).
+ return function (target: any, context: any, descriptor?: any): any {
+ if (context && typeof context === 'object' && 'kind' in context) {
+ return wrap(target, String(context.name));
+ }
+
+ descriptor.value = wrap(descriptor.value, String(context));
+
+ return descriptor;
+ };
}
diff --git a/src/helpers/fs.ts b/src/helpers/fs.ts
index 2fc1862..eef26ed 100644
--- a/src/helpers/fs.ts
+++ b/src/helpers/fs.ts
@@ -21,59 +21,61 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import * as fs from 'fs';
+import { access, mkdir as fsMkdir, writeFile as fsWriteFile } from 'node:fs';
/**
- * Checks if file exists at given path
+ * Checks whether a file exists at the given path.
*
- * @param {string} path
- * @return {Promise}
+ * @param {string} path - path to the file to check
+ * @return {Promise} - true if the file exists, false otherwise
*/
-export function fileExists(path: string) {
- return new Promise(resolve => fs.access(path, err => resolve(!err)));
+export function fileExists(path: string): Promise {
+ return new Promise(resolve => access(path, err => resolve(!err)));
}
/**
- * Async mkdir
+ * Asynchronously creates a directory at the given path.
*
- * @param {string} path
- * @return {Promise}
+ * @param {string} path - path of the directory to create
+ * @return {Promise}
*/
-export function mkdir(path: string) {
- return new Promise((resolve, reject) => fs.mkdir(
- path,
- generalCallback.bind(null, resolve, reject)));
+export function mkdir(path: string): Promise {
+ return new Promise((resolve, reject) =>
+ fsMkdir(path, generalCallback.bind(null, resolve, reject)),
+ );
}
/**
- * Async writeFile
+ * Asynchronously writes the given content to a file at the given path.
*
- * @param {string} path
- * @param {string} content
- * @return {Promise}
+ * @param {string} path - path of the file to write
+ * @param {string} content - content to write to the file
+ * @return {Promise}
*/
-export function writeFile(path: string, content: string) {
+export function writeFile(path: string, content: string): Promise {
return new Promise((resolve, reject) =>
- fs.writeFile(
+ fsWriteFile(
path,
content,
{ encoding: 'utf8' },
- generalCallback.bind(null, resolve, reject)));
+ generalCallback.bind(null, resolve, reject),
+ ),
+ );
}
/**
- * Constructs and return callback which will resolve promise using
- * given resolver and reject'or
+ * Constructs a callback that settles a promise using the given resolve and
+ * reject functions, rejecting when an error is provided.
*
- * @param {() => void} resolve
- * @param {(err: Error) => void} reject
- * @param {Error} err
+ * @param {(value: void | PromiseLike) => void} resolve - promise resolver
+ * @param {(reason?: unknown) => void} reject - promise rejecter
+ * @param {Error | null} [err] - error passed by the underlying fs operation
*/
function generalCallback(
- resolve: () => void,
- reject: (err: Error) => void,
- err?: Error
-) {
+ resolve: (value: void | PromiseLike) => void,
+ reject: (reason?: unknown) => void,
+ err?: Error | null,
+): void {
if (err) {
reject(err);
}
diff --git a/src/helpers/os-uuid.ts b/src/helpers/os-uuid.ts
index 710c7f2..77fe00f 100644
--- a/src/helpers/os-uuid.ts
+++ b/src/helpers/os-uuid.ts
@@ -21,13 +21,112 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-const { machineIdSync } = require('node-machine-id');
+import { execSync } from 'node:child_process';
+import { createHash } from 'node:crypto';
/**
- * Returns machine UUID
+ * Returns the base path to the Windows registry query tool, accounting for
+ * 32-bit processes running on 64-bit Windows (which must go through the
+ * "sysnative" redirector to reach the real System32).
+ *
+ * @returns {string}
+ */
+function winRegBase(): string {
+ const useSysnative =
+ process.arch === 'ia32' &&
+ Object.prototype.hasOwnProperty.call(
+ process.env,
+ 'PROCESSOR_ARCHITEW6432',
+ );
+
+ return useSysnative
+ ? '%windir%\\sysnative\\cmd.exe /c %windir%\\System32'
+ : '%windir%\\System32';
+}
+
+/**
+ * Returns the platform-specific shell command that emits the raw machine
+ * identifier. Mirrors the commands used by the node-machine-id package.
+ *
+ * @returns {string}
+ */
+function idCommand(): string {
+ switch (process.platform) {
+ case 'darwin':
+ return 'ioreg -rd1 -c IOPlatformExpertDevice';
+ case 'win32':
+ return (
+ `${winRegBase()}\\REG.exe QUERY ` +
+ 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' +
+ '/v MachineGuid'
+ );
+ case 'linux':
+ return (
+ '( cat /var/lib/dbus/machine-id /etc/machine-id ' +
+ '2> /dev/null || hostname ) | head -n 1 || :'
+ );
+ case 'freebsd':
+ return 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid';
+ default:
+ throw new Error(`Unsupported platform: ${process.platform}`);
+ }
+}
+
+/**
+ * Extracts the bare GUID from the raw command output for the current platform.
+ *
+ * @param {string} raw - unprocessed command output
+ * @returns {string}
+ */
+function parseId(raw: string): string {
+ switch (process.platform) {
+ case 'darwin':
+ return raw
+ .split('IOPlatformUUID')[1]
+ .split('\n')[0]
+ .replace(/=|\s+|"/gi, '')
+ .toLowerCase();
+ case 'win32':
+ return raw
+ .split('REG_SZ')[1]
+ .replace(/\r+|\n+|\s+/gi, '')
+ .toLowerCase();
+ default: // linux, freebsd
+ return raw.replace(/\r+|\n+|\s+/gi, '').toLowerCase();
+ }
+}
+
+/**
+ * Returns the OS machine identifier synchronously, replicating the approach
+ * used by the node-machine-id package: it runs the platform-specific command,
+ * parses out the GUID, and, unless the original id is requested, returns its
+ * sha256 hash.
+ *
+ * @param {boolean} [original] - when true returns the raw machine GUID,
+ * otherwise returns its sha256 hash
+ * @returns {string}
+ */
+function machineIdSync(original?: boolean): string {
+ const id = parseId(execSync(idCommand()).toString());
+
+ return original ? id : createHash('sha256').update(id).digest('hex');
+}
+
+// The machine id cannot change while the process is running, so it is
+// resolved once and memoized: osUuid() is called from every client
+// constructor, and shelling out each time would needlessly slow startup.
+let cachedUuid: string | undefined;
+
+/**
+ * Returns the machine UUID. The underlying platform command runs only once
+ * per process; subsequent calls return the memoized value.
*
* @returns {string}
*/
export function osUuid(): string {
- return machineIdSync({ original: true });
+ if (cachedUuid === undefined) {
+ cachedUuid = machineIdSync(true);
+ }
+
+ return cachedUuid;
}
diff --git a/src/helpers/pid.ts b/src/helpers/pid.ts
index f1e1d56..de31430 100644
--- a/src/helpers/pid.ts
+++ b/src/helpers/pid.ts
@@ -21,42 +21,43 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import * as path from 'path';
-import * as fs from 'fs';
+import { resolve } from 'node:path';
+import { existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
import { ILogger } from '@imqueue/core';
-export const SIGNALS: string[] = [
- 'SIGTERM',
- 'SIGINT',
- 'SIGHUP',
- 'SIGQUIT',
-];
-export const IMQ_TMP_DIR = process.env.TMPDIR ||
- // istanbul ignore next
- '/tmp';
-export const IMQ_PID_DIR = path.resolve(IMQ_TMP_DIR, '.imq-rpc');
+/**
+ * OS signals that should trigger pid file cleanup on process termination.
+ */
+export const SIGNALS: string[] = ['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGQUIT'];
+
+/**
+ * Base temporary directory used by imq-rpc.
+ */
+export const IMQ_TMP_DIR = process.env.TMPDIR || '/tmp';
+
+/**
+ * Directory where imq-rpc stores its pid files.
+ */
+export const IMQ_PID_DIR = resolve(IMQ_TMP_DIR, '.imq-rpc');
/**
- * Returns increment-based process identifier for a given name
+ * Returns an increment-based process identifier for the given service name,
+ * creating the corresponding pid file under the given directory.
*
- * @param {string} name - name of a service to create pid file for
- * @param {string} path - directory to
- * @returns {number}
+ * @param {string} name - name of the service to create the pid file for
+ * @param {string} [path] - directory to store the pid file in
+ * @returns {number} - the allocated increment-based identifier
*/
-export function pid(
- name: string,
- // istanbul ignore next
- path: string = IMQ_PID_DIR
-): number {
+export function pid(name: string, path: string = IMQ_PID_DIR): number {
const pidFile = `${path}/${name}`;
const pidOpts: {
- encoding: string;
+ encoding: BufferEncoding;
mode?: string | number | undefined;
flag?: string | undefined;
} = { encoding: 'utf8', flag: 'wx' };
- if (!fs.existsSync(path)) {
- fs.mkdirSync(path);
+ if (!existsSync(path)) {
+ mkdirSync(path);
}
let id: number = 0;
@@ -64,21 +65,12 @@ export function pid(
while (!done) {
try {
- fs.writeFileSync(
- `${pidFile}-${id}.pid`,
- process.pid + '',
- pidOpts as any,
- );
+ writeFileSync(`${pidFile}-${id}.pid`, process.pid + '', pidOpts);
done = true;
- }
-
- catch (err) {
- // istanbul ignore next
- if (err.code === 'EEXIST') {
+ } catch (err) {
+ if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
id++;
- }
-
- else {
+ } else {
throw err;
}
}
@@ -88,23 +80,22 @@ export function pid(
}
/**
- * Removes pid file for a given name and id
+ * Removes the pid file for the given service name and identifier.
*
- * @param {string} name
- * @param {number} id
- * @param {ILogger} logger
- * @param {string} [path]
+ * @param {string} name - name of the service whose pid file to remove
+ * @param {number} id - increment-based identifier of the pid file
+ * @param {ILogger} logger - logger instance
+ * @param {string} [path] - directory the pid file is stored in
*/
export function forgetPid(
name: string,
id: number,
logger: ILogger,
- // istanbul ignore next
- path: string = IMQ_PID_DIR
-) {
+ path: string = IMQ_PID_DIR,
+): void {
try {
- fs.unlinkSync(`${path}/${name}-${id}.pid`);
+ unlinkSync(`${path}/${name}-${id}.pid`);
+ } catch {
+ /* ignore */
}
-
- catch (err) { /* ignore */ }
}
diff --git a/src/helpers/signature.ts b/src/helpers/signature.ts
index b03977a..4fa2a31 100644
--- a/src/helpers/signature.ts
+++ b/src/helpers/signature.ts
@@ -21,23 +21,105 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import { fingerprint64 } from 'farmhash';
+import { hash } from 'node:crypto';
/**
- * Constructs and returns hash string for a given set of className, methodName
- * and arguments.
+ * Serializes a value into a deterministic, JSON-compatible string. It mirrors
+ * `JSON.stringify` semantics (including `toJSON` support and how `undefined`,
+ * functions, and non-finite numbers are treated), with two deliberate
+ * differences that make it safe for hashing:
*
- * @param {string} className
- * @param {string | symbol} methodName
- * @param {any[]} args
- * @returns {string}
+ * - object keys are sorted, so two objects that differ only in key insertion
+ * order serialize identically (for objects whose keys are already sorted,
+ * the output is byte-identical to `JSON.stringify`, which keeps previously
+ * generated cache/lock keys valid);
+ * - circular references serialize as the marker string `"[Circular]"` instead
+ * of throwing. Repeated non-circular references to the same object are
+ * serialized in full each time, exactly as `JSON.stringify` does.
+ *
+ * @param {unknown} value - value to serialize
+ * @param {object[]} ancestors - stack of objects on the current descent path,
+ * used for cycle detection
+ * @returns {string | undefined} - the serialized value, or `undefined` where
+ * JSON would omit it (`undefined`, functions, and symbols)
+ */
+function serialize(value: unknown, ancestors: object[]): string | undefined {
+ if (value === null) {
+ return 'null';
+ }
+
+ switch (typeof value) {
+ case 'string':
+ return JSON.stringify(value);
+ case 'number':
+ return isFinite(value) ? String(value) : 'null';
+ case 'boolean':
+ return value ? 'true' : 'false';
+ case 'bigint':
+ // JSON.stringify throws on bigint; serialize it as a string
+ // instead so hashing never fails on valid runtime input
+ return `"${String(value)}"`;
+ case 'undefined':
+ case 'function':
+ case 'symbol':
+ return undefined;
+ }
+
+ const obj = value as Record & { toJSON?: () => unknown };
+
+ if (typeof obj.toJSON === 'function') {
+ return serialize(obj.toJSON(), ancestors);
+ }
+
+ if (ancestors.includes(obj)) {
+ return '"[Circular]"';
+ }
+
+ ancestors.push(obj);
+
+ try {
+ if (Array.isArray(obj)) {
+ const items = obj.map(item => serialize(item, ancestors) ?? 'null');
+
+ return `[${items.join(',')}]`;
+ }
+
+ const parts: string[] = [];
+
+ for (const key of Object.keys(obj).sort()) {
+ const serialized = serialize(obj[key], ancestors);
+
+ if (serialized !== undefined) {
+ parts.push(`${JSON.stringify(key)}:${serialized}`);
+ }
+ }
+
+ return `{${parts.join(',')}}`;
+ } finally {
+ ancestors.pop();
+ }
+}
+
+/**
+ * Constructs and returns a hash string for the given set of className,
+ * methodName, and arguments. The hash is deterministic: argument objects that
+ * differ only in key insertion order produce the same signature, and circular
+ * arguments are handled without throwing.
+ *
+ * @param {string} className - name of the class the method belongs to
+ * @param {string | symbol} methodName - name of the method being called
+ * @param {readonly unknown[]} args - arguments passed to the method
+ * @returns {string} - hexadecimal hash string
*/
export function signature(
className: string,
methodName: string | symbol,
- args: any[]
+ args: readonly unknown[],
): string {
- const data = JSON.stringify([className, methodName, args]);
- const hashBigInt = fingerprint64(data);
- return hashBigInt.toString(16);
+ const data = serialize([className, methodName, args], []) as string;
+
+ // one-shot native hash truncated to 64 bits (16 hex chars) — same key
+ // space as the previous farmhash fingerprint64, but ~3-4x faster per
+ // call on typical payloads (no WASM boundary, no bigint conversion)
+ return hash('sha256', data, 'hex').slice(0, 16);
}
diff --git a/src/index.ts b/src/index.ts
index a5f9f59..37247d8 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -20,7 +20,6 @@
* to get commercial licensing options.
*/
export * from './decorators';
-export * from './helpers';
export * from './cache';
export * from './IMQDelay';
export * from './IMQCache';
diff --git a/test/IMQCache.ts b/test/IMQCache.spec.ts
similarity index 68%
rename from test/IMQCache.ts
rename to test/IMQCache.spec.ts
index 1d68c4b..34342c9 100644
--- a/test/IMQCache.ts
+++ b/test/IMQCache.spec.ts
@@ -22,9 +22,9 @@
* to get commercial licensing options.
*/
import { logger } from './mocks';
-import { expect } from 'chai';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
import { IMQCache, RedisCache } from '..';
-import * as sinon from 'sinon';
describe('IMQCache', () => {
IMQCache.adapters = {};
@@ -36,29 +36,26 @@ describe('IMQCache', () => {
});
it('should be a class', () => {
- expect(typeof IMQCache).to.equal('function');
+ assert.equal(typeof IMQCache, 'function');
});
describe('register', () => {
it('should accept adapter name', () => {
IMQCache.register('RedisCache');
- expect(IMQCache.adapters['RedisCache'])
- .to.be.instanceOf(RedisCache);
+ assert.ok(IMQCache.adapters['RedisCache'] instanceof RedisCache);
});
it('should accept constructor', () => {
IMQCache.register(RedisCache);
- expect(IMQCache.adapters['RedisCache'])
- .to.be.instanceOf(RedisCache);
+ assert.ok(IMQCache.adapters['RedisCache'] instanceof RedisCache);
});
it('should accept instance', () => {
IMQCache.register(new RedisCache());
- expect(IMQCache.adapters['RedisCache'])
- .to.be.instanceOf(RedisCache);
+ assert.ok(IMQCache.adapters['RedisCache'] instanceof RedisCache);
});
});
@@ -66,23 +63,21 @@ describe('IMQCache', () => {
it('should apply provided options to adapter', () => {
IMQCache.register(RedisCache);
- expect((IMQCache).options['RedisCache']).to.be.undefined;
+ assert.equal((IMQCache).options['RedisCache'], undefined);
IMQCache.apply('RedisCache', { logger });
- expect((IMQCache).options['RedisCache'].logger)
- .to.be.equal(logger);
+ assert.equal((IMQCache).options['RedisCache'].logger, logger);
});
it('should work the same if adapter name provided', () => {
IMQCache.register(RedisCache);
- expect((IMQCache).options['RedisCache']).to.be.undefined;
+ assert.equal((IMQCache).options['RedisCache'], undefined);
IMQCache.apply(RedisCache, { logger });
- expect((IMQCache).options['RedisCache'].logger)
- .to.be.equal(logger);
+ assert.equal((IMQCache).options['RedisCache'].logger, logger);
});
});
@@ -91,26 +86,25 @@ describe('IMQCache', () => {
IMQCache.register(RedisCache);
IMQCache.apply(RedisCache, { logger });
- const spy = sinon.spy(IMQCache.adapters['RedisCache'], 'init');
+ const spy = mock.method(IMQCache.adapters['RedisCache'], 'init');
await IMQCache.init();
- expect(spy.called).to.be.true;
+ assert.equal(spy.mock.callCount() > 0, true);
});
});
describe('get()', () => {
it('should return undefined if nothing registered', () => {
- expect(IMQCache.get('RedisCache')).to.be.undefined;
- expect(IMQCache.get(RedisCache)).to.be.undefined;
+ assert.equal(IMQCache.get('RedisCache'), undefined);
+ assert.equal(IMQCache.get(RedisCache), undefined);
});
it('should return adapter if registered', () => {
IMQCache.register(RedisCache);
- expect(IMQCache.get('RedisCache')).to.be.instanceOf(RedisCache);
- expect(IMQCache.get(RedisCache)).to.be.instanceOf(RedisCache);
+ assert.ok(IMQCache.get('RedisCache') instanceof RedisCache);
+ assert.ok(IMQCache.get(RedisCache) instanceof RedisCache);
});
});
-
});
diff --git a/test/IMQClient.callTimeout.spec.ts b/test/IMQClient.callTimeout.spec.ts
new file mode 100644
index 0000000..b37c32d
--- /dev/null
+++ b/test/IMQClient.callTimeout.spec.ts
@@ -0,0 +1,149 @@
+/*!
+ * IMQClient per-call timeout tests
+ */
+import './mocks';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { IMQClient, IMQDelay, remote } from '..';
+import { logger } from './mocks';
+
+class TimeoutClient extends IMQClient {
+ @remote()
+ public async ping(imqDelay?: IMQDelay): Promise {
+ return this.remoteCall(...arguments);
+ }
+}
+
+describe('IMQClient call timeout', () => {
+ let client: TimeoutClient;
+
+ afterEach(async () => {
+ mock.timers.reset();
+ await client?.destroy();
+ mock.restoreAll();
+ });
+
+ it(
+ 'should reject a call that never gets a response once callTimeout ' +
+ 'elapses, and clean up its resolver',
+ async () => {
+ client = new TimeoutClient({ logger, callTimeout: 50 });
+ await client.start();
+
+ const imq: any = (client as any).imq;
+
+ // send succeeds, but the service never responds
+ mock.method(imq, 'send', async () => 'T1');
+ mock.timers.enable({ apis: ['setTimeout'] });
+
+ const call = client.ping();
+
+ // let the internal send() settle so the timeout timer is registered
+ await new Promise(resolve => setImmediate(resolve));
+
+ mock.timers.tick(60);
+
+ await assert.rejects(call, (err: any) => {
+ assert.equal(err.code, 'IMQ_RPC_CALL_TIMEOUT');
+ return true;
+ });
+
+ // the pending resolver must not leak
+ assert.deepEqual(Object.keys((client as any).resolvers), []);
+ },
+ );
+
+ it('should not reject a call that responds before the timeout', async () => {
+ client = new TimeoutClient({ logger, callTimeout: 50 });
+ await client.start();
+
+ const imq: any = (client as any).imq;
+
+ mock.method(imq, 'send', async (to: string, request: any) => {
+ const id = 'T2';
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'pong' }),
+ );
+ return id;
+ });
+
+ const result = await client.ping();
+
+ assert.equal(result, 'pong');
+
+ // ticking past the timeout after completion must be harmless
+ mock.timers.enable({ apis: ['setTimeout'] });
+ mock.timers.tick(100);
+ assert.deepEqual(Object.keys((client as any).resolvers), []);
+ });
+
+ it('should extend the timeout budget by the requested delay', async () => {
+ client = new TimeoutClient({ logger, callTimeout: 50 });
+ await client.start();
+
+ const imq: any = (client as any).imq;
+
+ mock.method(imq, 'send', async () => 'T3');
+ mock.timers.enable({ apis: ['setTimeout'] });
+
+ const call = client.ping(new IMQDelay(100));
+
+ // let the internal send() settle so the timeout timer is registered
+ await new Promise(resolve => setImmediate(resolve));
+
+ // 100ms delay + 50ms timeout: at 120ms the call must still be pending
+ mock.timers.tick(120);
+ assert.equal(
+ Object.keys((client as any).resolvers).includes('T3'),
+ true,
+ );
+
+ mock.timers.tick(40);
+
+ await assert.rejects(call, (err: any) => {
+ assert.equal(err.code, 'IMQ_RPC_CALL_TIMEOUT');
+ return true;
+ });
+ });
+
+ it('should reject the call when the underlying send throws', async () => {
+ client = new TimeoutClient({ logger });
+ await client.start();
+
+ const imq: any = (client as any).imq;
+
+ mock.method(imq, 'send', async () => {
+ throw new Error('send boom');
+ });
+
+ await assert.rejects(client.ping());
+ });
+
+ it('should not time out calls when callTimeout is not set', async () => {
+ client = new TimeoutClient({ logger });
+ await client.start();
+
+ const imq: any = (client as any).imq;
+
+ mock.method(imq, 'send', async (to: string, request: any) => {
+ const id = 'T4';
+ // respond a long virtual time later
+ setTimeout(
+ () => imq.emit('message', { to: id, request, data: 'late' }),
+ 120000,
+ );
+ return id;
+ });
+
+ mock.timers.enable({ apis: ['setTimeout'] });
+
+ const call = client.ping();
+
+ // let the internal send() settle so the response timer is registered
+ await new Promise(resolve => setImmediate(resolve));
+
+ mock.timers.tick(120001);
+
+ assert.equal(await call, 'late');
+ });
+});
diff --git a/test/IMQClient.console.logger.spec.ts b/test/IMQClient.console.logger.spec.ts
index b26343f..06d153d 100644
--- a/test/IMQClient.console.logger.spec.ts
+++ b/test/IMQClient.console.logger.spec.ts
@@ -2,133 +2,205 @@
* IMQClient console logger fallback branches coverage
*/
import './mocks';
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { IMQClient, IMQDelay, IMQMetadata, remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR } from '..';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ IMQClient,
+ IMQDelay,
+ IMQMetadata,
+ remote,
+ AFTER_HOOK_ERROR,
+ BEFORE_HOOK_ERROR,
+} from '..';
class ConsoleClient extends IMQClient {
- @remote()
- public async ok(name?: string, meta?: IMQMetadata, delay?: IMQDelay) {
- return this.remoteCall(...arguments);
- }
- @remote()
- public async boom() {
- return this.remoteCall(...arguments);
- }
+ @remote()
+ public async ok(name?: string, meta?: IMQMetadata, delay?: IMQDelay) {
+ return this.remoteCall(...arguments);
+ }
+ @remote()
+ public async boom() {
+ return this.remoteCall(...arguments);
+ }
}
describe('IMQClient console logger fallbacks', () => {
- let client: ConsoleClient;
-
- afterEach(async () => {
- try { await client?.destroy(); } catch { /* ignore */ }
- sinon.restore();
- });
-
- it('should use console logger when BEFORE hook fails', async () => {
- const warn = sinon.stub(console, 'warn' as any).callsFake(() => {});
- client = new ConsoleClient({ beforeCall: async () => { throw new Error('before oops'); } });
- await client.start();
-
- const imq: any = (client as any).imq;
- sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => {
- const id = 'C1';
- setImmediate(() => imq.emit('message', { to: id, request, data: 'good' }));
- return id;
+ let client: ConsoleClient;
+
+ afterEach(async () => {
+ try {
+ await client?.destroy();
+ } catch {
+ /* ignore */
+ }
+ mock.restoreAll();
});
- const res = await client.ok('x');
- expect(res).to.equal('good');
- expect(warn.called).to.equal(true);
- expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR);
- });
-
- it('should use console logger when AFTER hook fails (resolve and reject paths)', async () => {
- const warn = sinon.stub(console, 'warn' as any).callsFake(() => {});
- client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); } });
- await client.start();
-
- const imq: any = (client as any).imq;
- const send = sinon.stub(imq, 'send');
-
- // success path
- send.onFirstCall().callsFake(async (_to: string, request: any) => {
- const id = 'C2';
- setImmediate(() => imq.emit('message', { to: id, request, data: 'S' }));
- return id;
- });
+ it('should use console logger when BEFORE hook fails', async () => {
+ const warn = mock.method(console, 'warn' as any, () => {});
+ client = new ConsoleClient({
+ beforeCall: async () => {
+ throw new Error('before oops');
+ },
+ });
+ await client.start();
+
+ const imq: any = (client as any).imq;
+ mock.method(imq, 'send', async (_to: string, request: any) => {
+ const id = 'C1';
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'good' }),
+ );
+ return id;
+ });
- // reject path
- send.onSecondCall().callsFake(async (_to: string, request: any) => {
- const id = 'C3';
- setImmediate(() => imq.emit('message', { to: id, request, error: new Error('bad') }));
- return id;
+ const res = await client.ok('x');
+ assert.equal(res, 'good');
+ assert.equal(warn.mock.callCount() > 0, true);
+ assert.ok(
+ String(warn.mock.calls[0].arguments[0]).includes(BEFORE_HOOK_ERROR),
+ );
});
- const ok = await client.ok('ok');
- expect(ok).to.equal('S');
+ it('should use console logger when AFTER hook fails (resolve and reject paths)', async () => {
+ const warn = mock.method(console, 'warn' as any, () => {});
+ client = new ConsoleClient({
+ afterCall: async () => {
+ throw new Error('after oops');
+ },
+ });
+ await client.start();
- try { await client.boom(); } catch { /* expected */ }
+ const imq: any = (client as any).imq;
+ const send = mock.method(imq, 'send', () => {});
+
+ // success path (first call)
+ send.mock.mockImplementationOnce(async (_to: string, request: any) => {
+ const id = 'C2';
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'S' }),
+ );
+ return id;
+ }, 0);
+
+ // reject path (second call)
+ send.mock.mockImplementationOnce(async (_to: string, request: any) => {
+ const id = 'C3';
+ setImmediate(() =>
+ imq.emit('message', {
+ to: id,
+ request,
+ error: new Error('bad'),
+ }),
+ );
+ return id;
+ }, 1);
- expect(warn.callCount).to.be.greaterThan(0);
- const messages = warn.getCalls().map(c => String(c.args[0])).join(' ');
- expect(messages).to.contain(AFTER_HOOK_ERROR);
- });
+ const ok = await client.ok('ok');
+ assert.equal(ok, 'S');
+
+ try {
+ await client.boom();
+ } catch {
+ /* expected */
+ }
+
+ assert.ok(warn.mock.callCount() > 0);
+ const messages = warn.mock.calls
+ .map((c: any) => String(c.arguments[0]))
+ .join(' ');
+ assert.ok(messages.includes(AFTER_HOOK_ERROR));
+ });
it('should use right-hand console branch in remoteCall when BEFORE hook fails', async () => {
- const warn = sinon.stub(console, 'warn' as any).callsFake(() => {});
+ const warn = mock.method(console, 'warn' as any, () => {});
// Explicitly override default logger to be undefined to force `|| console` take the right branch
- client = new ConsoleClient({ beforeCall: async () => { throw new Error('before oops'); }, logger: undefined as any });
+ client = new ConsoleClient({
+ beforeCall: async () => {
+ throw new Error('before oops');
+ },
+ logger: undefined as any,
+ });
await client.start();
const imq: any = (client as any).imq;
- sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => {
+ mock.method(imq, 'send', async (_to: string, request: any) => {
const id = 'MB1';
- setImmediate(() => imq.emit('message', { to: id, request, data: 'ok' }));
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'ok' }),
+ );
return id;
});
const res = await client.ok('x');
- expect(res).to.equal('ok');
- expect(warn.called).to.equal(true);
- expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR);
+ assert.equal(res, 'ok');
+ assert.equal(warn.mock.callCount() > 0, true);
+ assert.ok(
+ String(warn.mock.calls[0].arguments[0]).includes(BEFORE_HOOK_ERROR),
+ );
});
it('should use right-hand console branch in imqCallResolver when AFTER hook fails on resolve', async () => {
- const warn = sinon.stub(console, 'warn' as any).callsFake(() => {});
- client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); }, logger: undefined as any });
+ const warn = mock.method(console, 'warn' as any, () => {});
+ client = new ConsoleClient({
+ afterCall: async () => {
+ throw new Error('after oops');
+ },
+ logger: undefined as any,
+ });
await client.start();
const imq: any = (client as any).imq;
- sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => {
+ mock.method(imq, 'send', async (_to: string, request: any) => {
const id = 'MB2';
- setImmediate(() => imq.emit('message', { to: id, request, data: 'S' }));
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'S' }),
+ );
return id;
});
const ok = await client.ok('ok');
- expect(ok).to.equal('S');
- expect(warn.callCount).to.be.greaterThan(0);
- const messages = warn.getCalls().map(c => String(c.args[0])).join(' ');
- expect(messages).to.contain(AFTER_HOOK_ERROR);
+ assert.equal(ok, 'S');
+ assert.ok(warn.mock.callCount() > 0);
+ const messages = warn.mock.calls
+ .map((c: any) => String(c.arguments[0]))
+ .join(' ');
+ assert.ok(messages.includes(AFTER_HOOK_ERROR));
});
it('should use right-hand console branch in imqCallRejector when AFTER hook fails on reject', async () => {
- const warn = sinon.stub(console, 'warn' as any).callsFake(() => {});
- client = new ConsoleClient({ afterCall: async () => { throw new Error('after oops'); }, logger: undefined as any });
+ const warn = mock.method(console, 'warn' as any, () => {});
+ client = new ConsoleClient({
+ afterCall: async () => {
+ throw new Error('after oops');
+ },
+ logger: undefined as any,
+ });
await client.start();
const imq: any = (client as any).imq;
- sinon.stub(imq, 'send').callsFake(async (_to: string, request: any) => {
+ mock.method(imq, 'send', async (_to: string, request: any) => {
const id = 'MB3';
- setImmediate(() => imq.emit('message', { to: id, request, error: new Error('bad') }));
+ setImmediate(() =>
+ imq.emit('message', {
+ to: id,
+ request,
+ error: new Error('bad'),
+ }),
+ );
return id;
});
- try { await client.boom(); } catch { /* expected */ }
-
- expect(warn.callCount).to.be.greaterThan(0);
- const messages = warn.getCalls().map(c => String(c.args[0])).join(' ');
- expect(messages).to.contain(AFTER_HOOK_ERROR);
+ try {
+ await client.boom();
+ } catch {
+ /* expected */
+ }
+
+ assert.ok(warn.mock.callCount() > 0);
+ const messages = warn.mock.calls
+ .map((c: any) => String(c.arguments[0]))
+ .join(' ');
+ assert.ok(messages.includes(AFTER_HOOK_ERROR));
});
});
diff --git a/test/IMQClient.extra.spec.ts b/test/IMQClient.extra.spec.ts
index fe57e81..e253056 100644
--- a/test/IMQClient.extra.spec.ts
+++ b/test/IMQClient.extra.spec.ts
@@ -2,14 +2,25 @@
* IMQClient Extra Unit Tests (non-RPC, using send stubs)
*/
import './mocks';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
import { logger } from './mocks';
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { IMQClient, IMQDelay, IMQMetadata, remote, AFTER_HOOK_ERROR, BEFORE_HOOK_ERROR } from '..';
+import {
+ IMQClient,
+ IMQDelay,
+ IMQMetadata,
+ remote,
+ AFTER_HOOK_ERROR,
+ BEFORE_HOOK_ERROR,
+} from '..';
class ExtraClient extends IMQClient {
@remote()
- public async greet(name?: string, imqMetadata?: IMQMetadata, imqDelay?: IMQDelay) {
+ public async greet(
+ name?: string,
+ imqMetadata?: IMQMetadata,
+ imqDelay?: IMQDelay,
+ ) {
return this.remoteCall(...arguments);
}
@remote()
@@ -23,79 +34,123 @@ describe('IMQClient (extra branches without service)', () => {
afterEach(async () => {
await client?.destroy();
- sinon.restore();
+ mock.restoreAll();
});
- it('should warn on BEFORE_HOOK_ERROR and continue call', async function() {
- this.timeout(5000);
- const warn = sinon.stub(logger, 'warn');
- client = new ExtraClient({ logger, beforeCall: async () => { throw new Error('before'); } });
+ it('should warn on BEFORE_HOOK_ERROR and continue call', async () => {
+ const warn = mock.method(logger, 'warn', () => {});
+ client = new ExtraClient({
+ logger,
+ beforeCall: async () => {
+ throw new Error('before');
+ },
+ });
await client.start();
const imq: any = (client as any).imq;
- sinon.stub(imq, 'send').callsFake(async (to: string, request: any, delay?: number) => {
- const id = 'ID1';
- setImmediate(() => imq.emit('message', { to: id, request, data: 'ok' }));
- return id;
- });
+ mock.method(
+ imq,
+ 'send',
+ async (to: string, request: any, delay?: number) => {
+ const id = 'ID1';
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'ok' }),
+ );
+ return id;
+ },
+ );
const res = await client.greet('imq');
- expect(res).to.equal('ok');
- expect(warn.called).to.equal(true);
- expect(String(warn.firstCall.args[0])).to.contain(BEFORE_HOOK_ERROR);
+ assert.equal(res, 'ok');
+ assert.equal(warn.mock.callCount() > 0, true);
+ assert.ok(
+ String(warn.mock.calls[0].arguments[0]).includes(BEFORE_HOOK_ERROR),
+ );
});
it('should warn on AFTER_HOOK_ERROR for resolve and reject paths', async () => {
- const warn = sinon.stub(logger, 'warn');
- client = new ExtraClient({ logger, afterCall: async () => { throw new Error('after'); } });
+ const warn = mock.method(logger, 'warn', () => {});
+ client = new ExtraClient({
+ logger,
+ afterCall: async () => {
+ throw new Error('after');
+ },
+ });
await client.start();
const imq: any = (client as any).imq;
- const send = sinon.stub(imq, 'send');
- // success path
- send.onFirstCall().callsFake(async (to: string, request: any) => {
+ const send = mock.method(imq, 'send', () => {});
+ // success path (first call)
+ send.mock.mockImplementationOnce(async (to: string, request: any) => {
const id = 'ID2';
- setImmediate(() => imq.emit('message', { to: id, request, data: 'success' }));
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'success' }),
+ );
return id;
- });
- // reject path
- send.onSecondCall().callsFake(async (to: string, request: any) => {
+ }, 0);
+ // reject path (second call)
+ send.mock.mockImplementationOnce(async (to: string, request: any) => {
const id = 'ID3';
- setImmediate(() => imq.emit('message', { to: id, request, error: new Error('boom') }));
+ setImmediate(() =>
+ imq.emit('message', {
+ to: id,
+ request,
+ error: new Error('boom'),
+ }),
+ );
return id;
- });
+ }, 1);
const ok = await client.greet('ok');
- expect(ok).to.equal('success');
- try { await client.fail(); } catch (e) { /* expected */ }
+ assert.equal(ok, 'success');
+ try {
+ await client.fail();
+ } catch (e) {
+ /* expected */
+ }
// both paths should warn due to afterCall throwing
- expect(warn.callCount).to.be.greaterThan(0);
- expect(warn.getCalls().map(c => String(c.args[0])).join(' ')).to.contain(AFTER_HOOK_ERROR);
+ assert.ok(warn.mock.callCount() > 0);
+ assert.ok(
+ warn.mock.calls
+ .map((c: any) => String(c.arguments[0]))
+ .join(' ')
+ .includes(AFTER_HOOK_ERROR),
+ );
});
it('should emit event when resolver is missing', async () => {
client = new ExtraClient({ logger });
await client.start();
- const evt = sinon.spy();
+ const evt = mock.fn();
client.on('greet', evt);
(client as any).imq.emit('message', {
to: 'unknown-id',
request: { method: 'greet' },
data: { foo: 'bar' },
});
- expect(evt.calledOnce).to.equal(true);
+ assert.equal(evt.mock.callCount() === 1, true);
});
it('should sanitize invalid IMQDelay and pass IMQMetadata through request', async () => {
client = new ExtraClient({ logger });
await client.start();
const imq: any = (client as any).imq;
- const sendStub = sinon.stub(imq, 'send').callsFake(async (to: string, request: any, delay?: number) => {
- expect(delay).to.equal(0);
- expect(request.metadata).to.be.instanceOf(IMQMetadata);
- const id = 'ID4';
- setImmediate(() => imq.emit('message', { to: id, request, data: 'x' }));
- return id;
- });
+ const sendStub = mock.method(
+ imq,
+ 'send',
+ async (to: string, request: any, delay?: number) => {
+ assert.equal(delay, 0);
+ assert.ok(request.metadata instanceof IMQMetadata);
+ const id = 'ID4';
+ setImmediate(() =>
+ imq.emit('message', { to: id, request, data: 'x' }),
+ );
+ return id;
+ },
+ );
const meta = new IMQMetadata({ a: 1 } as any);
- const res = await client.greet('z', meta as any, new IMQDelay(-100) as any);
- expect(res).to.equal('x');
- expect(sendStub.calledOnce).to.equal(true);
+ const res = await client.greet(
+ 'z',
+ meta as any,
+ new IMQDelay(-100) as any,
+ );
+ assert.equal(res, 'x');
+ assert.equal(sendStub.mock.callCount() === 1, true);
});
});
diff --git a/test/IMQClient.generator.trailing.args.spec.ts b/test/IMQClient.generator.trailing.args.spec.ts
index 1bb516c..d88eb3d 100644
--- a/test/IMQClient.generator.trailing.args.spec.ts
+++ b/test/IMQClient.generator.trailing.args.spec.ts
@@ -1,80 +1,99 @@
/*!
* IMQClient generator trailing args removal coverage test
*/
-import * as fs from 'fs';
-import { expect } from 'chai';
-import { IMQService, IMQClient, IMQDelay, IMQMetadata, expose, remote } from '..';
+import * as fs from 'node:fs';
+import mockRequire from 'mock-require';
+import { describe, it, before, after } from 'node:test';
+import assert from 'node:assert/strict';
+import { IMQService, IMQClient, IMQDelay, IMQMetadata, expose } from '..';
+import * as imqRpc from '..';
+
+// The generated client imports from '@imqueue/rpc'; map it to this package so
+// the compiled-and-required client resolves without a self node_modules link.
+mockRequire('@imqueue/rpc', imqRpc);
const CLIENTS_PATH = './test/clients-generator-trailing';
class GenTrailingService extends IMQService {
- @expose()
- public greet(name: string, meta?: IMQMetadata, delay?: IMQDelay) {
- return `hi ${name}`;
- }
+ /**
+ * @param {string} name
+ * @param {IMQMetadata} [meta]
+ * @param {IMQDelay} [delay]
+ * @return {string}
+ */
+ @expose()
+ public greet(name: string, meta?: IMQMetadata, delay?: IMQDelay) {
+ return `hi ${name}`;
+ }
}
// We don't need a manual client; the generator will create it dynamically.
-describe('IMQClient.generator trailing args removal (IMQDelay/IMQMetadata)', function () {
- this.timeout(10000);
- let service: GenTrailingService;
+describe('IMQClient.generator trailing args removal (IMQDelay/IMQMetadata)', () => {
+ let service: GenTrailingService;
- function rmdirr(path: string) {
- if (fs.existsSync(path)) {
- fs.readdirSync(path).forEach((file) => {
- const curPath = `${path}/${file}`;
- if (fs.lstatSync(curPath).isDirectory()) {
- rmdirr(curPath);
- } else {
- fs.unlinkSync(curPath);
+ function rmdirr(path: string) {
+ if (fs.existsSync(path)) {
+ fs.readdirSync(path).forEach(file => {
+ const curPath = `${path}/${file}`;
+ if (fs.lstatSync(curPath).isDirectory()) {
+ rmdirr(curPath);
+ } else {
+ fs.unlinkSync(curPath);
+ }
+ });
+ fs.rmdirSync(path);
}
- });
- fs.rmdirSync(path);
}
- }
-
- before(async () => {
- service = new GenTrailingService();
- await service.start();
- });
- after(async () => {
- await service.destroy();
- rmdirr(CLIENTS_PATH);
- });
+ before(async () => {
+ service = new GenTrailingService();
+ await service.start();
+ });
- it('should strip trailing metadata/delay from service description and add imqMetadata/imqDelay once', async () => {
- const mod: any = await IMQClient.create('GenTrailingService', {
- path: CLIENTS_PATH,
- compile: true,
- write: true,
+ after(async () => {
+ await service.destroy();
+ rmdirr(CLIENTS_PATH);
});
- // Ensure client class exists and can be instantiated
- const client = new mod.GenTrailingClient();
- await client.start();
+ it('should strip trailing metadata/delay from service description and add imqMetadata/imqDelay once', async () => {
+ const mod: any = await IMQClient.create('GenTrailingService', {
+ path: CLIENTS_PATH,
+ compile: true,
+ write: true,
+ });
+
+ // Ensure client class exists and can be instantiated
+ const client = new mod.GenTrailingClient();
+ await client.start();
- // Read generated TypeScript to verify the signature
- const tsPath = `${CLIENTS_PATH}/GenTrailingService.ts`;
- expect(fs.existsSync(tsPath)).to.equal(true);
- const src = fs.readFileSync(tsPath, 'utf8');
+ // Read generated TypeScript to verify the signature
+ const tsPath = `${CLIENTS_PATH}/GenTrailingService.ts`;
+ assert.equal(fs.existsSync(tsPath), true);
+ const src = fs.readFileSync(tsPath, 'utf8');
- // The generated method should not keep original parameter names 'meta' or 'delay'
- // as they are stripped and replaced by imqMetadata/imqDelay at the end once.
- const signatureRe = /public\s+async\s+greet\(([^)]*)\)/;
- const m = src.match(signatureRe);
- expect(m, 'method signature not found in generated client').to.not.equal(null);
- const signature = (m as RegExpMatchArray)[1];
+ // The generated method should not keep original parameter names 'meta' or 'delay'
+ // as they are stripped and replaced by imqMetadata/imqDelay at the end once.
+ const signatureRe = /public\s+async\s+greet\(([^)]*)\)/;
+ const m = src.match(signatureRe);
+ assert.notEqual(
+ m,
+ null,
+ 'method signature not found in generated client',
+ );
+ const signature = (m as RegExpMatchArray)[1];
- // It should include imqMetadata?: IMQMetadata and imqDelay?: IMQDelay
- expect(signature).to.match(/imqMetadata\?\s*:\s*IMQMetadata/);
- expect(signature).to.match(/imqDelay\?\s*:\s*IMQDelay/);
+ // It should include imqMetadata?: IMQMetadata and imqDelay?: IMQDelay
+ assert.match(signature, /imqMetadata\?\s*:\s*IMQMetadata/);
+ assert.match(signature, /imqDelay\?\s*:\s*IMQDelay/);
- // Original 'delay' parameter could still be present due to service signature,
- // but generator must add imq* params at the end; presence is verified above.
+ // the real arg keeps its JSDoc-derived type (proves standard-decorator +
+ // JSDoc reproduces correctly-typed client signatures, no empty types)
+ assert.match(signature, /name\s*:\s*string/);
+ // a trailing framework arg is detected via its JSDoc type and stripped
+ assert.doesNotMatch(signature, /\bdelay\?\s*:\s*IMQDelay/);
- // Cleanup
- await client.destroy();
- });
+ // Cleanup
+ await client.destroy();
+ });
});
diff --git a/test/IMQClient.generator.types.spec.ts b/test/IMQClient.generator.types.spec.ts
new file mode 100644
index 0000000..8253622
--- /dev/null
+++ b/test/IMQClient.generator.types.spec.ts
@@ -0,0 +1,140 @@
+/*!
+ * IMQClient generator coverage: type interfaces, method JSDoc, Promise return,
+ * and the non-compiling generation branch.
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+import * as fs from 'node:fs';
+import mockRequire from 'mock-require';
+import { describe, it, before, after } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ IMQService,
+ IMQClient,
+ expose,
+ property,
+ classType,
+ indexed,
+} from '..';
+import * as imqRpc from '..';
+
+mockRequire('@imqueue/rpc', imqRpc);
+
+const CLIENTS_PATH = './test/clients-generator-types';
+
+@classType()
+class GenPoint {
+ @property('number')
+ public x!: number;
+
+ @property('number', true)
+ public y?: number;
+}
+
+@classType()
+class GenPoint3D extends GenPoint {
+ @property('number')
+ public z!: number;
+}
+
+@indexed('[key: string]: string')
+@classType()
+class GenBag {}
+
+class GenTypesService extends IMQService {
+ /**
+ * Saves a point in space.
+ *
+ * @param {GenPoint3D} p - the point to save
+ * @return {void}
+ */
+ @expose()
+ public save(p: GenPoint3D) {
+ void p;
+ }
+
+ /**
+ * @return {Promise}
+ */
+ @expose()
+ public bare() {
+ return Promise.resolve();
+ }
+}
+
+function rmdirr(path: string): void {
+ if (fs.existsSync(path)) {
+ fs.readdirSync(path).forEach(file => {
+ const curPath = `${path}/${file}`;
+
+ if (fs.lstatSync(curPath).isDirectory()) {
+ rmdirr(curPath);
+ } else {
+ fs.unlinkSync(curPath);
+ }
+ });
+ fs.rmdirSync(path);
+ }
+}
+
+describe('IMQClient.generator type interfaces', () => {
+ let service: GenTypesService;
+
+ before(async () => {
+ service = new GenTypesService();
+ await service.start();
+ });
+ after(async () => {
+ await service.destroy();
+ rmdirr(CLIENTS_PATH);
+ });
+
+ it('emits interfaces, JSDoc and Promise, returning null uncompiled', async () => {
+ // compile: false exercises the non-compiling generation branch and
+ // returns null, while write: true still emits the .ts we inspect
+ const result = await IMQClient.create('GenTypesService', {
+ path: CLIENTS_PATH,
+ compile: false,
+ write: true,
+ });
+
+ assert.equal(result, null);
+
+ const src = fs.readFileSync(
+ `${CLIENTS_PATH}/GenTypesService.ts`,
+ 'utf8',
+ );
+
+ // registered types become interfaces; an inheriting type uses extends
+ assert.match(src, /export interface GenPoint\s*\{/);
+ assert.match(src, /export interface GenPoint3D\s+extends GenPoint/);
+ // @indexed type emits its index signature
+ assert.match(src, /export interface GenBag/);
+ assert.match(src, /\[key: string\]: string/);
+ // required and optional properties
+ assert.match(src, /x: number;/);
+ assert.match(src, /y\?: number;/);
+ // a method description renders into the generated JSDoc
+ assert.match(src, /Saves a point in space\./);
+ // a bare Promise return type becomes Promise
+ assert.match(src, /bare\([^)]*\)\s*:\s*Promise/);
+ });
+});
diff --git a/test/IMQClient.methods.spec.ts b/test/IMQClient.methods.spec.ts
index f02c286..85aa6b1 100644
--- a/test/IMQClient.methods.spec.ts
+++ b/test/IMQClient.methods.spec.ts
@@ -2,8 +2,8 @@
* IMQClient methods Unit Tests (subscribe/unsubscribe/broadcast + signals)
*/
import './mocks';
-import { expect } from 'chai';
-import * as sinon from 'sinon';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
import { IMQClient, remote } from '..';
import { logger } from './mocks';
@@ -19,63 +19,71 @@ describe('IMQClient methods', () => {
let client: MethodsClient;
afterEach(async () => {
- try { await client?.destroy(); } catch { /* ignore */ }
- sinon.restore();
+ try {
+ await client?.destroy();
+ } catch {
+ /* ignore */
+ }
+ mock.restoreAll();
});
it('should delegate subscribe() to subscriptionImq with service name', async () => {
client = new MethodsClient({ logger });
const subImq: any = (client as any).subscriptionImq;
- const spy = sinon.stub(subImq, 'subscribe').resolves();
- const handler = sinon.spy();
+ const spy = mock.method(subImq, 'subscribe', async () => {});
+ const handler = mock.fn();
await client.subscribe(handler as any);
- expect(spy.calledOnce).to.equal(true);
- expect(spy.firstCall.args[0]).to.equal(client.serviceName);
- expect(spy.firstCall.args[1]).to.equal(handler);
+ assert.equal(spy.mock.callCount() === 1, true);
+ assert.equal(spy.mock.calls[0].arguments[0], client.serviceName);
+ assert.equal(spy.mock.calls[0].arguments[1], handler);
});
it('should delegate unsubscribe() to subscriptionImq', async () => {
client = new MethodsClient({ logger });
const subImq: any = (client as any).subscriptionImq;
- const spy = sinon.stub(subImq, 'unsubscribe').resolves();
+ const spy = mock.method(subImq, 'unsubscribe', async () => {});
await client.unsubscribe();
- expect(spy.calledOnce).to.equal(true);
+ assert.equal(spy.mock.callCount() === 1, true);
});
it('should delegate broadcast() to imq.publish with queueName', async () => {
client = new MethodsClient({ logger });
const imq: any = (client as any).imq;
- const spy = sinon.stub(imq, 'publish').resolves();
+ const spy = mock.method(imq, 'publish', async () => {});
const payload: any = { hello: 'world' };
await client.broadcast(payload);
- expect(spy.calledOnce).to.equal(true);
- expect(spy.firstCall.args[0]).to.equal(payload);
- expect(spy.firstCall.args[1]).to.equal(client.queueName);
+ assert.equal(spy.mock.callCount() === 1, true);
+ assert.equal(spy.mock.calls[0].arguments[0], payload);
+ assert.equal(spy.mock.calls[0].arguments[1], client.queueName);
});
it('should handle process signals by calling destroy and then process.exit(0)', async () => {
const callbacks: Array<() => any> = [];
- const onStub = sinon.stub(process as any, 'on').callsFake((sig: any, cb: any) => {
- callbacks.push(cb);
- return process as any;
- });
- const exitStub = sinon.stub(process as any, 'exit');
- const clock = sinon.useFakeTimers();
+ const onStub = mock.method(
+ process as any,
+ 'on',
+ (sig: any, cb: any) => {
+ callbacks.push(cb);
+ return process as any;
+ },
+ );
+ const exitStub = mock.method(process as any, 'exit', () => {});
+ mock.timers.enable({ apis: ['setTimeout'] });
client = new MethodsClient({ logger });
- const destroyStub = sinon.stub(client, 'destroy').resolves();
+ const destroyStub = mock.method(client, 'destroy', async () => {});
// invoke the first registered signal handler (e.g., SIGTERM)
await callbacks[0]();
// fast-forward shutdown timeout
- clock.tick(10000); // IMQ_SHUTDOWN_TIMEOUT default
+ mock.timers.tick(10000); // IMQ_SHUTDOWN_TIMEOUT default
- expect(destroyStub.calledOnce).to.equal(true);
- expect(exitStub.called).to.equal(true);
- expect(exitStub.firstCall.args[0]).to.equal(0);
+ assert.equal(destroyStub.mock.callCount(), 1);
+ assert.ok(exitStub.mock.callCount() > 0);
+ assert.equal(exitStub.mock.calls[0].arguments[0], 0);
- clock.restore();
- onStub.restore();
- exitStub.restore();
+ mock.timers.reset();
+ onStub.mock.restore();
+ exitStub.mock.restore();
});
});
diff --git a/test/IMQClient.singleQueue.spec.ts b/test/IMQClient.singleQueue.spec.ts
new file mode 100644
index 0000000..f8a626e
--- /dev/null
+++ b/test/IMQClient.singleQueue.spec.ts
@@ -0,0 +1,143 @@
+/*!
+ * IMQClient singleQueue lifecycle tests
+ */
+import './mocks';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { IMQClient, remote } from '..';
+import { logger } from './mocks';
+
+class SingleQueueClient extends IMQClient {
+ @remote()
+ public async ping(): Promise {
+ return this.remoteCall(...arguments);
+ }
+}
+
+describe('IMQClient singleQueue lifecycle', () => {
+ const clients: SingleQueueClient[] = [];
+
+ const makeClient = (): SingleQueueClient => {
+ const client = new SingleQueueClient(
+ { logger, singleQueue: true },
+ 'Ping',
+ `SingleQueueClient${clients.length}`,
+ );
+
+ clients.push(client);
+
+ return client;
+ };
+
+ afterEach(async () => {
+ while (clients.length) {
+ try {
+ await clients.pop()?.destroy();
+ } catch {
+ /* ignore */
+ }
+ }
+ mock.restoreAll();
+ });
+
+ it('should not destroy the shared queue while other clients use it', async () => {
+ const first = makeClient();
+ const second = makeClient();
+
+ const sharedImq: any = (first as any).imq;
+
+ assert.equal(
+ sharedImq,
+ (second as any).imq,
+ 'both clients must share the same queue in singleQueue mode',
+ );
+
+ const destroySpy = mock.method(sharedImq, 'destroy', async () => {});
+
+ await first.destroy();
+ assert.equal(
+ destroySpy.mock.callCount(),
+ 0,
+ 'shared queue must survive while another client still uses it',
+ );
+
+ await second.destroy();
+ assert.equal(
+ destroySpy.mock.callCount(),
+ 1,
+ 'last client must destroy the shared queue',
+ );
+ });
+
+ it(
+ 'should create a fresh shared queue after the last client destroyed ' +
+ 'the previous one',
+ async () => {
+ const first = makeClient();
+ const firstImq: any = (first as any).imq;
+
+ mock.method(firstImq, 'destroy', async () => {});
+ await first.destroy();
+
+ const second = makeClient();
+
+ assert.notEqual(
+ (second as any).imq,
+ firstImq,
+ 'a new client must not reuse a destroyed shared queue',
+ );
+ },
+ );
+
+ it('should destroy the per-client subscription queue', async () => {
+ const client = makeClient();
+ const subscriptionImq: any = (client as any).subscriptionImq;
+
+ assert.notEqual(
+ subscriptionImq,
+ (client as any).imq,
+ 'singleQueue mode must use a separate subscription queue',
+ );
+
+ const destroySpy = mock.method(
+ subscriptionImq,
+ 'destroy',
+ async () => {},
+ );
+
+ await client.destroy();
+
+ assert.equal(
+ destroySpy.mock.callCount(),
+ 1,
+ 'subscription queue must be destroyed with the client',
+ );
+ });
+
+ it(
+ 'should tolerate double destroy without over-releasing the shared ' +
+ 'queue',
+ async () => {
+ const first = makeClient();
+ const second = makeClient();
+ const sharedImq: any = (first as any).imq;
+ const destroySpy = mock.method(
+ sharedImq,
+ 'destroy',
+ async () => {},
+ );
+
+ await first.destroy();
+ await first.destroy(); // double destroy must be a no-op
+
+ assert.equal(
+ destroySpy.mock.callCount(),
+ 0,
+ 'double destroy must not tear down the shared queue early',
+ );
+
+ await second.destroy();
+ assert.equal(destroySpy.mock.callCount(), 1);
+ },
+ );
+});
diff --git a/test/IMQClient.ts b/test/IMQClient.spec.ts
similarity index 61%
rename from test/IMQClient.ts
rename to test/IMQClient.spec.ts
index ad4bba4..d1973f9 100644
--- a/test/IMQClient.ts
+++ b/test/IMQClient.spec.ts
@@ -21,9 +21,10 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import * as mock from 'mock-require';
+import mockRequire from 'mock-require';
+import { describe, it, before, after, mock } from 'node:test';
+import assert from 'node:assert/strict';
import { logger } from './mocks';
-import { expect } from 'chai';
import {
IMQClient,
IMQService,
@@ -32,14 +33,12 @@ import {
expose,
remote,
} from '..';
-import * as sinon from 'sinon';
-import * as fs from 'fs';
+import * as fs from 'node:fs';
import * as imqRpc from '..';
-mock('@imqueue/rpc', imqRpc);
+mockRequire('@imqueue/rpc', imqRpc);
class TestService extends IMQService {
-
@expose()
public exposed() {
return 'exposed';
@@ -52,9 +51,8 @@ class TestService extends IMQService {
@expose()
public throwable() {
- throw new Error('No way!')
+ throw new Error('No way!');
}
-
}
class TestServiceClient extends IMQClient {
@@ -72,19 +70,16 @@ class TestServiceClient extends IMQClient {
public async throwable(delay?: IMQDelay) {
return await this.remoteCall(...arguments);
}
-
}
function rmdirr(path: string) {
if (fs.existsSync(path)) {
- fs.readdirSync(path).forEach((file) => {
+ fs.readdirSync(path).forEach(file => {
const curPath = `${path}/${file}`;
if (fs.lstatSync(curPath).isDirectory()) {
rmdirr(curPath);
- }
-
- else {
+ } else {
fs.unlinkSync(curPath);
}
});
@@ -105,7 +100,7 @@ describe('IMQClient', () => {
await service.start();
client = new TestServiceClient({ logger });
await client.start();
- } catch (err) {
+ } catch (err: any) {
console.error(err);
}
});
@@ -116,70 +111,83 @@ describe('IMQClient', () => {
});
it('should be a class', () => {
- expect(typeof IMQClient).to.equal('function');
+ assert.equal(typeof IMQClient, 'function');
});
describe('constructor', () => {
it('should throw if directly called', () => {
const Client: any = IMQClient;
- expect(() => new Client()).to.throw(TypeError);
+ assert.throws(() => new Client(), TypeError);
});
it('should not throw', () => {
- expect(() => new TestServiceClient()).not.to.throw;
+ assert.doesNotThrow(() => new TestServiceClient());
});
it('should use constructor name by default', () => {
- expect(new TestServiceClient().name)
- .to.contain('TestServiceClient');
+ assert.ok(
+ new TestServiceClient().name.includes('TestServiceClient'),
+ );
});
it('should use given name if provided', () => {
- expect(new TestServiceClient({}, undefined, 'TestClient').name)
- .to.contain('TestClient');
+ assert.ok(
+ new TestServiceClient(
+ {},
+ undefined,
+ 'TestClient',
+ ).name.includes('TestClient'),
+ );
});
- it('should re-use existing redis connection if singleQueue option '
- + 'enabled', () => {
- const options = { logger, singleQueue: true };
- const client1: any = new TestServiceClient(options);
- const client2: any = new TestServiceClient(options);
-
- expect(client1.imq).to.equal(client2.imq);
- });
-
- it('should not re-use existing redis connection if singleQueue option '
- + 'disabled', () => {
- const options = { logger, singleQueue: false };
- const client1: any = new TestServiceClient(options);
- const client2: any = new TestServiceClient(options);
-
- expect(client1.imq).to.not.equal(client2.imq);
- });
+ it(
+ 'should re-use existing redis connection if singleQueue option ' +
+ 'enabled',
+ () => {
+ const options = { logger, singleQueue: true };
+ const client1: any = new TestServiceClient(options);
+ const client2: any = new TestServiceClient(options);
+
+ assert.equal(client1.imq, client2.imq);
+ },
+ );
+
+ it(
+ 'should not re-use existing redis connection if singleQueue option ' +
+ 'disabled',
+ () => {
+ const options = { logger, singleQueue: false };
+ const client1: any = new TestServiceClient(options);
+ const client2: any = new TestServiceClient(options);
+
+ assert.notEqual(client1.imq, client2.imq);
+ },
+ );
});
describe('describe()', () => {
it('should return service description', async () => {
const description: Description = await client.describe();
- expect(description.service).not.to.be.undefined;
- expect(description.types).not.to.be.undefined;
- expect(description.service.methods.exposed).not.to.be.undefined;
- expect(description.service.methods.unexposed).to.be.undefined;
+ assert.notEqual(description.service, undefined);
+ assert.notEqual(description.types, undefined);
+ assert.notEqual(description.service.methods.exposed, undefined);
+ assert.equal(description.service.methods.unexposed, undefined);
});
it('should allow delayed calls', async () => {
const start = Date.now();
const description: Description = await client.describe(
- new IMQDelay(100));
+ new IMQDelay(100),
+ );
const time = Date.now() - start;
- expect(time).to.be.gte(100);
- expect(description.service).not.to.be.undefined;
- expect(description.types).not.to.be.undefined;
- expect(description.service.methods.exposed).not.to.be.undefined;
- expect(description.service.methods.unexposed).to.be.undefined;
+ assert.ok(time >= 100);
+ assert.notEqual(description.service, undefined);
+ assert.notEqual(description.types, undefined);
+ assert.notEqual(description.service.methods.exposed, undefined);
+ assert.equal(description.service.methods.unexposed, undefined);
});
});
@@ -187,10 +195,8 @@ describe('IMQClient', () => {
it('should throw if service returned error', async () => {
try {
await client.throwable();
- }
-
- catch (err) {
- expect(err.code).to.equal('IMQ_RPC_CALL_ERROR');
+ } catch (err: any) {
+ assert.equal(err.code, 'IMQ_RPC_CALL_ERROR');
}
});
});
@@ -198,23 +204,22 @@ describe('IMQClient', () => {
describe('create()', () => {
it('should create and return compiled module dynamically', async () => {
try {
- const testService: any = await IMQClient.create(
- 'TestService', { logger, path: CLIENTS_PATH });
- // noinspection TypeScriptUnresolvedFunction
+ const testService: any = await IMQClient.create('TestService', {
+ logger,
+ path: CLIENTS_PATH,
+ });
const cli = new testService.TestClient({ logger });
- expect(cli).instanceOf(IMQClient);
+ assert.ok(cli instanceof IMQClient);
const notExists = await new Promise(resolve =>
- fs.access(`${CLIENTS_PATH}/TestService.ts`, resolve));
- expect(!notExists).to.be.equal(
- true,
- 'TestService.ts does not exit'
+ fs.access(`${CLIENTS_PATH}/TestService.ts`, resolve),
);
+ assert.equal(!notExists, true, 'TestService.ts does not exit');
cli.destroy();
rmdirr(CLIENTS_PATH);
- } catch (err) {
+ } catch (err: any) {
rmdirr(CLIENTS_PATH);
throw err;
}
@@ -227,17 +232,17 @@ describe('IMQClient', () => {
path: CLIENTS_PATH,
write: false,
});
- // noinspection TypeScriptUnresolvedFunction
const cli = new testService.TestClient({ logger });
- expect(cli).instanceOf(IMQClient);
+ assert.ok(cli instanceof IMQClient);
const notExists = await new Promise(resolve =>
- fs.access(`${CLIENTS_PATH}/TestService.ts`, resolve));
- expect(!notExists).to.be.equal(false, 'TestService.ts exits');
+ fs.access(`${CLIENTS_PATH}/TestService.ts`, resolve),
+ );
+ assert.equal(!notExists, false, 'TestService.ts exits');
cli.destroy();
- } catch (err) {
+ } catch (err: any) {
throw err;
}
});
@@ -247,22 +252,22 @@ describe('IMQClient', () => {
await IMQClient.create('SomeService', {
logger,
path: CLIENTS_PATH,
- timeout: 200
+ timeout: 200,
});
- }
-
- catch (err) {
- expect(err.message).to.contain('service remote call timed-out');
+ } catch (err: any) {
+ assert.ok(
+ err.message.includes('service remote call timed-out'),
+ );
}
});
});
describe('stop()', () => {
it('should stop client form serving messages', async () => {
- const spy = sinon.spy((client).imq, 'stop' as any);
+ const spy = mock.method((client).imq, 'stop' as any);
await client.stop();
- expect(spy.called).to.be.true;
- spy.restore();
+ assert.equal(spy.mock.callCount() > 0, true);
+ spy.mock.restore();
});
});
});
diff --git a/test/IMQDelay.ts b/test/IMQDelay.spec.ts
similarity index 71%
rename from test/IMQDelay.ts
rename to test/IMQDelay.spec.ts
index ba8afbc..a2f002a 100644
--- a/test/IMQDelay.ts
+++ b/test/IMQDelay.spec.ts
@@ -22,22 +22,23 @@
* to get commercial licensing options.
*/
import './mocks';
-import { expect } from 'chai';
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
import { IMQDelay } from '..';
describe('IMQDelay', () => {
it('should be a class', () => {
- expect(typeof IMQDelay).to.equal('function');
+ assert.equal(typeof IMQDelay, 'function');
});
describe('constructor()', () => {
it('should construct ms time from given args', () => {
- expect(new IMQDelay(5).ms).to.equal(5);
- expect(new IMQDelay(5, 'ms').ms).to.equal(5);
- expect(new IMQDelay(5, 's').ms).to.equal(5000);
- expect(new IMQDelay(5, 'm').ms).to.equal(300000);
- expect(new IMQDelay(5, 'h').ms).to.equal(3600*5000);
- expect(new IMQDelay(5, 'd').ms).to.equal(86400*5000);
+ assert.equal(new IMQDelay(5).ms, 5);
+ assert.equal(new IMQDelay(5, 'ms').ms, 5);
+ assert.equal(new IMQDelay(5, 's').ms, 5000);
+ assert.equal(new IMQDelay(5, 'm').ms, 300000);
+ assert.equal(new IMQDelay(5, 'h').ms, 3600 * 5000);
+ assert.equal(new IMQDelay(5, 'd').ms, 86400 * 5000);
});
});
});
diff --git a/test/IMQLock.spec.ts b/test/IMQLock.spec.ts
new file mode 100644
index 0000000..66d0f49
--- /dev/null
+++ b/test/IMQLock.spec.ts
@@ -0,0 +1,130 @@
+/*!
+ * IMQLock Unit Tests
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+import './mocks';
+import { describe, it, before, after } from 'node:test';
+import assert from 'node:assert/strict';
+import { IMQLock, AcquiredLock } from '..';
+
+const LOCK_TIMEOUT = 100;
+const ORIGINAL_LOCK_TIMEOUT = IMQLock.deadlockTimeout;
+
+async function deadLocked() {
+ const lock: AcquiredLock =
+ await IMQLock.acquire('deadLocked');
+
+ if (IMQLock.locked('lockable')) {
+ try {
+ const res = await new Promise(resolve =>
+ setTimeout(
+ () =>
+ resolve(Math.random() * Math.random() + Math.random()),
+ LOCK_TIMEOUT,
+ ),
+ );
+ IMQLock.release('deadLocked', res);
+ return res;
+ } catch (err: any) {
+ IMQLock.release('deadLocked', null, err);
+ throw err;
+ }
+ }
+
+ return lock;
+}
+
+describe('IMQLock', () => {
+ (this as any).timeout = 30000;
+
+ before(() => {
+ IMQLock.deadlockTimeout = LOCK_TIMEOUT;
+ });
+ after(() => {
+ IMQLock.deadlockTimeout = ORIGINAL_LOCK_TIMEOUT;
+ });
+
+ it('should be a class', () => {
+ assert.equal(typeof IMQLock, 'function');
+ });
+
+ describe('acquire()', () => {
+ it('should avoid dead-locks using timeout', async () => {
+ try {
+ for (let i = 0; i < 10; ++i) {
+ await deadLocked();
+ }
+ } catch (err: any) {
+ assert.ok(err.message.includes('Lock timeout'));
+ }
+ });
+ });
+});
+
+describe('IMQLock queued callback errors', () => {
+ const originalTimeout = IMQLock.deadlockTimeout;
+ const originalLogger = IMQLock.logger;
+
+ before(() => {
+ // no deadlock timer, and a silent logger so a thrown callback is
+ // contained rather than logged to the console
+ IMQLock.deadlockTimeout = 0;
+ IMQLock.logger = {
+ log: () => {},
+ info: () => {},
+ warn: () => {},
+ error: () => {},
+ } as any;
+ });
+ after(() => {
+ IMQLock.deadlockTimeout = originalTimeout;
+ IMQLock.logger = originalLogger;
+ });
+
+ it('contains a throwing callback on the resolve path', async () => {
+ const key = 'cbResolveThrow';
+
+ await IMQLock.acquire(key);
+
+ const pending = IMQLock.acquire(key, () => {
+ throw new Error('resolve callback boom');
+ });
+
+ IMQLock.release(key, 'ok');
+
+ assert.equal(await pending, 'ok');
+ });
+
+ it('surfaces a throwing callback on the reject path', async () => {
+ const key = 'cbRejectThrow';
+
+ await IMQLock.acquire(key);
+
+ const pending = IMQLock.acquire(key, () => {
+ throw new Error('reject callback boom');
+ });
+
+ IMQLock.release(key, null, new Error('original rejection'));
+
+ await assert.rejects(pending, /reject callback boom/);
+ });
+});
diff --git a/test/IMQLock.stringify.metadata.spec.ts b/test/IMQLock.stringify.metadata.spec.ts
index 4cb6469..f1c9fdb 100644
--- a/test/IMQLock.stringify.metadata.spec.ts
+++ b/test/IMQLock.stringify.metadata.spec.ts
@@ -1,37 +1,44 @@
/*!
* IMQLock stringify(metadata) failure branch coverage test
*/
-import { expect } from 'chai';
+import { describe, it, beforeEach, afterEach } from 'node:test';
+import assert from 'node:assert/strict';
import { IMQLock } from '..';
describe('IMQLock acquire() timeout with unstringifiable metadata', () => {
- const KEY = 'circular-metadata-key';
- let originalTimeout: number;
+ const KEY = 'circular-metadata-key';
+ let originalTimeout: number;
- beforeEach(() => {
- originalTimeout = IMQLock.deadlockTimeout;
- IMQLock.deadlockTimeout = 10; // keep test fast
- });
+ beforeEach(() => {
+ originalTimeout = IMQLock.deadlockTimeout;
+ IMQLock.deadlockTimeout = 10; // keep test fast
+ });
- afterEach(() => {
- IMQLock.deadlockTimeout = originalTimeout;
- });
+ afterEach(() => {
+ IMQLock.deadlockTimeout = originalTimeout;
+ });
- it('should reject with error containing "Unable to stringify metadata"', async () => {
- // Acquire and hold the lock
- const first = await IMQLock.acquire(KEY);
- expect(first).to.equal(true);
+ it('should reject with error containing "Unable to stringify metadata"', async () => {
+ // Acquire and hold the lock
+ const first = await IMQLock.acquire(KEY);
+ assert.equal(first, true);
- // Prepare circular metadata that will make JSON.stringify throw
- const meta: any = { className: 'X', methodName: 'y', args: [] as any[] };
- (meta as any).self = meta; // circular reference
+ // Prepare circular metadata that will make JSON.stringify throw
+ const meta: any = {
+ className: 'X',
+ methodName: 'y',
+ args: [] as any[],
+ };
+ (meta as any).self = meta; // circular reference
- try {
- await IMQLock.acquire(KEY, undefined as any, meta);
- expect.fail('should have been rejected by timeout');
- } catch (err: any) {
- expect(err).to.be.instanceOf(Error);
- expect(String(err.message)).to.contain('Unable to stringify metadata');
- }
- });
+ try {
+ await IMQLock.acquire(KEY, undefined as any, meta);
+ assert.fail('should have been rejected by timeout');
+ } catch (err: any) {
+ assert.ok(err instanceof Error);
+ assert.ok(
+ String(err.message).includes('Unable to stringify metadata'),
+ );
+ }
+ });
});
diff --git a/test/IMQLock.ts b/test/IMQLock.ts
deleted file mode 100644
index 9f15f67..0000000
--- a/test/IMQLock.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-/*!
- * IMQLock Unit Tests
- *
- * I'm Queue Software Project
- * Copyright (C) 2025 imqueue.com
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- * If you want to use this code in a closed source (commercial) project, you can
- * purchase a proprietary commercial license. Please contact us at
- * to get commercial licensing options.
- */
-import './mocks';
-import { expect } from 'chai';
-import { IMQLock, AcquiredLock } from '..';
-
-const LOCK_TIMEOUT = 100;
-const ORIGINAL_LOCK_TIMEOUT = IMQLock.deadlockTimeout;
-
-async function deadLocked() {
- const lock: AcquiredLock =
- await IMQLock.acquire('deadLocked')
- ;
-
- if (IMQLock.locked('lockable')) {
- try {
- const res = await new Promise(resolve => setTimeout(() =>
- resolve(Math.random() * Math.random() + Math.random()),
- LOCK_TIMEOUT
- ));
- IMQLock.release('deadLocked', res);
- return res;
- }
-
- catch (err) {
- IMQLock.release('deadLocked', null, err);
- throw err;
- }
- }
-
- return lock;
-}
-
-describe('IMQLock', () => {
- (this as any).timeout = 30000;
-
- before(() => { IMQLock.deadlockTimeout = LOCK_TIMEOUT });
- after(() => { IMQLock.deadlockTimeout = ORIGINAL_LOCK_TIMEOUT });
-
- it('should be a class', () => {
- expect(typeof IMQLock).to.equal('function');
- });
-
- describe('acquire()', () => {
- it('should avoid dead-locks using timeout', async () => {
- try {
- for (let i = 0; i < 10; ++i) {
- await deadLocked();
- }
- }
-
- catch (err) {
- expect(err.message).to.contain('Lock timeout');
- }
-
- });
- });
-});
diff --git a/test/IMQMetadata.ts b/test/IMQMetadata.spec.ts
similarity index 77%
rename from test/IMQMetadata.ts
rename to test/IMQMetadata.spec.ts
index cfa1bba..69a482c 100644
--- a/test/IMQMetadata.ts
+++ b/test/IMQMetadata.spec.ts
@@ -1,5 +1,5 @@
/*!
- * IMQMetadata Unit Tests
+ * osUuid() Function Unit Tests
*
* I'm Queue Software Project
* Copyright (C) 2025 imqueue.com
@@ -22,23 +22,24 @@
* to get commercial licensing options.
*/
import './mocks';
-import { expect } from 'chai';
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
import { IMQMetadata } from '..';
describe('IMQMetadata', () => {
it('should be a class', () => {
- expect(typeof IMQMetadata).to.equal('function');
+ assert.equal(typeof IMQMetadata, 'function');
});
it('should copy provided metadata props to instance index', () => {
const data = { a: 1, b: 'x', c: { y: true } } as any;
const m = new IMQMetadata(data);
- // direct property access
- expect((m as any).a).to.equal(1);
- expect((m as any).b).to.equal('x');
- expect((m as any).c).to.deep.equal({ y: true });
- // index signature behavior
+
+ assert.equal((m as any).a, 1);
+ assert.equal((m as any).b, 'x');
+ assert.deepEqual((m as any).c, { y: true });
+
const keys = Object.keys(m);
- expect(keys.sort()).to.deep.equal(['a','b','c']);
+ assert.deepEqual(keys.sort(), ['a', 'b', 'c']);
});
});
diff --git a/test/IMQService.afterCall.spec.ts b/test/IMQService.afterCall.spec.ts
index 9e81246..708e8c0 100644
--- a/test/IMQService.afterCall.spec.ts
+++ b/test/IMQService.afterCall.spec.ts
@@ -21,16 +21,11 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { uuid } from '@imqueue/core';
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomUUID as uuid } from 'node:crypto';
import { logger } from './mocks';
-import {
- IMQService,
- IMQRPCRequest,
- expose,
- AFTER_HOOK_ERROR,
-} from '..';
+import { IMQService, IMQRPCRequest, expose, AFTER_HOOK_ERROR } from '..';
class AfterHookService extends IMQService {
@expose()
@@ -40,13 +35,15 @@ class AfterHookService extends IMQService {
@expose()
public async asyncHello(name: string) {
- return await new Promise(resolve => setTimeout(() => resolve(`Hello, ${name}!`), 5));
+ return await new Promise(resolve =>
+ setTimeout(() => resolve(`Hello, ${name}!`), 5),
+ );
}
}
describe('IMQService hooks (afterCall) and promise branch', () => {
it('should execute afterCall hook without error', async () => {
- const afterCall = sinon.spy(async () => {});
+ const afterCall = mock.fn(async () => {});
const service: any = new AfterHookService({ logger, afterCall });
const request: IMQRPCRequest = {
from: 'HookClient',
@@ -54,27 +51,31 @@ describe('IMQService hooks (afterCall) and promise branch', () => {
args: [],
};
const id = uuid();
- const sendSpy = sinon.spy(service.imq, 'send');
+ const sendSpy = mock.method(service.imq, 'send');
await service.start();
service.imq.emit('message', request, id);
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(sendSpy.called).to.be.true;
- expect(afterCall.calledOnce).to.be.true;
- resolve(undefined);
- } catch (err) {
- reject(err);
- }
- }));
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(sendSpy.mock.callCount() > 0, true);
+ assert.equal(afterCall.mock.callCount() === 1, true);
+ resolve(undefined);
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
await service.destroy();
});
it('should catch afterCall error and log AFTER_HOOK_ERROR', async () => {
- const warnSpy = sinon.spy(logger, 'warn');
- const afterCall = async () => { throw new Error('after boom'); };
+ const warnSpy = mock.method(logger, 'warn');
+ const afterCall = async () => {
+ throw new Error('after boom');
+ };
const service: any = new AfterHookService({ logger, afterCall });
const request: IMQRPCRequest = {
from: 'HookClient',
@@ -86,25 +87,33 @@ describe('IMQService hooks (afterCall) and promise branch', () => {
await service.start();
service.imq.emit('message', request, id);
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(warnSpy.called).to.be.true;
- const hasAfter = warnSpy.getCalls().some((c: any) => c.args && c.args[0] === AFTER_HOOK_ERROR);
- expect(hasAfter).to.be.true;
- resolve(undefined);
- } catch (err) {
- reject(err);
- }
- }));
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(warnSpy.mock.callCount() > 0, true);
+ const hasAfter = warnSpy.mock.calls.some(
+ (c: any) =>
+ c.arguments && c.arguments[0] === AFTER_HOOK_ERROR,
+ );
+ assert.equal(hasAfter, true);
+ resolve(undefined);
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
- warnSpy.restore();
+ warnSpy.mock.restore();
await service.destroy();
});
it('should await promise-returning method result before sending', async () => {
const service: any = new AfterHookService({ logger });
// make an exposed method return a Promise to hit thenable branch
- service.exposed = async () => await new Promise(resolve => setTimeout(() => resolve('Hello, IMQ!'), 5));
+ service.exposed = async () =>
+ await new Promise(resolve =>
+ setTimeout(() => resolve('Hello, IMQ!'), 5),
+ );
const request: IMQRPCRequest = {
from: 'HookClient',
method: 'exposed',
@@ -114,15 +123,19 @@ describe('IMQService hooks (afterCall) and promise branch', () => {
// stub to assert when send() is actually called, after promise resolved
const done = new Promise((resolve, reject) => {
- sinon.stub(service.imq, 'send').callsFake(async (_to: string, response: any) => {
- try {
- expect(response.data).to.equal('Hello, IMQ!');
- resolve();
- } catch (err) {
- reject(err);
- }
- return id;
- });
+ mock.method(
+ service.imq,
+ 'send',
+ async (_to: string, response: any) => {
+ try {
+ assert.equal(response.data, 'Hello, IMQ!');
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ return id;
+ },
+ );
});
await service.start();
diff --git a/test/IMQService.console.logger.spec.ts b/test/IMQService.console.logger.spec.ts
index 72058c8..b1272bb 100644
--- a/test/IMQService.console.logger.spec.ts
+++ b/test/IMQService.console.logger.spec.ts
@@ -1,74 +1,108 @@
/*!
* IMQService logger fallback coverage test
*/
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { uuid } from '@imqueue/core';
-import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR } from '..';
+import { describe, it, beforeEach, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomUUID as uuid } from 'node:crypto';
+import {
+ IMQService,
+ IMQRPCRequest,
+ expose,
+ BEFORE_HOOK_ERROR,
+ AFTER_HOOK_ERROR,
+} from '..';
class ConsoleLoggerService extends IMQService {
- @expose()
- public ping() { return 'pong'; }
+ @expose()
+ public ping() {
+ return 'pong';
+ }
}
describe('IMQService handleRequest logger fallback to console', () => {
- let warnStub: sinon.SinonSpy;
-
- beforeEach(() => {
- warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {});
- });
-
- afterEach(async () => {
- sinon.restore();
- });
-
- it('should use console when no custom logger provided and catch BEFORE hook error', async () => {
- const beforeCall = async () => { throw new Error('before fails'); };
- const service: any = new ConsoleLoggerService({ beforeCall }); // no logger provided
-
- const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] };
- const id = uuid();
-
- await service.start();
-
- // Spy send to ensure regular flow continues and send is called even without afterCall
- const sendSpy = sinon.spy(service.imq, 'send');
-
- service.imq.emit('message', request, id);
-
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(warnStub.called).to.equal(true);
- const hasBefore = warnStub.getCalls().some(c => c.args && c.args[0] === BEFORE_HOOK_ERROR);
- expect(hasBefore).to.equal(true);
- expect(sendSpy.called).to.equal(true);
- resolve(undefined);
- } catch (e) { reject(e); }
- }, 1));
-
- await service.destroy();
- });
-
- it('should use console when afterCall throws (send() logger fallback)', async () => {
- const afterCall = async () => { throw new Error('after fails'); };
- const service: any = new ConsoleLoggerService({ afterCall }); // no logger provided
-
- const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] };
- const id = uuid();
-
- await service.start();
-
- service.imq.emit('message', request, id);
-
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(warnStub.called).to.equal(true);
- const hasAfter = warnStub.getCalls().some(c => c.args && c.args[0] === AFTER_HOOK_ERROR);
- expect(hasAfter).to.equal(true);
- resolve(undefined);
- } catch (e) { reject(e); }
- }, 1));
-
- await service.destroy();
- });
+ let warnStub: any;
+
+ beforeEach(() => {
+ warnStub = mock.method(console, 'warn' as any, () => {});
+ });
+
+ afterEach(async () => {
+ mock.restoreAll();
+ });
+
+ it('should use console when no custom logger provided and catch BEFORE hook error', async () => {
+ const beforeCall = async () => {
+ throw new Error('before fails');
+ };
+ const service: any = new ConsoleLoggerService({ beforeCall }); // no logger provided
+
+ const request: IMQRPCRequest = {
+ from: 'Client',
+ method: 'ping',
+ args: [],
+ };
+ const id = uuid();
+
+ await service.start();
+
+ // Spy send to ensure regular flow continues and send is called even without afterCall
+ const sendSpy = mock.method(service.imq, 'send');
+
+ service.imq.emit('message', request, id);
+
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(warnStub.mock.callCount() > 0, true);
+ const hasBefore = warnStub.mock.calls.some(
+ (c: any) =>
+ c.arguments && c.arguments[0] === BEFORE_HOOK_ERROR,
+ );
+ assert.equal(hasBefore, true);
+ assert.equal(sendSpy.mock.callCount() > 0, true);
+ resolve(undefined);
+ } catch (e) {
+ reject(e);
+ }
+ }, 1),
+ );
+
+ await service.destroy();
+ });
+
+ it('should use console when afterCall throws (send() logger fallback)', async () => {
+ const afterCall = async () => {
+ throw new Error('after fails');
+ };
+ const service: any = new ConsoleLoggerService({ afterCall }); // no logger provided
+
+ const request: IMQRPCRequest = {
+ from: 'Client',
+ method: 'ping',
+ args: [],
+ };
+ const id = uuid();
+
+ await service.start();
+
+ service.imq.emit('message', request, id);
+
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(warnStub.mock.callCount() > 0, true);
+ const hasAfter = warnStub.mock.calls.some(
+ (c: any) =>
+ c.arguments && c.arguments[0] === AFTER_HOOK_ERROR,
+ );
+ assert.equal(hasAfter, true);
+ resolve(undefined);
+ } catch (e) {
+ reject(e);
+ }
+ }, 1),
+ );
+
+ await service.destroy();
+ });
});
diff --git a/test/IMQService.hooks.spec.ts b/test/IMQService.hooks.spec.ts
index f3e0b09..6420d8d 100644
--- a/test/IMQService.hooks.spec.ts
+++ b/test/IMQService.hooks.spec.ts
@@ -21,16 +21,11 @@
* purchase a proprietary commercial license. Please contact us at
* to get commercial licensing options.
*/
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { uuid } from '@imqueue/core';
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomUUID as uuid } from 'node:crypto';
import { logger } from './mocks';
-import {
- IMQService,
- IMQRPCRequest,
- expose,
- BEFORE_HOOK_ERROR,
-} from '..';
+import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR } from '..';
class HookTestService extends IMQService {
@expose()
@@ -41,7 +36,7 @@ class HookTestService extends IMQService {
describe('IMQService hooks (beforeCall)', () => {
it('should execute beforeCall hook without error', async () => {
- const beforeCall = sinon.spy(async () => {});
+ const beforeCall = mock.fn(async () => {});
const service: any = new HookTestService({ logger, beforeCall });
const request: IMQRPCRequest = {
from: 'HookClient',
@@ -49,28 +44,32 @@ describe('IMQService hooks (beforeCall)', () => {
args: [],
};
const id = uuid();
- const sendSpy = sinon.spy(service.imq, 'send');
+ const sendSpy = mock.method(service.imq, 'send');
await service.start();
service.imq.emit('message', request, id);
// wait for async send
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(beforeCall.calledOnce).to.be.true;
- expect(sendSpy.called).to.be.true;
- resolve(undefined);
- } catch (err) {
- reject(err);
- }
- }));
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(beforeCall.mock.callCount() === 1, true);
+ assert.equal(sendSpy.mock.callCount() > 0, true);
+ resolve(undefined);
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
await service.destroy();
});
it('should catch beforeCall error and log BEFORE_HOOK_ERROR', async () => {
- const warnSpy = sinon.spy(logger, 'warn');
- const beforeCall = async () => { throw new Error('boom'); };
+ const warnSpy = mock.method(logger, 'warn');
+ const beforeCall = async () => {
+ throw new Error('boom');
+ };
const service: any = new HookTestService({ logger, beforeCall });
const request: IMQRPCRequest = {
from: 'HookClient',
@@ -82,18 +81,23 @@ describe('IMQService hooks (beforeCall)', () => {
await service.start();
service.imq.emit('message', request, id);
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(warnSpy.called).to.be.true;
- const calledWithBefore = warnSpy.getCalls().some((c: any) => c.args && c.args[0] === BEFORE_HOOK_ERROR);
- expect(calledWithBefore).to.be.true;
- resolve(undefined);
- } catch (err) {
- reject(err);
- }
- }));
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(warnSpy.mock.callCount() > 0, true);
+ const calledWithBefore = warnSpy.mock.calls.some(
+ (c: any) =>
+ c.arguments && c.arguments[0] === BEFORE_HOOK_ERROR,
+ );
+ assert.equal(calledWithBefore, true);
+ resolve(undefined);
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
- warnSpy.restore();
+ warnSpy.mock.restore();
await service.destroy();
});
});
diff --git a/test/IMQService.logger.fallback.spec.ts b/test/IMQService.logger.fallback.spec.ts
index 558aab5..cf5d200 100644
--- a/test/IMQService.logger.fallback.spec.ts
+++ b/test/IMQService.logger.fallback.spec.ts
@@ -1,64 +1,104 @@
/*!
* IMQService logger fallback branches coverage tests
*/
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { uuid } from '@imqueue/core';
-import { IMQService, IMQRPCRequest, expose, BEFORE_HOOK_ERROR, AFTER_HOOK_ERROR } from '..';
+import { describe, it, beforeEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomUUID as uuid } from 'node:crypto';
+import {
+ IMQService,
+ IMQRPCRequest,
+ expose,
+ BEFORE_HOOK_ERROR,
+ AFTER_HOOK_ERROR,
+} from '..';
class FallbackLoggerService extends IMQService {
- @expose()
- public ping() { return 'pong'; }
+ @expose()
+ public ping() {
+ return 'pong';
+ }
}
describe('IMQService logger fallback branches', () => {
- beforeEach(() => sinon.restore());
+ beforeEach(() => mock.restoreAll());
- it('should use console (fallback) inside handleRequest when beforeCall throws and options.logger is undefined', async () => {
- const warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {});
- const beforeCall = async () => { throw new Error('boom'); };
- // Explicitly override default logger to undefined to force fallback branch
- const service: any = new FallbackLoggerService({ beforeCall, logger: undefined as any });
+ it('should use console (fallback) inside handleRequest when beforeCall throws and options.logger is undefined', async () => {
+ const warnStub = mock.method(console, 'warn' as any, () => {});
+ const beforeCall = async () => {
+ throw new Error('boom');
+ };
+ // Explicitly override default logger to undefined to force fallback branch
+ const service: any = new FallbackLoggerService({
+ beforeCall,
+ logger: undefined as any,
+ });
- const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] };
- const id = uuid();
+ const request: IMQRPCRequest = {
+ from: 'Client',
+ method: 'ping',
+ args: [],
+ };
+ const id = uuid();
- await service.start();
- service.imq.emit('message', request, id);
+ await service.start();
+ service.imq.emit('message', request, id);
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(warnStub.called).to.equal(true);
- const hasBefore = warnStub.getCalls().some(c => c.args && c.args[0] === BEFORE_HOOK_ERROR);
- expect(hasBefore).to.equal(true);
- resolve(undefined);
- } catch (e) { reject(e); }
- }, 1));
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(warnStub.mock.callCount() > 0, true);
+ const hasBefore = warnStub.mock.calls.some(
+ (c: any) =>
+ c.arguments && c.arguments[0] === BEFORE_HOOK_ERROR,
+ );
+ assert.equal(hasBefore, true);
+ resolve(undefined);
+ } catch (e) {
+ reject(e);
+ }
+ }, 1),
+ );
- await service.destroy();
- });
+ await service.destroy();
+ });
- it('should use console (fallback) inside send() when afterCall throws and options.logger is undefined', async () => {
- const warnStub = sinon.stub(console, 'warn' as any).callsFake(() => {});
- const afterCall = async () => { throw new Error('after fails'); };
- // Explicitly override default logger to undefined to force fallback branch
- const service: any = new FallbackLoggerService({ afterCall, logger: undefined as any });
+ it('should use console (fallback) inside send() when afterCall throws and options.logger is undefined', async () => {
+ const warnStub = mock.method(console, 'warn' as any, () => {});
+ const afterCall = async () => {
+ throw new Error('after fails');
+ };
+ // Explicitly override default logger to undefined to force fallback branch
+ const service: any = new FallbackLoggerService({
+ afterCall,
+ logger: undefined as any,
+ });
- const request: IMQRPCRequest = { from: 'Client', method: 'ping', args: [] };
- const id = uuid();
+ const request: IMQRPCRequest = {
+ from: 'Client',
+ method: 'ping',
+ args: [],
+ };
+ const id = uuid();
- await service.start();
- service.imq.emit('message', request, id);
+ await service.start();
+ service.imq.emit('message', request, id);
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- expect(warnStub.called).to.equal(true);
- const hasAfter = warnStub.getCalls().some(c => c.args && c.args[0] === AFTER_HOOK_ERROR);
- expect(hasAfter).to.equal(true);
- resolve(undefined);
- } catch (e) { reject(e); }
- }, 1));
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ assert.equal(warnStub.mock.callCount() > 0, true);
+ const hasAfter = warnStub.mock.calls.some(
+ (c: any) =>
+ c.arguments && c.arguments[0] === AFTER_HOOK_ERROR,
+ );
+ assert.equal(hasAfter, true);
+ resolve(undefined);
+ } catch (e) {
+ reject(e);
+ }
+ }, 1),
+ );
- await service.destroy();
- });
+ await service.destroy();
+ });
});
diff --git a/test/IMQService.replyFailure.spec.ts b/test/IMQService.replyFailure.spec.ts
new file mode 100644
index 0000000..62318d3
--- /dev/null
+++ b/test/IMQService.replyFailure.spec.ts
@@ -0,0 +1,78 @@
+/*!
+ * IMQService reply-publish failure handling tests
+ */
+import './mocks';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomUUID as uuid } from 'node:crypto';
+import { IMQService, IMQRPCRequest, expose } from '..';
+
+class ReplyFailService extends IMQService {
+ @expose()
+ public ping(): string {
+ return 'pong';
+ }
+}
+
+describe('IMQService reply-publish failure', () => {
+ let service: any;
+
+ afterEach(async () => {
+ try {
+ await service?.destroy();
+ } catch {
+ /* ignore */
+ }
+ mock.restoreAll();
+ });
+
+ it(
+ 'should not produce an unhandled rejection when publishing the ' +
+ 'response fails, and should log the error',
+ async () => {
+ const error = mock.fn();
+ const logger: any = {
+ info: () => {},
+ warn: () => {},
+ error,
+ log: () => {},
+ };
+
+ service = new ReplyFailService({ logger });
+ await service.start();
+
+ // make the response publish fail (e.g. broker went away)
+ mock.method(service.imq, 'send', async () => {
+ throw new Error('broker down');
+ });
+
+ const unhandled = mock.fn();
+
+ process.once('unhandledRejection', unhandled as any);
+
+ const request: IMQRPCRequest = {
+ from: 'ReplyFailClient',
+ method: 'ping',
+ args: [],
+ };
+
+ service.imq.emit('message', request, uuid());
+
+ // allow the async handler chain (and any unhandled rejection
+ // detection) to settle
+ await new Promise(resolve => setTimeout(resolve, 20));
+
+ assert.equal(
+ unhandled.mock.callCount(),
+ 0,
+ 'reply failure must not surface as an unhandled rejection',
+ );
+ assert.ok(
+ error.mock.callCount() > 0,
+ 'reply failure must be logged via logger.error',
+ );
+
+ process.removeListener('unhandledRejection', unhandled as any);
+ },
+ );
+});
diff --git a/test/IMQService.signal.publish.spec.ts b/test/IMQService.signal.publish.spec.ts
index 79c0ab4..ba8689d 100644
--- a/test/IMQService.signal.publish.spec.ts
+++ b/test/IMQService.signal.publish.spec.ts
@@ -1,56 +1,68 @@
/*!
* IMQService signal handler and publish() coverage tests
*/
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { uuid } from '@imqueue/core';
-import { IMQService, IMQRPCRequest, expose } from '..';
+import { describe, it, afterEach, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { randomUUID as uuid } from 'node:crypto';
+import { IMQService, expose } from '..';
class SignalTestService extends IMQService {
- @expose()
- public ping() {
- return 'pong';
- }
+ @expose()
+ public ping() {
+ return 'pong';
+ }
}
describe('IMQService signal handler and publish()', () => {
- afterEach(async () => {
- sinon.restore();
- });
+ afterEach(async () => {
+ mock.restoreAll();
+ });
- it('should call destroy() and log error on process signal (without exiting)', async () => {
- const onStub = sinon.stub(process as any, 'on');
- const handlers: { sig: string; fn: (...args: any[]) => any }[] = [];
- onStub.callsFake((sig: string, fn: any) => { handlers.push({ sig, fn }); return process as any; });
+ it('should call destroy() and log error on process signal (without exiting)', async () => {
+ const handlers: { sig: string; fn: (...args: any[]) => any }[] = [];
+ mock.method(process as any, 'on', (sig: string, fn: any) => {
+ handlers.push({ sig, fn });
+ return process as any;
+ });
- // use fake timers to avoid triggering real process.exit from setTimeout
- const clock = sinon.useFakeTimers();
+ // use fake timers to avoid triggering real process.exit from setTimeout
+ mock.timers.enable({ apis: ['setTimeout'] });
- const logger: any = { info: () => {}, warn: () => {}, error: sinon.spy() };
- const service: any = new SignalTestService({ logger });
+ const logger: any = {
+ info: () => {},
+ warn: () => {},
+ error: mock.fn(),
+ };
+ const service: any = new SignalTestService({ logger });
- // make destroy reject to hit catch(logger.error)
- sinon.stub(service, 'destroy').callsFake(async () => { throw new Error('boom'); });
+ // make destroy reject to hit catch(logger.error)
+ mock.method(service, 'destroy', async () => {
+ throw new Error('boom');
+ });
- // simulate first registered signal handler
- expect(handlers.length).to.be.greaterThan(0);
- await handlers[0].fn();
+ // simulate first registered signal handler
+ assert.ok(handlers.length > 0);
+ await handlers[0].fn();
- // let microtasks settle
- await Promise.resolve();
+ // let microtasks settle
+ await Promise.resolve();
- expect(logger.error.called).to.equal(true);
+ assert.ok(logger.error.mock.callCount() > 0);
- clock.restore();
- });
+ mock.timers.reset();
+ });
- it('publish() should delegate to imq.publish', async () => {
- const logger: any = { info: () => {}, warn: () => {}, error: () => {} };
- const service: any = new SignalTestService({ logger });
- const stub = sinon.stub(service.imq, 'publish').resolves(undefined as any);
+ it('publish() should delegate to imq.publish', async () => {
+ const logger: any = { info: () => {}, warn: () => {}, error: () => {} };
+ const service: any = new SignalTestService({ logger });
+ const stub = mock.method(
+ service.imq,
+ 'publish',
+ async () => undefined as any,
+ );
- await service.publish({ id: uuid() } as any);
+ await service.publish({ id: uuid() } as any);
- expect(stub.calledOnce).to.equal(true);
- });
+ assert.equal(stub.mock.callCount() === 1, true);
+ });
});
diff --git a/test/IMQService.spec.ts b/test/IMQService.spec.ts
new file mode 100644
index 0000000..bc272c6
--- /dev/null
+++ b/test/IMQService.spec.ts
@@ -0,0 +1,458 @@
+/*!
+ * IMQService Unit Tests
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+import { logger } from './mocks';
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert/strict';
+import { IMQService, IMQRPCRequest, Description, expose } from '..';
+import { randomUUID as uuid } from 'node:crypto';
+
+const cluster: any = require('node:cluster');
+
+class TestService extends IMQService {
+ @expose()
+ public exposed() {
+ return 'exposed';
+ }
+
+ @expose()
+ public testArgs(a: string, b?: number) {
+ return a + b;
+ }
+
+ @expose()
+ public throwable() {
+ throw new Error('No way!');
+ }
+
+ public unexposed() {
+ return 'unexposed';
+ }
+}
+
+describe('IMQService', () => {
+ it('should be a class', () => {
+ assert.equal(typeof IMQService, 'function');
+ });
+
+ describe('constructor()', () => {
+ it('should throw if directly called', () => {
+ const Service: any = IMQService;
+
+ assert.throws(() => new Service(), TypeError);
+ });
+
+ it('should not throw if inherited', () => {
+ assert.doesNotThrow(() => new TestService());
+ });
+
+ it('should use constructor name by default', () => {
+ assert.equal(new TestService().name, 'TestService');
+ });
+
+ it('should use given name if provided', () => {
+ assert.equal(new TestService({}, 'Test').name, 'Test');
+ });
+ });
+
+ describe('start()', () => {
+ it('should start messaging queue', async () => {
+ const service: any = new TestService({ logger });
+
+ mock.method(service.imq, 'start');
+
+ await service.start();
+
+ assert.equal(service.imq.start.mock.callCount() > 0, true);
+
+ await service.destroy();
+ });
+
+ it('should start multi-process if configured', async () => {
+ const service: any = new TestService({
+ multiProcess: true,
+ logger,
+ });
+
+ let counter = 0;
+ const spy = mock.method(cluster, 'fork', (async () => {
+ (cluster).isMaster = false;
+ ++counter == 1 && (await service.start());
+ }) as any);
+
+ await service.start();
+
+ assert.equal(spy.mock.callCount() > 0, true);
+
+ spy.mock.restore();
+ await service.destroy();
+ });
+ });
+
+ describe('stop()', () => {
+ it('should properly stop service', async () => {
+ const service: any = new TestService({ logger });
+
+ mock.method(service.imq, 'stop');
+
+ await service.stop();
+
+ assert.equal(service.imq.stop.mock.callCount() > 0, true);
+ });
+ });
+
+ describe('destroy()', () => {
+ it('should properly destroy service', async () => {
+ const service: any = new TestService({ logger });
+
+ mock.method(service.imq, 'destroy');
+
+ await service.destroy();
+
+ assert.equal(service.imq.destroy.mock.callCount() > 0, true);
+ });
+ });
+
+ describe('describe()', () => {
+ it('should return proper service description', async () => {
+ const service = new TestService({ logger });
+ await service.start();
+ const description: Description = service.describe();
+
+ assert.notEqual(description.service, undefined);
+ assert.notEqual(description.types, undefined);
+ assert.notEqual(description.service.methods.exposed, undefined);
+ assert.equal(description.service.methods.unexposed, undefined);
+
+ await service.destroy();
+ });
+ });
+
+ describe('handleRequest()', () => {
+ it('should handle properly incoming requests', async () => {
+ const reqSpy = mock.method(TestService.prototype, 'handleRequest');
+ const service: any = new TestService({ logger });
+ const request: IMQRPCRequest = {
+ from: 'TestClient',
+ method: 'exposed',
+ args: [],
+ };
+ const id = uuid();
+ const imqSpy = mock.method(service.imq, 'send');
+
+ await service.start();
+
+ service.imq.emit('message', request, id);
+
+ assert.ok(
+ reqSpy.mock.calls.some(
+ (c: any) =>
+ c.arguments[0] === request && c.arguments[1] === id,
+ ),
+ );
+
+ // we need to defer here because emit is not async but
+ // it calls send asynchronously
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ resolve(
+ assert.equal(imqSpy.mock.callCount() > 0, true),
+ );
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
+
+ await service.destroy();
+ reqSpy.mock.restore();
+ });
+
+ it('should throw IMQ_RPC_NO_METHOD if non-existing method called', async () => {
+ const service: any = new TestService({ logger });
+ const request: IMQRPCRequest = {
+ from: 'TestClient',
+ method: 'nonExistingMethod',
+ args: [],
+ };
+ const imqSpy = mock.method(service.imq, 'send');
+
+ await service.start();
+
+ service.imq.emit('message', request, uuid());
+
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ resolve(
+ assert.equal(
+ imqSpy.mock.calls.at(-1).arguments[1].error
+ .code,
+ 'IMQ_RPC_NO_METHOD',
+ ),
+ );
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
+
+ await service.destroy();
+ });
+
+ it('should throw IMQ_RPC_NO_ACCESS if unexposed method called', async () => {
+ const service: any = new TestService({ logger });
+ const request: IMQRPCRequest = {
+ from: 'TestClient',
+ method: 'unexposed',
+ args: [],
+ };
+ const imqSpy = mock.method(service.imq, 'send');
+
+ await service.start();
+
+ service.imq.emit('message', request, uuid());
+
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ resolve(
+ assert.equal(
+ imqSpy.mock.calls.at(-1).arguments[1].error
+ .code,
+ 'IMQ_RPC_NO_ACCESS',
+ ),
+ );
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
+
+ await service.destroy();
+ });
+
+ it(
+ 'should throw IMQ_RPC_INVALID_ARGS_COUNT if unexposed ' +
+ 'number of arguments given',
+ async () => {
+ const service: any = new TestService({ logger });
+ const request: IMQRPCRequest = {
+ from: 'TestClient',
+ method: 'testArgs',
+ args: [],
+ };
+ const imqSpy = mock.method(service.imq, 'send');
+
+ await service.start();
+
+ service.imq.emit('message', request, uuid());
+
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ resolve(
+ assert.equal(
+ imqSpy.mock.calls.at(-1).arguments[1].error
+ .code,
+ 'IMQ_RPC_INVALID_ARGS_COUNT',
+ ),
+ );
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
+
+ await service.destroy();
+ },
+ );
+
+ it(
+ 'should throw IMQ_RPC_CALL_ERROR if unexposed ' +
+ 'number of arguments given',
+ async () => {
+ const service: any = new TestService({ logger });
+ const request: IMQRPCRequest = {
+ from: 'TestClient',
+ method: 'throwable',
+ args: [],
+ };
+ const imqSpy = mock.method(service.imq, 'send');
+
+ await service.start();
+
+ service.imq.emit('message', request, uuid());
+
+ await new Promise((resolve, reject) =>
+ setTimeout(() => {
+ try {
+ resolve(
+ assert.equal(
+ imqSpy.mock.calls.at(-1).arguments[1].error
+ .code,
+ 'IMQ_RPC_CALL_ERROR',
+ ),
+ );
+ } catch (err) {
+ reject(err);
+ }
+ }),
+ );
+
+ await service.destroy();
+ },
+ );
+ });
+});
+
+describe('IMQService metrics server', () => {
+ it('serves the queue length and 404s other paths', async () => {
+ const service: any = new TestService(
+ { logger, metricsServer: { enabled: true, port: 0 } },
+ uuid(),
+ );
+
+ mock.method(service.imq, 'start', async () => undefined);
+ mock.method(service.imq, 'queueLength', async () => 7);
+
+ await service.startWithMetricsServer();
+
+ const server = service.metricsServer;
+
+ if (!server.listening) {
+ await new Promise(resolve => server.once('listening', resolve));
+ }
+
+ const port = server.address().port;
+
+ try {
+ const metrics = await fetch(`http://127.0.0.1:${port}/metrics`);
+ assert.equal(metrics.status, 200);
+ // the default formatter renders a prometheus-style line
+ assert.match(await metrics.text(), /7/);
+
+ const other = await fetch(`http://127.0.0.1:${port}/nope`);
+ assert.equal(other.status, 404);
+ await other.text();
+ } finally {
+ await new Promise(resolve =>
+ server.close(() => resolve(undefined)),
+ );
+ for (const [sig, h] of service.signalHandlers) {
+ process.removeListener(sig, h);
+ }
+ }
+ });
+
+ it('does not start a server when metrics are disabled', async () => {
+ const service: any = new TestService({ logger }, uuid());
+
+ mock.method(service.imq, 'start', async () => undefined);
+
+ await service.startWithMetricsServer();
+
+ assert.equal(service.metricsServer, undefined);
+
+ for (const [sig, h] of service.signalHandlers) {
+ process.removeListener(sig, h);
+ }
+ });
+
+ it('signal handler closes the metrics server and exits', () => {
+ const service: any = new TestService({ logger }, uuid());
+ const closeSpy = mock.fn();
+
+ service.metricsServer = { close: closeSpy };
+ mock.method(service, 'destroy', async () => undefined);
+ const exit = mock.method(process, 'exit', (() => undefined) as any);
+
+ mock.timers.enable({ apis: ['setTimeout'] });
+
+ try {
+ service.signalHandlers[0][1]();
+
+ assert.ok(closeSpy.mock.callCount() > 0);
+
+ mock.timers.tick(60000);
+ assert.ok(exit.mock.callCount() > 0);
+ } finally {
+ mock.timers.reset();
+ for (const [sig, h] of service.signalHandlers) {
+ process.removeListener(sig, h);
+ }
+ }
+ });
+});
+
+describe('IMQService multi-process master', () => {
+ it('forks workers and exits when one dies', async () => {
+ if (!cluster.isMaster) {
+ return;
+ }
+
+ const service: any = new TestService(
+ { logger, multiProcess: true, childrenPerCore: 1 },
+ uuid(),
+ );
+ const exit = mock.method(process, 'exit', (() => undefined) as any);
+ const fork = mock.method(cluster, 'fork', () => ({
+ process: { pid: 111 },
+ }));
+ const before = cluster.listeners('exit').slice();
+
+ try {
+ await service.start();
+
+ assert.ok(fork.mock.callCount() > 0);
+
+ // invoke the handler start() registered, directly, so its body is
+ // exercised without going through cluster's internal 'exit' plumbing
+ const added = cluster
+ .listeners('exit')
+ .filter((l: any) => !before.includes(l));
+
+ assert.ok(added.length > 0);
+
+ const infoSpy = mock.method(service.logger, 'info');
+ added[0]({ process: { pid: 222 } });
+
+ assert.ok(
+ infoSpy.mock.calls.some((c: any) =>
+ String(c.arguments[0]).includes('died'),
+ ),
+ );
+ assert.ok(exit.mock.callCount() > 0);
+ } finally {
+ for (const l of cluster.listeners('exit')) {
+ if (!before.includes(l)) {
+ cluster.removeListener('exit', l);
+ }
+ }
+ for (const [sig, h] of service.signalHandlers) {
+ process.removeListener(sig, h);
+ }
+ }
+ });
+});
diff --git a/test/IMQService.ts b/test/IMQService.ts
deleted file mode 100644
index e460432..0000000
--- a/test/IMQService.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-/*!
- * IMQService Unit Tests
- *
- * I'm Queue Software Project
- * Copyright (C) 2025 imqueue.com
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- * If you want to use this code in a closed source (commercial) project, you can
- * purchase a proprietary commercial license. Please contact us at
- * to get commercial licensing options.
- */
-import { logger } from './mocks';
-import { expect } from 'chai';
-import * as sinon from 'sinon';
-import { IMQService, IMQRPCRequest, Description, expose } from '..';
-import { uuid } from '@imqueue/core';
-
-const cluster: any = require('cluster');
-
-class TestService extends IMQService {
-
- @expose()
- public exposed() {
- return 'exposed';
- }
-
- @expose()
- public testArgs(a: string, b?: number) {
- return a + b;
- }
-
- @expose()
- public throwable() {
- throw new Error('No way!')
- }
-
- // noinspection JSMethodCanBeStatic
- public unexposed() {
- return 'unexposed';
- }
-
-}
-
-describe('IMQService', () => {
- it('should be a class', () => {
- expect(typeof IMQService).to.equal('function');
- });
-
- describe('constructor()', () => {
- it('should throw if directly called', () => {
- const Service: any = IMQService;
-
- expect(() => new Service()).to.throw(TypeError);
- });
-
- it('should not throw if inherited', () => {
- expect(new TestService()).not.to.throw;
- });
-
- it('should use constructor name by default', () => {
- expect(new TestService().name).to.equal('TestService');
- });
-
- it('should use given name if provided', () => {
- expect(new TestService({}, 'Test').name).to.equal('Test');
- });
- });
-
- describe('start()', () => {
- it('should start messaging queue', async () => {
- const service: any = new TestService({ logger });
-
- sinon.spy(service.imq, 'start');
-
- await service.start();
-
- expect(service.imq.start.called).to.be.true;
-
- await service.destroy();
- });
-
- it('should start multi-process if configured', async () => {
- const service: any = new TestService({
- multiProcess: true,
- logger
- });
-
- let counter = 0;
- const spy = sinon.stub(cluster, 'fork').callsFake((async () => {
- (cluster).isMaster = false;
- (++counter == 1) && await service.start();
- }) as any);
-
- await service.start();
-
- expect(spy.called).to.be.true;
-
- spy.restore();
- await service.destroy();
- });
- });
-
- describe('stop()', () => {
- it('should properly stop service', async () => {
- const service: any = new TestService({ logger });
-
- sinon.spy(service.imq, 'stop');
-
- await service.stop();
-
- expect(service.imq.stop.called).to.be.true;
- });
- });
-
- describe('destroy()', () => {
- it('should properly destroy service', async () => {
- const service: any = new TestService({ logger });
-
- sinon.spy(service.imq, 'destroy');
-
- await service.destroy();
-
- expect(service.imq.destroy.called).to.be.true;
- });
- });
-
- describe('describe()', () => {
- it('should return proper service description', async () => {
- const service = new TestService({ logger });
- await service.start();
- const description: Description = service.describe();
-
- expect(description.service).not.to.be.undefined;
- expect(description.types).not.to.be.undefined;
- expect(description.service.methods.exposed).not.to.be.undefined;
- expect(description.service.methods.unexposed).to.be.undefined;
-
- await service.destroy();
- });
- });
-
- describe('handleRequest()', () => {
- it('should handle properly incoming requests', async () => {
- const reqSpy = sinon.spy(TestService.prototype, 'handleRequest');
- const service: any = new TestService({ logger });
- const request: IMQRPCRequest = {
- from: 'TestClient',
- method: 'exposed',
- args: []
- };
- const id = uuid();
- const imqSpy = sinon.spy(service.imq, 'send');
-
- await service.start();
-
- service.imq.emit('message', request, id);
-
- expect(reqSpy.calledWith(request, id)).to.be.true;
-
- // we need to defer here because emit is not async but
- // it calls send asynchronously
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- resolve(expect(imqSpy.called).to.be.true);
- }
-
- catch (err) {
- reject(err);
- }
- }));
-
- await service.destroy();
- reqSpy.restore();
- });
-
- it('should throw IMQ_RPC_NO_METHOD if non-existing method called',
- async () => {
- const service: any = new TestService({ logger });
- const request: IMQRPCRequest = {
- from: 'TestClient',
- method: 'nonExistingMethod',
- args: []
- };
- const imqSpy = sinon.spy(service.imq, 'send');
-
- await service.start();
-
- service.imq.emit('message', request, uuid());
-
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- resolve(expect(imqSpy.lastCall.args[1].error.code)
- .to.equal('IMQ_RPC_NO_METHOD')
- );
- }
-
- catch (err) {
- reject(err);
- }
- }));
-
- await service.destroy();
- });
-
- it('should throw IMQ_RPC_NO_ACCESS if unexposed method called',
- async () => {
- const service: any = new TestService({ logger });
- const request: IMQRPCRequest = {
- from: 'TestClient',
- method: 'unexposed',
- args: []
- };
- const imqSpy = sinon.spy(service.imq, 'send');
-
- await service.start();
-
- service.imq.emit('message', request, uuid());
-
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- resolve(expect(imqSpy.lastCall.args[1].error.code)
- .to.equal('IMQ_RPC_NO_ACCESS')
- );
- }
-
- catch (err) {
- reject(err);
- }
- }));
-
- await service.destroy();
- });
-
- it('should throw IMQ_RPC_INVALID_ARGS_COUNT if unexposed ' +
- 'number of arguments given',
- async () => {
- const service: any = new TestService({ logger });
- const request: IMQRPCRequest = {
- from: 'TestClient',
- method: 'testArgs',
- args: []
- };
- const imqSpy = sinon.spy(service.imq, 'send');
-
- await service.start();
-
- service.imq.emit('message', request, uuid());
-
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- resolve(expect(imqSpy.lastCall.args[1].error.code)
- .to.equal('IMQ_RPC_INVALID_ARGS_COUNT')
- );
- }
-
- catch (err) {
- reject(err);
- }
- }));
-
- await service.destroy();
- });
-
- it('should throw IMQ_RPC_CALL_ERROR if unexposed ' +
- 'number of arguments given',
- async () => {
- const service: any = new TestService({ logger });
- const request: IMQRPCRequest = {
- from: 'TestClient',
- method: 'throwable',
- args: []
- };
- const imqSpy = sinon.spy(service.imq, 'send');
-
- await service.start();
-
- service.imq.emit('message', request, uuid());
-
- await new Promise((resolve, reject) => setTimeout(() => {
- try {
- resolve(expect(imqSpy.lastCall.args[1].error.code)
- .to.equal('IMQ_RPC_CALL_ERROR')
- );
- }
-
- catch (err) {
- reject(err);
- }
- }));
-
- await service.destroy();
- });
- });
-});
diff --git a/test/cache/RedisCache.errors.spec.ts b/test/cache/RedisCache.errors.spec.ts
index 552b81e..05f9d03 100644
--- a/test/cache/RedisCache.errors.spec.ts
+++ b/test/cache/RedisCache.errors.spec.ts
@@ -1,61 +1,83 @@
/*!
* RedisCache error branches: methods before init should throw
*/
-import { expect } from 'chai';
import { RedisCache, REDIS_CLIENT_INIT_ERROR } from '../..';
+import { describe, it, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
describe('cache/RedisCache errors when not initialized', () => {
- beforeEach(() => { delete (RedisCache as any).redis; });
-
- it('get() should throw if redis not initialized', async () => {
- const cache = new RedisCache();
- let threw = false;
- try {
- await cache.get('x');
- } catch (e: any) {
- threw = true;
- expect(e).to.be.instanceOf(TypeError);
- expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
- }
- expect(threw).to.equal(true);
- });
-
- it('set() should throw if redis not initialized', async () => {
- const cache = new RedisCache();
- let threw = false;
- try {
- await cache.set('x', 1);
- } catch (e: any) {
- threw = true;
- expect(e).to.be.instanceOf(TypeError);
- expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
- }
- expect(threw).to.equal(true);
- });
-
- it('del() should throw if redis not initialized', async () => {
- const cache = new RedisCache();
- let threw = false;
- try {
- await cache.del('x');
- } catch (e: any) {
- threw = true;
- expect(e).to.be.instanceOf(TypeError);
- expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
- }
- expect(threw).to.equal(true);
- });
-
- it('purge() should throw if redis not initialized', async () => {
- const cache = new RedisCache();
- let threw = false;
- try {
- await cache.purge('mask');
- } catch (e: any) {
- threw = true;
- expect(e).to.be.instanceOf(TypeError);
- expect(e.message).to.equal(REDIS_CLIENT_INIT_ERROR);
- }
- expect(threw).to.equal(true);
- });
+ beforeEach(() => {
+ delete (RedisCache as any).redis;
+ });
+
+ it('get() should throw if redis not initialized', async () => {
+ const cache = new RedisCache();
+ let threw = false;
+ try {
+ await cache.get('x');
+ } catch (e: any) {
+ threw = true;
+ assert.ok(e instanceof TypeError);
+ assert.equal(e.message, REDIS_CLIENT_INIT_ERROR);
+ }
+ assert.equal(threw, true);
+ });
+
+ it('set() should throw if redis not initialized', async () => {
+ const cache = new RedisCache();
+ let threw = false;
+ try {
+ await cache.set('x', 1);
+ } catch (e: any) {
+ threw = true;
+ assert.ok(e instanceof TypeError);
+ assert.equal(e.message, REDIS_CLIENT_INIT_ERROR);
+ }
+ assert.equal(threw, true);
+ });
+
+ it('del() should throw if redis not initialized', async () => {
+ const cache = new RedisCache();
+ let threw = false;
+ try {
+ await cache.del('x');
+ } catch (e: any) {
+ threw = true;
+ assert.ok(e instanceof TypeError);
+ assert.equal(e.message, REDIS_CLIENT_INIT_ERROR);
+ }
+ assert.equal(threw, true);
+ });
+
+ it('purge() should throw if redis not initialized', async () => {
+ const cache = new RedisCache();
+ let threw = false;
+ try {
+ await cache.purge('mask');
+ } catch (e: any) {
+ threw = true;
+ assert.ok(e instanceof TypeError);
+ assert.equal(e.message, REDIS_CLIENT_INIT_ERROR);
+ }
+ assert.equal(threw, true);
+ });
+
+ it('init() rejects and logs when the connection errors', async () => {
+ delete (RedisCache as any).redis;
+ (RedisCache as any).initPromise = undefined;
+
+ const cache: any = new RedisCache();
+ const pending = cache.init();
+
+ // the mock emits 'ready' on a later tick, so emitting 'error'
+ // synchronously here reaches the reject branch first
+ (RedisCache as any).redis.emit('error', new Error('connection boom'));
+
+ await assert.rejects(pending, /connection boom/);
+
+ // the failed attempt still left a live mock connection — close it so it
+ // does not keep the process alive
+ await RedisCache.destroy().catch(() => undefined);
+ (RedisCache as any).initPromise = undefined;
+ });
});
diff --git a/test/cache/RedisCache.purge.promise.spec.ts b/test/cache/RedisCache.purge.promise.spec.ts
index 9717723..08a5ed2 100644
--- a/test/cache/RedisCache.purge.promise.spec.ts
+++ b/test/cache/RedisCache.purge.promise.spec.ts
@@ -1,116 +1,140 @@
/*!
* RedisCache additional coverage: purge() and set() with Promise
*/
-import { expect } from 'chai';
import { RedisCache } from '../..';
+import { describe, it, afterEach } from 'node:test';
+import assert from 'node:assert/strict';
class FakeRedis {
- // minimal in-memory store
- private store = new Map();
- public status: string = 'ready';
- public connected: boolean = true;
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- public on(event: string, listener: (...args: any[]) => void): any { return this }
- public once?: any;
- public emit?: any;
- public removeAllListeners(): void {}
- public disconnect(_reconnect: boolean): void {}
- public quit(): void {}
- public end?(): void {}
+ // minimal in-memory store
+ private store = new Map();
+ public status: string = 'ready';
+ public connected: boolean = true;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ public on(event: string, listener: (...args: any[]) => void): any {
+ return this;
+ }
+ public once?: any;
+ public emit?: any;
+ public removeAllListeners(): void {}
+ public disconnect(_reconnect: boolean): void {}
+ public quit(): void {}
+ public end?(): void {}
- public async set(key: string, val: string, ..._args: any[]): Promise {
- this.store.set(key, val);
- return true;
- }
- public async get(key: string, ..._args: any[]): Promise {
- return this.store.get(key) as any;
- }
- public async del(key: string): Promise {
- const existed = this.store.delete(key);
- return existed ? 1 : 0;
- }
- public async eval(script: string, _numkeys: number, ..._args: any[]): Promise {
- // Extract mask from: redis.call('keys','')
- const m = script.match(/redis\.call\('keys','([^']*)'\)/);
- const mask = m ? m[1] : '';
- if (mask.endsWith('*')) {
- const prefix = mask.slice(0, -1);
- for (const key of Array.from(this.store.keys())) {
- if (key.startsWith(prefix)) {
- this.store.delete(key);
+ public async set(key: string, val: string, ..._args: any[]): Promise {
+ this.store.set(key, val);
+ return true;
+ }
+ public async get(key: string, ..._args: any[]): Promise {
+ return this.store.get(key) as any;
+ }
+ public async del(key: string): Promise {
+ const existed = this.store.delete(key);
+ return existed ? 1 : 0;
+ }
+ public async eval(
+ script: string,
+ _numkeys: number,
+ ..._args: any[]
+ ): Promise {
+ // Extract mask from: redis.call('keys','')
+ const m = script.match(/redis\.call\('keys','([^']*)'\)/);
+ const mask = m ? m[1] : '';
+ if (mask.endsWith('*')) {
+ const prefix = mask.slice(0, -1);
+ for (const key of Array.from(this.store.keys())) {
+ if (key.startsWith(prefix)) {
+ this.store.delete(key);
+ }
+ }
+ } else if (mask) {
+ this.store.delete(mask);
}
- }
- } else if (mask) {
- this.store.delete(mask);
+ return true;
}
- return true;
- }
}
class ErrorEvalRedis extends FakeRedis {
- public async eval(): Promise {
- throw new Error('eval failed');
- }
+ public override async eval(): Promise {
+ throw new Error('eval failed');
+ }
}
describe('cache/RedisCache purge() and set() with Promise', () => {
- afterEach(async () => {
- await RedisCache.destroy();
- });
+ afterEach(async () => {
+ await RedisCache.destroy();
+ });
- it('purge() should delete keys by wildcard and return true', async () => {
- const conn = new FakeRedis();
- const cache = new RedisCache();
- await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any });
+ it('purge() should delete keys by wildcard and return true', async () => {
+ const conn = new FakeRedis();
+ const cache = new RedisCache();
+ await cache.init({
+ conn: conn as any,
+ prefix: 'cov-cache',
+ logger: console as any,
+ });
- // prepare some keys
- await cache.set('keep', 'x'); // fully qualified: cov-cache:RedisCache:keep
- await cache.set('del1', 1);
- await cache.set('del2', 2);
+ // prepare some keys
+ await cache.set('keep', 'x'); // fully qualified: cov-cache:RedisCache:keep
+ await cache.set('del1', 1);
+ await cache.set('del2', 2);
- // unrelated key stored directly in connection should not be removed
- await conn.set('other:namespace:key', 'y');
+ // unrelated key stored directly in connection should not be removed
+ await conn.set('other:namespace:key', 'y');
- const mask = `${cache.options.prefix}:${cache.name}:del*`;
- const ok = await cache.purge(mask);
- expect(ok).to.equal(true);
+ const mask = `${cache.options.prefix}:${cache.name}:del*`;
+ const ok = await cache.purge(mask);
+ assert.equal(ok, true);
- // del* gone, keep remains, and unrelated stays
- expect(await cache.get('del1')).to.equal(undefined);
- expect(await cache.get('del2')).to.equal(undefined);
- expect(await cache.get('keep')).to.equal('x');
- // Sanity: unrelated
- expect(await (conn as any).get('other:namespace:key')).to.equal('y');
- });
+ // del* gone, keep remains, and unrelated stays
+ assert.equal(await cache.get('del1'), undefined);
+ assert.equal(await cache.get('del2'), undefined);
+ assert.equal(await cache.get('keep'), 'x');
+ // Sanity: unrelated
+ assert.equal(await (conn as any).get('other:namespace:key'), 'y');
+ });
- it('purge() should return false and log when eval throws', async () => {
- const conn = new ErrorEvalRedis();
- const cache = new RedisCache();
- const logs: any[] = [];
- const logger = { info: () => {}, error: (e: any) => logs.push(e) } as any;
- await cache.init({ conn: conn as any, prefix: 'cov-cache', logger });
- const res = await cache.purge('cov-cache:RedisCache:*');
- expect(res).to.equal(false);
- expect(logs.length).to.be.greaterThan(0);
- });
+ it('purge() should return false and log when eval throws', async () => {
+ const conn = new ErrorEvalRedis();
+ const cache = new RedisCache();
+ const logs: any[] = [];
+ const logger = {
+ info: () => {},
+ error: (e: any) => logs.push(e),
+ } as any;
+ await cache.init({ conn: conn as any, prefix: 'cov-cache', logger });
+ const res = await cache.purge('cov-cache:RedisCache:*');
+ assert.equal(res, false);
+ assert.ok(logs.length > 0);
+ });
- it('set() should accept Promise value and store resolved value', async () => {
- const conn = new FakeRedis();
- const cache = new RedisCache();
- await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any });
+ it('set() should accept Promise value and store resolved value', async () => {
+ const conn = new FakeRedis();
+ const cache = new RedisCache();
+ await cache.init({
+ conn: conn as any,
+ prefix: 'cov-cache',
+ logger: console as any,
+ });
- const key = 'promised';
- const valuePromise = new Promise((resolve) => setTimeout(() => resolve(42), 1));
- await cache.set(key, valuePromise);
- expect(await cache.get(key)).to.equal(42);
- });
+ const key = 'promised';
+ const valuePromise = new Promise(resolve =>
+ setTimeout(() => resolve(42), 1),
+ );
+ await cache.set(key, valuePromise);
+ assert.equal(await cache.get(key), 42);
+ });
- it('del() should return boolean false when key did not exist', async () => {
- const conn = new FakeRedis();
- const cache = new RedisCache();
- await cache.init({ conn: conn as any, prefix: 'cov-cache', logger: console as any });
+ it('del() should return boolean false when key did not exist', async () => {
+ const conn = new FakeRedis();
+ const cache = new RedisCache();
+ await cache.init({
+ conn: conn as any,
+ prefix: 'cov-cache',
+ logger: console as any,
+ });
- const res = await cache.del('missing');
- expect(res).to.equal(false);
- });
+ const res = await cache.del('missing');
+ assert.equal(res, false);
+ });
});
diff --git a/test/cache/RedisCache.race.spec.ts b/test/cache/RedisCache.race.spec.ts
new file mode 100644
index 0000000..bd9ebdf
--- /dev/null
+++ b/test/cache/RedisCache.race.spec.ts
@@ -0,0 +1,44 @@
+/*!
+ * RedisCache concurrent-initialization tests
+ */
+import '../mocks';
+import { describe, it, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
+import { RedisCache } from '../..';
+import { RedisClientMock } from '../mocks';
+import { logger } from '../mocks';
+
+describe('cache/RedisCache concurrent init', () => {
+ beforeEach(async () => {
+ await RedisCache.destroy();
+ });
+
+ it('should open a single connection for concurrent init() calls', async () => {
+ const before = RedisClientMock.__constructed;
+ const one = new RedisCache();
+ const two = new RedisCache();
+
+ await Promise.all([one.init({ logger }), two.init({ logger })]);
+
+ assert.equal(
+ RedisClientMock.__constructed - before,
+ 1,
+ 'concurrent init() must share one connection, not open two',
+ );
+ assert.equal(one.ready, true);
+ assert.equal(two.ready, true);
+ });
+
+ it('should allow re-initialization after destroy()', async () => {
+ const one = new RedisCache();
+
+ await one.init({ logger });
+ await RedisCache.destroy();
+
+ const two = new RedisCache();
+
+ await two.init({ logger });
+
+ assert.equal(two.ready, true);
+ });
+});
diff --git a/test/cache/RedisCache.ts b/test/cache/RedisCache.spec.ts
similarity index 69%
rename from test/cache/RedisCache.ts
rename to test/cache/RedisCache.spec.ts
index 29cf68e..d4db73f 100644
--- a/test/cache/RedisCache.ts
+++ b/test/cache/RedisCache.spec.ts
@@ -22,28 +22,33 @@
* to get commercial licensing options.
*/
import { Redis, logger } from '../mocks';
-import { expect } from 'chai';
+import { describe, it, before, after, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
import { RedisCache } from '../..';
-import { IRedisClient, uuid } from '@imqueue/core';
+import { IRedisClient } from '@imqueue/core';
+import { randomUUID as uuid } from 'node:crypto';
describe('cache/RedisCache', () => {
it('should be a class', () => {
- expect(typeof RedisCache).to.equal('function');
+ assert.equal(typeof RedisCache, 'function');
});
describe('constructor()', () => {
it('should not throw', () => {
- expect(() => new RedisCache()).not.to.throw;
+ assert.doesNotThrow(() => new RedisCache());
});
});
describe('init()', () => {
- beforeEach(() => { delete (RedisCache).redis });
+ beforeEach(() => {
+ delete (RedisCache).redis;
+ delete (RedisCache).initPromise;
+ });
it('should not throw without options', async () => {
const cache = new RedisCache();
- expect(async () => await cache.init({ logger })).not.to.throw;
+ assert.doesNotThrow(async () => await cache.init({ logger }));
});
it('should re-use existing conn if given', async () => {
@@ -57,7 +62,7 @@ describe('cache/RedisCache', () => {
await cache.init({ logger });
- expect(oldConn).to.equal((RedisCache).redis);
+ assert.equal(oldConn, (RedisCache).redis);
});
it('should use connection from options if passed', async () => {
@@ -66,14 +71,14 @@ describe('cache/RedisCache', () => {
await cache.init({ conn, logger });
- expect((RedisCache).redis).to.equal(conn);
+ assert.equal((RedisCache).redis, conn);
});
});
describe('get()', () => {
let cache: RedisCache;
- before(async() => {
+ before(async () => {
cache = new RedisCache();
await cache.init({ logger });
});
@@ -81,26 +86,26 @@ describe('cache/RedisCache', () => {
after(async () => RedisCache.destroy());
it('should return undefined if nothing found', async () => {
- expect(await cache.get(uuid())).to.be.undefined;
+ assert.equal(await cache.get(uuid()), undefined);
});
it('should return stored value if found', async () => {
const key = uuid();
await cache.set(key, 1000);
- expect(await cache.get(key)).to.equal(1000);
+ assert.equal(await cache.get(key), 1000);
await cache.set(key, { a: 'b' });
- expect(await cache.get(key)).to.deep.equal({ a: 'b' });
+ assert.deepEqual(await cache.get(key), { a: 'b' });
await cache.set(key, false);
- expect(await cache.get(key)).to.be.false;
+ assert.equal(await cache.get(key), false);
await cache.set(key, null);
- expect(await cache.get(key)).to.be.null;
+ assert.equal(await cache.get(key), null);
});
});
describe('set()', () => {
let cache: RedisCache;
- before(async() => {
+ before(async () => {
cache = new RedisCache();
await cache.init({ logger });
});
@@ -110,18 +115,20 @@ describe('cache/RedisCache', () => {
it('should not overwrite if asked', async () => {
const key = uuid();
await cache.set(key, 'initial value');
- expect(await cache.get(key)).to.equal('initial value');
+ assert.equal(await cache.get(key), 'initial value');
await cache.set(key, 'new value', undefined, true);
- expect(await cache.get(key)).to.equal('initial value');
+ assert.equal(await cache.get(key), 'initial value');
});
it('should expire if ttl given', async () => {
const key = uuid();
await cache.set(key, 'initial value', 10);
- expect(await cache.get(key)).to.equal('initial value');
- await new Promise(resolve => setTimeout(async () => {
- resolve(expect(await cache.get(key)).to.be.undefined)
- }, 10));
+ assert.equal(await cache.get(key), 'initial value');
+ await new Promise(resolve =>
+ setTimeout(async () => {
+ resolve(assert.equal(await cache.get(key), undefined));
+ }, 10),
+ );
});
});
@@ -132,17 +139,16 @@ describe('cache/RedisCache', () => {
await cache.init({ logger });
await cache.set(key, 'value');
await cache.del(key);
- expect(await cache.get(key)).to.be.undefined;
+ assert.equal(await cache.get(key), undefined);
});
});
describe('destroy()', () => {
it('should destroy redis connection', async () => {
new RedisCache();
- expect((RedisCache).redis).to.be.ok;
+ assert.ok((RedisCache).redis);
await RedisCache.destroy();
- expect((RedisCache).redis).to.be.undefined;
+ assert.equal((RedisCache).redis, undefined);
});
});
-
-});
\ No newline at end of file
+});
diff --git a/test/coverage-extra.spec.ts b/test/coverage-extra.spec.ts
new file mode 100644
index 0000000..695940b
--- /dev/null
+++ b/test/coverage-extra.spec.ts
@@ -0,0 +1,158 @@
+/*!
+ * Additional coverage: legacy decorator signatures, serialize edge cases,
+ * error and request-context helpers.
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+import '../test/mocks';
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ remote,
+ logged,
+ cache,
+ lock,
+ indexed,
+ property,
+ classType,
+ IMQError,
+ currentMetadata,
+ runWithRequest,
+ IMQRPCDescription,
+} from '..';
+import { signature } from '../src/helpers';
+
+describe('legacy decorator signatures', () => {
+ // legacy form: (target, propertyKey, descriptor) with no TC39 context
+ const legacyMethod = (dec: any) => {
+ const descriptor: any = { value: function original() {} };
+ const result = dec({}, 'legacyMethod', descriptor);
+
+ assert.equal(result, descriptor);
+ assert.equal(typeof descriptor.value, 'function');
+ };
+
+ it('remote() wraps via the legacy signature', () => {
+ legacyMethod(remote());
+ });
+
+ it('logged() wraps via the legacy signature', () => {
+ legacyMethod(logged());
+ });
+
+ it('cache() wraps via the legacy signature', () => {
+ legacyMethod(cache());
+ });
+
+ it('lock() wraps via the legacy signature', () => {
+ legacyMethod(lock());
+ });
+
+ it('classType() returns the class unchanged in legacy mode', () => {
+ class LegacyClassTypeTarget {}
+
+ assert.equal(classType()(LegacyClassTypeTarget), LegacyClassTypeTarget);
+ });
+
+ it('indexed() is a no-op without an index typedef (legacy)', () => {
+ class NoIndex {}
+
+ assert.equal(indexed('')(NoIndex), NoIndex);
+ });
+
+ it('indexed() attaches the index type in legacy mode', () => {
+ class LegacyIndexed {}
+
+ // a non-string typedef exercises the String() coercion branch too
+ indexed(42 as any)(LegacyIndexed);
+
+ assert.equal(
+ IMQRPCDescription.typesDescription.LegacyIndexed.indexType,
+ '42',
+ );
+ });
+
+ it('property() returns undefined for a falsy type', () => {
+ assert.equal(property(''), undefined);
+ });
+
+ it('property() registers the field directly in legacy mode', () => {
+ class LegacyProp {}
+
+ // a plain object type drives resolveTypeDef down its String() fallback
+ property({} as any)(LegacyProp.prototype, 'field');
+
+ assert.equal(
+ typeof IMQRPCDescription.typesDescription.LegacyProp.properties
+ .field.type,
+ 'string',
+ );
+ });
+});
+
+describe('signature() serialize edge cases', () => {
+ const sig = (arg: unknown) => signature('C', 'm', [arg]);
+
+ it('serializes every primitive kind without throwing', () => {
+ for (const arg of [
+ null,
+ true,
+ false,
+ 10n,
+ undefined,
+ () => undefined,
+ Symbol('x'),
+ NaN,
+ ]) {
+ assert.match(sig(arg), /^[0-9a-f]{16}$/);
+ }
+ });
+
+ it('is deterministic across primitive kinds', () => {
+ assert.equal(sig(true), sig(true));
+ assert.notEqual(sig(true), sig(false));
+ });
+});
+
+describe('IMQError()', () => {
+ it('drops an original error that cannot be serialized', () => {
+ const circular: any = {};
+ circular.self = circular;
+
+ const err = IMQError('E', 'msg', 'stack', 'method', [1], circular);
+
+ assert.equal(err.original, undefined);
+ assert.equal(err.code, 'E');
+ });
+});
+
+describe('IMQRequestContext.currentMetadata()', () => {
+ it('returns undefined outside of a request scope', () => {
+ assert.equal(currentMetadata(), undefined);
+ });
+
+ it('returns the in-flight request metadata within a scope', () => {
+ const request: any = { metadata: { traceId: 'abc' } };
+ const meta = runWithRequest(request, () => currentMetadata());
+
+ assert.deepEqual(meta, { traceId: 'abc' });
+ });
+});
diff --git a/test/decorators/cache.spec.ts b/test/decorators/cache.spec.ts
new file mode 100644
index 0000000..acefc7d
--- /dev/null
+++ b/test/decorators/cache.spec.ts
@@ -0,0 +1,156 @@
+/*!
+ * cache() Function Unit Tests
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+import { logger } from '../mocks';
+import { describe, it, before, after } from 'node:test';
+import assert from 'node:assert/strict';
+import { RedisCache, cache, IMQCache } from '../..';
+import { ILogger } from '@imqueue/core';
+
+class CacheTestClass {
+ private logger: ILogger = logger;
+
+ @cache()
+ public async testMethod() {
+ return Math.random() * Math.random() + Math.random();
+ }
+
+ @cache({ ttl: 100 })
+ public async testMethodWithTTL() {
+ return Math.random() * Math.random() + Math.random();
+ }
+}
+
+// minimal in-memory adapter used to exercise the decorator's init/error paths
+// without touching a real redis connection
+class MemoryAdapter {
+ public name = 'MemoryAdapter';
+ public ready = false;
+ public logger: any = logger;
+ public initOptions: any;
+ public throwOnGet = false;
+
+ public init(options?: any): void {
+ this.initOptions = options;
+ this.ready = true;
+ }
+ public async get(): Promise {
+ if (this.throwOnGet) {
+ throw new Error('cache get failure');
+ }
+
+ return undefined;
+ }
+ public async set(): Promise {
+ return true;
+ }
+ public async del(): Promise {
+ return true;
+ }
+ public async purge(): Promise {
+ return true;
+ }
+}
+
+class CacheConnClass {
+ public imq: any = { writer: { fake: 'writer' }, logger };
+
+ @cache({ adapter: MemoryAdapter as any })
+ public async withConn() {
+ return 1;
+ }
+}
+
+class CacheErrClass {
+ public logger: ILogger = logger;
+
+ @cache({ adapter: MemoryAdapter as any })
+ public async willFallBack() {
+ return 42;
+ }
+}
+
+describe('decorators/cache()', () => {
+ let obj: any;
+
+ before(() => {
+ obj = new CacheTestClass();
+ });
+ after(async () => await RedisCache.destroy());
+
+ it('should be a function', () => {
+ assert.equal(typeof cache, 'function');
+ });
+
+ it('should return decorator function', () => {
+ assert.equal(typeof cache(), 'function');
+ });
+
+ it('should cache method execution result', async () => {
+ const callOne = await obj.testMethod();
+ const callTwo = await obj.testMethod();
+
+ const callThree = await obj.testMethod(1);
+ const callFour = await obj.testMethod(1);
+
+ assert.equal(callOne, callTwo);
+ assert.equal(callThree, callFour);
+ });
+
+ it('should expire if ttl specified', async () => {
+ const callOne = await obj.testMethodWithTTL();
+ const callTwo = await obj.testMethodWithTTL();
+
+ assert.equal(callOne, callTwo);
+
+ await (async () =>
+ setTimeout(async () => {
+ const callThree = await obj.testMethodWithTTL();
+
+ assert.notEqual(callThree, callOne);
+ }, 100));
+ });
+
+ it('passes the imq writer connection to a fresh adapter', async () => {
+ const conn = new CacheConnClass();
+
+ assert.equal(await conn.withConn(), 1);
+ const adapter: any = IMQCache.get(MemoryAdapter as any);
+
+ assert.ok(adapter.ready);
+ assert.deepEqual(adapter.initOptions.conn, { fake: 'writer' });
+ });
+
+ it('reuses a ready adapter and falls back on a cache error', async () => {
+ // first instance registers + inits the (now ready) shared adapter
+ await new CacheErrClass().willFallBack();
+ const adapter: any = IMQCache.get(MemoryAdapter as any);
+ adapter.throwOnGet = true;
+
+ // a second instance finds the adapter already ready, then get() throws
+ // and the decorator falls back to the original method
+ const result = await new CacheErrClass().willFallBack();
+
+ assert.equal(result, 42);
+ });
+});
diff --git a/test/decorators/cache.ts b/test/decorators/cache.ts
deleted file mode 100644
index 7ef5097..0000000
--- a/test/decorators/cache.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/*!
- * cache() Function Unit Tests
- *
- * I'm Queue Software Project
- * Copyright (C) 2025 imqueue.com
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- * If you want to use this code in a closed source (commercial) project, you can
- * purchase a proprietary commercial license. Please contact us at
- * to get commercial licensing options.
- */
-import { logger } from '../mocks';
-import { expect } from 'chai';
-import { RedisCache, cache } from '../..';
-import { ILogger } from '@imqueue/core';
-
-class CacheTestClass {
-
- // noinspection JSUnusedLocalSymbols
- private logger: ILogger = logger;
-
- @cache()
- public async testMethod() {
- return Math.random() * Math.random() + Math.random();
- }
-
- @cache({ ttl: 100 })
- public async testMethodWithTTL() {
- return Math.random() * Math.random() + Math.random();
- }
-
-}
-
-describe('decorators/cache()', () => {
- let obj: any;
-
- before(() => { obj = new CacheTestClass() });
- after(async () => await RedisCache.destroy());
-
- it('should be a function', () => {
- expect(typeof cache).to.equal('function');
- });
-
- it('should return decorator function', () => {
- expect(typeof cache()).to.equal('function');
- });
-
- it('should cache method execution result', async () => {
- const callOne = await obj.testMethod();
- const callTwo = await obj.testMethod();
-
- const callThree = await obj.testMethod(1);
- const callFour = await obj.testMethod(1);
-
- expect(callOne).to.be.equal(callTwo);
- expect(callThree).to.be.equal(callFour);
- });
-
- it('should expire if ttl specified', async () => {
- const callOne = await obj.testMethodWithTTL();
- const callTwo = await obj.testMethodWithTTL();
-
- expect(callOne).to.equal(callTwo);
-
- await (async () => setTimeout(async () => {
- const callThree = await obj.testMethodWithTTL();
-
- expect(callThree).not.to.equal(callOne);
- }, 100));
- });
-});
diff --git a/test/decorators/expose.spec.ts b/test/decorators/expose.spec.ts
new file mode 100644
index 0000000..59ba63d
--- /dev/null
+++ b/test/decorators/expose.spec.ts
@@ -0,0 +1,275 @@
+/*!
+ * expose() Function Unit Tests
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * If you want to use this code in a closed source (commercial) project, you can
+ * purchase a proprietary commercial license. Please contact us at
+ * to get commercial licensing options.
+ */
+import '../mocks';
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { expose, IMQRPCDescription } from '../..';
+
+const description = IMQRPCDescription.serviceDescription;
+
+class ExposeTestClass {
+ @expose()
+ public testMethod() {}
+
+ @expose()
+ public async anotherMethod(strArr: string[], num?: number) {}
+
+ /**
+ * Commented service method
+ *
+ * @param {string[]} strArr - array of strings
+ * @param {number} [num] - some number
+ * @return {string} - dummy string
+ */
+ @expose()
+ public async oneMoreMethod(strArr: string[], num?: number) {
+ return await new Promise((res, rej) => {
+ try {
+ res('Hello, World!');
+ } catch (e) {
+ rej(e);
+ }
+ });
+ }
+
+ public nonExposedMethod() {}
+
+ /**
+ * @param {number} a
+ * @returns {string}
+ */
+ @expose()
+ async withEmbeddedAsync(a: number) {
+ return await new Promise(async resolve => {
+ try {
+ resolve(a.toString());
+ } catch (e) {
+ resolve('');
+ }
+ });
+ }
+}
+
+class ExposeTestClassExtended extends ExposeTestClass {
+ @expose()
+ public extendedMethod() {}
+}
+
+// Standard decorators cannot see their class at decoration time, so the RPC
+// service description is built when a service is first constructed (which is
+// always the case in real usage) rather than at class-definition time.
+new ExposeTestClass();
+new ExposeTestClassExtended();
+
+describe('decorators/expose()', () => {
+ it('should be a function', () => {
+ assert.equal(typeof expose, 'function');
+ });
+ it('should return decorator function', () => {
+ assert.equal(typeof expose(), 'function');
+ });
+ it('should properly fill exposed description', () => {
+ assert.notEqual(description.ExposeTestClass, undefined);
+ assert.equal(description.ExposeTestClass.inherits, 'Function');
+ assert.notEqual(description.ExposeTestClass.methods, undefined);
+ assert.notEqual(
+ description.ExposeTestClass.methods.testMethod,
+ undefined,
+ );
+ assert.notEqual(
+ description.ExposeTestClass.methods.anotherMethod,
+ undefined,
+ );
+ assert.notEqual(
+ description.ExposeTestClass.methods.oneMoreMethod,
+ undefined,
+ );
+ assert.equal(
+ description.ExposeTestClass.methods.nonExposedMethod,
+ undefined,
+ );
+
+ assert.equal(
+ description.ExposeTestClass.methods.oneMoreMethod.description,
+ 'Commented service method',
+ );
+
+ // oneMoreMethod
+ assert.equal(
+ description.ExposeTestClass.methods.oneMoreMethod.arguments.length,
+ 2,
+ );
+ assert.deepEqual(
+ description.ExposeTestClass.methods.oneMoreMethod.returns,
+ {
+ description: 'dummy string',
+ type: 'string',
+ tsType: 'string',
+ },
+ );
+ assert.deepEqual(
+ description.ExposeTestClass.methods.oneMoreMethod.arguments[0],
+ {
+ description: 'array of strings',
+ type: 'string[]',
+ tsType: 'string[]',
+ name: 'strArr',
+ isOptional: false,
+ },
+ );
+ assert.deepEqual(
+ description.ExposeTestClass.methods.oneMoreMethod.arguments[1],
+ {
+ description: 'some number',
+ type: 'number',
+ tsType: 'number',
+ name: 'num',
+ isOptional: true,
+ },
+ );
+
+ // anotherMethod
+ assert.equal(
+ description.ExposeTestClass.methods.anotherMethod.arguments.length,
+ 2,
+ );
+ assert.deepEqual(
+ description.ExposeTestClass.methods.anotherMethod.returns,
+ {
+ description: '',
+ type: 'any',
+ tsType: 'any',
+ },
+ );
+ assert.deepEqual(
+ description.ExposeTestClass.methods.anotherMethod.arguments[0],
+ {
+ description: '',
+ type: 'any',
+ tsType: 'any',
+ name: 'strArr',
+ isOptional: false,
+ },
+ );
+ assert.deepEqual(
+ description.ExposeTestClass.methods.anotherMethod.arguments[1],
+ {
+ description: '',
+ type: 'any',
+ tsType: 'any',
+ name: 'num',
+ isOptional: false,
+ },
+ );
+
+ // testMethod
+ assert.equal(
+ description.ExposeTestClass.methods.testMethod.arguments.length,
+ 0,
+ );
+ assert.deepEqual(
+ description.ExposeTestClass.methods.testMethod.returns,
+ {
+ description: '',
+ type: 'any',
+ tsType: 'any',
+ },
+ );
+
+ assert.notEqual(description.ExposeTestClassExtended, undefined);
+ assert.equal(
+ description.ExposeTestClassExtended.inherits,
+ 'ExposeTestClass',
+ );
+ });
+ it('should parse embedded async functions', () => {
+ assert.deepEqual(
+ description.ExposeTestClass.methods.withEmbeddedAsync.returns,
+ {
+ description: '',
+ type: 'string',
+ tsType: 'string',
+ },
+ );
+ });
+
+ it('should skip parsed classes whose name does not match', () => {
+ class RenamedExposeClass {
+ /**
+ * Doc for a renamed class method
+ *
+ * @return {void}
+ */
+ public renamedMethod() {}
+ }
+
+ // force ctor.name to differ from the class name in ctor.toString(), so
+ // the source parser finds a class declaration that does not match
+ Object.defineProperty(RenamedExposeClass, 'name', {
+ value: 'MismatchedExposeName',
+ });
+
+ const descriptor = Object.getOwnPropertyDescriptor(
+ RenamedExposeClass.prototype,
+ 'renamedMethod',
+ );
+
+ assert.doesNotThrow(() =>
+ expose()(RenamedExposeClass.prototype, 'renamedMethod', descriptor),
+ );
+ });
+
+ it('should register the method via the legacy signature', () => {
+ class LegacyExposeClass {
+ /**
+ * Legacy documented method
+ *
+ * @param {number} a - some number
+ * @return {string}
+ */
+ public legacyMethod(a: number) {
+ return String(a);
+ }
+ }
+
+ const descriptor = Object.getOwnPropertyDescriptor(
+ LegacyExposeClass.prototype,
+ 'legacyMethod',
+ );
+
+ // (target, propertyKey, descriptor) with no TC39 context object
+ const result = expose()(
+ LegacyExposeClass.prototype,
+ 'legacyMethod',
+ descriptor,
+ );
+
+ assert.equal(result, descriptor);
+ assert.notEqual(description.LegacyExposeClass, undefined);
+ assert.equal(
+ description.LegacyExposeClass.methods.legacyMethod.description,
+ 'Legacy documented method',
+ );
+ });
+});
diff --git a/test/decorators/expose.ts b/test/decorators/expose.ts
deleted file mode 100644
index 4762c32..0000000
--- a/test/decorators/expose.ts
+++ /dev/null
@@ -1,184 +0,0 @@
-/*!
- * expose() Function Unit Tests
- *
- * I'm Queue Software Project
- * Copyright (C) 2025 imqueue.com
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- * If you want to use this code in a closed source (commercial) project, you can
- * purchase a proprietary commercial license. Please contact us at
- * to get commercial licensing options.
- */
-import '../mocks';
-import { expect } from 'chai';
-import { expose, IMQRPCDescription } from '../..';
-
-const description = IMQRPCDescription.serviceDescription;
-
-class ExposeTestClass {
-
- @expose()
- public testMethod() {}
-
- // noinspection JSUnusedLocalSymbols
- @expose()
- public async anotherMethod(strArr: string[], num?: number) {}
-
- /**
- * Commented service method
- *
- * @param {string[]} strArr - array of strings
- * @param {number} [num] - some number
- * @return {string} - dummy string
- */
- @expose()
- public async oneMoreMethod(strArr: string[], num?: number) {
- return await new Promise((res, rej) => {
- try {
- res('Hello, World!');
- }
-
- catch (e) {
- rej(e);
- }
- });
- }
-
- // noinspection JSUnusedGlobalSymbols
- public nonExposedMethod() {}
-
- /**
- * @param {number} a
- * @returns {string}
- */
- @expose()
- async withEmbeddedAsync(a: number) {
- return await new Promise(async (resolve) => {
- try {
- resolve(a.toString());
- } catch(e) {
- resolve('');
- }
- });
- }
-}
-
-// noinspection JSUnusedLocalSymbols
-class ExposeTestClassExtended extends ExposeTestClass {
- @expose()
- public extendedMethod() {}
-}
-
-describe('decorators/expose()', () => {
- it('should be a function', () => {
- expect(typeof expose).to.equal('function');
- });
- it('should return decorator function', () => {
- expect(typeof expose()).to.equal('function');
- });
- it('should properly fill exposed description', () => {
- expect(description.ExposeTestClass).not.to.be.undefined;
- expect(description.ExposeTestClass.inherits).to.equal('Function');
- expect(description.ExposeTestClass.methods).not.to.be.undefined;
- expect(description.ExposeTestClass.methods.testMethod)
- .not.to.be.undefined;
- expect(description.ExposeTestClass.methods.anotherMethod)
- .not.to.be.undefined;
- expect(description.ExposeTestClass.methods.oneMoreMethod)
- .not.to.be.undefined;
- expect(description.ExposeTestClass.methods.nonExposedMethod)
- .to.be.undefined;
-
- expect(description.ExposeTestClass.methods.oneMoreMethod.description)
- .to.equal('Commented service method');
-
- // oneMoreMethod
- expect(description.ExposeTestClass.methods.oneMoreMethod
- .arguments.length
- ).to.equal(2);
- expect(description.ExposeTestClass.methods.oneMoreMethod.returns)
- .to.deep.equal({
- description: 'dummy string',
- type: 'Promise',
- tsType: 'string'
- });
- expect(description.ExposeTestClass.methods.oneMoreMethod.arguments[0])
- .to.deep.equal({
- description: 'array of strings',
- type: 'Array',
- tsType: 'string[]',
- name: 'strArr',
- isOptional: false
- });
- expect(description.ExposeTestClass.methods.oneMoreMethod.arguments[1])
- .to.deep.equal({
- description: 'some number',
- type: 'Number',
- tsType: 'number',
- name: 'num',
- isOptional: true
- });
-
- // anotherMethod
- expect(description.ExposeTestClass.methods.anotherMethod
- .arguments.length
- ).to.equal(2);
- expect(description.ExposeTestClass.methods.anotherMethod.returns)
- .to.deep.equal({
- description: '',
- type: 'Promise',
- tsType: 'Promise'
- });
- expect(description.ExposeTestClass.methods.anotherMethod.arguments[0])
- .to.deep.equal({
- description: '',
- type: 'Array',
- tsType: 'any[]',
- name: 'strArr',
- isOptional: false
- });
- expect(description.ExposeTestClass.methods.anotherMethod.arguments[1])
- .to.deep.equal({
- description: '',
- type: 'Number',
- tsType: 'number',
- name: 'num',
- isOptional: false
- });
-
- // testMethod
- expect(description.ExposeTestClass.methods.testMethod.arguments.length)
- .to.equal(0);
- expect(description.ExposeTestClass.methods.testMethod.returns)
- .to.deep.equal({
- description: '',
- type: 'undefined',
- tsType: 'void'
- });
-
- expect(description.ExposeTestClassExtended)
- .not.to.be.undefined;
- expect(description.ExposeTestClassExtended.inherits)
- .to.be.equal('ExposeTestClass');
- });
- it('should parse embedded async functions', () => {
- expect(description.ExposeTestClass.methods.withEmbeddedAsync.returns)
- .to.deep.equal({
- description: '',
- type: 'Promise',
- tsType: 'string'
- });
- });
-});
diff --git a/test/decorators/indexed.ts b/test/decorators/indexed.spec.ts
similarity index 73%
rename from test/decorators/indexed.ts
rename to test/decorators/indexed.spec.ts
index 0bf5230..0892358 100644
--- a/test/decorators/indexed.ts
+++ b/test/decorators/indexed.spec.ts
@@ -22,7 +22,8 @@
* to get commercial licensing options.
*/
import '../mocks';
-import { expect } from 'chai';
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
import { indexed, property, IMQRPCDescription } from '../..';
@indexed('[fieldName: string]: SchemaField')
@@ -47,22 +48,28 @@ const typesMetadata = IMQRPCDescription.typesDescription;
describe('decorators/property()', () => {
it('should be a function', () => {
- expect(typeof indexed).to.equal('function');
+ assert.equal(typeof indexed, 'function');
});
it('should return decorator function', () => {
- expect(typeof indexed('[name: string]: any')).to.equal('function');
+ assert.equal(typeof indexed('[name: string]: any'), 'function');
});
it('should properly fill exposed metadata', () => {
- expect(typesMetadata.Schema).not.to.be.undefined;
- expect(typesMetadata.Schema.indexType)
- .contains('[fieldName: string]: SchemaField');
+ assert.notEqual(typesMetadata.Schema, undefined);
+ assert.ok(
+ typesMetadata.Schema!.indexType!.includes(
+ '[fieldName: string]: SchemaField',
+ ),
+ );
});
it('should accept thunk definition', () => {
- expect(typesMetadata.SchemaThunk).not.to.be.undefined;
- expect(typesMetadata.SchemaThunk.indexType)
- .contains('[fieldName: string]: SchemaField');
+ assert.notEqual(typesMetadata.SchemaThunk, undefined);
+ assert.ok(
+ typesMetadata.SchemaThunk!.indexType!.includes(
+ '[fieldName: string]: SchemaField',
+ ),
+ );
});
});
diff --git a/test/decorators/lock.skipArgs.spec.ts b/test/decorators/lock.skipArgs.spec.ts
index 2e181eb..a1b2196 100644
--- a/test/decorators/lock.skipArgs.spec.ts
+++ b/test/decorators/lock.skipArgs.spec.ts
@@ -1,5 +1,6 @@
import '../mocks';
-import { expect } from 'chai';
+import { describe, it, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
import { lock } from '../..';
class SkipArgsClass {
@@ -29,8 +30,8 @@ describe('decorators/lock() with skipArgs', () => {
]);
// Since b (index 1) is skipped in signature, all concurrent calls share one lock
const uniq = [...new Set(results)];
- expect(uniq.length).to.equal(1);
+ assert.equal(uniq.length, 1);
// Original method body should be executed only once
- expect(obj.calls).to.equal(1);
+ assert.equal(obj.calls, 1);
});
});
diff --git a/test/decorators/lock.ts b/test/decorators/lock.spec.ts
similarity index 57%
rename from test/decorators/lock.ts
rename to test/decorators/lock.spec.ts
index da5467e..3a83387 100644
--- a/test/decorators/lock.ts
+++ b/test/decorators/lock.spec.ts
@@ -22,11 +22,11 @@
* to get commercial licensing options.
*/
import '../mocks';
-import { expect } from 'chai';
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
import { lock } from '../..';
class TestLockClass {
-
@lock()
public async dynamicLock() {
return Math.random() * Math.random() + Math.random();
@@ -46,45 +46,66 @@ class TestLockClass {
public static async rejected() {
throw new Error('Rejected!');
}
-
}
describe('decorators/lock()', () => {
it('should be a function', () => {
- expect(typeof lock).to.equal('function');
+ assert.equal(typeof lock, 'function');
});
it('should return decorator function', () => {
- expect(typeof lock()).to.equal('function');
+ assert.equal(typeof lock(), 'function');
});
- it('should resolve all called with the first resolved', async () => {
+ it('should resolve all called with the first resolved', async () => {
const obj = new TestLockClass();
- const results = [...new Set(await Promise.all(
- new Array(10).fill(0).map(() => obj.dynamicLock())
- ))];
- expect(results.length).to.equal(1);
+ const results = [
+ ...new Set(
+ await Promise.all(
+ new Array(10).fill(0).map(() => obj.dynamicLock()),
+ ),
+ ),
+ ];
+ assert.equal(results.length, 1);
});
- it('should work for static methods as well', async () => {
- const results = [...new Set(await Promise.all(
- new Array(10).fill(0).map(() => TestLockClass.staticLock())
- ))];
- expect(results.length).to.equal(1);
+ it('should work for static methods as well', async () => {
+ const results = [
+ ...new Set(
+ await Promise.all(
+ new Array(10).fill(0).map(() => TestLockClass.staticLock()),
+ ),
+ ),
+ ];
+ assert.equal(results.length, 1);
});
it('should work with non-promised methods as well', async () => {
- const results = [...new Set(await Promise.all(
- new Array(10).fill(0).map(() => TestLockClass.nonPromised())
- ))];
- expect(results.length).to.equal(1);
+ const results = [
+ ...new Set(
+ await Promise.all(
+ new Array(10)
+ .fill(0)
+ .map(() => TestLockClass.nonPromised()),
+ ),
+ ),
+ ];
+ assert.equal(results.length, 1);
+ });
+
+ it('should release the lock and rethrow when the method throws', async () => {
+ await assert.rejects(TestLockClass.rejected(), /Rejected!/);
});
it('should be turned off if DISABLE_LOCKS env var set', async () => {
process.env['DISABLE_LOCKS'] = '1';
- const results = [...new Set(await Promise.all(
- new Array(10).fill(0).map(() => TestLockClass.staticLock())
- ))];
- expect(results.length).to.equal(10);
+ const results = [
+ ...new Set(
+ await Promise.all(
+ new Array(10).fill(0).map(() => TestLockClass.staticLock()),
+ ),
+ ),
+ ];
+ assert.equal(results.length, 10);
});
});
diff --git a/test/decorators/logged.ts b/test/decorators/logged.spec.ts
similarity index 58%
rename from test/decorators/logged.ts
rename to test/decorators/logged.spec.ts
index f5a8b54..326dadb 100644
--- a/test/decorators/logged.ts
+++ b/test/decorators/logged.spec.ts
@@ -1,19 +1,18 @@
-import 'reflect-metadata';
-import { expect } from 'chai';
-import * as sinon from 'sinon';
+import { describe, it, mock } from 'node:test';
+import assert from 'node:assert/strict';
import { logged } from '../../src/decorators/logged';
describe('decorators/logged()', () => {
it('should be a function and return decorator function', () => {
- expect(typeof logged).to.equal('function');
+ assert.equal(typeof logged, 'function');
// @ts-ignore
const decorator = logged();
- expect(typeof decorator).to.equal('function');
+ assert.equal(typeof decorator, 'function');
});
it('should fallback to console logger and rethrow by default', async () => {
const error = new Error('boom');
- const stub = sinon.stub(console, 'error');
+ const stub = mock.method(console, 'error', () => {});
class A {
// @ts-ignore
@logged()
@@ -24,23 +23,23 @@ describe('decorators/logged()', () => {
try {
await new A().fail();
- expect.fail('should throw');
+ assert.fail('should throw');
} catch (e) {
- expect(e).to.equal(error);
- expect(stub.calledOnce).to.equal(true);
- expect(stub.firstCall.args[0]).to.equal(error);
+ assert.equal(e, error);
+ assert.equal(stub.mock.callCount() === 1, true);
+ assert.equal(stub.mock.calls[0].arguments[0], error);
} finally {
- stub.restore();
+ stub.mock.restore();
}
});
it('should use provided logger, level and suppress throw when doNotThrow', async () => {
const error = new Error('warned');
const myLogger = {
- warn: sinon.stub(),
- error: sinon.stub(),
- log: sinon.stub(),
- info: sinon.stub(),
+ warn: mock.fn(),
+ error: mock.fn(),
+ log: mock.fn(),
+ info: mock.fn(),
} as any;
class B {
@@ -52,18 +51,18 @@ describe('decorators/logged()', () => {
}
const res = await new B().fail();
- expect(res).to.equal(undefined);
- expect(myLogger.warn.calledOnce).to.equal(true);
- expect(myLogger.warn.firstCall.args[0]).to.equal(error);
+ assert.equal(res, undefined);
+ assert.equal(myLogger.warn.mock.callCount() === 1, true);
+ assert.equal(myLogger.warn.mock.calls[0].arguments[0], error);
});
it('should accept ILogger directly and rethrow by default', async () => {
const error = new Error('as-logger');
const myLogger = {
- warn: sinon.stub(),
- error: sinon.stub(),
- log: sinon.stub(),
- info: sinon.stub(),
+ warn: mock.fn(),
+ error: mock.fn(),
+ log: mock.fn(),
+ info: mock.fn(),
} as any;
class C {
@@ -76,18 +75,18 @@ describe('decorators/logged()', () => {
try {
await new C().fail();
- expect.fail('should throw');
+ assert.fail('should throw');
} catch (e) {
- expect(e).to.equal(error);
- expect(myLogger.error.calledOnce).to.equal(true);
- expect(myLogger.error.firstCall.args[0]).to.equal(error);
+ assert.equal(e, error);
+ assert.equal(myLogger.error.mock.callCount() === 1, true);
+ assert.equal(myLogger.error.mock.calls[0].arguments[0], error);
}
});
it('should use instance logger when present and rethrow', async () => {
const error = new Error('inst-logger');
const myLogger = {
- error: sinon.stub(),
+ error: mock.fn(),
} as any;
class E {
public logger = myLogger;
@@ -99,30 +98,32 @@ describe('decorators/logged()', () => {
}
try {
await new E().fail();
- expect.fail('should throw');
+ assert.fail('should throw');
} catch (e) {
- expect(e).to.equal(error);
- expect(myLogger.error.calledOnce).to.equal(true);
- expect(myLogger.error.firstCall.args[0]).to.equal(error);
+ assert.equal(e, error);
+ assert.equal(myLogger.error.mock.callCount() === 1, true);
+ assert.equal(myLogger.error.mock.calls[0].arguments[0], error);
}
});
it('should use target logger when present on prototype for instance method', async () => {
const error = new Error('proto-logger');
- const protologger = { error: sinon.stub() } as any;
+ const protologger = { error: mock.fn() } as any;
class F {
// @ts-ignore
@logged()
- public fail() { throw error; }
+ public fail() {
+ throw error;
+ }
}
(F.prototype as any).logger = protologger;
try {
await new F().fail();
- expect.fail('should throw');
+ assert.fail('should throw');
} catch (e) {
- expect(e).to.equal(error);
- expect(protologger.error.calledOnce).to.equal(true);
- expect(protologger.error.firstCall.args[0]).to.equal(error);
+ assert.equal(e, error);
+ assert.equal(protologger.error.mock.callCount() === 1, true);
+ assert.equal(protologger.error.mock.calls[0].arguments[0], error);
}
});
@@ -136,6 +137,6 @@ describe('decorators/logged()', () => {
}
const v = await new D().ok();
- expect(v).to.equal(42);
+ assert.equal(v, 42);
});
});
diff --git a/test/decorators/logged.targetLogger.spec.ts b/test/decorators/logged.targetLogger.spec.ts
deleted file mode 100644
index e490c1d..0000000
--- a/test/decorators/logged.targetLogger.spec.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { expect } from 'chai';
-import { logged } from '../../src/decorators/logged';
-
-// Small, targeted tests to hit missing branches in logged.ts (lines 64 and 72)
-describe('decorators/logged() target logger and no-original branches', () => {
- it('should return undefined when original is missing (no call, no throw)', async () => {
- const descriptor: PropertyDescriptor = { value: undefined } as any;
- const target: any = {};
-
- // Apply decorator to a descriptor without original implementation
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged() as any)(target, 'missing', descriptor);
-
- // Call decorated function directly; should just return undefined
- const result = await (descriptor.value as any)();
- expect(result).to.be.undefined;
- });
-
- it('should use target logger when this is undefined and rethrow by default', async () => {
- const error = new Error('boom');
- const original = () => { throw error; };
- const descriptor: PropertyDescriptor = { value: original } as any;
-
- const targetLoggerCalls: Error[] = [];
- const target: any = {
- logger: {
- error: (e: Error) => { targetLoggerCalls.push(e); },
- warn: () => void 0,
- info: () => void 0,
- log: () => void 0,
- },
- };
-
- // Apply decorator; we will call without binding `this` so inside wrapper
- // `this` will be undefined and the code should select target.logger (line 72)
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged() as any)(target, 'willThrow', descriptor);
-
- try {
- // Call as a plain function to keep `this` undefined in strict mode
- await (descriptor.value as any)();
- expect.fail('should have thrown');
- } catch (e) {
- expect(e).to.equal(error);
- expect(targetLoggerCalls).to.have.length(1);
- expect(targetLoggerCalls[0]).to.equal(error);
- }
- });
-
- it('should return undefined when original is missing (no call, no throw)', async () => {
- const descriptor: PropertyDescriptor = { value: undefined } as any;
- const target: any = {};
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged() as any)(target, 'missing', descriptor);
- const result = await (descriptor.value as any)();
- expect(result).to.be.undefined;
- });
-
- it('should use target logger when this is undefined and rethrow by default', async () => {
- const error = new Error('boom');
- const original = () => { throw error; };
- const descriptor: PropertyDescriptor = { value: original } as any;
-
- const targetLoggerCalls: Error[] = [];
- const target: any = {
- logger: {
- error: (e: Error) => { targetLoggerCalls.push(e); },
- warn: () => void 0,
- info: () => void 0,
- log: () => void 0,
- },
- };
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged() as any)(target, 'willThrow', descriptor);
-
- try {
- await (descriptor.value as any)();
- expect.fail('should have thrown');
- } catch (e) {
- expect(e).to.equal(error);
- expect(targetLoggerCalls).to.have.length(1);
- expect(targetLoggerCalls[0]).to.equal(error);
- }
- });
-});
diff --git a/test/decorators/logged.thisLogger.spec.ts b/test/decorators/logged.thisLogger.spec.ts
deleted file mode 100644
index 1bfb404..0000000
--- a/test/decorators/logged.thisLogger.spec.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import { expect } from 'chai';
-import { logged } from '../../src/decorators/logged';
-
-/*
- * Additional coverage for logged.ts to reach 100% branches:
- * - Exercise `this || target` left-hand path (this is truthy)
- * - Ensure `this.logger` is preferred when available
- * - Cover options.logger usage and default rethrow behavior
- * - Cover passing an ILogger directly as options
- */
-
-describe('decorators/logged() this logger and options logger branches', () => {
- afterEach(() => {
- // nothing to cleanup
- });
-
- it('should use bound `this` in apply(), prefer this.logger, not rethrow when doNotThrow=true', async () => {
- const error = new Error('use-this');
- let capturedThis: any;
- // original captures `this` then throws
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const original: any = function original(this: any) {
- capturedThis = this;
- throw error;
- };
-
- const descriptor: PropertyDescriptor = { value: original } as any;
- const target: any = { logger: { error: () => void 0, warn: () => void 0, info: () => void 0, log: () => void 0 } };
-
- // Apply decorator: do not throw and use non-default level to verify dynamic call
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged({ doNotThrow: true, level: 'warn' }) as any)(target, 'x', descriptor);
-
- const thisLoggerCalls: { level: string; error: Error }[] = [];
- const self: any = {
- logger: {
- error: (_e: Error) => thisLoggerCalls.push({ level: 'error', error: _e }),
- warn: (_e: Error) => thisLoggerCalls.push({ level: 'warn', error: _e }),
- info: (_e: Error) => thisLoggerCalls.push({ level: 'info', error: _e }),
- log: (_e: Error) => thisLoggerCalls.push({ level: 'log', error: _e }),
- },
- };
-
- // Call with `this` bound so (this || target) takes left-hand branch
- const res = await (descriptor.value as any).call(self);
- expect(res).to.be.undefined; // swallowed due to doNotThrow
- expect(capturedThis).to.equal(self);
- expect(thisLoggerCalls).to.have.length(1);
- expect(thisLoggerCalls[0].level).to.equal('warn');
- expect(thisLoggerCalls[0].error).to.equal(error);
- });
-
- it('should use options.logger when provided and rethrow by default', async () => {
- const error = new Error('use-options-logger');
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const original: any = () => { throw error; };
- const descriptor: PropertyDescriptor = { value: original } as any;
- const target: any = {};
-
- const calls: Error[] = [];
- const logger = { info: (e: Error) => { calls.push(e); }, error: () => {}, warn: () => {}, log: () => {} } as any;
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged({ logger, level: 'info' }) as any)(target, 'y', descriptor);
-
- try {
- await (descriptor.value as any)();
- expect.fail('should have thrown');
- } catch (e) {
- expect(e).to.equal(error);
- expect(calls).to.have.length(1);
- expect(calls[0]).to.equal(error);
- }
- });
-
- it('should treat options itself as ILogger when it has error() method', async () => {
- const error = new Error('options-is-logger');
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const original: any = () => { throw error; };
- const descriptor: PropertyDescriptor = { value: original } as any;
- const target: any = {};
-
- const calls: Error[] = [];
- const optionsLogger = { error: (e: Error) => calls.push(e), log: () => {}, warn: () => {}, info: () => {} } as any;
-
- // Pass logger directly instead of options object
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged(optionsLogger) as any)(target, 'z', descriptor);
-
- try {
- await (descriptor.value as any)();
- expect.fail('should have thrown');
- } catch (e) {
- expect(e).to.equal(error);
- expect(calls).to.have.length(1);
- expect(calls[0]).to.equal(error);
- }
- });
-
- it('should use bound `this` in apply() and return value when original resolves', async () => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const original: any = function(this: any) { return this.answer; };
- const descriptor: PropertyDescriptor = { value: original } as any;
- const target: any = {};
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged() as any)(target, 'ok', descriptor);
- const self: any = { answer: 42 };
- const res = await (descriptor.value as any).call(self);
- expect(res).to.equal(42);
- });
-
- it('should use target as `this` when caller `this` is falsy (right branch of this||target)', async () => {
- let gotThis: any;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const original: any = function(this: any) { gotThis = this; return this.mark; };
- const descriptor: PropertyDescriptor = { value: original } as any;
- const target: any = { mark: 'TARGET' };
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged() as any)(target, 'right', descriptor);
- // Explicitly call with undefined `this`
- const res = await (descriptor.value as any).call(undefined);
- expect(res).to.equal('TARGET');
- expect(gotThis).to.equal(target);
- });
-
- it('should use target when caller `this` is null (second condition false)', async () => {
- let gotThis: any;
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const original: any = function(this: any) { gotThis = this; return this.mark; };
- const descriptor: PropertyDescriptor = { value: original } as any;
- const target: any = { mark: 'TARGET-NULL' };
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (logged() as any)(target, 'right-null', descriptor);
- // Explicitly call with null `this` to pass first check and fail second
- const res = await (descriptor.value as any).call(null);
- expect(res).to.equal('TARGET-NULL');
- expect(gotThis).to.equal(target);
- });
-});
diff --git a/test/decorators/property.spec.ts b/test/decorators/property.spec.ts
new file mode 100644
index 0000000..3818a2f
--- /dev/null
+++ b/test/decorators/property.spec.ts
@@ -0,0 +1,150 @@
+/*!
+ * property() Function Unit Tests
+ *
+ * I'm Queue Software Project
+ * Copyright (C) 2025 imqueue.com