diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af12b5313..6de9e951d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 'v22.15.0' + node-version: 'v22.22.2' - name: Cache node_modules uses: actions/cache@v3 env: @@ -42,7 +42,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - node: ['18.20.4', 'v20.19.0', 'v22.15.0'] + node: ['v22.22.2'] steps: - uses: actions/checkout@v4 @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 'v22.15.0' + node-version: 'v22.22.2' - name: Cache node_modules uses: actions/cache@v3 env: @@ -102,7 +102,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 'v22.15.0' + node-version: 'v22.22.2' - name: Cache node_modules uses: actions/cache@v3 env: @@ -197,7 +197,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 'v22.15.0' + node-version: 'v22.22.2' - name: Cache node_modules uses: actions/cache@v3 diff --git a/.nvmrc b/.nvmrc index 42126c054..a937bb366 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1,2 +1,2 @@ -22 +22.22.2 diff --git a/CLAUDE.md b/CLAUDE.md index c40e0a900..59dc0fd7d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,12 +12,12 @@ of which dispatch to the same set of command handlers. ## 1. Environment & tooling prerequisites -- **Node.js 22 is required** (`.nvmrc` pins `22`). Always run `nvm use` (or - `source ~/.nvm/nvm.sh && nvm use`) before any `npm`, build, or test command. The wrong - Node version fails with errors like `Unexpected token 'with'` or missing `GLIBC_2.38`. +- **Node.js ≥ 22.13 is required** (`.nvmrc` pins `22.22.2`, matching the Dockerfile and CI; + `package.json` `engines` requires `>=22.13.0`). Always run `nvm use` (or `source ~/.nvm/nvm.sh && nvm use`) before + any `npm`, build, or test command. The wrong Node version fails with errors like + `Unexpected token 'with'`, missing `GLIBC_2.38`, or — since the SQLite layer uses the + built-in `node:sqlite` module — `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite` on Node < 22.13. This is enforced by `.cursor/rules/tests-nvm.mdc` and the in-repo `CLAUDE.md`. -- If `sqlite3` native bindings break after switching to Node 22, rebuild from source: - `npm_config_build_from_source=true npm rebuild sqlite3`. - **`postinstall` runs `scripts/fix-libp2p-http-utils.js`** — a patch applied to a libp2p dependency. Expect it to run on every `npm install`; don't remove it. - **Docker + docker-compose** are needed for the metadata database (Typesense or @@ -270,7 +270,8 @@ Handler source is grouped under `src/components/core/`: indexer state, logs, orders, ddoState, access lists, escrow events — behind the `Abstract*Database` interfaces in `BaseDatabase.ts`. **SQLite** is always used for the nonce DB, config DB, C2D job DB, and auth-token DB (works even with no metadata DB - configured). See `docs/database.md`. + configured) — via Node's built-in `node:sqlite` module (no native addon), wrapped by + `SqliteClient` in `src/components/database/sqliteClient.ts`. See `docs/database.md`. - **KeyManager/** — provider-abstraction over the node key (`docs/KeyManager.md`). Currently `RawPrivateKeyProvider` (from `PRIVATE_KEY`); derives the libp2p peerId/keys and the EVM address, and caches the ethers signer. Designed to add KMS providers (GCP/AWS) later. diff --git a/package-lock.json b/package-lock.json index 38519ff42..0066e6c4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,7 +57,6 @@ "lodash": "^4.18.1", "lzma-purejs-requirejs": "^1.0.0", "node-cron": "^3.0.3", - "sqlite3": "^6.0.1", "stream-concat": "^1.0.0", "tar": "^7.5.16", "uint8arrays": "^4.0.6", @@ -98,6 +97,9 @@ "sinon": "^19.0.2", "tsx": "^4.22.4", "typescript": "^5.9.3" + }, + "engines": { + "node": ">=22.13.0" } }, "node_modules/@achingbrain/http-parser-js": { @@ -2764,16 +2766,6 @@ "node": ">=14" } }, - "node_modules/@gar/promise-retry": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", - "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@grpc/grpc-js": { "version": "1.14.4", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", @@ -5185,56 +5177,6 @@ "lodash": "^4.15.0" } }, - "node_modules/@npmcli/agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", - "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", - "license": "ISC", - "optional": true, - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^11.2.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", - "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/fs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", - "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", - "license": "ISC", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/@npmcli/redact": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", - "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/@oceanprotocol/contracts": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/@oceanprotocol/contracts/-/contracts-2.9.0.tgz", @@ -7203,16 +7145,6 @@ "integrity": "sha512-fbOadP7twxt0ZYT9mgIC+xQMk6f3pYYLI5a/2UJ/mc/ygqb/NoVv2ryK3lTtoi74xwkdpUeDwIuFQSosowzUgg==", "license": "MIT" }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -7330,16 +7262,6 @@ "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "license": "MIT" }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -7884,15 +7806,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -8226,125 +8139,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/cacache": { - "version": "20.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", - "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^5.0.0", - "fs-minipass": "^3.0.0", - "glob": "^13.0.0", - "lru-cache": "^11.1.0", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/cacache/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==", - "license": "MIT", - "optional": true, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "optional": true, - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", - "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "optional": true, - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacache/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "license": "BlueOak-1.0.0", - "optional": true, - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "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", @@ -9254,21 +9048,6 @@ "node": ">=0.10.0" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -9282,15 +9061,6 @@ "node": ">=6" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9464,15 +9234,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "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", @@ -9767,16 +9528,6 @@ "once": "^1.4.0" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/err-code": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", @@ -11145,22 +10896,6 @@ "safe-buffer": "^5.1.1" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0", - "optional": true - }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", @@ -11438,12 +11173,6 @@ "moment": "^2.29.1" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -11733,19 +11462,6 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -11997,12 +11713,6 @@ "git-up": "^8.1.0" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -12124,7 +11834,7 @@ "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==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -12395,13 +12105,6 @@ "dev": true, "license": "MIT" }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause", - "optional": true - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -12422,34 +12125,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/humanhash": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/humanhash/-/humanhash-1.0.4.tgz", @@ -12621,12 +12296,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, "node_modules/interface-datastore": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/interface-datastore/-/interface-datastore-9.0.2.tgz", @@ -12671,7 +12340,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -14345,40 +14014,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-fetch-happen": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.5.tgz", - "integrity": "sha512-uCbIa8jWWmQZt4dSnEStkVC6gdakiinAm4PiGsywIkguF0eWMdcjDz0ECYhUolFU3pFLOev9VNPCEygydXnddg==", - "license": "ISC", - "optional": true, - "dependencies": { - "@gar/promise-retry": "^1.0.0", - "@npmcli/agent": "^4.0.0", - "@npmcli/redact": "^4.0.0", - "cacache": "^20.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^6.0.0", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -14517,18 +14152,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -14561,6 +14184,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14575,119 +14199,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", - "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^2.0.0", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - }, - "optionalDependencies": { - "iconv-lite": "^0.7.2" - } - }, - "node_modules/minipass-fetch/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", - "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", - "license": "BlueOak-1.0.0", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", - "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/minizlib": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", @@ -14900,12 +14411,6 @@ "node": "^18 || >=20" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, "node_modules/napi-macros": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", @@ -15007,18 +14512,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/node-abi": { - "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", @@ -15122,31 +14615,6 @@ "node": ">= 6.13.0" } }, - "node_modules/node-gyp": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", - "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -15158,32 +14626,6 @@ "node-gyp-build-test": "build-test.js" } }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -15204,22 +14646,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/null-prototype-object": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/null-prototype-object/-/null-prototype-object-1.2.6.tgz", @@ -16451,33 +15877,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16532,16 +15931,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "license": "ISC", - "optional": true, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -16893,30 +16282,6 @@ "node": ">= 0.8" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -17948,51 +17313,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/sinon": { "version": "19.0.5", "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.5.tgz", @@ -18026,7 +17346,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -18037,7 +17357,7 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "ip-address": "^10.0.1", @@ -18048,21 +17368,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -18131,42 +17436,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sqlite3": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz", - "integrity": "sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^8.0.0", - "prebuild-install": "^7.1.3", - "tar": "^7.5.10" - }, - "engines": { - "node": ">=20.17.0" - }, - "optionalDependencies": { - "node-gyp": "12.x" - }, - "peerDependencies": { - "node-gyp": "12.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/sqlite3/node_modules/node-addon-api": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", - "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", @@ -18191,19 +17460,6 @@ "license": "MIT", "optional": true }, - "node_modules/ssri": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", - "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -18782,7 +18038,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -18799,7 +18055,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -18817,7 +18073,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -18989,18 +18245,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", diff --git a/package.json b/package.json index d73fa4187..0d268e2ca 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "license": "Apache-2.0", "type": "module", "main": "index.js", + "engines": { + "node": ">=22.13.0" + }, "bugs": { "url": "https://github.com/oceanprotocol/ocean-node/issues" }, @@ -96,7 +99,6 @@ "lodash": "^4.18.1", "lzma-purejs-requirejs": "^1.0.0", "node-cron": "^3.0.3", - "sqlite3": "^6.0.1", "stream-concat": "^1.0.0", "tar": "^7.5.16", "uint8arrays": "^4.0.6", diff --git a/src/components/database/AuthTokenDatabase.ts b/src/components/database/AuthTokenDatabase.ts index 662489a41..b1f6c860a 100644 --- a/src/components/database/AuthTokenDatabase.ts +++ b/src/components/database/AuthTokenDatabase.ts @@ -1,8 +1,6 @@ import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { AbstractDatabase } from './BaseDatabase.js' import { OceanNodeDBConfig } from '../../@types/OceanNode.js' -import path from 'path' -import * as fs from 'fs' import { SQLiteAuthToken } from './sqliteAuthToken.js' export interface AuthToken { @@ -24,10 +22,7 @@ export class AuthTokenDatabase extends AbstractDatabase { static async create(config: OceanNodeDBConfig): Promise { DATABASE_LOGGER.info('Creating AuthTokenDatabase with SQLite') - const dbDir = path.dirname('databases/authTokenDatabase.sqlite') - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }) - } + // SqliteClient creates the parent directory on construction. const provider = new SQLiteAuthToken('databases/authTokenDatabase.sqlite') await provider.createTable() return new AuthTokenDatabase(config, provider) diff --git a/src/components/database/SQLLiteConfigDatabase.ts b/src/components/database/SQLLiteConfigDatabase.ts index 72f9f0942..540f991e2 100644 --- a/src/components/database/SQLLiteConfigDatabase.ts +++ b/src/components/database/SQLLiteConfigDatabase.ts @@ -1,5 +1,3 @@ -import fs from 'fs' -import path from 'path' import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { GENERIC_EMOJIS, LOG_LEVELS_STR } from '../../utils/logging/Logger.js' import { SQLiteProvider } from './sqlite.js' @@ -11,11 +9,7 @@ export class SQLLiteConfigDatabase { return (async (): Promise => { DATABASE_LOGGER.info('Config Database initiated with SQLite provider') - // Ensure the directory exists before instantiating SQLiteProvider - const dbDir = path.dirname('databases/config.sqlite') - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }) - } + // SqliteClient creates the parent directory on construction. this.provider = new SQLiteProvider('databases/config.sqlite') await this.provider.createTableForConfig() diff --git a/src/components/database/SQLLiteNonceDatabase.ts b/src/components/database/SQLLiteNonceDatabase.ts index 06334c6e7..039518050 100644 --- a/src/components/database/SQLLiteNonceDatabase.ts +++ b/src/components/database/SQLLiteNonceDatabase.ts @@ -1,5 +1,3 @@ -import fs from 'fs' -import path from 'path' import { OceanNodeDBConfig } from '../../@types/OceanNode.js' import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { GENERIC_EMOJIS, LOG_LEVELS_STR } from '../../utils/logging/Logger.js' @@ -15,11 +13,7 @@ export class SQLLiteNonceDatabase extends AbstractNonceDatabase { return (async (): Promise => { DATABASE_LOGGER.info('Nonce Database initiated with SQLite provider') - // Ensure the directory exists before instantiating SQLiteProvider - const dbDir = path.dirname('databases/nonceDatabase.sqlite') - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }) - } + // SqliteClient creates the parent directory on construction. this.provider = new SQLiteProvider('databases/nonceDatabase.sqlite') await this.provider.createTableForNonce() diff --git a/src/components/database/sqlite.ts b/src/components/database/sqlite.ts index 61231688e..4a081e55d 100644 --- a/src/components/database/sqlite.ts +++ b/src/components/database/sqlite.ts @@ -1,5 +1,5 @@ import { TypesenseSchema, typesenseSchemas } from './TypesenseSchemas.js' -import sqlite3 from 'sqlite3' +import { SqliteClient } from './sqliteClient.js' interface DatabaseProvider { createNonce(address: string, nonce: number): Promise<{ id: string; nonce: number }> @@ -9,46 +9,34 @@ interface DatabaseProvider { } export class SQLiteProvider implements DatabaseProvider { - private db: sqlite3.Database + private db: SqliteClient private schemaNonce: TypesenseSchema private configSchema: string constructor(dbFilePath: string) { - this.db = new sqlite3.Database(dbFilePath) + this.db = new SqliteClient(dbFilePath) this.schemaNonce = typesenseSchemas.nonceSchemas this.configSchema = 'config' } // eslint-disable-next-line require-await async createTableForNonce() { - const createTableSQL = ` + this.db.exec(` CREATE TABLE IF NOT EXISTS ${this.schemaNonce.name} ( id TEXT PRIMARY KEY, nonce INTEGER ); - ` - return new Promise((resolve, reject) => { - this.db.run(createTableSQL, (err) => { - if (err) reject(err) - else resolve() - }) - }) + `) } // eslint-disable-next-line require-await async createTableForConfig() { - const createTableSQL = ` + this.db.exec(` CREATE TABLE IF NOT EXISTS ${this.configSchema} ( key TEXT NOT NULL PRIMARY KEY, value TEXT ); - ` - return new Promise((resolve, reject) => { - this.db.run(createTableSQL, (err) => { - if (err) reject(err) - else resolve() - }) - }) + `) } // eslint-disable-next-line require-await @@ -58,12 +46,8 @@ export class SQLiteProvider implements DatabaseProvider { VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET nonce=excluded.nonce; ` - return new Promise<{ id: string; nonce: number }>((resolve, reject) => { - this.db.run(insertSQL, [address, nonce], (err) => { - if (err) reject(err) - else resolve({ id: address, nonce }) - }) - }) + this.db.run(insertSQL, [address, nonce]) + return { id: address, nonce } } // eslint-disable-next-line require-await @@ -73,12 +57,8 @@ export class SQLiteProvider implements DatabaseProvider { VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value; ` - return new Promise<{ key: string; value: string }>((resolve, reject) => { - this.db.run(insertSQL, [key, value], (err) => { - if (err) reject(err) - else resolve({ key, value }) - }) - }) + this.db.run(insertSQL, [key, value]) + return { key, value } } // eslint-disable-next-line require-await @@ -86,13 +66,8 @@ export class SQLiteProvider implements DatabaseProvider { const selectSQL = ` SELECT * FROM ${this.schemaNonce.name} WHERE id = ? ` - return new Promise<{ id: string; nonce: number | null }>((resolve, reject) => { - this.db.get(selectSQL, [address], (err, row: { nonce: number } | undefined) => { - if (err) reject(err) - else - resolve(row ? { id: address, nonce: row.nonce } : { id: address, nonce: null }) - }) - }) + const row = this.db.get<{ nonce: number }>(selectSQL, [address]) + return row ? { id: address, nonce: row.nonce } : { id: address, nonce: null } } // eslint-disable-next-line require-await @@ -100,12 +75,9 @@ export class SQLiteProvider implements DatabaseProvider { const selectSQL = ` SELECT value FROM ${this.configSchema} WHERE key = ?; ` - return new Promise<{ value: string | null }>((resolve, reject) => { - this.db.get(selectSQL, [key], (err, row: { value: string } | undefined) => { - if (err) reject(err) - else resolve(row ? { value: row.value } : { value: null }) // Returns null if no version exists - }) - }) + const row = this.db.get<{ value: string }>(selectSQL, [key]) + // Returns null if no version exists + return row ? { value: row.value } : { value: null } } // eslint-disable-next-line require-await @@ -123,16 +95,10 @@ export class SQLiteProvider implements DatabaseProvider { DELETE FROM ${this.schemaNonce.name} WHERE id = ? ` - return new Promise<{ id: string; nonce: number | null }>((resolve, reject) => { - this.db.get(selectSQL, [address], (err, row: { nonce: number } | undefined) => { - if (err) return reject(err) - if (!row) return resolve({ id: address, nonce: null }) + const row = this.db.get<{ nonce: number }>(selectSQL, [address]) + if (!row) return { id: address, nonce: null } - this.db.run(deleteSQL, [address], (err) => { - if (err) reject(err) - else resolve({ id: address, nonce: row.nonce }) - }) - }) - }) + this.db.run(deleteSQL, [address]) + return { id: address, nonce: row.nonce } } } diff --git a/src/components/database/sqliteAuthToken.ts b/src/components/database/sqliteAuthToken.ts index 5f607dafd..bdc8ba596 100644 --- a/src/components/database/sqliteAuthToken.ts +++ b/src/components/database/sqliteAuthToken.ts @@ -1,5 +1,5 @@ import { AuthToken } from './AuthTokenDatabase.js' -import sqlite3 from 'sqlite3' +import { SqliteClient } from './sqliteClient.js' import { DATABASE_LOGGER } from '../../utils/logging/common.js' interface AuthTokenDatabaseProvider { @@ -15,14 +15,15 @@ interface AuthTokenDatabaseProvider { } export class SQLiteAuthToken implements AuthTokenDatabaseProvider { - private db: sqlite3.Database + private db: SqliteClient constructor(dbFilePath: string) { - this.db = new sqlite3.Database(dbFilePath) + this.db = new SqliteClient(dbFilePath) } + // eslint-disable-next-line require-await async createTable(): Promise { - await this.db.exec(` + this.db.exec(` CREATE TABLE IF NOT EXISTS authTokens ( token TEXT PRIMARY KEY, address TEXT NOT NULL, @@ -33,16 +34,20 @@ export class SQLiteAuthToken implements AuthTokenDatabaseProvider { ) `) - // Migration: Add chainId column if it doesn't exist - return new Promise((resolve) => { - this.db.run(`ALTER TABLE authTokens ADD COLUMN chainId TEXT`, (_err) => { - // Ignore error if column already exists - resolve() - }) - }) + // Migration for DBs created before the chainId column existed: add it if missing. + // A fresh table already has the column, so ALTER throws "duplicate column name" — + // that's the only expected failure, so swallow it. With the synchronous exec above this + // now runs strictly after the CREATE TABLE, unlike the old callback-based `await exec` + // which never actually waited and let the CREATE race the ALTER. + try { + this.db.exec(`ALTER TABLE authTokens ADD COLUMN chainId TEXT`) + } catch { + // column already exists + } } - createToken( + // eslint-disable-next-line require-await + async createToken( token: string, address: string, createdAt: number, @@ -52,73 +57,60 @@ export class SQLiteAuthToken implements AuthTokenDatabaseProvider { const insertSQL = ` INSERT INTO authTokens (token, address, createdAt, validUntil, chainId) VALUES (?, ?, ?, ?, ?) ` - return new Promise((resolve, reject) => { - this.db.run(insertSQL, [token, address, createdAt, validUntil, chainId], (err) => { - if (err) { - DATABASE_LOGGER.error(`Error creating auth token: ${err}`) - reject(err) - } else { - resolve() - } - }) - }) + try { + this.db.run(insertSQL, [token, address, createdAt, validUntil, chainId]) + } catch (err) { + DATABASE_LOGGER.error(`Error creating auth token: ${err}`) + throw err + } } - validateTokenEntry(token: string): Promise { + async validateTokenEntry(token: string): Promise { const selectSQL = ` SELECT * FROM authTokens WHERE token = ? ` - return new Promise((resolve, reject) => { - this.db.get(selectSQL, [token], async (err, row: AuthToken) => { - if (err) { - DATABASE_LOGGER.error(`Error validating auth token: ${err}`) - reject(err) - return - } + let row: AuthToken | undefined + try { + row = this.db.get(selectSQL, [token]) + } catch (err) { + DATABASE_LOGGER.error(`Error validating auth token: ${err}`) + throw err + } - if (!row) { - resolve(null) - return - } + if (!row) { + return null + } - if (!row.isValid) { - resolve(null) - return - } + if (!row.isValid) { + return null + } - if (row.validUntil === null) { - resolve(row) - return - } + if (row.validUntil === null) { + return row + } - const validUntilDate = new Date(row.validUntil).getTime() - const now = Date.now() + const validUntilDate = new Date(row.validUntil).getTime() + const now = Date.now() - if (validUntilDate < now) { - resolve(null) - DATABASE_LOGGER.info(`Auth token ${token} is invalid`) - await this.invalidateTokenEntry(token) - return - } + if (validUntilDate < now) { + DATABASE_LOGGER.info(`Auth token ${token} is invalid`) + await this.invalidateTokenEntry(token) + return null + } - resolve(row) - }) - }) + return row } - invalidateTokenEntry(token: string): Promise { + // eslint-disable-next-line require-await + async invalidateTokenEntry(token: string): Promise { const deleteSQL = ` UPDATE authTokens SET isValid = FALSE WHERE token = ? ` - return new Promise((resolve, reject) => { - this.db.run(deleteSQL, [token], (err) => { - if (err) { - DATABASE_LOGGER.error(`Error invalidating auth token: ${err}`) - reject(err) - } else { - resolve() - } - }) - }) + try { + this.db.run(deleteSQL, [token]) + } catch (err) { + DATABASE_LOGGER.error(`Error invalidating auth token: ${err}`) + throw err + } } } diff --git a/src/components/database/sqliteClient.ts b/src/components/database/sqliteClient.ts new file mode 100644 index 000000000..bd9a31f84 --- /dev/null +++ b/src/components/database/sqliteClient.ts @@ -0,0 +1,59 @@ +import { DatabaseSync } from 'node:sqlite' +import fs from 'fs' +import path from 'path' + +type Bindable = string | number | bigint | Uint8Array | null + +// node:sqlite refuses `undefined` and boolean bindings (throws ERR_INVALID_ARG_TYPE); +// the old `sqlite3` package silently coerced them. Keep that lenient behaviour centrally +// so every call site binds the same way it did before the engine swap. +function sanitize(v: unknown): Bindable { + if (v === undefined) return null + if (typeof v === 'boolean') return v ? 1 : 0 + return v as Bindable +} + +/** + * Thin synchronous wrapper around Node's built-in `node:sqlite` (`DatabaseSync`). + * Owns a single database handle per file and the sqlite3-compatibility concerns + * (bind sanitization, eager parent-directory creation). This replaces the former + * `sqlite3` native addon; all embedded DBs (nonce, config, C2D jobs, auth tokens, + * persistent-storage registry) go through this client. + * + * The queries in this project are all single-row or small local-table operations, so + * running them synchronously on the main thread is acceptable. Do not point this client + * at large or unbounded datasets without reconsidering. + */ +export class SqliteClient { + private db: DatabaseSync + + constructor(dbFilePath: string) { + // DatabaseSync opens eagerly and throws synchronously if the parent directory is + // missing (unlike the old sqlite3.Database, which deferred opening). mkdir here so + // every DB path is covered uniformly and callers no longer need to mkdir themselves. + fs.mkdirSync(path.dirname(dbFilePath), { recursive: true }) + this.db = new DatabaseSync(dbFilePath) + // Long-running server process: wait briefly rather than fail if the DB is momentarily + // locked (e.g. an external `sqlite3` CLI reading a live file). + this.db.exec('PRAGMA busy_timeout = 5000;') + } + + exec(sql: string): void { + this.db.exec(sql) + } + + run(sql: string, params: unknown[] = []): { changes: number } { + const result = this.db.prepare(sql).run(...params.map(sanitize)) + // readBigInts is not enabled, so `changes` is a JS number already; coerce to keep the + // public type a plain number (all counts here are tiny, well below Number.MAX_SAFE_INTEGER). + return { changes: Number(result.changes) } + } + + get(sql: string, params: unknown[] = []): T | undefined { + return this.db.prepare(sql).get(...params.map(sanitize)) as T | undefined + } + + all(sql: string, params: unknown[] = []): T[] { + return this.db.prepare(sql).all(...params.map(sanitize)) as T[] + } +} diff --git a/src/components/database/sqliteCompute.ts b/src/components/database/sqliteCompute.ts index 59b5a477b..e3c4e775d 100644 --- a/src/components/database/sqliteCompute.ts +++ b/src/components/database/sqliteCompute.ts @@ -9,7 +9,7 @@ import { SERVICE_START_PENDING_STATUSES, type ServiceJob } from '../../@types/C2D/ServiceOnDemand.js' -import sqlite3, { RunResult } from 'sqlite3' +import { SqliteClient } from './sqliteClient.js' import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { create256Hash } from '../../utils/crypt.js' @@ -65,7 +65,9 @@ export function generateBlobFromJSON(job: DBComputeJob): Buffer { } export function generateJSONFromBlob(blob: any): Promise { - return JSON.parse(blob.toString()) + // node:sqlite returns BLOB columns as Uint8Array (the old sqlite3 addon returned Buffer). + // Buffer.from() handles both, so decode through it before parsing. + return JSON.parse(Buffer.from(blob).toString()) } // we cannot store array of strings, so we use string separators instead @@ -88,28 +90,26 @@ export function convertStringToArray(str: string) { } export class SQLiteCompute implements ComputeDatabaseProvider { - private db: sqlite3.Database + private db: SqliteClient private schema: TypesenseSchema constructor(dbFilePath: string) { - this.db = new sqlite3.Database(dbFilePath) + this.db = new SqliteClient(dbFilePath) this.schema = typesenseSchemas.c2dSchemas } - deleteJob(jobId: string): Promise { + // eslint-disable-next-line require-await + async deleteJob(jobId: string): Promise { const deleteSQL = ` DELETE FROM ${this.schema.name} WHERE jobId = ? ` - return new Promise((resolve, reject) => { - this.db.run(deleteSQL, [jobId], function (this: RunResult, err) { - if (err) reject(err) - else resolve(this.changes === 1) - }) - }) + const { changes } = this.db.run(deleteSQL, [jobId]) + return changes === 1 } - createTable() { - /* although we have field called expiteTimestamp, we are actually storing maxJobDuration in it */ + // eslint-disable-next-line require-await + async createTable() { + /* although we have field called expireTimestamp, we are actually storing maxJobDuration in it */ const createTableSQL = ` CREATE TABLE IF NOT EXISTS ${this.schema.name} ( owner TEXT, @@ -128,31 +128,28 @@ export class SQLiteCompute implements ComputeDatabaseProvider { body BLOB ); ` - return new Promise((resolve, reject) => { - this.db.run(createTableSQL, (err) => { - if (err) reject(err) - else resolve() - }) - }) + try { + this.db.exec(createTableSQL) + } catch (err) { + DATABASE_LOGGER.error(`Could not create ${this.schema.name} table: ${err.message}`) + throw err + } } - createImageTable(): Promise { + // eslint-disable-next-line require-await + async createImageTable(): Promise { const createTableSQL = ` CREATE TABLE IF NOT EXISTS docker_images ( image TEXT PRIMARY KEY, lastUsedTimestamp INTEGER NOT NULL ); ` - return new Promise((resolve, reject) => { - this.db.run(createTableSQL, (err) => { - if (err) { - DATABASE_LOGGER.error('Could not create docker_images table: ' + err.message) - reject(err) - } else { - resolve() - } - }) - }) + try { + this.db.exec(createTableSQL) + } catch (err) { + DATABASE_LOGGER.error('Could not create docker_images table: ' + err.message) + throw err + } } // ── Service-on-Demand jobs ────────────────────────────────────────── @@ -164,7 +161,8 @@ export class SQLiteCompute implements ComputeDatabaseProvider { // This extends the engine's in-memory serviceOpsInFlight guarantee to setups where // several node processes share the same DB file + Docker daemon (e.g. a stale // container still running during a redeploy) — in-memory sets cannot see each other. - createServiceLocksTable(): Promise { + // eslint-disable-next-line require-await + async createServiceLocksTable(): Promise { const createTableSQL = ` CREATE TABLE IF NOT EXISTS service_locks ( serviceId TEXT PRIMARY KEY, @@ -172,22 +170,19 @@ export class SQLiteCompute implements ComputeDatabaseProvider { acquiredAt INTEGER NOT NULL ); ` - return new Promise((resolve, reject) => { - this.db.run(createTableSQL, (err) => { - if (err) { - DATABASE_LOGGER.error('Could not create service_locks table: ' + err.message) - reject(err) - } else { - resolve() - } - }) - }) + try { + this.db.exec(createTableSQL) + } catch (err) { + DATABASE_LOGGER.error('Could not create service_locks table: ' + err.message) + throw err + } } // Atomically takes the lock for serviceId: inserts a fresh row, or steals one whose // acquiredAt is older than staleMs (crashed holder). The single upsert statement is // the atomicity guarantee — two processes racing it can never both see success. - acquireServiceLock( + // eslint-disable-next-line require-await + async acquireServiceLock( serviceId: string, holder: string, staleMs: number @@ -199,72 +194,58 @@ export class SQLiteCompute implements ComputeDatabaseProvider { SET holder = excluded.holder, acquiredAt = excluded.acquiredAt WHERE service_locks.acquiredAt <= ?; ` - return new Promise((resolve, reject) => { - this.db.run( - upsertSQL, - [serviceId, holder, now, now - staleMs], - function (this: RunResult, err: Error | null) { - if (err) { - DATABASE_LOGGER.error(`Could not acquire service lock: ${err.message}`) - reject(err) - } else { - resolve(this.changes === 1) - } - } - ) - }) + try { + const { changes } = this.db.run(upsertSQL, [serviceId, holder, now, now - staleMs]) + return changes === 1 + } catch (err) { + DATABASE_LOGGER.error(`Could not acquire service lock: ${err.message}`) + throw err + } } // Releases only a lock we still hold — a stale lock stolen by another process must // not be deleted out from under its new holder. - releaseServiceLock(serviceId: string, holder: string): Promise { + // eslint-disable-next-line require-await + async releaseServiceLock(serviceId: string, holder: string): Promise { const deleteSQL = `DELETE FROM service_locks WHERE serviceId = ? AND holder = ?;` - return new Promise((resolve, reject) => { - this.db.run(deleteSQL, [serviceId, holder], (err) => { - if (err) { - DATABASE_LOGGER.error(`Could not release service lock: ${err.message}`) - reject(err) - } else { - resolve() - } - }) - }) + try { + this.db.run(deleteSQL, [serviceId, holder]) + } catch (err) { + DATABASE_LOGGER.error(`Could not release service lock: ${err.message}`) + throw err + } } // Heartbeat: re-stamps every lock this holder owns so long operations (multi-minute // image pulls/builds) are not stolen as stale. - refreshServiceLocks(holder: string): Promise { + // eslint-disable-next-line require-await + async refreshServiceLocks(holder: string): Promise { const updateSQL = `UPDATE service_locks SET acquiredAt = ? WHERE holder = ?;` - return new Promise((resolve, reject) => { - this.db.run(updateSQL, [Date.now(), holder], (err) => { - if (err) { - DATABASE_LOGGER.error(`Could not refresh service locks: ${err.message}`) - reject(err) - } else { - resolve() - } - }) - }) + try { + this.db.run(updateSQL, [Date.now(), holder]) + } catch (err) { + DATABASE_LOGGER.error(`Could not refresh service locks: ${err.message}`) + throw err + } } // True while ANY process holds a fresh lock on serviceId — used by read-only // observers (e.g. the container health check) to avoid judging a service that // another process is mid-way through restarting. - isServiceLocked(serviceId: string, staleMs: number): Promise { + // eslint-disable-next-line require-await + async isServiceLocked(serviceId: string, staleMs: number): Promise { const selectSQL = `SELECT acquiredAt FROM service_locks WHERE serviceId = ?;` - return new Promise((resolve, reject) => { - this.db.get(selectSQL, [serviceId], (err, row: any) => { - if (err) { - DATABASE_LOGGER.error(`Could not read service lock: ${err.message}`) - reject(err) - } else { - resolve(!!row && row.acquiredAt > Date.now() - staleMs) - } - }) - }) + try { + const row = this.db.get<{ acquiredAt: number }>(selectSQL, [serviceId]) + return !!row && row.acquiredAt > Date.now() - staleMs + } catch (err) { + DATABASE_LOGGER.error(`Could not read service lock: ${err.message}`) + throw err + } } - createServiceTable(): Promise { + // eslint-disable-next-line require-await + async createServiceTable(): Promise { const createTableSQL = ` CREATE TABLE IF NOT EXISTS service_jobs ( serviceId TEXT PRIMARY KEY, @@ -276,83 +257,68 @@ export class SQLiteCompute implements ComputeDatabaseProvider { body BLOB ); ` - return new Promise((resolve, reject) => { - this.db.run(createTableSQL, (err) => { - if (err) { - DATABASE_LOGGER.error('Could not create service_jobs table: ' + err.message) - reject(err) - } else { - resolve() - } - }) - }) + try { + this.db.exec(createTableSQL) + } catch (err) { + DATABASE_LOGGER.error('Could not create service_jobs table: ' + err.message) + throw err + } } - newServiceJob(job: ServiceJob): Promise { + // eslint-disable-next-line require-await + async newServiceJob(job: ServiceJob): Promise { const insertSQL = ` INSERT INTO service_jobs (serviceId, owner, clusterHash, status, expiresAt, dateCreated, body) VALUES (?, ?, ?, ?, ?, ?, ?); ` - return new Promise((resolve, reject) => { - this.db.run( - insertSQL, - [ - job.serviceId, - job.owner, - job.clusterHash, - job.status, - job.expiresAt, - job.dateCreated, - Buffer.from(JSON.stringify(job)) - ], - (err) => { - if (err) { - DATABASE_LOGGER.error('Could not insert service job on DB: ' + err.message) - reject(err) - } else { - resolve() - } - } - ) - }) + try { + this.db.run(insertSQL, [ + job.serviceId, + job.owner, + job.clusterHash, + job.status, + job.expiresAt, + job.dateCreated, + Buffer.from(JSON.stringify(job)) + ]) + } catch (err) { + DATABASE_LOGGER.error('Could not insert service job on DB: ' + err.message) + throw err + } } - updateServiceJob(job: ServiceJob): Promise { + // eslint-disable-next-line require-await + async updateServiceJob(job: ServiceJob): Promise { const updateSQL = ` UPDATE service_jobs SET owner = ?, clusterHash = ?, status = ?, expiresAt = ?, body = ? WHERE serviceId = ?; ` - return new Promise((resolve, reject) => { - this.db.run( - updateSQL, - [ - job.owner, - job.clusterHash, - job.status, - job.expiresAt, - Buffer.from(JSON.stringify(job)), - job.serviceId - ], - function (this: RunResult, err: Error | null) { - if (err) { - DATABASE_LOGGER.error(`Error while updating service job: ${err.message}`) - reject(err) - } else { - resolve(this.changes) - } - } - ) - }) + try { + const { changes } = this.db.run(updateSQL, [ + job.owner, + job.clusterHash, + job.status, + job.expiresAt, + Buffer.from(JSON.stringify(job)), + job.serviceId + ]) + return changes + } catch (err) { + DATABASE_LOGGER.error(`Error while updating service job: ${err.message}`) + throw err + } } private mapServiceRows(rows: any[] | undefined): ServiceJob[] { if (!rows || rows.length === 0) return [] - return rows.map((row) => JSON.parse(row.body.toString()) as ServiceJob) + // BLOB comes back as Uint8Array from node:sqlite; decode through Buffer before parsing. + return rows.map((row) => JSON.parse(Buffer.from(row.body).toString()) as ServiceJob) } - getServiceJob(serviceId?: string, owner?: string): Promise { + // eslint-disable-next-line require-await + async getServiceJob(serviceId?: string, owner?: string): Promise { const params: any[] = [] let selectSQL = `SELECT * FROM service_jobs WHERE 1=1` if (serviceId) { @@ -363,19 +329,17 @@ export class SQLiteCompute implements ComputeDatabaseProvider { selectSQL += ` AND owner = ?` params.push(owner) } - return new Promise((resolve, reject) => { - this.db.all(selectSQL, params, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - resolve(this.mapServiceRows(rows)) - } - }) - }) + try { + const rows = this.db.all(selectSQL, params) + return this.mapServiceRows(rows) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } } - getRunningServiceJobs(clusterHash?: string): Promise { + // eslint-disable-next-line require-await + async getRunningServiceJobs(clusterHash?: string): Promise { // Every status before Expired is "active": the consumer paid for the resources for a // TIME WINDOW, so the reservation holds from the moment the record is created // (Starting) through the whole start pipeline (Locking, image, Claiming), while @@ -402,32 +366,28 @@ export class SQLiteCompute implements ComputeDatabaseProvider { selectSQL += ` AND clusterHash = ?` params.push(clusterHash) } - return new Promise((resolve, reject) => { - this.db.all(selectSQL, params, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - // The reservation is tied to PAYMENT: an Error/Stopped job whose payment was - // never claimed (escrow lock failed — e.g. insufficient funds — or refunded) - // must not hold resources, or anyone could squat a node's GPU for free by - // starting services against an empty escrow account. Mid-pipeline statuses - // keep reserving even without claimTx — they are en route to payment. - // JS-side filter because payment lives in the JSON body, not a SQL column. - resolve( - this.mapServiceRows(rows).filter( - (j) => - (j.status !== ServiceStatusNumber.Error && - j.status !== ServiceStatusNumber.Stopped) || - !!j.payment?.claimTx - ) - ) - } - }) - }) + try { + const rows = this.db.all(selectSQL, params) + // The reservation is tied to PAYMENT: an Error/Stopped job whose payment was + // never claimed (escrow lock failed — e.g. insufficient funds — or refunded) + // must not hold resources, or anyone could squat a node's GPU for free by + // starting services against an empty escrow account. Mid-pipeline statuses + // keep reserving even without claimTx — they are en route to payment. + // JS-side filter because payment lives in the JSON body, not a SQL column. + return this.mapServiceRows(rows).filter( + (j) => + (j.status !== ServiceStatusNumber.Error && + j.status !== ServiceStatusNumber.Stopped) || + !!j.payment?.claimTx + ) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } } - getExpiredServiceJobs(clusterHash?: string): Promise { + // eslint-disable-next-line require-await + async getExpiredServiceJobs(clusterHash?: string): Promise { // Running, Error AND Stopped all still hold their paid reservation (see activeStatuses // above), so all three must be swept once past expiresAt: the sweep is the ONLY place // the reservation is released. Without it an abandoned Error or Stopped service would @@ -449,22 +409,20 @@ export class SQLiteCompute implements ComputeDatabaseProvider { selectSQL += ` AND clusterHash = ?` params.push(clusterHash) } - return new Promise((resolve, reject) => { - this.db.all(selectSQL, params, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - resolve(this.mapServiceRows(rows)) - } - }) - }) + try { + const rows = this.db.all(selectSQL, params) + return this.mapServiceRows(rows) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } } // Service jobs that are mid-start and need the background loop to advance them. // Starting = fresh (handler just created it); the intermediate states are picked up too so // the loop can resume / orphan-recover them after a node restart. - getPendingServiceStarts(clusterHash?: string): Promise { + // eslint-disable-next-line require-await + async getPendingServiceStarts(clusterHash?: string): Promise { const startStatuses = SERVICE_START_PENDING_STATUSES const placeholders = startStatuses.map(() => '?').join(',') const params: Array = [...startStatuses] @@ -473,125 +431,105 @@ export class SQLiteCompute implements ComputeDatabaseProvider { selectSQL += ` AND clusterHash = ?` params.push(clusterHash) } - return new Promise((resolve, reject) => { - this.db.all(selectSQL, params, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - resolve(this.mapServiceRows(rows)) - } - }) - }) + try { + const rows = this.db.all(selectSQL, params) + return this.mapServiceRows(rows) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } } - updateImage(image: string): Promise { + // eslint-disable-next-line require-await + async updateImage(image: string): Promise { const timestamp = Math.floor(Date.now() / 1000) // Unix timestamp in seconds const insertSQL = ` INSERT OR REPLACE INTO docker_images (image, lastUsedTimestamp) VALUES (?, ?); ` - return new Promise((resolve, reject) => { - this.db.run(insertSQL, [image, timestamp], (err) => { - if (err) { - DATABASE_LOGGER.error( - `Could not update image usage for ${image}: ${err.message}` - ) - reject(err) - } else { - DATABASE_LOGGER.debug(`Updated image usage timestamp for ${image}`) - resolve() - } - }) - }) + try { + this.db.run(insertSQL, [image, timestamp]) + DATABASE_LOGGER.debug(`Updated image usage timestamp for ${image}`) + } catch (err) { + DATABASE_LOGGER.error(`Could not update image usage for ${image}: ${err.message}`) + throw err + } } - deleteImage(image: string): Promise { + // eslint-disable-next-line require-await + async deleteImage(image: string): Promise { const deleteSQL = ` DELETE FROM docker_images WHERE image = ?; ` - return new Promise((resolve, reject) => { - this.db.run(deleteSQL, [image], (err) => { - if (err) { - DATABASE_LOGGER.error(`Could not delete image ${image}: ${err.message}`) - reject(err) - } else { - DATABASE_LOGGER.debug(`Deleted image ${image}`) - resolve() - } - }) - }) + try { + this.db.run(deleteSQL, [image]) + DATABASE_LOGGER.debug(`Deleted image ${image}`) + } catch (err) { + DATABASE_LOGGER.error(`Could not delete image ${image}: ${err.message}`) + throw err + } } - getOldImages(retentionDays: number = 7): Promise { + // eslint-disable-next-line require-await + async getOldImages(retentionDays: number = 7): Promise { const cutoffTimestamp = Math.floor(Date.now() / 1000) - retentionDays * 24 * 60 * 60 const selectSQL = ` SELECT image FROM docker_images WHERE lastUsedTimestamp < ? ORDER BY lastUsedTimestamp ASC; ` - return new Promise((resolve, reject) => { - this.db.all(selectSQL, [cutoffTimestamp], (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(`Could not get old images: ${err.message}`) - reject(err) - } else { - const images = rows ? rows.map((row) => row.image) : [] - resolve(images) - } - }) - }) + try { + const rows = this.db.all<{ image: string }>(selectSQL, [cutoffTimestamp]) + return rows.map((row) => row.image) + } catch (err) { + DATABASE_LOGGER.error(`Could not get old images: ${err.message}`) + throw err + } } - newJob(job: DBComputeJob): Promise { + // eslint-disable-next-line require-await + async newJob(job: DBComputeJob): Promise { // TO DO C2D const insertSQL = ` - INSERT INTO ${this.schema.name} + INSERT INTO ${this.schema.name} ( - owner, - did, - jobId, - dateCreated, - status, - statusText, - inputDID, - algoDID, - agreementId, - expireTimestamp, - environment, + owner, + did, + jobId, + dateCreated, + status, + statusText, + inputDID, + algoDID, + agreementId, + expireTimestamp, + environment, body ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); ` - return new Promise((resolve, reject) => { - this.db.run( - insertSQL, - [ - job.owner, - job.did, - job.jobId, - job.dateCreated || String(Date.now() / 1000), // seconds from epoch, - job.status || C2DStatusNumber.JobStarted, - job.statusText || C2DStatusText.JobStarted, - job.inputDID ? convertArrayToString(job.inputDID) : job.inputDID, - job.algoDID, - job.agreementId, - job.maxJobDuration, - job.environment, - generateBlobFromJSON(job) - ], - (err) => { - if (err) { - DATABASE_LOGGER.error('Could not insert C2D job on DB: ' + err.message) - reject(err) - } else { - DATABASE_LOGGER.info('Successfully inserted job with id:' + job.jobId) - resolve(job.jobId) - } - } - ) - }) + try { + this.db.run(insertSQL, [ + job.owner, + job.did, + job.jobId, + job.dateCreated || String(Date.now() / 1000), // seconds from epoch, + job.status || C2DStatusNumber.JobStarted, + job.statusText || C2DStatusText.JobStarted, + job.inputDID ? convertArrayToString(job.inputDID) : job.inputDID, + job.algoDID, + job.agreementId, + job.maxJobDuration, + job.environment, + generateBlobFromJSON(job) + ]) + DATABASE_LOGGER.info('Successfully inserted job with id:' + job.jobId) + return job.jobId + } catch (err) { + DATABASE_LOGGER.error('Could not insert C2D job on DB: ' + err.message) + throw err + } } /** @@ -604,7 +542,12 @@ export class SQLiteCompute implements ComputeDatabaseProvider { * @param owner the consumer address / job owner * @returns job(s) */ - getJob(jobId?: string, agreementId?: string, owner?: string): Promise { + // eslint-disable-next-line require-await + async getJob( + jobId?: string, + agreementId?: string, + owner?: string + ): Promise { const params: any = [] let selectSQL = `SELECT * FROM ${this.schema.name} WHERE 1=1` if (jobId) { @@ -623,35 +566,33 @@ export class SQLiteCompute implements ComputeDatabaseProvider { params.push(owner) } - return new Promise((resolve, reject) => { - this.db.all(selectSQL, params, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - // also decode the internal data into job data - if (rows && rows.length > 0) { - const all: DBComputeJob[] = rows.map((row) => { - const body = generateJSONFromBlob(row.body) - delete row.body - const maxJobDuration = row.expireTimestamp - delete row.expireTimestamp - const job: DBComputeJob = { ...row, ...body, maxJobDuration } - return job - }) - resolve(all) - } else { - DATABASE_LOGGER.error( - `Could not find any job with jobId: ${jobId}, agreementId: ${agreementId}, or owner: ${owner} in database!` - ) - resolve([]) - } - } + let rows: any[] + try { + rows = this.db.all(selectSQL, params) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } + // also decode the internal data into job data + if (rows && rows.length > 0) { + const all: DBComputeJob[] = rows.map((row) => { + const body = generateJSONFromBlob(row.body) + delete row.body + const maxJobDuration = row.expireTimestamp + delete row.expireTimestamp + const job: DBComputeJob = { ...row, ...body, maxJobDuration } + return job }) - }) + return all + } + DATABASE_LOGGER.error( + `Could not find any job with jobId: ${jobId}, agreementId: ${agreementId}, or owner: ${owner} in database!` + ) + return [] } - updateJob(job: DBComputeJob): Promise { + // eslint-disable-next-line require-await + async updateJob(job: DBComputeJob): Promise { // if (job.dateFinished && job.isRunning) { // job.isRunning = false // } @@ -666,76 +607,72 @@ export class SQLiteCompute implements ComputeDatabaseProvider { job.jobId ] const updateSQL = ` - UPDATE ${this.schema.name} - SET + UPDATE ${this.schema.name} + SET owner = ?, status = ?, statusText = ?, - expireTimestamp = ?, + expireTimestamp = ?, body = ?, dateFinished = ? WHERE jobId = ?; ` - return new Promise((resolve, reject) => { - this.db.run(updateSQL, data, function (this: RunResult, err: Error | null) { - if (err) { - DATABASE_LOGGER.error(`Error while updating job: ${err.message}`) - reject(err) - } else { - // number of rows updated successfully - resolve(this.changes) - } - }) - }) + try { + // number of rows updated successfully + const { changes } = this.db.run(updateSQL, data) + return changes + } catch (err) { + DATABASE_LOGGER.error(`Error while updating job: ${err.message}`) + throw err + } } - getRunningJobs(engine?: string, environment?: string): Promise { + // eslint-disable-next-line require-await + async getRunningJobs(engine?: string, environment?: string): Promise { const selectSQL = ` SELECT * FROM ${this.schema.name} WHERE dateFinished IS NULL ORDER by dateCreated ` - return new Promise((resolve, reject) => { - this.db.all(selectSQL, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - // also decode the internal data into job data - // get them all running - if (rows && rows.length > 0) { - const all: DBComputeJob[] = rows.map((row) => { - const body = generateJSONFromBlob(row.body) - delete row.body - const maxJobDuration = row.expireTimestamp - delete row.expireTimestamp - const job: DBComputeJob = { ...row, ...body, maxJobDuration } - return job - }) - // filter them out - const filtered = all.filter((job) => { - let include = true - if (engine && engine !== job.clusterHash) { - include = false - } - if (environment && environment !== job.environment) { - include = false - } - if (job.dateFinished) { - include = false - } - return include - }) - resolve(filtered) - } else { - // DATABASE_LOGGER.info('Could not find any running C2D jobs!') - resolve([]) - } + let rows: any[] + try { + rows = this.db.all(selectSQL) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } + // also decode the internal data into job data + // get them all running + if (rows && rows.length > 0) { + const all: DBComputeJob[] = rows.map((row) => { + const body = generateJSONFromBlob(row.body) + delete row.body + const maxJobDuration = row.expireTimestamp + delete row.expireTimestamp + const job: DBComputeJob = { ...row, ...body, maxJobDuration } + return job + }) + // filter them out + const filtered = all.filter((job) => { + let include = true + if (engine && engine !== job.clusterHash) { + include = false } + if (environment && environment !== job.environment) { + include = false + } + if (job.dateFinished) { + include = false + } + return include }) - }) + return filtered + } + // DATABASE_LOGGER.info('Could not find any running C2D jobs!') + return [] } - getFinishedJobs(environments?: string[]): Promise { + // eslint-disable-next-line require-await + async getFinishedJobs(environments?: string[]): Promise { let selectSQL = ` SELECT * FROM ${this.schema.name} WHERE (dateFinished IS NOT NULL OR results IS NOT NULL) ` @@ -748,35 +685,32 @@ export class SQLiteCompute implements ComputeDatabaseProvider { selectSQL += ` ORDER BY dateFinished DESC` - return new Promise((resolve, reject) => { - this.db.all(selectSQL, params, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - // also decode the internal data into job data - // get them all running - if (rows && rows.length > 0) { - const all: DBComputeJob[] = rows.map((row) => { - const body = generateJSONFromBlob(row.body) - delete row.body - const maxJobDuration = row.expireTimestamp - delete row.expireTimestamp - const job: DBComputeJob = { ...row, ...body, maxJobDuration } - return job - }) - resolve(all) - } else { - environments - ? DATABASE_LOGGER.info( - 'No jobs found for the specified enviroments: ' + environments.join(',') - ) - : DATABASE_LOGGER.info('No jobs found') - resolve([]) - } - } + let rows: any[] + try { + rows = this.db.all(selectSQL, params) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } + // also decode the internal data into job data + // get them all running + if (rows && rows.length > 0) { + const all: DBComputeJob[] = rows.map((row) => { + const body = generateJSONFromBlob(row.body) + delete row.body + const maxJobDuration = row.expireTimestamp + delete row.expireTimestamp + const job: DBComputeJob = { ...row, ...body, maxJobDuration } + return job }) - }) + return all + } + environments + ? DATABASE_LOGGER.info( + 'No jobs found for the specified enviroments: ' + environments.join(',') + ) + : DATABASE_LOGGER.info('No jobs found') + return [] } async getJobs( @@ -834,7 +768,7 @@ export class SQLiteCompute implements ComputeDatabaseProvider { ): Promise { let selectSQL = `SELECT * FROM ${this.schema.name}` - // sqlite3 bindings accept both strings and numbers; `status` is a numeric enum. + // node:sqlite bindings accept both strings and numbers; `status` is a numeric enum. const params: Array = [] const conditions: string[] = [] @@ -858,42 +792,40 @@ export class SQLiteCompute implements ComputeDatabaseProvider { return await this.doQuery(selectSQL, params, environments) } - private doQuery( + // eslint-disable-next-line require-await + private async doQuery( selectSQL: string, params: Array, environments: string[] - ) { - return new Promise((resolve, reject) => { - this.db.all(selectSQL, params, (err, rows: any[] | undefined) => { - if (err) { - DATABASE_LOGGER.error(err.message) - reject(err) - } else { - // also decode the internal data into job data - // get them all running - if (rows && rows.length > 0) { - const all: DBComputeJob[] = rows.map((row) => { - const body = generateJSONFromBlob(row.body) - delete row.body - const maxJobDuration = row.expireTimestamp - delete row.expireTimestamp - const job: DBComputeJob = { ...row, ...body, maxJobDuration } - if (!job.jobIdHash && job.jobId) { - job.jobIdHash = create256Hash(job.jobId) - } - return job - }) - resolve(all) - } else { - environments - ? DATABASE_LOGGER.info( - 'No jobs found for the specified enviroments: ' + environments.join(',') - ) - : DATABASE_LOGGER.info('No jobs found') - resolve([]) - } + ): Promise { + let rows: any[] + try { + rows = this.db.all(selectSQL, params) + } catch (err) { + DATABASE_LOGGER.error(err.message) + throw err + } + // also decode the internal data into job data + // get them all running + if (rows && rows.length > 0) { + const all: DBComputeJob[] = rows.map((row) => { + const body = generateJSONFromBlob(row.body) + delete row.body + const maxJobDuration = row.expireTimestamp + delete row.expireTimestamp + const job: DBComputeJob = { ...row, ...body, maxJobDuration } + if (!job.jobIdHash && job.jobId) { + job.jobIdHash = create256Hash(job.jobId) } + return job }) - }) + return all + } + environments + ? DATABASE_LOGGER.info( + 'No jobs found for the specified enviroments: ' + environments.join(',') + ) + : DATABASE_LOGGER.info('No jobs found') + return [] } } diff --git a/src/components/persistentStorage/PersistentStorageFactory.ts b/src/components/persistentStorage/PersistentStorageFactory.ts index c00baf151..380ecd92e 100644 --- a/src/components/persistentStorage/PersistentStorageFactory.ts +++ b/src/components/persistentStorage/PersistentStorageFactory.ts @@ -6,9 +6,7 @@ import type { PersistentStorageObject } from '../../@types/PersistentStorage.js' -import sqlite3, { RunResult } from 'sqlite3' -import path from 'path' -import fs from 'fs' +import { SqliteClient } from '../database/sqliteClient.js' import { getAddress } from 'ethers' import { OceanNode } from '../../OceanNode.js' import { checkAddressOnAccessList } from '../../utils/accessList.js' @@ -69,19 +67,16 @@ export type PersistentStorageBucketRecord = { } export abstract class PersistentStorageFactory { - private db: sqlite3.Database + private db: SqliteClient private node: OceanNode private dbReady = false private dbReadyPromise: Promise constructor(node: OceanNode) { this.node = node - const dbDir = path.dirname('databases/persistentStorage.sqlite') - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }) - } - this.db = new sqlite3.Database('databases/persistentStorage.sqlite') - const createBucketsSQL = ` + // SqliteClient creates the parent directory on construction. + this.db = new SqliteClient('databases/persistentStorage.sqlite') + this.db.exec(` CREATE TABLE IF NOT EXISTS persistent_storage_buckets ( bucketId TEXT PRIMARY KEY, owner TEXT NOT NULL, @@ -89,29 +84,20 @@ export abstract class PersistentStorageFactory { createdAt INTEGER NOT NULL, label TEXT ); - ` - this.dbReadyPromise = new Promise((resolve, reject) => { - this.db.run(createBucketsSQL, (err) => { - if (err) { - reject(err) - return - } - // Migration: add the label column if it doesn't exist - this.db.run( - `ALTER TABLE persistent_storage_buckets ADD COLUMN label TEXT`, - (alterErr) => { - // Ignore "duplicate column name" (expected once the column exists); - // surface any other failure instead of starting with a broken schema. - if (alterErr && !/duplicate column name/i.test(alterErr.message)) { - reject(alterErr) - return - } - this.dbReady = true - resolve() - } - ) - }) - }) + `) + // Migration: add the label column if it doesn't exist. A fresh table already has it, + // so ALTER throws "duplicate column name" — swallow only that; surface any other + // failure instead of starting with a broken schema. Schema setup is synchronous now, + // so the DB is ready by the time the constructor returns. + try { + this.db.exec(`ALTER TABLE persistent_storage_buckets ADD COLUMN label TEXT`) + } catch (alterErr) { + if (!/duplicate column name/i.test(alterErr.message)) { + throw alterErr + } + } + this.dbReady = true + this.dbReadyPromise = Promise.resolve() } public isDbReady(): boolean { @@ -261,7 +247,7 @@ export abstract class PersistentStorageFactory { * with constructor-time schema creation. */ - dbUpsertBucket( + async dbUpsertBucket( bucketId: string, owner: string, accessListJson: string, @@ -274,71 +260,39 @@ export abstract class PersistentStorageFactory { VALUES (?, ?, ?, ?, ?) ON CONFLICT(bucketId) DO UPDATE SET accessListJson=excluded.accessListJson; ` - return this.ensureDbReady().then( - () => - new Promise((resolve, reject) => { - this.db.run(sql, [bucketId, owner, accessListJson, createdAt, label], (err) => { - if (err) reject(err) - else resolve() - }) - }) - ) + await this.ensureDbReady() + this.db.run(sql, [bucketId, owner, accessListJson, createdAt, label]) } - dbGetBucket(bucketId: string): Promise { + async dbGetBucket(bucketId: string): Promise { const sql = `SELECT bucketId, owner, accessListJson, createdAt, label FROM persistent_storage_buckets WHERE bucketId = ?` - return this.ensureDbReady().then( - () => - new Promise((resolve, reject) => { - this.db.get(sql, [bucketId], (err, row: BucketRow | undefined) => { - if (err) reject(err) - else resolve(row ?? null) - }) - }) - ) + await this.ensureDbReady() + const row = this.db.get(sql, [bucketId]) + return row ?? null } - dbListBucketsByOwner(owner: string): Promise { + async dbListBucketsByOwner(owner: string): Promise { const sql = `SELECT bucketId, owner, accessListJson, createdAt, label FROM persistent_storage_buckets WHERE owner = ? ORDER BY createdAt ASC` - return this.ensureDbReady().then( - () => - new Promise((resolve, reject) => { - this.db.all(sql, [owner], (err, rows: BucketRow[]) => { - if (err) reject(err) - else resolve(rows ?? []) - }) - }) - ) + await this.ensureDbReady() + return this.db.all(sql, [owner]) } - dbDeleteBucket(bucketId: string): Promise { + async dbDeleteBucket(bucketId: string): Promise { const sql = `DELETE FROM persistent_storage_buckets WHERE bucketId = ?` - return this.ensureDbReady().then( - () => - new Promise((resolve, reject) => { - this.db.run(sql, [bucketId], function (this: RunResult, err) { - if (err) reject(err) - else resolve(this.changes === 1) - }) - }) - ) + await this.ensureDbReady() + const { changes } = this.db.run(sql, [bucketId]) + return changes === 1 } - dbUpdateBucketLabel( + async dbUpdateBucketLabel( bucketId: string, owner: string, label: string | null ): Promise { const sql = `UPDATE persistent_storage_buckets SET label = ? WHERE bucketId = ? AND owner = ?` - return this.ensureDbReady().then( - () => - new Promise((resolve, reject) => { - this.db.run(sql, [label, bucketId, owner], function (this: RunResult, err) { - if (err) reject(err) - else resolve(this.changes === 1) - }) - }) - ) + await this.ensureDbReady() + const { changes } = this.db.run(sql, [label, bucketId, owner]) + return changes === 1 } isAllowed(consumerAddress: string, accessLists: AccessList[]): Promise { diff --git a/src/test/integration/imageCleanup.test.ts b/src/test/integration/imageCleanup.test.ts index 625d9e2f2..acabc7482 100644 --- a/src/test/integration/imageCleanup.test.ts +++ b/src/test/integration/imageCleanup.test.ts @@ -90,19 +90,13 @@ describe('********** Docker Image Cleanup Integration Tests', () => { const testImage = 'test-image:latest' await sqliteProvider.updateImage(testImage) - // Verify image was recorded by querying directly - // getOldImages(0) looks for images older than now, so we query the DB directly - const imageRecord = await new Promise((resolve, reject) => { - const { db } = sqliteProvider as any - db.get( - 'SELECT image, lastUsedTimestamp FROM docker_images WHERE image = ?', - [testImage], - (err: Error | null, row: any) => { - if (err) reject(err) - else resolve(row) - } - ) - }) + // Verify image was recorded by querying directly via the SqliteClient handle. + // getOldImages(0) looks for images older than now, so we query the DB directly. + const sqliteClient = (sqliteProvider as any).db + const imageRecord = sqliteClient.get( + 'SELECT image, lastUsedTimestamp FROM docker_images WHERE image = ?', + [testImage] + ) assert(imageRecord, 'Image should be recorded in database') expect(imageRecord.image).to.equal(testImage) @@ -125,17 +119,11 @@ describe('********** Docker Image Cleanup Integration Tests', () => { await sqliteProvider.updateImage(testImage) // Verify image exists with updated timestamp - const imageRecord = await new Promise((resolve, reject) => { - const { db } = sqliteProvider as any - db.get( - 'SELECT image, lastUsedTimestamp FROM docker_images WHERE image = ?', - [testImage], - (err: Error | null, row: any) => { - if (err) reject(err) - else resolve(row) - } - ) - }) + const sqliteClient = (sqliteProvider as any).db + const imageRecord = sqliteClient.get( + 'SELECT image, lastUsedTimestamp FROM docker_images WHERE image = ?', + [testImage] + ) assert(imageRecord, 'Image should be recorded in database') expect(imageRecord.image).to.equal(testImage) @@ -151,17 +139,11 @@ describe('********** Docker Image Cleanup Integration Tests', () => { // Manually insert an old image by directly updating the database const oldTimestamp = Math.floor(Date.now() / 1000) - 8 * 24 * 60 * 60 // 8 days ago - await new Promise((resolve, reject) => { - const { db } = sqliteProvider as any - db.run( - 'INSERT OR REPLACE INTO docker_images (image, lastUsedTimestamp) VALUES (?, ?)', - [oldImage, oldTimestamp], - (err: Error | null) => { - if (err) reject(err) - else resolve(undefined) - } - ) - }) + const sqliteClient = (sqliteProvider as any).db + sqliteClient.run( + 'INSERT OR REPLACE INTO docker_images (image, lastUsedTimestamp) VALUES (?, ?)', + [oldImage, oldTimestamp] + ) // Get images older than 7 days const oldImages = await sqliteProvider.getOldImages(7) @@ -206,17 +188,11 @@ describe('********** Docker Image Cleanup Integration Tests', () => { await (dockerEngine as any).updateImageUsage(testImage) // Verify image was recorded in database - const imageRecord = await new Promise((resolve, reject) => { - const { db } = sqliteProvider as any - db.get( - 'SELECT image, lastUsedTimestamp FROM docker_images WHERE image = ?', - [testImage], - (err: Error | null, row: any) => { - if (err) reject(err) - else resolve(row) - } - ) - }) + const sqliteClient = (sqliteProvider as any).db + const imageRecord = sqliteClient.get( + 'SELECT image, lastUsedTimestamp FROM docker_images WHERE image = ?', + [testImage] + ) assert(imageRecord, 'Image should be recorded in database after updateImageUsage') expect(imageRecord.image).to.equal(testImage) @@ -284,17 +260,11 @@ describe('********** Docker Image Cleanup Integration Tests', () => { // Track the image with old timestamp (8 days ago) const oldTimestamp = Math.floor(Date.now() / 1000) - 8 * 24 * 60 * 60 - await new Promise((resolve, reject) => { - const { db } = sqliteProvider as any - db.run( - 'INSERT OR REPLACE INTO docker_images (image, lastUsedTimestamp) VALUES (?, ?)', - [testImageName, oldTimestamp], - (err: Error | null) => { - if (err) reject(err) - else resolve(undefined) - } - ) - }) + const sqliteClient = (sqliteProvider as any).db + sqliteClient.run( + 'INSERT OR REPLACE INTO docker_images (image, lastUsedTimestamp) VALUES (?, ?)', + [testImageName, oldTimestamp] + ) // Verify image exists before cleanup const imagesBefore = await docker.listImages() diff --git a/src/test/unit/sqliteClient.test.ts b/src/test/unit/sqliteClient.test.ts new file mode 100644 index 000000000..6425b3a89 --- /dev/null +++ b/src/test/unit/sqliteClient.test.ts @@ -0,0 +1,110 @@ +import { expect } from 'chai' +import fs from 'fs' +import os from 'os' +import path from 'path' +import { SqliteClient } from '../../components/database/sqliteClient.js' + +describe('SqliteClient', () => { + let tmpDir: string + let client: SqliteClient + + before(() => { + // Nested subdir on purpose: exercises the client's eager mkdir of the parent dir. + tmpDir = path.join( + os.tmpdir(), + `ocean-node-sqliteclient-${process.pid}-${Date.now()}`, + 'nested' + ) + client = new SqliteClient(path.join(tmpDir, 'test.sqlite')) + client.exec(` + CREATE TABLE IF NOT EXISTS kv ( + id TEXT PRIMARY KEY, + n INTEGER, + flag INTEGER, + blob BLOB + ); + `) + }) + + after(() => { + // best-effort cleanup of the temp DB dir (incl. any -wal/-shm siblings) + fs.rmSync(path.dirname(tmpDir), { recursive: true, force: true }) + }) + + it('creates the parent directory eagerly', () => { + expect(fs.existsSync(tmpDir)).to.equal(true) + }) + + it('run() reports changes: 1 on a hit, 0 on a miss', () => { + const inserted = client.run('INSERT INTO kv (id, n) VALUES (?, ?)', ['a', 1]) + expect(inserted.changes).to.equal(1) + expect(inserted.changes).to.be.a('number') + + const hit = client.run('UPDATE kv SET n = ? WHERE id = ?', [2, 'a']) + expect(hit.changes).to.equal(1) + + const miss = client.run('UPDATE kv SET n = ? WHERE id = ?', [9, 'does-not-exist']) + expect(miss.changes).to.equal(0) + }) + + it('get() returns undefined when no row matches', () => { + const row = client.get('SELECT * FROM kv WHERE id = ?', ['missing']) + expect(row).to.equal(undefined) + }) + + it('all() always returns an array', () => { + const rows = client.all('SELECT * FROM kv WHERE id = ?', ['still-missing']) + expect(rows).to.be.an('array').with.lengthOf(0) + }) + + it('sanitizes undefined -> NULL and boolean -> 1/0 bindings', () => { + // undefined would otherwise throw ERR_INVALID_ARG_TYPE in node:sqlite + client.run('INSERT INTO kv (id, n, flag) VALUES (?, ?, ?)', [ + 'sanitize', + undefined, + true + ]) + const row = client.get<{ n: number | null; flag: number }>( + 'SELECT n, flag FROM kv WHERE id = ?', + ['sanitize'] + ) + expect(row?.n).to.equal(null) + expect(row?.flag).to.equal(1) + + client.run('INSERT INTO kv (id, flag) VALUES (?, ?)', ['sanitize-false', false]) + const row2 = client.get<{ flag: number }>('SELECT flag FROM kv WHERE id = ?', [ + 'sanitize-false' + ]) + expect(row2?.flag).to.equal(0) + }) + + it('round-trips a BLOB written as Buffer (comes back as Uint8Array)', () => { + const original = JSON.stringify({ hello: 'world', n: 123 }) + client.run('INSERT INTO kv (id, blob) VALUES (?, ?)', ['blob', Buffer.from(original)]) + const row = client.get<{ blob: Uint8Array }>('SELECT blob FROM kv WHERE id = ?', [ + 'blob' + ]) + expect(row?.blob).to.be.an.instanceOf(Uint8Array) + // This is exactly the decode the compute provider must do: Buffer.from(blob).toString() + expect(Buffer.from(row!.blob).toString()).to.equal(original) + expect(JSON.parse(Buffer.from(row!.blob).toString())).to.deep.equal({ + hello: 'world', + n: 123 + }) + }) + + it('supports INSERT ... ON CONFLICT upsert (nonce/config pattern)', () => { + const sql = ` + INSERT INTO kv (id, n) VALUES (?, ?) + ON CONFLICT(id) DO UPDATE SET n = excluded.n; + ` + client.run(sql, ['upsert', 10]) + expect( + client.get<{ n: number }>('SELECT n FROM kv WHERE id = ?', ['upsert'])?.n + ).to.equal(10) + client.run(sql, ['upsert', 20]) + expect( + client.get<{ n: number }>('SELECT n FROM kv WHERE id = ?', ['upsert'])?.n + ).to.equal(20) + }) +})