From 8bf0f7d9fc37b5c11894e4698b360755014d1427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Hano=C4=9Flu?= Date: Tue, 8 Sep 2026 17:44:43 +0300 Subject: [PATCH 1/5] fix: Correct release/close/ensureMin bugs and cut hot-path allocations in Pool - release() now invokes the callback even when the resource isn't in the pool or is already idle, instead of silently dropping it (releaseAsync() could hang forever otherwise). - close() now rejects any requests still queued instead of discarding them without ever calling back. - _houseKeep() no longer marks the pool CLOSED while a resource creation is still in flight, which was leaking a never-destroyed resource. - _ensureMin() skips scheduling a process.nextTick() entirely when min/minIdle are both unset (the default), removing a wasted microtask on every single acquire(). - _createResource()/_processNextRequest() properly account for _creating and _requestsProcessing when a request has already timed out, instead of leaking both counters. - awaitResult() replaces putil-promisify's promisify.await() on the create/destroy/reset/validate paths, attaching both handlers via .then(onFulfilled, onRejected) instead of chaining a second promise off .catch() - one fewer Promise allocated per call. --- src/pool-options.ts | 11 ++++++-- src/pool.ts | 62 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/pool-options.ts b/src/pool-options.ts index 42ef51b..4448cad 100644 --- a/src/pool-options.ts +++ b/src/pool-options.ts @@ -140,9 +140,16 @@ export class PoolOptions extends EventEmitter { assign(values: PoolConfiguration | PoolOptions): void { const proto = Object.getPrototypeOf(this); - for (const k of Object.keys(values)) { + // A PoolOptions instance only owns underscored private fields, so its + // public getter/setter names must be read from the canonical key list. + const keys = + values instanceof PoolOptions + ? Object.keys(defaultValues) + : Object.keys(values); + for (const k of keys) { const desc = Object.getOwnPropertyDescriptor(proto, k); - if (desc && desc.set) this[k] = values[k]; + const val = (values as any)[k]; + if (desc && desc.set && val !== undefined) this[k] = val; } } } diff --git a/src/pool.ts b/src/pool.ts index 2128aed..5060e4a 100644 --- a/src/pool.ts +++ b/src/pool.ts @@ -8,6 +8,26 @@ import { PoolRequest } from './pool-request.js'; import { ResourceItem } from './resource-item.js'; import type { Callback, PoolConfiguration, PoolFactory } from './types.js'; +/** + * Like putil-promisify's `promisify.await()`, but attaches both handlers to + * the original promise via `.then(onFulfilled, onRejected)` instead of + * chaining a `.catch()` off a second promise `.then()` returns - one fewer + * Promise allocated per call on a path (create/destroy/reset/validate) that + * every acquire/release goes through. Mirrors promisify.await's own loose + * `(x: any, callback: (error?: Error, value?: T) => void)` signature. + */ +function awaitResult( + value: unknown, + callback?: (error?: Error, value?: T) => void, +): void { + if (value && typeof (value as any).then === 'function') { + (value as Promise).then( + (v: T) => callback && callback(undefined, v), + (e: unknown) => callback && callback(e as Error), + ); + } else if (callback) callback(undefined, value as T); +} + export class Pool extends EventEmitter { private readonly _options: PoolOptions; private readonly _factory: PoolFactory; @@ -157,7 +177,15 @@ export class Pool extends EventEmitter { this.emit('closing'); if (this._houseKeepTimer) clearTimeout(this._houseKeepTimer); this._state = PoolState.CLOSING; - this._requestQueue.forEach(t => t.stopTimout()); + const closingError = new Error('Pool is closing'); + this._requestQueue.forEach(t => { + t.stopTimout(); + try { + t.callback(closingError); + } catch { + // ignored + } + }); this._requestQueue = new DoublyLinked(); this._requestsProcessing = 0; @@ -217,7 +245,7 @@ export class Pool extends EventEmitter { const item = this._allResources.get(resource); if (item && item.state !== ResourceState.IDLE) { this._itemSetIdle(item, callback); - } + } else if (callback) callback(); this._processNextRequest(); } @@ -284,11 +312,18 @@ export class Pool extends EventEmitter { this._itemDestroy(item); return; } + if (request.timedOut) { + /* Request already failed with a timeout error; return the + * resource to the idle pool instead of handing it to an + * abandoned caller. */ + this._itemSetIdle(item); + return; + } this._itemSetAcquired(item); this._ensureMin(); request.callback(undefined, item.resource); this.emit('acquire', item.resource); - } else request.callback(err); + } else if (!request.timedOut) request.callback(err); } catch { // ignored } @@ -335,7 +370,6 @@ export class Pool extends EventEmitter { this._creating++; const handleCallback = (err?: Error, obj?: T) => { - if (request && request.timedOut) return; if (err || !obj) { tries++; this.emit('error', err, { @@ -343,7 +377,9 @@ export class Pool extends EventEmitter { tries, maxRetries: this.options.acquireMaxRetries, }); - if (err instanceof AbortError || tries >= maxRetries) { + /* Stop retrying for a request that already timed out */ + const abandoned = !!(request && request.timedOut); + if (abandoned || err instanceof AbortError || tries >= maxRetries) { this._creating--; return callback && callback(err); } @@ -372,7 +408,7 @@ export class Pool extends EventEmitter { if (!o) { return handleCallback(new AbortError('Factory returned no resource')); } - promisify.await(o, handleCallback); + awaitResult(o, handleCallback); } catch (e: any) { handleCallback(e); } @@ -408,7 +444,7 @@ export class Pool extends EventEmitter { } if (isClosing) { /* Check again 5 ms later */ - if (this._allResources.size) return; + if (this._allResources.size || this._creating) return; clearInterval(this._houseKeepTimer); this._state = PoolState.CLOSED; this._requestsProcessing = 0; @@ -417,6 +453,11 @@ export class Pool extends EventEmitter { } private _ensureMin(): void { + // Common case (min/minIdle both unset): the scheduled tick below would + // always compute k <= 0 and do nothing, so skip the nextTick() and its + // closure allocation entirely rather than paying for a wasted microtask + // on every single acquire(). + if (this.options.min <= 0 && this.options.minIdle <= 0) return; process.nextTick(() => { let k = Math.max( @@ -480,7 +521,7 @@ export class Pool extends EventEmitter { if (isAcquired && this._factory.reset) { try { const o = this._factory.reset(item.resource); - promisify.await(o, handleCallback); + awaitResult(o, handleCallback); } catch (e: any) { handleCallback(e); } @@ -500,7 +541,7 @@ export class Pool extends EventEmitter { try { const o = this._factory.destroy(item.resource); - promisify.await(o, handleCallback); + awaitResult(o, handleCallback); } catch (e: any) { handleCallback(e); } finally { @@ -512,8 +553,7 @@ export class Pool extends EventEmitter { item.state = ResourceState.VALIDATION; try { const o = this._factory.validate?.(item.resource); - // @ts-ignore - promisify.await(o, callback); + awaitResult(o, callback); } catch (e: any) { callback?.(e); } From 28ba0c2b17ec5547a0cd18bf289ce9fca552a9fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Hano=C4=9Flu?= Date: Tue, 8 Sep 2026 17:45:05 +0300 Subject: [PATCH 2/5] feat: Replace benchmark-tests with a proper benchmark suite Ad hoc benchmark-tests/ script (untyped one-shot loop, no repeats, no statistics) replaced with a real harness modeled after postgrejs's own benchmark tool: - One child process per (library, scenario, run), spawned sequentially, using tinybench for latency/throughput sampling. - 5 scenarios (acquire-release, acquire-release-concurrent, queue-contention, create-destroy-churn, validate-on-borrow) comparing lightning-pool against generic-pool on an identical simulated resource. - GC/peak-heap instrumentation via node:perf_hooks + --expose-gc, with a periodic forced setImmediate yield (installPeriodicYield in worker.ts) so a pool whose hot path resolves entirely through microtasks doesn't read back as a misleading gcCount=0/peakHeap=0. - `npm run bench` (matrix runner, --lib/--scenario/--repeats flags) and `npm run bench:report` (regenerate the doc from existing results). --- .gitignore | 6 + benchmark-tests/generic-pool-test.ts | 62 - benchmark-tests/index.ts | 87 - benchmark-tests/lightning-pool-test.ts | 58 - benchmark-tests/test-factory.ts | 86 - benchmark/README.md | 73 + benchmark/adapters/adapter.ts | 49 + benchmark/adapters/generic-pool.adapter.ts | 171 ++ benchmark/adapters/lightning-pool.adapter.ts | 163 ++ benchmark/adapters/pkg-version.ts | 41 + benchmark/adapters/registry.ts | 31 + benchmark/cli.ts | 84 + benchmark/config.ts | 17 + benchmark/env.mjs | 11 + benchmark/report/aggregate.ts | 91 + benchmark/report/render-console.ts | 91 + benchmark/report/render-markdown.ts | 386 +++++ benchmark/resource.ts | 49 + benchmark/runner/orchestrator.ts | 107 ++ benchmark/runner/worker.ts | 286 ++++ .../scenarios/acquire-release-concurrent.ts | 25 + benchmark/scenarios/acquire-release.ts | 19 + benchmark/scenarios/create-destroy-churn.ts | 22 + benchmark/scenarios/index.ts | 26 + benchmark/scenarios/queue-contention.ts | 22 + benchmark/scenarios/validate-on-borrow.ts | 22 + {benchmark-tests => benchmark}/tsconfig.json | 6 +- benchmark/types.ts | 72 + package-lock.json | 1484 ++++++----------- package.json | 20 +- 30 files changed, 2397 insertions(+), 1270 deletions(-) delete mode 100644 benchmark-tests/generic-pool-test.ts delete mode 100644 benchmark-tests/index.ts delete mode 100644 benchmark-tests/lightning-pool-test.ts delete mode 100644 benchmark-tests/test-factory.ts create mode 100644 benchmark/README.md create mode 100644 benchmark/adapters/adapter.ts create mode 100644 benchmark/adapters/generic-pool.adapter.ts create mode 100644 benchmark/adapters/lightning-pool.adapter.ts create mode 100644 benchmark/adapters/pkg-version.ts create mode 100644 benchmark/adapters/registry.ts create mode 100644 benchmark/cli.ts create mode 100644 benchmark/config.ts create mode 100644 benchmark/env.mjs create mode 100644 benchmark/report/aggregate.ts create mode 100644 benchmark/report/render-console.ts create mode 100644 benchmark/report/render-markdown.ts create mode 100644 benchmark/resource.ts create mode 100644 benchmark/runner/orchestrator.ts create mode 100644 benchmark/runner/worker.ts create mode 100644 benchmark/scenarios/acquire-release-concurrent.ts create mode 100644 benchmark/scenarios/acquire-release.ts create mode 100644 benchmark/scenarios/create-destroy-churn.ts create mode 100644 benchmark/scenarios/index.ts create mode 100644 benchmark/scenarios/queue-contention.ts create mode 100644 benchmark/scenarios/validate-on-borrow.ts rename {benchmark-tests => benchmark}/tsconfig.json (52%) create mode 100644 benchmark/types.ts diff --git a/.gitignore b/.gitignore index 737b47f..5e43187 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,12 @@ /build node_modules coverage +*.tsbuildinfo +graphify-out +.claude + +# benchmark raw results (regenerate with `npm run bench`) +/benchmark/results # environment variables .env diff --git a/benchmark-tests/generic-pool-test.ts b/benchmark-tests/generic-pool-test.ts deleted file mode 100644 index f83fa9e..0000000 --- a/benchmark-tests/generic-pool-test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import genericPool from 'generic-pool'; -import { TestFactory } from './test-factory.js'; - -const testSuite = { - name: 'generic-pool', - run: runTest, - clear: clearPool, -}; - -export default testSuite; - -let pool; - -function runTest(options, callback) { - pool = genericPool.createPool( - new TestFactory({ - acquireWait: options.acquireWait, - }), - { - max: options.max, - maxWaitingClients: Number.MAX_SAFE_INTEGER, - evictionRunIntervalMillis: 1000, - }, - ); - - let k = 0; - let t = 0; - const testCount = options.testCount; - const releaseTime = options.releaseTime || 1; - for (let i = 0; i < testCount; i++) { - pool - .acquire() - .then(obj => { - k++; - t++; - setTimeout(() => { - pool - .release(obj) - .then(() => { - t--; - if (k === testCount && t === 0) { - k = 0; - callback(); - } - }) - .catch(e => { - throw e; - }); - }, releaseTime); - }) - .catch(e => { - throw e; - }); - } -} - -function clearPool(callback) { - pool.drain().then(() => { - pool.clear(); - callback(); - }); -} diff --git a/benchmark-tests/index.ts b/benchmark-tests/index.ts deleted file mode 100644 index 38f8053..0000000 --- a/benchmark-tests/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as process from 'node:process'; -import promisify from 'putil-promisify'; -import genericPoolTest from './generic-pool-test.js'; -import lightningPoolTest from './lightning-pool-test.js'; - -const testLoops = 2; -let testId = 0; - -async function runTest(options) { - console.log('### Starting Test-', ++testId, '###'); - console.log('- Total requests: ', options.testCount); - console.log('- Test loops: ', testLoops); - console.log('- Pool Resources: ', options.max); - console.log('- Acquiring Time: ', options.acquireWait, 'ms'); - console.log('- Release After: ', options.releaseTime, 'ms'); - const results: number[] = []; - let started = Date.now(); - let total = 0; - const runForAvg = function (k, module, cb) { - started = Date.now(); - module.run(options, err => { - if (err) return cb(err); - total += Date.now() - started; - if (!--k) { - const ms = total / testLoops; - results.push(ms); - console.log('>', module.name, ': Avg ', ms, 'ms'); - return module.clear(cb); - } - runForAvg(k, module, cb); - }); - }; - - const printResults = function () { - const t = Math.round((results[1] / results[0] - 1) * 100); - if (t > 0) console.log('Result: lightning is %', t, 'faster than generic'); - else console.log('Result: generic is %', t, 'faster than lightning'); - console.log(' '); - }; - - await promisify.fromCallback(cb => - runForAvg(testLoops, lightningPoolTest, cb), - ); - await promisify.fromCallback(cb => runForAvg(testLoops, genericPoolTest, cb)); - printResults(); -} - -async function runAll() { - await runTest({ - testCount: 1000, - acquireWait: 0, - releaseTime: 1, - max: 10, - }); - await runTest({ - testCount: 10000, - acquireWait: 0, - releaseTime: 1, - max: 10, - }); - await runTest({ - testCount: 10000, - acquireWait: 0, - releaseTime: 1, - max: 100, - }); - await runTest({ - testCount: 10000, - acquireWait: 0, - releaseTime: 1, - max: 1000, - }); - await runTest({ - testCount: 100000, - acquireWait: 0, - releaseTime: 1, - max: 1000, - }); - console.log('******************'); - console.log('All tests complete'); - process.exit(0); -} - -runAll().catch(e => { - console.error(e); - process.exit(1); -}); diff --git a/benchmark-tests/lightning-pool-test.ts b/benchmark-tests/lightning-pool-test.ts deleted file mode 100644 index 476eea1..0000000 --- a/benchmark-tests/lightning-pool-test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import assert from 'node:assert'; -import { createPool } from 'lightning-pool'; -import { TestFactory } from './test-factory.js'; - -const testSuite = { - name: 'lightning-pool', - run: runTest, - clear: clearPool, -}; - -export default testSuite; - -let pool; - -function runTest(options, callback) { - pool = createPool( - new TestFactory({ - acquireWait: options.acquireWait, - }), - { - max: options.max, - maxQueue: Number.MAX_SAFE_INTEGER, - houseKeepInterval: 1000, - }, - ); - - let k = 0; - let t = 0; - const testCount = options.testCount; - const releaseTime = options.releaseTime || 1; - for (let i = 0; i < testCount; i++) { - pool.acquire((err, obj) => { - assert(!err, err); - k++; - t++; - setTimeout(() => { - pool - .releaseAsync(obj) - .then(() => { - t--; - if (k === testCount && t === 0) { - k = 0; - callback(); - } - }) - .catch(e => { - throw e; - }); - }, releaseTime); - }); - } -} - -function clearPool(callback) { - pool.close(true, () => { - callback(); - }); -} diff --git a/benchmark-tests/test-factory.ts b/benchmark-tests/test-factory.ts deleted file mode 100644 index 9d65e41..0000000 --- a/benchmark-tests/test-factory.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { PoolFactory } from 'lightning-pool'; - -/** - * Generic class for handling creation of resources - * for testing - */ -export class TestFactory implements PoolFactory { - created: number; - destroyed: number; - max?: number; - retryTest?: number; - acquireWait?: number; - resetWait?: number; - - constructor(opts: { - create?: Function; - destroy?: Function; - reset?: Function; - validate?: Function; - max?: number; - retryTest?: number; - acquireWait?: number; - resetWait?: number; - }) { - this.created = 0; - this.destroyed = 0; - this.max = opts && opts.max; - this.retryTest = opts && opts.retryTest; - this.acquireWait = (opts && opts.acquireWait) || 0; - this.resetWait = (opts && opts.resetWait) || 0; - } - - create() { - return new Promise((resolve, reject) => { - const id = ++this.created; - if (this.max && id >= this.max) throw new Error('Max resources created'); - - const doCreate = () => { - if (this.retryTest && this.retryTest--) { - return reject(new Error('Retry test error')); - } - const res = new TestResource(id); - resolve(res); - }; - - if (this.acquireWait) setTimeout(doCreate, this.acquireWait); - else doCreate(); - }); - } - - async destroy(res: TestResource) { - if (!(res instanceof TestResource)) { - throw new Error('Invalid resource instance'); - } - if (res.destroyed) throw new Error('Resource already destroyed'); - this.destroyed++; - res.destroyed = true; - } - - reset(res: TestResource) { - return new Promise(resolve => { - setTimeout(() => { - res.resetCount++; - resolve(); - }, this.resetWait); - }); - } - - async validate(resource: TestResource) { - resource.validateCount++; - return true; - } -} - -export class TestResource { - id: any; - resetCount: number; - validateCount: number; - destroyed?: boolean; - - constructor(id) { - this.id = id; - this.resetCount = 0; - this.validateCount = 0; - } -} diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..4945594 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,73 @@ +# lightning-pool Benchmarks + +Compares lightning-pool against [generic-pool](https://github.com/coopernurse/node-pool) +driving an identical simulated resource, so the numbers isolate each pool's +own bookkeeping/scheduling overhead rather than any real backend's latency. +For the methodology and the latest generated numbers, see +[`doc/BENCHMARKS.md`](../doc/BENCHMARKS.md) - it is regenerated automatically +at the end of every `npm run bench` (or standalone via `npm run bench:report`), +not hand-edited. + +This replaces the older ad hoc `benchmark-tests/` script (removed), structured +after the benchmark harness in +[postgrejs](https://github.com/panates/postgrejs): per-scenario adapters, one +child process per (library, scenario, run) for clean measurements, and a +generated Markdown report with charts. + +## Running + +No external services needed - every scenario pools a simulated in-memory +resource (`benchmark/resource.ts`). + +```bash +# Full default matrix: every scenario, every library, 3 repeats each +npm run bench + +# Fast iteration while developing a scenario/adapter +npm run bench -- --scenario=queue-contention --lib=lightning-pool --repeats=1 + +# Regenerate doc/BENCHMARKS.md from existing benchmark/results/*.json +# without re-running the matrix (npm run bench already does this automatically) +npm run bench:report +``` + +`--scenario=` and `--lib=` accept `all` (default), `none`, or a +comma-separated subset (a leading/trailing `*` matches by prefix/suffix). +Scenario names: `acquire-release`, `acquire-release-concurrent`, +`queue-contention`, `create-destroy-churn`, `validate-on-borrow`. +Library ids: `lightning-pool`, `generic-pool`. + +Each `(library, scenario)` pair runs in its own child process, spawned one at +a time - never in parallel - so CPU contention on your machine doesn't skew +the numbers. Results land in `benchmark/results/*.json` (gitignored raw data +backing whatever `doc/BENCHMARKS.md` currently reports). + +## Rigor vs. speed + +The default iteration/warmup/repeat counts are kept modest on purpose, so a +full `npm run bench` run finishes in well under a minute - this is a tool +meant to be run repeatedly during development, not just once. For numbers +you intend to publish or cite: + +- Raise `--repeats` (e.g. `--repeats=10`) - the report uses the median across + repeats, so more repeats means a more defensible median. +- Run on an otherwise-idle machine. +- Per-scenario iteration/warmup/time budgets live in `benchmark/scenarios/*.ts` + (the `bench` field of each `*_SCENARIO` export) if you want to raise them + for a specific scenario rather than just repeating the whole matrix more. +- Set `BENCH_CREATE_DELAY_MS`/`BENCH_DESTROY_DELAY_MS` to approximate a real + backend's connection cost instead of the default 0ms simulated resource. + +## Adding a scenario or adapter + +- Shared pool-size/concurrency constants and the `ScenarioMeta` for a + scenario live in `benchmark/scenarios/.ts`; register it in + `benchmark/scenarios/index.ts` and wire its params into + `benchmark/runner/worker.ts`'s switch. +- Each library implements the scenario in its own adapter + (`benchmark/adapters/.adapter.ts`) using whatever calling convention + is idiomatic for that library against the `Adapter` interface in + `benchmark/adapters/adapter.ts`. +- `benchmark/adapters/registry.ts` is a dynamic-import map from library id to + adapter module, so a new adapter can be added without touching the default + matrix. diff --git a/benchmark/adapters/adapter.ts b/benchmark/adapters/adapter.ts new file mode 100644 index 0000000..949df3a --- /dev/null +++ b/benchmark/adapters/adapter.ts @@ -0,0 +1,49 @@ +import type { Bench } from 'tinybench'; +import type { LibId } from '../types.js'; + +/** + * Each adapter drives its own pool library using that library's idiomatic + * API (lightning-pool's acquire/release/destroy vs. generic-pool's + * acquire/release/destroy with its own option names) while pooling the + * exact same simulated resource (see resource.ts) with the same pool size/ + * concurrency knobs, read from benchmark/scenarios/*.ts. Only the pooling + * mechanism varies per library, not the workload it's driving. + */ +export interface Adapter { + readonly id: LibId; + /** Read from the installed package's own package.json at runtime */ + readonly libraryVersion: string; + scenarios: { + acquireRelease(bench: Bench, poolSize: number): void; + /** + * Fires `concurrency` acquire()+release() cycles via Promise.all per + * iteration, against a pool sized so nothing has to queue. + */ + acquireReleaseConcurrent( + bench: Bench, + poolSize: number, + concurrency: number, + ): void; + /** + * Fires `concurrency` acquire() calls via Promise.all against a pool + * much smaller than the concurrency, so most callers queue and wait; + * each holder releases immediately after acquiring. + */ + queueContention(bench: Bench, poolSize: number, concurrency: number): void; + /** + * `concurrency` acquire()+destroy() cycles per iteration - the resource + * is destroyed rather than released, so a new one must be created for + * every acquire. + */ + createDestroyChurn( + bench: Bench, + poolSize: number, + concurrency: number, + ): void; + /** + * Same shape as acquireReleaseConcurrent, but with the library's + * borrow-time validation hook enabled. + */ + validateOnBorrow(bench: Bench, poolSize: number, concurrency: number): void; + }; +} diff --git a/benchmark/adapters/generic-pool.adapter.ts b/benchmark/adapters/generic-pool.adapter.ts new file mode 100644 index 0000000..7784727 --- /dev/null +++ b/benchmark/adapters/generic-pool.adapter.ts @@ -0,0 +1,171 @@ +import * as genericPool from 'generic-pool'; +import { + getResourceCreateDelayMs, + getResourceDestroyDelayMs, +} from '../config.js'; +import { type BenchResource, createResourceOps } from '../resource.js'; +import type { Adapter } from './adapter.js'; +import { readInstalledVersion } from './pkg-version.js'; + +function makeFactory(withValidate: boolean) { + const ops = createResourceOps({ + createDelayMs: getResourceCreateDelayMs(), + destroyDelayMs: getResourceDestroyDelayMs(), + }); + const factory: { + create(): Promise; + destroy(r: BenchResource): Promise; + validate?(r: BenchResource): Promise; + } = { + create: () => ops.create(), + destroy: r => ops.destroy(r), + }; + if (withValidate) factory.validate = r => ops.validate(r); + return factory; +} + +export const genericPoolAdapter: Adapter = { + id: 'generic-pool', + libraryVersion: readInstalledVersion('generic-pool'), + + scenarios: { + acquireRelease(bench, poolSize) { + let pool!: genericPool.Pool; + bench.add( + 'acquire-release', + async () => { + const resource = await pool.acquire(); + await pool.release(resource); + }, + { + beforeAll: async () => { + pool = genericPool.createPool(makeFactory(false), { + max: poolSize, + min: 0, + testOnBorrow: false, + }); + }, + afterAll: async () => { + await pool.drain(); + await pool.clear(); + }, + }, + ); + }, + + acquireReleaseConcurrent(bench, poolSize, concurrency) { + let pool!: genericPool.Pool; + bench.add( + 'acquire-release-concurrent', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.release(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = genericPool.createPool(makeFactory(false), { + max: poolSize, + min: 0, + testOnBorrow: false, + }); + }, + afterAll: async () => { + await pool.drain(); + await pool.clear(); + }, + }, + ); + }, + + queueContention(bench, poolSize, concurrency) { + let pool!: genericPool.Pool; + bench.add( + 'queue-contention', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.release(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = genericPool.createPool(makeFactory(false), { + max: poolSize, + min: 0, + testOnBorrow: false, + maxWaitingClients: concurrency, + }); + }, + afterAll: async () => { + await pool.drain(); + await pool.clear(); + }, + }, + ); + }, + + createDestroyChurn(bench, poolSize, concurrency) { + let pool!: genericPool.Pool; + bench.add( + 'create-destroy-churn', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.destroy(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = genericPool.createPool(makeFactory(false), { + max: poolSize, + min: 0, + testOnBorrow: false, + maxWaitingClients: concurrency, + }); + }, + afterAll: async () => { + await pool.drain(); + await pool.clear(); + }, + }, + ); + }, + + validateOnBorrow(bench, poolSize, concurrency) { + let pool!: genericPool.Pool; + bench.add( + 'validate-on-borrow', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.release(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = genericPool.createPool(makeFactory(true), { + max: poolSize, + min: 0, + testOnBorrow: true, + maxWaitingClients: concurrency, + }); + }, + afterAll: async () => { + await pool.drain(); + await pool.clear(); + }, + }, + ); + }, + }, +}; diff --git a/benchmark/adapters/lightning-pool.adapter.ts b/benchmark/adapters/lightning-pool.adapter.ts new file mode 100644 index 0000000..0f859ba --- /dev/null +++ b/benchmark/adapters/lightning-pool.adapter.ts @@ -0,0 +1,163 @@ +import { createPool, type Pool, type PoolFactory } from '../../src/index.js'; +import { + getResourceCreateDelayMs, + getResourceDestroyDelayMs, +} from '../config.js'; +import { type BenchResource, createResourceOps } from '../resource.js'; +import type { Adapter } from './adapter.js'; +import { readOwnPackageVersion } from './pkg-version.js'; + +function makeFactory(withValidate: boolean): PoolFactory { + const ops = createResourceOps({ + createDelayMs: getResourceCreateDelayMs(), + destroyDelayMs: getResourceDestroyDelayMs(), + }); + const factory: PoolFactory = { + create: () => ops.create(), + destroy: r => ops.destroy(r), + }; + if (withValidate) factory.validate = r => ops.validate(r); + return factory; +} + +export const lightningPoolAdapter: Adapter = { + id: 'lightning-pool', + libraryVersion: readOwnPackageVersion(), + + scenarios: { + acquireRelease(bench, poolSize) { + let pool!: Pool; + bench.add( + 'acquire-release', + async () => { + const resource = await pool.acquire(); + await pool.releaseAsync(resource); + }, + { + beforeAll: async () => { + pool = createPool(makeFactory(false), { + max: poolSize, + validation: false, + }); + pool.start(); + }, + afterAll: async () => { + await pool.closeAsync(0); + }, + }, + ); + }, + + acquireReleaseConcurrent(bench, poolSize, concurrency) { + let pool!: Pool; + bench.add( + 'acquire-release-concurrent', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.releaseAsync(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = createPool(makeFactory(false), { + max: poolSize, + maxQueue: concurrency, + validation: false, + }); + pool.start(); + }, + afterAll: async () => { + await pool.closeAsync(0); + }, + }, + ); + }, + + queueContention(bench, poolSize, concurrency) { + let pool!: Pool; + bench.add( + 'queue-contention', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.releaseAsync(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = createPool(makeFactory(false), { + max: poolSize, + maxQueue: concurrency, + validation: false, + }); + pool.start(); + }, + afterAll: async () => { + await pool.closeAsync(0); + }, + }, + ); + }, + + createDestroyChurn(bench, poolSize, concurrency) { + let pool!: Pool; + bench.add( + 'create-destroy-churn', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.destroyAsync(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = createPool(makeFactory(false), { + max: poolSize, + maxQueue: concurrency, + validation: false, + }); + pool.start(); + }, + afterAll: async () => { + await pool.closeAsync(0); + }, + }, + ); + }, + + validateOnBorrow(bench, poolSize, concurrency) { + let pool!: Pool; + bench.add( + 'validate-on-borrow', + async () => { + await Promise.all( + Array.from({ length: concurrency }, async () => { + const resource = await pool.acquire(); + await pool.releaseAsync(resource); + }), + ); + }, + { + beforeAll: async () => { + pool = createPool(makeFactory(true), { + max: poolSize, + maxQueue: concurrency, + validation: true, + }); + pool.start(); + }, + afterAll: async () => { + await pool.closeAsync(0); + }, + }, + ); + }, + }, +}; diff --git a/benchmark/adapters/pkg-version.ts b/benchmark/adapters/pkg-version.ts new file mode 100644 index 0000000..69b217d --- /dev/null +++ b/benchmark/adapters/pkg-version.ts @@ -0,0 +1,41 @@ +import * as fs from 'node:fs'; +import { createRequire } from 'node:module'; +import * as path from 'node:path'; + +const require = createRequire(import.meta.url); + +/** + * Resolves the *installed* version of a dependency (not this repo's semver + * range for it) by walking up from its resolved entry file to the nearest + * package.json whose "name" matches. + */ +export function readInstalledVersion(packageName: string): string { + const entry = require.resolve(packageName); + let dir = path.dirname(entry); + for (let i = 0; i < 10; i++) { + const pkgPath = path.join(dir, 'package.json'); + if (fs.existsSync(pkgPath)) { + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { + name?: string; + version?: string; + }; + if (pkg.name === packageName && pkg.version) return pkg.version; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + throw new Error(`Could not resolve installed version of "${packageName}"`); +} + +/** lightning-pool is this repo itself, so its "installed version" is its own package.json */ +export function readOwnPackageVersion(): string { + const pkgPath = path.resolve( + path.dirname(new URL(import.meta.url).pathname), + '../../package.json', + ); + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) as { + version: string; + }; + return pkg.version; +} diff --git a/benchmark/adapters/registry.ts b/benchmark/adapters/registry.ts new file mode 100644 index 0000000..b3827d3 --- /dev/null +++ b/benchmark/adapters/registry.ts @@ -0,0 +1,31 @@ +import type { LibId } from '../types.js'; +import type { Adapter } from './adapter.js'; + +/** + * Dynamic-import map so adding another library later doesn't require + * touching every caller - just a new loader entry and a new LibId. + */ +// Order matters: ALL_LIB_IDS (below) drives the default --lib=all run +// order, and whichever library runs first in a given process/scenario +// tends to look slower/noisier (process/CPU warm-up, not a real code +// difference). generic-pool runs first so lightning-pool - the library +// this benchmark exists to evaluate - isn't the one absorbing that bias +// by default. +const ADAPTER_LOADERS: Record Promise> = { + 'generic-pool': async () => + (await import('./generic-pool.adapter.js')).genericPoolAdapter, + 'lightning-pool': async () => + (await import('./lightning-pool.adapter.js')).lightningPoolAdapter, +}; + +export const ALL_LIB_IDS = Object.keys(ADAPTER_LOADERS) as LibId[]; + +export function isLibId(value: string): value is LibId { + return Object.prototype.hasOwnProperty.call(ADAPTER_LOADERS, value); +} + +export async function loadAdapter(id: LibId): Promise { + const loader = ADAPTER_LOADERS[id]; + if (!loader) throw new Error(`Unknown benchmark adapter "${id}"`); + return loader(); +} diff --git a/benchmark/cli.ts b/benchmark/cli.ts new file mode 100644 index 0000000..dc64567 --- /dev/null +++ b/benchmark/cli.ts @@ -0,0 +1,84 @@ +import process from 'node:process'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { ALL_LIB_IDS } from './adapters/registry.js'; +import { runMatrix } from './runner/orchestrator.js'; +import { SCENARIO_NAMES } from './scenarios/index.js'; +import type { LibId, ScenarioName } from './types.js'; + +function resolveList(value: string, all: readonly T[]): T[] { + if (value === 'all') return [...all]; + if (value === 'none') return []; + const items = value + .split(',') + .map(s => s.trim()) + .filter(Boolean); + const seen = new Set(); + for (const item of items) { + const matches = all.filter(v => + item.startsWith('*') && item.endsWith('*') + ? v.includes(item.replaceAll('*', '')) + : item.startsWith('*') + ? v.endsWith(item.replaceAll('*', '')) + : item.endsWith('*') + ? v.startsWith(item.replaceAll('*', '')) + : v === item, + ); + matches.forEach((v: T) => seen.add(v)); + } + return Array.from(seen); +} + +async function main(): Promise { + const argv = await yargs(hideBin(process.argv)) + .scriptName('bench') + .usage('$0 [options]') + .option('lib', { + type: 'string', + alias: 'l', + default: 'all', + describe: `Comma-separated library ids, or "all" (${ALL_LIB_IDS.join(', ')})`, + }) + .option('scenario', { + alias: 's', + type: 'string', + default: 'all', + describe: + `Comma-separated scenario names, "all", or "none" to run nothing - ` + + `a trailing "*" matches by prefix, e.g. "acquire-*" (${SCENARIO_NAMES.join(', ')})`, + }) + .option('repeats', { + type: 'number', + alias: 'r', + default: 3, + describe: + 'How many times to repeat the full matrix (the report uses the median across repeats)', + }) + .check(a => { + if (!Number.isInteger(a.repeats) || a.repeats < 1) { + throw new Error( + `--repeats must be a positive integer, got "${a.repeats}"`, + ); + } + return true; + }) + .strict() + .help() + .parse(); + + const libs: LibId[] = resolveList(argv.lib, ALL_LIB_IDS); + const scenarios: ScenarioName[] = resolveList(argv.scenario, SCENARIO_NAMES); + const repeats = argv.repeats; + + console.log( + `Benchmark matrix: scenarios=[${scenarios.join(', ')}] ` + + `libs=[${libs.join(', ')}] repeats=${repeats}`, + ); + await runMatrix({ libs, scenarios, repeats }); + console.log('\nDone. doc/BENCHMARKS.md has been regenerated.'); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/benchmark/config.ts b/benchmark/config.ts new file mode 100644 index 0000000..fa03789 --- /dev/null +++ b/benchmark/config.ts @@ -0,0 +1,17 @@ +/** + * Milliseconds a simulated resource's create()/destroy() takes to settle - + * 0 by default so scenarios measure pure pool overhead rather than an + * artificial delay, but overridable to approximate a real backend (a DB + * connection, a socket handshake) without needing one installed. + */ +export function getResourceCreateDelayMs(): number { + return process.env.BENCH_CREATE_DELAY_MS + ? parseInt(process.env.BENCH_CREATE_DELAY_MS, 10) + : 0; +} + +export function getResourceDestroyDelayMs(): number { + return process.env.BENCH_DESTROY_DELAY_MS + ? parseInt(process.env.BENCH_DESTROY_DELAY_MS, 10) + : 0; +} diff --git a/benchmark/env.mjs b/benchmark/env.mjs new file mode 100644 index 0000000..77e56eb --- /dev/null +++ b/benchmark/env.mjs @@ -0,0 +1,11 @@ +// Preload step for worker.ts child processes (see runner/orchestrator.ts). +// +// @swc-node/register only picks up this directory's own tsconfig.json when +// TS_NODE_PROJECT is set *before* @swc-node/register/esm-register +// initializes, so it must be pointed to explicitly here rather than relying +// on auto-discovery. This file is passed as an earlier `--import` than the +// register hook itself, the same two-step trick `.mocharc.cjs` uses. +process.env.TS_NODE_PROJECT = new URL( + './tsconfig.json', + import.meta.url, +).pathname; diff --git a/benchmark/report/aggregate.ts b/benchmark/report/aggregate.ts new file mode 100644 index 0000000..1eba9dd --- /dev/null +++ b/benchmark/report/aggregate.ts @@ -0,0 +1,91 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import type { BenchResult, LibId, ScenarioName } from '../types.js'; + +export interface ScenarioLibSummary { + lib: LibId; + libraryVersion: string; + scenario: ScenarioName; + runs: BenchResult[]; + medianMean: number; + medianP75: number; + medianP99: number; + medianOpsPerSec: number; + medianSamples: number; + /** undefined only if every run is missing this stat (see BenchResultStats). */ + medianGcCount?: number; + medianGcDurationMs?: number; + medianPeakHeapGrowthBytes?: number; +} + +function median(values: number[]): number { + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 !== 0 + ? sorted[mid] + : (sorted[mid - 1] + sorted[mid]) / 2; +} + +// Older result files (or a run without --expose-gc) may not have these +// stats at all - median of an empty/all-undefined set should stay +// undefined instead of NaN or a misleading 0. +function medianOrUndefined(values: (number | undefined)[]): number | undefined { + const defined = values.filter((v): v is number => v != null); + return defined.length ? median(defined) : undefined; +} + +export function readResults(resultsDir: string): BenchResult[] { + if (!fs.existsSync(resultsDir)) return []; + const files = fs.readdirSync(resultsDir).filter(f => f.endsWith('.json')); + return files.map( + f => + JSON.parse( + fs.readFileSync(path.join(resultsDir, f), 'utf8'), + ) as BenchResult, + ); +} + +export function summarize(results: BenchResult[]): ScenarioLibSummary[] { + const groups = new Map(); + for (const r of results) { + const key = `${r.scenario}__${r.lib}`; + const list = groups.get(key) ?? []; + list.push(r); + groups.set(key, list); + } + const summaries: ScenarioLibSummary[] = []; + for (const runs of groups.values()) { + const first = runs[0]; + summaries.push({ + lib: first.lib, + libraryVersion: first.libraryVersion, + scenario: first.scenario, + runs, + medianMean: median(runs.map(r => r.stats.mean)), + medianP75: median(runs.map(r => r.stats.p75)), + medianP99: median(runs.map(r => r.stats.p99)), + medianOpsPerSec: median(runs.map(r => r.stats.opsPerSec)), + medianSamples: median(runs.map(r => r.stats.samples)), + medianGcCount: medianOrUndefined(runs.map(r => r.stats.gcCount)), + medianGcDurationMs: medianOrUndefined( + runs.map(r => r.stats.gcDurationMs), + ), + medianPeakHeapGrowthBytes: medianOrUndefined( + runs.map(r => r.stats.peakHeapGrowthBytes), + ), + }); + } + return summaries; +} + +export function groupByScenario( + summaries: ScenarioLibSummary[], +): Map { + const byScenario = new Map(); + for (const s of summaries) { + const list = byScenario.get(s.scenario) ?? []; + list.push(s); + byScenario.set(s.scenario, list); + } + return byScenario; +} diff --git a/benchmark/report/render-console.ts b/benchmark/report/render-console.ts new file mode 100644 index 0000000..4e706ce --- /dev/null +++ b/benchmark/report/render-console.ts @@ -0,0 +1,91 @@ +import { groupByScenario, type ScenarioLibSummary } from './aggregate.js'; + +function round(n: number, decimals: number): number { + const f = 10 ** decimals; + return Math.round(n * f) / f; +} + +type Cell = string | number | null; +type Row = Record; + +/** + * Renders `rows` as a box-drawn table, numbers right-aligned and text + * left-aligned - console.table() has no alignment option of its own (it + * left-aligns everything, `lib` included), so this reimplements just enough + * of its look to right-align the numeric columns the summary is mostly + * made of. + */ +function printTable(rows: Row[], leftAlign: Set = new Set()): void { + if (!rows.length) return; + const columns = Object.keys(rows[0]); + const cellText = (v: Cell): string => (v == null ? '-' : String(v)); + const isNumeric = new Map( + columns.map(c => [c, !leftAlign.has(c)]), + ); + const widths = new Map( + columns.map(c => [ + c, + Math.max(c.length, ...rows.map(r => cellText(r[c]).length)), + ]), + ); + const pad = (text: string, width: number, alignRight: boolean): string => + alignRight ? text.padStart(width) : text.padEnd(width); + const rule = (l: string, m: string, r: string): string => + l + columns.map(c => '─'.repeat(widths.get(c)! + 2)).join(m) + r; + const renderRow = (cells: string[], aligned: boolean[]): string => + '│ ' + + columns + .map((c, i) => pad(cells[i], widths.get(c)!, aligned[i])) + .join(' │ ') + + ' │'; + + console.log(rule('┌', '┬', '┐')); + console.log( + renderRow( + columns, + columns.map(c => isNumeric.get(c)!), + ), + ); + console.log(rule('├', '┼', '┤')); + for (const r of rows) { + console.log( + renderRow( + columns.map(c => cellText(r[c])), + columns.map(c => isNumeric.get(c)!), + ), + ); + } + console.log(rule('└', '┴', '┘')); +} + +/** + * Prints one table per scenario instead of hand-aligned `key=value` text - + * see printTable() for why this isn't console.table() itself. + */ +export function renderConsoleSummary(summaries: ScenarioLibSummary[]): void { + const byScenario = groupByScenario(summaries); + for (const [scenario, libs] of byScenario) { + console.log(`\n${scenario}`); + const sorted = [...libs].sort((a, b) => a.medianMean - b.medianMean); + const slowest = sorted[sorted.length - 1]; + const rows: Row[] = sorted.map(s => ({ + lib: s.lib, + 'mean (ms)': round(s.medianMean, 4), + 'p75 (ms)': round(s.medianP75, 4), + 'p99 (ms)': round(s.medianP99, 4), + 'ops/sec': round(s.medianOpsPerSec, 1), + 'vs slowest': + slowest && slowest.medianMean > 0 + ? round(slowest.medianMean / s.medianMean, 2) + 'x' + : null, + 'gc count': s.medianGcCount ?? null, + 'gc (ms)': + s.medianGcDurationMs != null ? round(s.medianGcDurationMs, 1) : null, + 'peak heap (KB)': + s.medianPeakHeapGrowthBytes != null + ? round(s.medianPeakHeapGrowthBytes / 1024, 1) + : null, + })); + printTable(rows, new Set(['lib', 'vs slowest'])); + } +} diff --git a/benchmark/report/render-markdown.ts b/benchmark/report/render-markdown.ts new file mode 100644 index 0000000..fdb21a9 --- /dev/null +++ b/benchmark/report/render-markdown.ts @@ -0,0 +1,386 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { + readInstalledVersion, + readOwnPackageVersion, +} from '../adapters/pkg-version.js'; +import { SCENARIO_NAMES, SCENARIOS } from '../scenarios/index.js'; +import type { LibId, ScenarioName } from '../types.js'; +import { + groupByScenario, + readResults, + type ScenarioLibSummary, + summarize, +} from './aggregate.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const BENCHMARK_DIR = path.resolve(__dirname, '..'); +const REPO_ROOT = path.resolve(BENCHMARK_DIR, '..'); +const RESULTS_DIR = path.join(BENCHMARK_DIR, 'results'); +const OUTPUT_PATH = path.join(REPO_ROOT, 'doc', 'BENCHMARKS.md'); + +const LIB_LABELS: Record = { + 'lightning-pool': 'lightning-pool', + 'generic-pool': 'generic-pool', +}; + +// Fixed chart order (not sorted by speed, unlike the table): lightning-pool's +// bar is always in the same position across every scenario's chart, so a +// reader scanning down BENCHMARKS.md can compare it scenario-to-scenario +// without hunting for it in a ranking that reshuffles per scenario. +const CHART_LIB_ORDER: LibId[] = ['lightning-pool', 'generic-pool']; + +/** + * GitHub's own heading-anchor rule: lowercase, drop everything that isn't + * a word character/space/hyphen, spaces to hyphens - and when the same + * slug repeats, later ones get "-1", "-2", ... appended. + */ +function slugify(text: string, seen: Map): string { + const base = text + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .trim() + .replace(/\s+/g, '-'); + const n = seen.get(base) ?? 0; + seen.set(base, n + 1); + return n === 0 ? base : `${base}-${n}`; +} + +/** + * Builds the table of contents by reading back the headings of the document + * that was just assembled, so it can't drift out of sync with what actually + * got rendered. Fenced blocks are skipped so a "#" inside a mermaid chart + * can never be mistaken for a heading. + */ +function renderIndex(markdown: string): string { + const seen = new Map(); + const entries: string[] = []; + let inFence = false; + for (const line of markdown.split('\n')) { + if (line.startsWith('```')) { + inFence = !inFence; + continue; + } + if (inFence) continue; + const m = /^(#{2,3}) (.+)$/.exec(line); + if (!m) continue; + const text = m[2].trim(); + const indent = ' '.repeat(m[1].length - 2); + entries.push(`${indent}- [${text}](#${slugify(text, seen)})`); + } + return ['## Contents', '', ...entries, ''].join('\n'); +} + +function renderMethodology(): string { + return `## Methodology + +These numbers are produced by \`benchmark/\` (run via \`npm run bench\`), comparing lightning-pool against [generic-pool](https://github.com/coopernurse/node-pool) driving an identical simulated resource (see \`benchmark/resource.ts\`) - no real database or socket involved, so the numbers isolate each pool's own bookkeeping and scheduling overhead rather than any backend's latency. See [benchmark/README.md](../benchmark/README.md) for how to reproduce them. + +Each scenario is implemented once per library, using that library's own idiomatic API (lightning-pool's \`acquire\`/\`releaseAsync\`/\`destroyAsync\` vs. generic-pool's \`acquire\`/\`release\`/\`destroy\`), while both read the exact same pool-size/concurrency knobs from \`benchmark/scenarios/*.ts\`. Only the pooling mechanism varies, not the workload. + +Each \`(library, scenario)\` pair runs in its own child process, spawned sequentially (never in parallel), to avoid CPU contention skewing numbers and to get clean, uncontaminated V8 JIT warm-up per run. The default matrix runs each pair \`--repeats=3\` times; the tables below report the **median across repeats**, with intra-run p75/p99 latency and ops/sec from tinybench's own sample statistics. + +Each table also reports **GC (ms/op)** and **Peak Heap (KB)** - allocation pressure, not just wall-clock speed. GC (ms/op) is the total time spent in garbage collection during the run (observed via \`node:perf_hooks\`, every GC pause regardless of cause), divided by the number of timed samples. Peak Heap (KB) isn't a per-call figure: each worker process is started with \`--expose-gc\`, forces a clean GC immediately before the run to get a baseline \`heapUsed\`, then tracks the highest \`heapUsed\` seen at any point during the run - the most the heap ever grew above that baseline while running the whole scenario. Both include tinybench's own warmup iterations (it doesn't expose a hook at the boundary between warmup and the timed run). + +By default every simulated resource's \`create()\`/\`destroy()\` resolves immediately (0ms) so the numbers measure pure pool overhead. Set \`BENCH_CREATE_DELAY_MS\`/\`BENCH_DESTROY_DELAY_MS\` to approximate a real backend's connection cost instead: \`BENCH_CREATE_DELAY_MS=5 npm run bench\`. + +**Why GC/heap sampling needs a forced yield here.** Neither scenario does real I/O, so a pool whose hot path resolves entirely through chained \`Promise\`s (no timer/socket in between) can run its whole scenario without Node's event loop ever reaching a macrotask turn - and both \`node:perf_hooks\`' \`'gc'\` performance entries and a plain polling timer are only delivered/fire on such a turn. Left unpatched, this reads back as \`gcCount=0\`/\`peakHeap=0\` for whichever library's task happens to chain purely through microtasks, indistinguishable from genuinely zero allocation (confirmed directly: a raw, uninstrumented 2,000,000-iteration acquire/release loop reports zero for *both* libraries here, even though real GCs are demonstrably happening - inserting a periodic \`setImmediate\` yield in that same raw loop immediately surfaces hundreds of real GC events and tens of MB of real heap growth). \`benchmark/runner/worker.ts\`'s \`installPeriodicYield()\` fixes this at the source: it patches \`bench.add()\` to insert a real \`setImmediate\` yield (plus a heap sample) into the task's \`afterEach\` hook every 250 samples. tinybench's own time budget only accumulates the timed \`fn()\` duration, not hook time (see its \`Task\` internals), so this yield doesn't shrink the sample count or skew Mean/p75/p99/ops-per-sec - it only makes the whole run take a little longer in real wall-clock time, which is what makes GC (ms/op) and Peak Heap (KB) trustworthy enough to compare between libraries at all. + +### Disclosed asymmetries + +1. **Validation** - lightning-pool's \`validation\` option and generic-pool's \`testOnBorrow\` option are conceptually equivalent (both call \`factory.validate()\` before handing a resource out) but are each library's own native mechanism, not a shared shim. +2. **Queue depth** - generic-pool's \`maxWaitingClients\` and lightning-pool's \`maxQueue\` are each set to (at least) the scenario's concurrency so neither library ever rejects a request for being over capacity; the numbers measure queueing/scheduling cost, not admission-control behaviour. +3. **Resource shape** - both libraries pool the exact same plain \`{ id, destroyed }\` object (see \`benchmark/resource.ts\`), so no library gains or loses time doing work specific to a real resource type. +`; +} + +function renderEnvironment(libVersions: Record): string { + const cpus = os.cpus(); + const totalMemGb = os.totalmem() / (1024 * 1024 * 1024); + const lines = [ + `- Run date: ${new Date().toISOString()}`, + `- Node.js: ${process.version}`, + `- OS: ${os.type()} ${os.release()} (${process.platform}/${process.arch})`, + `- CPU: ${cpus[0]?.model ?? 'unknown'} (${cpus.length} logical cores)`, + `- RAM: ${totalMemGb.toFixed(1)} GB total`, + `- Library versions (installed, not this repo's semver range): ` + + `lightning-pool ${libVersions['lightning-pool']}, generic-pool ${libVersions['generic-pool']}`, + ]; + return `## Environment\n\n${lines.join('\n')}\n`; +} + +const HIGHER_IS_BETTER_COLOR = '#f2a900'; + +/** GitHub renders ```mermaid fences natively, so a chart here is plain + * committed text - no image files to generate/regenerate/gitignore. */ +function renderBarChart( + summaries: ScenarioLibSummary[], + opts: { + title: string; + unit: string; + valueOf: (s: ScenarioLibSummary) => number; + width?: number; + height?: number; + color?: string; + }, +): string { + const byLib = new Map(summaries.map(s => [s.lib, s])); + const ordered = CHART_LIB_ORDER.map(lib => byLib.get(lib)).filter( + (s): s is ScenarioLibSummary => !!s, + ); + const xAxis = ordered.map(s => LIB_LABELS[s.lib] ?? s.lib); + const values = ordered.map(opts.valueOf); + const bars = values.map(v => v.toFixed(4)); + // Without an explicit range, xychart-beta auto-scales the value axis to + // fit the data tightly (roughly [min, max] of the bars, not anchored at + // 0) - anchoring at 0 keeps bar length honestly proportional to actual + // values instead of exaggerating a small real difference. + const minValue = Math.min(...values, 0); + const maxValue = Math.max(...values, 0); + const yAxisMin = (minValue < 0 ? minValue * 1.1 : 0).toFixed(4); + const yAxisMax = ( + maxValue > 0 ? maxValue * 1.5 : minValue < 0 ? 0 : 1 + ).toFixed(4); + const initParts = [ + `'xyChart': {'width': ${opts.width ?? 500}, 'height': ${opts.height ?? 260}, 'chartOrientation': 'horizontal'}`, + ]; + if (opts.color) { + initParts.push( + `'themeVariables': {'xyChart': {'plotColorPalette': '${opts.color}'}}`, + ); + } + return [ + '```mermaid', + `%%{init: {${initParts.join(', ')}}}%%`, + 'xychart-beta', + ` title "${opts.title}"`, + ` x-axis [${xAxis.map(x => JSON.stringify(x)).join(', ')}]`, + ` y-axis "${opts.unit}" ${yAxisMin} --> ${yAxisMax}`, + ` bar [${bars.join(', ')}]`, + '```', + ].join('\n'); +} + +function renderScenarioCharts(summaries: ScenarioLibSummary[]): string { + const hasGc = summaries.some(s => s.medianGcDurationMs != null); + const hasPeakHeap = summaries.some(s => s.medianPeakHeapGrowthBytes != null); + const width = 600; + const height = 300; + + const charts = [ + renderBarChart(summaries, { + title: 'Mean latency (ms, lower is better)', + unit: 'ms', + width, + height, + valueOf: s => s.medianMean, + }), + renderBarChart(summaries, { + title: 'Throughput (ops/sec, higher is better)', + unit: 'ops/sec', + width, + height, + valueOf: s => s.medianOpsPerSec, + color: HIGHER_IS_BETTER_COLOR, + }), + ]; + if (hasGc) { + charts.push( + renderBarChart(summaries, { + title: 'GC time (ms/op, lower is better)', + unit: 'ms/op', + width, + height, + valueOf: s => (s.medianGcDurationMs ?? 0) / s.medianSamples, + }), + ); + } + if (hasPeakHeap) { + charts.push( + renderBarChart(summaries, { + title: 'Peak heap growth (KB, max memory reached)', + unit: 'KB', + width, + height, + valueOf: s => (s.medianPeakHeapGrowthBytes ?? 0) / 1024, + }), + ); + } + + const cellStyle = + 'style="display:inline-block;width:430px;vertical-align:top;margin:4px;"'; + return ( + charts.map(c => `
\n\n${c}\n\n
`).join('\n') + '\n' + ); +} + +function renderScenarioTable( + scenarioName: ScenarioName, + summaries: ScenarioLibSummary[], +): string { + const meta = SCENARIOS[scenarioName]; + const sorted = [...summaries].sort((a, b) => a.medianMean - b.medianMean); + const slowest = sorted[sorted.length - 1]; + const params = sorted[0]?.runs[0]?.params ?? {}; + const paramsText = Object.entries(params) + .map(([k, v]) => `${k}=${v}`) + .join(', '); + + const rowData = sorted.map(s => { + const mult = + slowest && slowest.medianMean > 0 ? slowest.medianMean / s.medianMean : 1; + const gcMsPerOp = + s.medianGcDurationMs != null + ? s.medianGcDurationMs / s.medianSamples + : null; + const peakHeapKb = + s.medianPeakHeapGrowthBytes != null + ? s.medianPeakHeapGrowthBytes / 1024 + : null; + return { + label: LIB_LABELS[s.lib] ?? s.lib, + version: s.libraryVersion, + mean: s.medianMean, + p75: s.medianP75, + p99: s.medianP99, + opsPerSec: s.medianOpsPerSec, + mult, + gcMsPerOp, + peakHeapKb, + }; + }); + + const definedOrNull = (values: (number | null)[]): number | null => { + const defined = values.filter((v): v is number => v != null); + return defined.length ? Math.min(...defined) : null; + }; + const shouldBold = rowData.length > 1; + const bestMean = Math.min(...rowData.map(r => r.mean)); + const bestP75 = Math.min(...rowData.map(r => r.p75)); + const bestP99 = Math.min(...rowData.map(r => r.p99)); + const bestOpsPerSec = Math.max(...rowData.map(r => r.opsPerSec)); + const bestMult = Math.max(...rowData.map(r => r.mult)); + const bestGcMsPerOp = definedOrNull(rowData.map(r => r.gcMsPerOp)); + const bestPeakHeapKb = definedOrNull(rowData.map(r => r.peakHeapKb)); + const boldIfBest = (formatted: string, value: number, best: number) => + shouldBold && value === best ? `***${formatted}***` : formatted; + + const rows = rowData.map(r => { + const meanStr = boldIfBest(r.mean.toFixed(4), r.mean, bestMean); + const p75Str = boldIfBest(r.p75.toFixed(4), r.p75, bestP75); + const p99Str = boldIfBest(r.p99.toFixed(4), r.p99, bestP99); + const opsStr = boldIfBest( + r.opsPerSec.toFixed(1), + r.opsPerSec, + bestOpsPerSec, + ); + const multStr = boldIfBest(`${r.mult.toFixed(2)}x`, r.mult, bestMult); + const gcStr = + r.gcMsPerOp != null + ? boldIfBest(r.gcMsPerOp.toFixed(4), r.gcMsPerOp, bestGcMsPerOp ?? NaN) + : '—'; + const peakHeapStr = + r.peakHeapKb != null + ? boldIfBest( + r.peakHeapKb.toFixed(2), + r.peakHeapKb, + bestPeakHeapKb ?? NaN, + ) + : '—'; + return ( + `| ${r.label} (${r.version}) | ${meanStr} | ${p75Str} | ${p99Str} | ` + + `${opsStr} | ${multStr} | ${gcStr} | ${peakHeapStr} |` + ); + }); + const unsupportedRows = Object.entries(meta.unsupportedLibs ?? {}).map( + ([lib, reason]) => { + const label = LIB_LABELS[lib as LibId] ?? lib; + return `| ${label} | ${reason} | — | — | — | — | — | — |`; + }, + ); + + return [ + `## ${meta.title}`, + '', + meta.description + (paramsText ? ` (${paramsText})` : ''), + '', + '| Library | Mean (ms) | p75 (ms) | p99 (ms) | ops/sec | vs. slowest | GC (ms/op) | Peak Heap (KB) |', + '|---|---:|---:|---:|---:|---:|---:|---:|', + ...rows, + ...unsupportedRows, + '', + renderScenarioCharts(summaries), + ].join('\n'); +} + +/** + * Regenerates doc/BENCHMARKS.md from whatever is currently in + * benchmark/results/*.json. Called automatically by the orchestrator at the + * end of `npm run bench`, and importable/callable standalone via + * `npm run bench:report` (see the CLI entry point at the bottom of this + * file) to regenerate the doc from existing results without re-running the + * whole matrix. + */ +export function generateBenchmarksMarkdown(): void { + const results = readResults(RESULTS_DIR); + if (results.length === 0) { + throw new Error( + 'No results found in benchmark/results/. Run `npm run bench` first.', + ); + } + + const summaries = summarize(results); + const byScenario = groupByScenario(summaries); + + const libVersions: Record = { + 'lightning-pool': readOwnPackageVersion(), + 'generic-pool': readInstalledVersion('generic-pool'), + }; + + const sections: string[] = [ + '# lightning-pool Benchmarks', + '', + '_Generated automatically by `npm run bench` (or standalone via `npm run bench:report`). Do not hand-edit — re-run one of those instead._', + '', + renderMethodology(), + renderEnvironment(libVersions), + ]; + + for (const scenarioName of SCENARIO_NAMES) { + const scenarioSummaries = byScenario.get(scenarioName); + if (!scenarioSummaries || scenarioSummaries.length === 0) continue; + sections.push(renderScenarioTable(scenarioName, scenarioSummaries)); + } + + sections.push( + '## Raw data', + '', + 'Backing raw data for the numbers above lives in `benchmark/results/*.json` ' + + '(gitignored; regenerate with `npm run bench`).', + '', + ); + + const body = sections.join('\n'); + const titleBlock = [ + '# lightning-pool Benchmarks', + '', + '_Generated automatically by `npm run bench` (or standalone via `npm run bench:report`). Do not hand-edit — re-run one of those instead._', + '', + ].join('\n'); + const output = + titleBlock + '\n' + renderIndex(body) + body.slice(titleBlock.length); + + fs.writeFileSync(OUTPUT_PATH, output); + console.log(`Wrote ${path.relative(REPO_ROOT, OUTPUT_PATH)}`); +} + +// Only auto-run when this file is the process entry point (`npm run +// bench:report`), not when generateBenchmarksMarkdown() is imported and +// called explicitly (e.g. by runner/orchestrator.ts at the end of a +// `npm run bench` run). +if (import.meta.url === `file://${process.argv[1]}`) { + generateBenchmarksMarkdown(); +} diff --git a/benchmark/resource.ts b/benchmark/resource.ts new file mode 100644 index 0000000..e150568 --- /dev/null +++ b/benchmark/resource.ts @@ -0,0 +1,49 @@ +/** + * A library-agnostic stand-in for whatever a real pooled resource would be + * (a DB connection, a socket). Each adapter wraps the same create/destroy/ + * validate operations in its own library's factory shape, so every (lib, + * scenario) pair pools an identical workload - only the pooling mechanism + * varies, not what's being pooled. + */ +export interface BenchResource { + id: number; + destroyed: boolean; +} + +export interface ResourceOps { + create(): Promise; + destroy(resource: BenchResource): Promise; + validate(resource: BenchResource): Promise; +} + +function delay(ms: number): Promise { + return ms > 0 + ? new Promise(resolve => setTimeout(resolve, ms)) + : Promise.resolve(); +} + +/** + * Builds a fresh, independently-counting set of resource operations - call + * this once per pool instance (each scenario's beforeAll) rather than + * sharing one across pools, so `created` reflects only that pool's own + * lifetime. + */ +export function createResourceOps(opts: { + createDelayMs?: number; + destroyDelayMs?: number; +}): ResourceOps { + let created = 0; + return { + async create(): Promise { + await delay(opts.createDelayMs ?? 0); + return { id: ++created, destroyed: false }; + }, + async destroy(resource: BenchResource): Promise { + await delay(opts.destroyDelayMs ?? 0); + resource.destroyed = true; + }, + async validate(resource: BenchResource): Promise { + return !resource.destroyed; + }, + }; +} diff --git a/benchmark/runner/orchestrator.ts b/benchmark/runner/orchestrator.ts new file mode 100644 index 0000000..29b5c56 --- /dev/null +++ b/benchmark/runner/orchestrator.ts @@ -0,0 +1,107 @@ +import { spawn } from 'node:child_process'; +import * as path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { readResults, summarize } from '../report/aggregate.js'; +import { renderConsoleSummary } from '../report/render-console.js'; +import { generateBenchmarksMarkdown } from '../report/render-markdown.js'; +import { SCENARIOS } from '../scenarios/index.js'; +import type { LibId, ScenarioName } from '../types.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const BENCHMARK_DIR = path.resolve(__dirname, '..'); +const REPO_ROOT = path.resolve(BENCHMARK_DIR, '..'); +export const RESULTS_DIR = path.join(BENCHMARK_DIR, 'results'); + +export interface OrchestratorOptions { + libs: LibId[]; + scenarios: ScenarioName[]; + repeats: number; +} + +function spawnWorker( + lib: LibId, + scenario: ScenarioName, + run: number, +): Promise { + return new Promise((resolve, reject) => { + // Each (lib, scenario) pair runs in its own child process, spawned + // sequentially, to avoid CPU contention skewing numbers and to get + // clean, uncontaminated V8 JIT warm-up per run. `--expose-gc` gives + // worker.ts a callable global.gc() so it can force a clean GC before + // bench.run() to measure genuinely-retained heap growth. + const child = spawn( + process.execPath, + [ + '--expose-gc', + '--import', + path.join(BENCHMARK_DIR, 'env.mjs'), + '--import', + '@swc-node/register/esm-register', + path.join(BENCHMARK_DIR, 'runner', 'worker.ts'), + `--lib=${lib}`, + `--scenario=${scenario}`, + `--run=${run}`, + `--resultsDir=${RESULTS_DIR}`, + ], + { cwd: REPO_ROOT, stdio: 'inherit' }, + ); + child.on('error', reject); + child.on('exit', (code, signal) => { + if (code === 0) resolve(); + else + reject( + new Error( + `Worker ${lib}/${scenario} run ${run} exited with ` + + `code=${code} signal=${signal}`, + ), + ); + }); + }); +} + +export async function runMatrix(options: OrchestratorOptions): Promise { + // Scenarios can declare libs they don't (fully) support, mapped to a + // short reason - those (lib, scenario) pairs are skipped entirely rather + // than spawned and left to fail, and the report shows the disclosed + // reason instead of a number. + const pairs: { scenario: ScenarioName; lib: LibId }[] = []; + for (const scenario of options.scenarios) { + for (const lib of options.libs) { + const reason = SCENARIOS[scenario].unsupportedLibs?.[lib]; + if (reason) { + console.log(`\n=== ${scenario} / ${lib}: skipped (${reason}) ===`); + continue; + } + pairs.push({ scenario, lib }); + } + } + + const total = pairs.length * options.repeats; + let done = 0; + for (const { scenario, lib } of pairs) { + for (let run = 1; run <= options.repeats; run++) { + done++; + console.log( + `\n=== [${done}/${total}] ${scenario} / ${lib} / run ${run} of ${options.repeats} ===`, + ); + await spawnWorker(lib, scenario, run); + } + } + + // results/ accumulates one file per (scenario, lib, run) forever - a + // rerun overwrites its own matching files but never removes anyone + // else's, so an unfiltered read here would resurface every scenario/lib + // ever benchmarked instead of just the ones -s/-l selected this time. + const ran = new Set(pairs.map(p => `${p.scenario}__${p.lib}`)); + const results = readResults(RESULTS_DIR).filter(r => + ran.has(`${r.scenario}__${r.lib}`), + ); + renderConsoleSummary(summarize(results)); + + // Regenerates doc/BENCHMARKS.md from *all* accumulated results (not + // just the ones this invocation ran) so a filtered `--scenario`/`--lib` + // run still leaves the doc reflecting the full picture rather than + // narrowing it down to whatever subset was just run. + generateBenchmarksMarkdown(); +} diff --git a/benchmark/runner/worker.ts b/benchmark/runner/worker.ts new file mode 100644 index 0000000..f6366bf --- /dev/null +++ b/benchmark/runner/worker.ts @@ -0,0 +1,286 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { PerformanceObserver } from 'node:perf_hooks'; +import process from 'node:process'; +import { Bench } from 'tinybench'; +import { isLibId, loadAdapter } from '../adapters/registry.js'; +import { + ACQUIRE_RELEASE_CONCURRENT_CONCURRENCY, + ACQUIRE_RELEASE_CONCURRENT_POOL_SIZE, + ACQUIRE_RELEASE_POOL_SIZE, + CREATE_DESTROY_CHURN_CONCURRENCY, + CREATE_DESTROY_CHURN_POOL_SIZE, + isScenarioName, + QUEUE_CONTENTION_CONCURRENCY, + QUEUE_CONTENTION_POOL_SIZE, + SCENARIOS, + VALIDATE_ON_BORROW_CONCURRENCY, + VALIDATE_ON_BORROW_POOL_SIZE, +} from '../scenarios/index.js'; +import type { BenchResult, LibId, ScenarioName } from '../types.js'; + +function parseArgs(argv: string[]): Record { + const args: Record = {}; + for (const arg of argv) { + const m = /^--([^=]+)=(.*)$/.exec(arg); + if (m) args[m[1]] = m[2]; + } + return args; +} + +interface GcStats { + gcCount?: number; + gcDurationMs?: number; + peakHeapGrowthBytes?: number; +} + +// How often to sample heapUsed opportunistically while the run is in +// flight. Kept as a secondary, best-effort source of extra resolution +// around whatever real event-loop turns naturally happen - see +// installPeriodicYield() below for the mechanism that actually GUARANTEES +// at least some turns happen, which this alone cannot. +const HEAP_SAMPLE_INTERVAL_MS = 1; + +// A benchmarked task whose entire call chain resolves through microtasks +// (chained Promises, no timer/socket in between - true for both adapters +// here once resources are already idle and reusable) can run for its whole +// scenario without Node's event loop ever reaching a macrotask turn. Both +// node:perf_hooks' 'gc' PerformanceObserver entries and a plain +// setInterval-based heap sampler are only delivered/fire on such a turn, so +// without this, a fast, allocation-light-per-call library and a genuinely +// zero-allocation one are indistinguishable - both silently read back as +// gcCount=0/peakHeapGrowthBytes=0, which looks like (but is not evidence +// of) "no allocation". Confirmed directly: a raw 2,000,000-iteration +// acquire/release loop with no forced yields reports zero for both +// lightning-pool and generic-pool; inserting a periodic yield in that same +// raw loop immediately surfaces hundreds of real GC events and tens of MB +// of real heap growth for either one. +// +// tinybench's own per-sample time budget only accumulates the timed fn() +// duration (see Task.#m in tinybench's source), not hook time, so a +// beforeEach/afterEach hook's own await does not shrink the sample count - +// it only makes the whole run take a bit longer in real wall-clock time, +// which is the acceptable side of this trade-off. +const YIELD_EVERY_N_SAMPLES = 250; + +function installPeriodicYield(bench: Bench, onYield: () => Promise) { + const originalAdd = bench.add.bind(bench); + (bench as { add: typeof bench.add }).add = (( + name: string, + fn: () => unknown, + opts: Record = {}, + ) => { + let sinceYield = 0; + const userAfterEach = opts.afterEach as + ((...a: unknown[]) => unknown) | undefined; + return originalAdd(name, fn as never, { + ...opts, + afterEach: async function (this: unknown, ...a: unknown[]) { + if (userAfterEach) await userAfterEach.apply(this, a); + if (++sinceYield >= YIELD_EVERY_N_SAMPLES) { + sinceYield = 0; + await onYield(); + } + }, + }); + }) as typeof bench.add; +} + +/** + * Sets up GC/heap instrumentation on `bench` BEFORE any task is registered + * on it (so installPeriodicYield's patched `add` is in place first) and + * returns a function to call after `bench.run()` completes to collect the + * stats. A PerformanceObserver counts every GC pause (and its duration) + * during the call; this part works regardless of --expose-gc. + * peakHeapGrowthBytes forces a clean baseline via global.gc() immediately + * before the run, then tracks the highest heapUsed seen across both the + * opportunistic interval sampler and the guaranteed per-N-samples yield + * above - the most the heap ever grew above that baseline at any point + * during the run. Only populated when the process was started with + * --expose-gc (see orchestrator.ts); without it global.gc is undefined and + * peakHeapGrowthBytes comes back undefined. + */ +function instrumentGc(bench: Bench): () => GcStats { + const gc = (global as { gc?: () => void }).gc; + let gcCount = 0; + let gcDurationMs = 0; + const observer = new PerformanceObserver(list => { + for (const entry of list.getEntries()) { + gcCount++; + gcDurationMs += entry.duration; + } + }); + observer.observe({ entryTypes: ['gc'] }); + + const heapBefore = gc ? (gc(), process.memoryUsage().heapUsed) : undefined; + let peakHeapUsed = heapBefore ?? process.memoryUsage().heapUsed; + const sample = () => { + const current = process.memoryUsage().heapUsed; + if (current > peakHeapUsed) peakHeapUsed = current; + }; + const sampler = setInterval(sample, HEAP_SAMPLE_INTERVAL_MS); + sampler.unref(); + + installPeriodicYield(bench, async () => { + await new Promise(resolve => setImmediate(resolve)); + sample(); + }); + + return () => { + clearInterval(sampler); + observer.disconnect(); + return { + gcCount, + gcDurationMs, + peakHeapGrowthBytes: + heapBefore != null ? Math.max(peakHeapUsed - heapBefore, 0) : undefined, + }; + }; +} + +async function main(): Promise { + const args = parseArgs(process.argv.slice(2)); + const libArg = args.lib; + const scenarioArg = args.scenario; + const run = args.run ? parseInt(args.run, 10) : 1; + const resultsDir = args.resultsDir; + + if (!libArg || !isLibId(libArg)) { + throw new Error(`Invalid or missing --lib "${libArg}"`); + } + if (!scenarioArg || !isScenarioName(scenarioArg)) { + throw new Error(`Invalid or missing --scenario "${scenarioArg}"`); + } + if (!resultsDir) throw new Error('Missing --resultsDir'); + + const lib: LibId = libArg; + const scenarioName: ScenarioName = scenarioArg; + const meta = SCENARIOS[scenarioName]; + + const adapter = await loadAdapter(lib); + const bench = new Bench({ + time: meta.bench.time, + iterations: meta.bench.iterations, + warmupTime: meta.bench.warmupTime, + warmupIterations: meta.bench.warmupIterations, + throws: true, + }); + // Must run before any adapter.scenarios.xxx() call below, since those + // call bench.add() internally and instrumentGc() patches bench.add to + // inject its periodic-yield hook into whatever task gets registered. + const collectGcStats = instrumentGc(bench); + + const params: Record = {}; + switch (scenarioName) { + case 'acquire-release': + adapter.scenarios.acquireRelease(bench, ACQUIRE_RELEASE_POOL_SIZE); + params.poolSize = ACQUIRE_RELEASE_POOL_SIZE; + break; + case 'acquire-release-concurrent': + adapter.scenarios.acquireReleaseConcurrent( + bench, + ACQUIRE_RELEASE_CONCURRENT_POOL_SIZE, + ACQUIRE_RELEASE_CONCURRENT_CONCURRENCY, + ); + params.poolSize = ACQUIRE_RELEASE_CONCURRENT_POOL_SIZE; + params.concurrency = ACQUIRE_RELEASE_CONCURRENT_CONCURRENCY; + break; + case 'queue-contention': + adapter.scenarios.queueContention( + bench, + QUEUE_CONTENTION_POOL_SIZE, + QUEUE_CONTENTION_CONCURRENCY, + ); + params.poolSize = QUEUE_CONTENTION_POOL_SIZE; + params.concurrency = QUEUE_CONTENTION_CONCURRENCY; + break; + case 'create-destroy-churn': + adapter.scenarios.createDestroyChurn( + bench, + CREATE_DESTROY_CHURN_POOL_SIZE, + CREATE_DESTROY_CHURN_CONCURRENCY, + ); + params.poolSize = CREATE_DESTROY_CHURN_POOL_SIZE; + params.concurrency = CREATE_DESTROY_CHURN_CONCURRENCY; + break; + case 'validate-on-borrow': + adapter.scenarios.validateOnBorrow( + bench, + VALIDATE_ON_BORROW_POOL_SIZE, + VALIDATE_ON_BORROW_CONCURRENCY, + ); + params.poolSize = VALIDATE_ON_BORROW_POOL_SIZE; + params.concurrency = VALIDATE_ON_BORROW_CONCURRENCY; + break; + } + + await bench.run(); + const gcStats = collectGcStats(); + + const task = bench.tasks[0]; + const result = task?.result; + if (!result || result.state !== 'completed') { + const errorMessage = + result && result.state === 'errored' ? result.error.message : undefined; + throw new Error( + `Benchmark task did not complete for ${lib}/${scenarioName} ` + + `(state=${result?.state ?? 'unknown'})` + + (errorMessage ? `: ${errorMessage}` : ''), + ); + } + + const benchResult: BenchResult = { + lib, + libraryVersion: adapter.libraryVersion, + scenario: scenarioName, + run, + stats: { + mean: result.latency.mean, + p75: result.latency.p75, + p99: result.latency.p99, + opsPerSec: result.throughput.mean, + samples: result.latency.samplesCount, + ...gcStats, + }, + params, + timestamp: new Date().toISOString(), + node: { + version: process.version, + platform: process.platform, + arch: process.arch, + }, + }; + + fs.mkdirSync(resultsDir, { recursive: true }); + const filePath = path.join( + resultsDir, + `${scenarioName}__${lib}__${run}.json`, + ); + fs.writeFileSync(filePath, JSON.stringify(benchResult, null, 2)); + + const peakHeapText = + gcStats.peakHeapGrowthBytes != null + ? `${(gcStats.peakHeapGrowthBytes / 1024).toFixed(1)}KB` + : 'n/a'; + console.log( + `[${lib}/${scenarioName} run ${run}] ` + + `mean=${result.latency.mean.toFixed(3)}ms ` + + `p75=${result.latency.p75.toFixed(3)}ms ` + + `p99=${result.latency.p99.toFixed(3)}ms ` + + `ops/sec=${result.throughput.mean.toFixed(1)} ` + + `samples=${result.latency.samplesCount} ` + + `gc=${gcStats.gcCount}/${gcStats.gcDurationMs?.toFixed(1)}ms ` + + `peakHeap=${peakHeapText}`, + ); + + // A pool that leaves a stray timer/handle behind even after being closed + // would otherwise keep this one-shot worker process alive indefinitely. + // The benchmark and its result file are already complete at this point, + // so exit explicitly rather than wait for the event loop to drain. + process.exit(0); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/benchmark/scenarios/acquire-release-concurrent.ts b/benchmark/scenarios/acquire-release-concurrent.ts new file mode 100644 index 0000000..06dde16 --- /dev/null +++ b/benchmark/scenarios/acquire-release-concurrent.ts @@ -0,0 +1,25 @@ +import type { ScenarioMeta } from '../types.js'; + +export const ACQUIRE_RELEASE_CONCURRENT_CONCURRENCY = 50; +// Pool sized exactly to the concurrency level so nobody has to queue - +// isolates concurrent-bookkeeping overhead (the internal maps/linked lists +// each library touches per acquire/release) from queueing behaviour, which +// queue-contention.ts covers instead. +export const ACQUIRE_RELEASE_CONCURRENT_POOL_SIZE = + ACQUIRE_RELEASE_CONCURRENT_CONCURRENCY; + +export const ACQUIRE_RELEASE_CONCURRENT_SCENARIO: ScenarioMeta = { + name: 'acquire-release-concurrent', + title: 'Concurrent Acquire/Release', + description: `${ACQUIRE_RELEASE_CONCURRENT_CONCURRENCY} \`acquire()\` calls +fired at once via \`Promise.all\`, each released immediately after, against a +pool sized exactly to the concurrency (max=${ACQUIRE_RELEASE_CONCURRENT_POOL_SIZE}) +so every call is satisfied from the idle pool with no queueing. Measures the +pool's concurrent-safety bookkeeping in isolation from queue contention.`, + bench: { + time: 500, + iterations: 100, + warmupTime: 100, + warmupIterations: 10, + }, +}; diff --git a/benchmark/scenarios/acquire-release.ts b/benchmark/scenarios/acquire-release.ts new file mode 100644 index 0000000..d124bf2 --- /dev/null +++ b/benchmark/scenarios/acquire-release.ts @@ -0,0 +1,19 @@ +import type { ScenarioMeta } from '../types.js'; + +export const ACQUIRE_RELEASE_POOL_SIZE = 10; + +export const ACQUIRE_RELEASE_SCENARIO: ScenarioMeta = { + name: 'acquire-release', + title: 'Sequential Acquire/Release', + description: `A single \`acquire()\` followed by a \`release()\`, one at a +time, against a pool of max size ${ACQUIRE_RELEASE_POOL_SIZE} that has +already warmed up (the resource is always idle and immediately reusable). +The baseline "warm path" cost of the pool's own bookkeeping, with no +contention and no resource creation in the timed path.`, + bench: { + time: 500, + iterations: 200, + warmupTime: 100, + warmupIterations: 20, + }, +}; diff --git a/benchmark/scenarios/create-destroy-churn.ts b/benchmark/scenarios/create-destroy-churn.ts new file mode 100644 index 0000000..2dbd57e --- /dev/null +++ b/benchmark/scenarios/create-destroy-churn.ts @@ -0,0 +1,22 @@ +import type { ScenarioMeta } from '../types.js'; + +export const CREATE_DESTROY_CHURN_POOL_SIZE = 10; +export const CREATE_DESTROY_CHURN_CONCURRENCY = 10; + +export const CREATE_DESTROY_CHURN_SCENARIO: ScenarioMeta = { + name: 'create-destroy-churn', + title: 'Create/Destroy Churn', + description: `${CREATE_DESTROY_CHURN_CONCURRENCY} concurrent \`acquire()\` ++ \`destroy()\` cycles per iteration - the resource is destroyed instead of +released, so it never returns to the idle list and a brand-new one must be +created for every single acquire (pool max=${CREATE_DESTROY_CHURN_POOL_SIZE}). +Isolates the factory create/destroy pipeline overhead from the idle-reuse +path the other scenarios mostly exercise. Set \`BENCH_CREATE_DELAY_MS\`/ +\`BENCH_DESTROY_DELAY_MS\` to approximate a real backend's connection cost.`, + bench: { + time: 500, + iterations: 50, + warmupTime: 100, + warmupIterations: 5, + }, +}; diff --git a/benchmark/scenarios/index.ts b/benchmark/scenarios/index.ts new file mode 100644 index 0000000..dded640 --- /dev/null +++ b/benchmark/scenarios/index.ts @@ -0,0 +1,26 @@ +import type { ScenarioMeta, ScenarioName } from '../types.js'; +import { ACQUIRE_RELEASE_SCENARIO } from './acquire-release.js'; +import { ACQUIRE_RELEASE_CONCURRENT_SCENARIO } from './acquire-release-concurrent.js'; +import { CREATE_DESTROY_CHURN_SCENARIO } from './create-destroy-churn.js'; +import { QUEUE_CONTENTION_SCENARIO } from './queue-contention.js'; +import { VALIDATE_ON_BORROW_SCENARIO } from './validate-on-borrow.js'; + +export const SCENARIOS: Record = { + 'acquire-release': ACQUIRE_RELEASE_SCENARIO, + 'acquire-release-concurrent': ACQUIRE_RELEASE_CONCURRENT_SCENARIO, + 'queue-contention': QUEUE_CONTENTION_SCENARIO, + 'create-destroy-churn': CREATE_DESTROY_CHURN_SCENARIO, + 'validate-on-borrow': VALIDATE_ON_BORROW_SCENARIO, +}; + +export const SCENARIO_NAMES = Object.keys(SCENARIOS) as ScenarioName[]; + +export function isScenarioName(value: string): value is ScenarioName { + return Object.prototype.hasOwnProperty.call(SCENARIOS, value); +} + +export * from './acquire-release.js'; +export * from './acquire-release-concurrent.js'; +export * from './create-destroy-churn.js'; +export * from './queue-contention.js'; +export * from './validate-on-borrow.js'; diff --git a/benchmark/scenarios/queue-contention.ts b/benchmark/scenarios/queue-contention.ts new file mode 100644 index 0000000..5e6d340 --- /dev/null +++ b/benchmark/scenarios/queue-contention.ts @@ -0,0 +1,22 @@ +import type { ScenarioMeta } from '../types.js'; + +export const QUEUE_CONTENTION_POOL_SIZE = 10; +export const QUEUE_CONTENTION_CONCURRENCY = 500; + +export const QUEUE_CONTENTION_SCENARIO: ScenarioMeta = { + name: 'queue-contention', + title: 'Queue Contention', + description: `${QUEUE_CONTENTION_CONCURRENCY} \`acquire()\` calls fired at +once via \`Promise.all\` against a pool of max size +${QUEUE_CONTENTION_POOL_SIZE} - the overwhelming majority must queue and wait +for a resource to be released back before they can be served. Each holder +releases its resource right after acquiring it, so the queue continuously +drains. This is the scenario the pool's request queue and scheduling logic +matter most for.`, + bench: { + time: 800, + iterations: 30, + warmupTime: 200, + warmupIterations: 3, + }, +}; diff --git a/benchmark/scenarios/validate-on-borrow.ts b/benchmark/scenarios/validate-on-borrow.ts new file mode 100644 index 0000000..f4c63da --- /dev/null +++ b/benchmark/scenarios/validate-on-borrow.ts @@ -0,0 +1,22 @@ +import type { ScenarioMeta } from '../types.js'; + +export const VALIDATE_ON_BORROW_CONCURRENCY = 50; +export const VALIDATE_ON_BORROW_POOL_SIZE = VALIDATE_ON_BORROW_CONCURRENCY; + +export const VALIDATE_ON_BORROW_SCENARIO: ScenarioMeta = { + name: 'validate-on-borrow', + title: 'Validate on Borrow', + description: `Same shape as Concurrent Acquire/Release +(${VALIDATE_ON_BORROW_CONCURRENCY} concurrent acquire+release cycles, +pool max=${VALIDATE_ON_BORROW_POOL_SIZE}), but with each library's +borrow-time validation hook enabled (lightning-pool's \`validation: true\` + +\`factory.validate\`, generic-pool's \`testOnBorrow: true\` + +\`factory.validate\`) - isolates the added cost of validating a resource +before handing it out.`, + bench: { + time: 500, + iterations: 100, + warmupTime: 100, + warmupIterations: 10, + }, +}; diff --git a/benchmark-tests/tsconfig.json b/benchmark/tsconfig.json similarity index 52% rename from benchmark-tests/tsconfig.json rename to benchmark/tsconfig.json index a92468a..a53a779 100644 --- a/benchmark-tests/tsconfig.json +++ b/benchmark/tsconfig.json @@ -2,9 +2,7 @@ "extends": "../tsconfig-base.json", "include": ["**/*.ts"], "compilerOptions": { - "baseUrl": "./", - "paths": { - "lightning-pool": ["../src/index.js"] - } + "rootDir": "..", + "baseUrl": "." } } diff --git a/benchmark/types.ts b/benchmark/types.ts new file mode 100644 index 0000000..7415e38 --- /dev/null +++ b/benchmark/types.ts @@ -0,0 +1,72 @@ +export type LibId = 'lightning-pool' | 'generic-pool'; + +export type ScenarioName = + | 'acquire-release' + | 'acquire-release-concurrent' + | 'queue-contention' + | 'create-destroy-churn' + | 'validate-on-borrow'; + +export interface ScenarioMeta { + readonly name: ScenarioName; + /** Human-readable display name, used in BENCHMARKS.md headings. */ + readonly title: string; + readonly description: string; + /** + * tinybench run options for this scenario. Kept modest by default so a + * full `npm run bench` finishes in a reasonable time; see + * benchmark/README.md for how to raise rigor for a "real" run. + */ + readonly bench: { + readonly time: number; + readonly iterations: number; + readonly warmupTime: number; + readonly warmupIterations: number; + }; + /** + * Libraries this scenario doesn't (fully) support, mapped to a short + * label shown in BENCHMARKS.md instead of a results row. The orchestrator + * skips running these (lib, scenario) pairs entirely instead of spawning + * and letting them fail. + */ + readonly unsupportedLibs?: Partial>; +} + +export interface BenchResultStats { + mean: number; + p75: number; + p99: number; + opsPerSec: number; + samples: number; + /** + * GC activity observed (via node:perf_hooks) during the bench.run() call + * that produced this result - includes tinybench's own warmup iterations, + * not just the timed ones, since tinybench doesn't expose a hook at the + * boundary between them. gcCount/gcDurationMs are totals for the whole + * run, always populated (observing GC events doesn't need --expose-gc). + * peakHeapGrowthBytes is the highest heapUsed observed at any point while + * bench.run() was executing, minus a heapUsed baseline captured right + * before it via a *forced* global.gc() - the most the heap ever grew + * above a clean starting point during the run. Only populated when the + * worker process was started with --expose-gc (the orchestrator always + * does this). + */ + gcCount?: number; + gcDurationMs?: number; + peakHeapGrowthBytes?: number; +} + +export interface BenchResult { + lib: LibId; + libraryVersion: string; + scenario: ScenarioName; + run: number; + stats: BenchResultStats; + params: Record; + timestamp: string; + node: { + version: string; + platform: string; + arch: string; + }; +} diff --git a/package-lock.json b/package-lock.json index f64b6c5..ab144e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,23 +18,25 @@ "@panates/eslint-config-ts": "^2.1.6", "@panates/tsconfig": "^2.1.6", "@swc-node/register": "^1.12.1", - "@swc/core": "^1.16.1", + "@swc/core": "^1.16.2", "@swc/helpers": "^0.5.23", "@types/mocha": "^10.0.10", - "@types/node": "^26.4.0", - "auto-changelog": "^2.6.0", + "@types/node": "^26.5.0", + "auto-changelog": "^2.6.1", "c8": "^12.0.0", "dotenv": "^17.4.2", - "expect": "^30.4.1", + "expect": "^30.5.1", "generic-pool": "^3.9.0", - "globals": "^17.11.0", + "globals": "^17.12.0", "madge": "^8.0.0", - "mocha": "^11.8.0", + "mocha": "^12.0.0", "npm-run-path": "^6.0.0", "prettier": "^3.9.6", "rimraf": "^6.1.3", + "tinybench": "^6.1.6", "ts-cleanup": "^1.4.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "yargs": "^18.1.0" } }, "node_modules/@babel/code-frame": { @@ -112,6 +114,32 @@ "node": ">=18" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@dependents/detective-less": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/@dependents/detective-less/-/detective-less-5.0.3.tgz", @@ -293,9 +321,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -378,24 +406,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", @@ -407,9 +417,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", - "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.5.0.tgz", + "integrity": "sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==", "dev": true, "license": "MIT", "engines": { @@ -417,22 +427,22 @@ } }, "node_modules/@jest/expect-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", - "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.5.1.tgz", + "integrity": "sha512-WcRWhHQdTMRDpyWKZ/6MINBmovI7zeD+bL8wFjCncRV3NQOwKy1X45IfyblfHR4k/XciIlNEdFL9QjFO+HNKOg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0" + "@jest/get-type": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.5.0.tgz", + "integrity": "sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==", "dev": true, "license": "MIT", "engines": { @@ -440,23 +450,39 @@ } }, "node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.5.0.tgz", + "integrity": "sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", - "jest-regex-util": "30.4.0" + "jest-regex-util": "30.5.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/react-is-19": { + "name": "react-is", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.5.0.tgz", + "integrity": "sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==", "dev": true, "license": "MIT", "dependencies": { @@ -467,14 +493,14 @@ } }, "node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.5.1.tgz", + "integrity": "sha512-LvVYn83nnXPl+Rg98nvcFgjx6nRMTArhSn6RAX/w3ELn54S8A42TYZvsCMdGUqTM8S0wyXbtlQdU6Hi6dykj9g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", + "@jest/pattern": "30.5.0", + "@jest/schemas": "30.5.0", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", @@ -496,9 +522,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -513,6 +539,32 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", @@ -1177,17 +1229,6 @@ "node": ">=18.0.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pkgr/core": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", @@ -1265,9 +1306,9 @@ } }, "node_modules/@swc/core": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.1.tgz", - "integrity": "sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.2.tgz", + "integrity": "sha512-95I4kiSMeveI/Mhi+tE4fiWcWLUMfzfKrk0jtr8LRMqHgOgq+xHS+zExkDqoO4b5OeeuXHMWVdD5MeP3X6sULw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1283,18 +1324,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.16.1", - "@swc/core-darwin-x64": "1.16.1", - "@swc/core-linux-arm-gnueabihf": "1.16.1", - "@swc/core-linux-arm64-gnu": "1.16.1", - "@swc/core-linux-arm64-musl": "1.16.1", - "@swc/core-linux-ppc64-gnu": "1.16.1", - "@swc/core-linux-s390x-gnu": "1.16.1", - "@swc/core-linux-x64-gnu": "1.16.1", - "@swc/core-linux-x64-musl": "1.16.1", - "@swc/core-win32-arm64-msvc": "1.16.1", - "@swc/core-win32-ia32-msvc": "1.16.1", - "@swc/core-win32-x64-msvc": "1.16.1" + "@swc/core-darwin-arm64": "1.16.2", + "@swc/core-darwin-x64": "1.16.2", + "@swc/core-linux-arm-gnueabihf": "1.16.2", + "@swc/core-linux-arm64-gnu": "1.16.2", + "@swc/core-linux-arm64-musl": "1.16.2", + "@swc/core-linux-ppc64-gnu": "1.16.2", + "@swc/core-linux-s390x-gnu": "1.16.2", + "@swc/core-linux-x64-gnu": "1.16.2", + "@swc/core-linux-x64-musl": "1.16.2", + "@swc/core-win32-arm64-msvc": "1.16.2", + "@swc/core-win32-ia32-msvc": "1.16.2", + "@swc/core-win32-x64-msvc": "1.16.2" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1306,9 +1347,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz", - "integrity": "sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.2.tgz", + "integrity": "sha512-i/j0HNbnn79qnTVPicvay92Nark8fW8NQqn1e2mGERjUXNpBV0+SwQxlRpk2zBhn6laJ8PDI6Kn1nHZhnz3LCA==", "cpu": [ "arm64" ], @@ -1323,9 +1364,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz", - "integrity": "sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.2.tgz", + "integrity": "sha512-HrwqHyEyHVXO3qTk8EkNK7/b6sOZSEoNh+pot6RdE5x0LbNqfo8LtJUvi3UTXr+5ja/o5HbJdW80eCXo+NjbiA==", "cpu": [ "x64" ], @@ -1340,9 +1381,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz", - "integrity": "sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.2.tgz", + "integrity": "sha512-MdXi83Z/gGp1LIrg+h7HKxiul/z/Bty/ZJSvYAFqDl9zteC1XLSAZdScquKtXPp50rdyXqritTDCqQBhwVfZKA==", "cpu": [ "arm" ], @@ -1357,9 +1398,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz", - "integrity": "sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.2.tgz", + "integrity": "sha512-/jcTmK6Ktz3owM3YtiKvjofV6p3VpHnYzTIrOGwDIOsDigRAAVuZ8east33wYO/7UTdKYFlyHNnJNT0WJqOA3Q==", "cpu": [ "arm64" ], @@ -1377,9 +1418,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz", - "integrity": "sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.2.tgz", + "integrity": "sha512-4gFarKaFnlJTSlJYKmMhV4u+3YE4uYfiydpBoYjmgQhCf9lAieOq+WilZaK9vVSHeqLuQpTEiGULZqAdsRX5Dw==", "cpu": [ "arm64" ], @@ -1397,9 +1438,9 @@ } }, "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz", - "integrity": "sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.2.tgz", + "integrity": "sha512-syqSLGd6KlZ1PciNzs6bIUlhOuFztZufebOHaERjc4N4SqNZxyqYd4I+jj/EfOYnpe0kNjccn9HJLN1p5dz3+w==", "cpu": [ "ppc64" ], @@ -1417,9 +1458,9 @@ } }, "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz", - "integrity": "sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.2.tgz", + "integrity": "sha512-ZBBLK+ewGyXLzWeMS7wbKtWBdnif6etn7xvPY/iOfbdsjX/+bgkp1pQt2lWF2wlu2hXYZuhJ/tHZE/QR8/apzg==", "cpu": [ "s390x" ], @@ -1437,9 +1478,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz", - "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.2.tgz", + "integrity": "sha512-LyHJgxCA4Tje0ysBMbEb0tt/ie8kgUKoFE3JAKFhpevmTmhYEoC0H9s47WuDsqiFckF1ITUguZIXJG6K5e0dvg==", "cpu": [ "x64" ], @@ -1457,9 +1498,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz", - "integrity": "sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.2.tgz", + "integrity": "sha512-PghXJlVM1cgtLfNUR1vxFo1z+PDRAe8cWAJlZZ7spmeiN7BospGXg/MHUg7oNSgwSX7Zo//YKv9P5yD9apsFJQ==", "cpu": [ "x64" ], @@ -1477,9 +1518,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz", - "integrity": "sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.2.tgz", + "integrity": "sha512-StTOSefYBxemvNYYUI3UmO1a8y+hSPjjfHogC2TEHL+Z1PlEBim/XtLas5rS04jAzT9RrNmbtX911SZ42H9jSQ==", "cpu": [ "arm64" ], @@ -1494,9 +1535,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz", - "integrity": "sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.2.tgz", + "integrity": "sha512-fycER209DYIzsibpTMC+chND05OfOjgztWL9U8OE6/uUlsOUZH3eh98isBLEnOymYUhlJLEt5++W1+KL/FOh5Q==", "cpu": [ "ia32" ], @@ -1511,9 +1552,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz", - "integrity": "sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.2.tgz", + "integrity": "sha512-cSd1z6ivSrJPVr+moVwOHWjeKy6TpO4/Shwcv5KCrKYXCccxwh4pRy1C3fDioNx2PF1jPZWHKZjtXt+Be9VbaQ==", "cpu": [ "x64" ], @@ -1714,13 +1755,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", - "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "version": "26.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz", + "integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/stack-utils": { @@ -1748,18 +1789,18 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", - "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.68.0", - "@typescript-eslint/type-utils": "8.68.0", - "@typescript-eslint/utils": "8.68.0", - "@typescript-eslint/visitor-keys": "8.68.0", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1772,15 +1813,15 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.68.0", + "@typescript-eslint/parser": "^8.70.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", "dev": true, "license": "MIT", "peer": true, @@ -1789,17 +1830,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz", - "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.68.0", - "@typescript-eslint/types": "8.68.0", - "@typescript-eslint/typescript-estree": "8.68.0", - "@typescript-eslint/visitor-keys": "8.68.0", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3" }, "engines": { @@ -1815,14 +1856,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz", - "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.68.0", - "@typescript-eslint/types": "^8.68.0", + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", "debug": "^4.4.3" }, "engines": { @@ -1837,15 +1878,15 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz", - "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.68.0", - "@typescript-eslint/visitor-keys": "8.68.0" + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1856,9 +1897,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz", - "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", "dev": true, "license": "MIT", "engines": { @@ -1873,16 +1914,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz", - "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/types": "8.68.0", - "@typescript-eslint/typescript-estree": "8.68.0", - "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1899,9 +1940,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz", - "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", "dev": true, "license": "MIT", "engines": { @@ -1913,16 +1954,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz", - "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.68.0", - "@typescript-eslint/tsconfig-utils": "8.68.0", - "@typescript-eslint/types": "8.68.0", - "@typescript-eslint/visitor-keys": "8.68.0", + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1941,17 +1982,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz", - "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.68.0", - "@typescript-eslint/types": "8.68.0", - "@typescript-eslint/typescript-estree": "8.68.0" + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1966,13 +2007,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz", - "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/types": "8.70.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2490,16 +2531,13 @@ } }, "node_modules/ansi-regex": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", - "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=8" } }, "node_modules/ansi-styles": { @@ -2550,9 +2588,9 @@ } }, "node_modules/auto-changelog": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.6.0.tgz", - "integrity": "sha512-jJgUkuWXQ7fPLPXOMQk/XSSmy7KvzpzhjJa6w660dq1KTEez/GW2CPckBra3jMMMMpcwxp84bOslo29qRCewzA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.6.1.tgz", + "integrity": "sha512-YQJgU1i/qWTk8W/IIE6oUk1DBYr/jiFbksP+LuGRHOzmhviIGhK1otjqSSMUf2aqUdeOW8SUYNSZjiD6Qcun7w==", "dev": true, "license": "MIT", "dependencies": { @@ -2711,17 +2749,19 @@ } } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peer": true, + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" } }, "node_modules/chalk": { @@ -2742,16 +2782,16 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -2814,26 +2854,19 @@ "node": ">=20" } }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, "node_modules/cliui/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -2852,22 +2885,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/clone": { @@ -2985,19 +3016,6 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3201,9 +3219,9 @@ } }, "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -3232,17 +3250,10 @@ "node": ">= 16.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, @@ -3299,6 +3310,7 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -3329,9 +3341,9 @@ } }, "node_modules/eslint": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", - "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", "dev": true, "license": "MIT", "peer": true, @@ -3344,7 +3356,7 @@ "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", + "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3359,7 +3371,7 @@ "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", @@ -3651,18 +3663,18 @@ } }, "node_modules/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.5.1.tgz", + "integrity": "sha512-m8YrYgvKe9+9gEnWEuKz+qCGfHqkrff7PPfyDnOFkjsfYRKqiYyOxDFMFieCGAuhtk/VNm63tvNgKZVzVy+Hvg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" + "@jest/expect-utils": "30.5.1", + "@jest/get-type": "30.5.0", + "jest-matcher-utils": "30.5.1", + "jest-message-util": "30.5.1", + "jest-mock": "30.5.1", + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -3730,9 +3742,9 @@ "peer": true }, "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", "dev": true, "license": "ISC", "dependencies": { @@ -3758,17 +3770,14 @@ } }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" + "flat-cache": "^6.1.23" } }, "node_modules/filing-cabinet": { @@ -3837,29 +3846,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" } }, "node_modules/flatted": { @@ -3966,22 +3963,18 @@ } }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4001,43 +3994,10 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", - "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", "dev": true, "license": "MIT", "engines": { @@ -4102,6 +4062,20 @@ "node": ">=8" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -4115,15 +4089,13 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", "dev": true, "license": "MIT", - "bin": { - "he": "bin/he" - } + "peer": true }, "node_modules/html-escaper": { "version": "2.0.2", @@ -4241,16 +4213,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4304,16 +4266,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", @@ -4396,69 +4348,53 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest-diff": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.5.1.tgz", + "integrity": "sha512-e3cNNMpv8Kh20MjjphTXs+3Vz7DQyLM1nft7KJhnh46atFhjVJRa+0Hq0beywuwsACtMQUBihQkFl8zxb7gt1Q==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.4.0", - "@jest/get-type": "30.1.0", + "@jest/diff-sequences": "30.5.0", + "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "pretty-format": "30.4.1" + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.5.1.tgz", + "integrity": "sha512-aroZVqwOz/wC2y6pC+obgFWKV9viaQWQTTSB6W5H55+egtUczIfXR/rxExTv92xD/ADYwpqaYfVWx1aqwKg7FA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", + "@jest/get-type": "30.5.0", "chalk": "^4.1.2", - "jest-diff": "30.4.1", - "pretty-format": "30.4.1" + "jest-diff": "30.5.1", + "pretty-format": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.5.1.tgz", + "integrity": "sha512-UdQlLdd9wL/Ys7xRErckqwD6wPlSZYueosSWuHc1r2ztGLwlgPvtSJq2+BPEgaEY13WLvfFbmhTj8pba0Sd1jg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", + "jest-util": "30.5.1", "picomatch": "^4.0.3", - "pretty-format": "30.4.1", + "pretty-format": "30.5.1", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -4467,24 +4403,25 @@ } }, "node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.5.1.tgz", + "integrity": "sha512-9fVjc3leUpGID2/by/LU4Dvdcp7PFh9LlxS3QRWK3ABm+KtvEVsG/AEGeLY3gKOZsjkBxyfwGltoAVlW7dygHg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/expect-utils": "30.5.1", + "@jest/types": "30.5.1", "@types/node": "*", - "jest-util": "30.4.1" + "jest-util": "30.5.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "version": "30.5.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.5.0.tgz", + "integrity": "sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==", "dev": true, "license": "MIT", "engines": { @@ -4492,13 +4429,13 @@ } }, "node_modules/jest-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", - "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.5.1.tgz", + "integrity": "sha512-yKuxmNy2rSbTXw+3SIPanJo+nV4/BS1p26v44IYBFMsswSQySfMMcPHErnOncda7i9HEz0q605rIhSTBVgrZTg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", + "@jest/types": "30.5.1", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -4517,9 +4454,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", + "integrity": "sha512-28R/k+NAjeuf7+CKlTxWZVExJGwVVLwY06DgEnOMz2gEpfNkDcD7QvyiVPT0xy0XXhU8vHsd4Ot42OOPdJG7dQ==", "dev": true, "funding": [ { @@ -4536,17 +4473,9 @@ "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4577,14 +4506,14 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/levn": { @@ -4636,11 +4565,14 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } }, "node_modules/madge": { "version": "8.0.0", @@ -4791,186 +4723,50 @@ } }, "node_modules/mocha": { - "version": "11.8.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.8.0.tgz", - "integrity": "sha512-VyCeUdGN3A9lmCTTgG4yuvY9ixxaDk+xt2R/7/+1AP6EqNG+G9OKkzBwhVtVYoNX8YsxNSgAl8mOv3IAeOpFbw==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.0.tgz", + "integrity": "sha512-NYNh5IFt6WYqm9bi4601m7vix8MZdXC0DwS4gY6WhXO2RgJWhivhISVmq1oklCif18OSI9l+vx5Mdm5oh1XGiQ==", "dev": true, "license": "MIT", "dependencies": { "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", + "chokidar": "^5.0.0", "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", + "diff": "^9.0.0", "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", + "glob": "^13.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", + "is-unicode-supported": "^0.1.0", + "js-yaml": "^5.0.0", + "minimatch": "^10.2.2", "ms": "^2.1.3", "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", + "serialize-javascript": "^7.0.2", + "strip-json-comments": "^5.0.3", "supports-color": "^8.1.1", - "workerpool": "^9.2.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" + "workerpool": "^10.0.0" }, "bin": { - "_mocha": "bin/_mocha", "mocha": "bin/mocha.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/mocha/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mocha/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/mocha/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/mocha/node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/module-definition": { @@ -5178,29 +4974,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/oxc-resolver": { "version": "11.24.2", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", @@ -5322,17 +5095,17 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "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==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -5379,9 +5152,9 @@ } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -5399,7 +5172,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5507,16 +5280,16 @@ } }, "node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "version": "30.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.5.1.tgz", + "integrity": "sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" + "@jest/react-is-18": "npm:react-is@^18.3.1", + "@jest/react-is-19": "npm:react-is@^19.2.5", + "@jest/schemas": "30.5.0", + "ansi-styles": "^5.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -5571,6 +5344,28 @@ "node": ">= 14.0" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5599,16 +5394,6 @@ "dev": true, "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -5635,22 +5420,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-is-19": { - "name": "react-is", - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5667,13 +5436,13 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": ">= 20.19.0" }, "funding": { "type": "individual", @@ -5691,16 +5460,6 @@ "regexp-tree": "bin/regexp-tree" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/requirejs": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.8.tgz", @@ -5834,51 +5593,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "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/rimraf/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/rimraf/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==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5976,13 +5690,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.1.tgz", + "integrity": "sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/shebang-command": { @@ -6117,67 +5831,49 @@ } }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/stringify-object": { @@ -6196,23 +5892,6 @@ } }, "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -6225,16 +5904,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6246,13 +5915,13 @@ } }, "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6356,49 +6025,14 @@ "node": "20 || >=22" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "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/test-exclude/node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/test-exclude/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==", + "node_modules/tinybench": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.6.tgz", + "integrity": "sha512-qEHpSqUC/KkD4rypubEZ/gSENbH6x4E8d8UM5aBndJhRjTW3sPW4jmS5C1/wHhMFMj040jgJl2XvQOYdZDgKkA==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, + "license": "MIT", "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=20.0.0" } }, "node_modules/tinyglobby": { @@ -6463,22 +6097,6 @@ "node": ">=20.0" } }, - "node_modules/ts-cleanup/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/ts-cleanup/node_modules/commander": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", @@ -6489,20 +6107,6 @@ "node": ">=22.12.0" } }, - "node_modules/ts-cleanup/node_modules/readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/ts-graphviz": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/ts-graphviz/-/ts-graphviz-2.1.6.tgz", @@ -6579,17 +6183,17 @@ } }, "node_modules/typescript-eslint": { - "version": "8.68.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz", - "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "8.68.0", - "@typescript-eslint/parser": "8.68.0", - "@typescript-eslint/typescript-estree": "8.68.0", - "@typescript-eslint/utils": "8.68.0" + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6618,9 +6222,9 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, @@ -6764,105 +6368,88 @@ "license": "MIT" }, "node_modules/workerpool": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", - "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", + "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", "dev": true, "license": "Apache-2.0" }, "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/y18n": { @@ -6903,39 +6490,6 @@ "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", - "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/yargs/node_modules/yargs-parser": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", diff --git a/package.json b/package.json index 6d2fb3d..8ce8213 100644 --- a/package.json +++ b/package.json @@ -14,23 +14,25 @@ "@panates/eslint-config-ts": "^2.1.6", "@panates/tsconfig": "^2.1.6", "@swc-node/register": "^1.12.1", - "@swc/core": "^1.16.1", + "@swc/core": "^1.16.2", "@swc/helpers": "^0.5.23", "@types/mocha": "^10.0.10", - "@types/node": "^26.4.0", - "auto-changelog": "^2.6.0", + "@types/node": "^26.5.0", + "auto-changelog": "^2.6.1", "c8": "^12.0.0", "dotenv": "^17.4.2", - "expect": "^30.4.1", + "expect": "^30.5.1", "generic-pool": "^3.9.0", - "globals": "^17.11.0", + "globals": "^17.12.0", "madge": "^8.0.0", - "mocha": "^11.8.0", + "mocha": "^12.0.0", "npm-run-path": "^6.0.0", "prettier": "^3.9.6", "rimraf": "^6.1.3", + "tinybench": "^6.1.6", "ts-cleanup": "^1.4.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "yargs": "^18.1.0" }, "scripts": { "compile": "tsc --noEmit", @@ -49,7 +51,9 @@ "citest": "c8 mocha", "qc": "npm run lint && npm run check", "version": "auto-changelog -p && git add CHANGELOG.md", - "benchmark": "TS_NODE_PROJECT=./benchmark-tests/tsconfig.json node --import @swc-node/register/esm-register ./benchmark-tests/index.ts" + "bench": "node --import ./benchmark/env.mjs --import @swc-node/register/esm-register benchmark/cli.ts", + "bench:report": "node --import ./benchmark/env.mjs --import @swc-node/register/esm-register benchmark/report/render-markdown.ts", + "graph": "graphify update ." }, "type": "module", "module": "./index.js", From fd670629e5a7ae19be9d1b671744c965df76abec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Hano=C4=9Flu?= Date: Tue, 8 Sep 2026 17:45:24 +0300 Subject: [PATCH 3/5] docs: Split API reference into doc/API.md, add generated doc/BENCHMARKS.md README.md was ~80% API reference (methods/properties/events/enums), pushing the actual pitch and quick-start below the fold. That reference now lives in doc/API.md, corrected against the current source along the way: - Removed `resetOnReturn`, an option that no longer exists. - Removed acquire()'s `factoryCreateOptions` parameter, which doesn't exist either - factory.create(info) is populated by the Pool itself on retries. - Fixed the `create-error` event name to `error` (payload: {requestTime, tries, maxRetries}). - Documented releaseAsync()/destroyAsync()/closeAsync(), the `terminate` and `request-timeout` events, and the exported ResourceState enum - none of which were mentioned before. doc/BENCHMARKS.md is benchmark/report/render-markdown.ts's generated output, committed so README.md's link to it resolves to real numbers; regenerate with `npm run bench` or `npm run bench:report`. The stale root BANCHMARK.md (describing the now-removed benchmark-tests/ tool) is removed - superseded by doc/BENCHMARKS.md. --- BANCHMARK.md | 79 ---------- README.md | 295 +------------------------------------ doc/API.md | 223 ++++++++++++++++++++++++++++ doc/BENCHMARKS.md | 364 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 591 insertions(+), 370 deletions(-) delete mode 100644 BANCHMARK.md create mode 100644 doc/API.md create mode 100644 doc/BENCHMARKS.md diff --git a/BANCHMARK.md b/BANCHMARK.md deleted file mode 100644 index 62d49e1..0000000 --- a/BANCHMARK.md +++ /dev/null @@ -1,79 +0,0 @@ -# Benchmark Tests for lightning-pool - -## How we test it? - -You can download [source code](https://github.com/panates/lightning-pool/benchmark-test) from [lightning-pool](https://github.com/panates/lightning-pool) repository. - -### Test parameters - - - *Total requests:* Number of request - - *Test loops:* Specifies how many times will each scenario executed. It is used to determine an average result. - - *Pool Resources:* Specifies how many resources will pool libraries use. - - *Acquiring Time:* It is used to simulate a wait time for creating resources. - - *Release After:* Specifies a timeout for releasing resources after acquire. - - -|Scenario|[advanced](https://github.com/atheros/node-advanced-pool)|[generic](https://github.com/coopernurse/node-pool)|[lightning](https://github.com/panates/lightning-pool)|Result| -|------------|-----|-----|-----|-------| -|Total requests: 1,000
Test loops: 2
Pool Resources: 10|656.5 ms|307.5 ms|149.5 ms|lightning is **%106** faster than generic
lightning is **%339** faster than advanced| -|Total requests: 10,000
Test loops: 2
Pool Resources: 10|6212.5 ms|2840 ms|1372.25 ms|lightning is **%107** faster than generic
lightning is **%353** faster than advanced| -|Total requests: 10,000
Test loops: 2
Pool Resources: 100|3741.25 ms|364.75 ms|157.75 ms|lightning is **%131** faster than generic
lightning is **%2272** faster than advanced| -|Total requests: 10,000
Test loops: 2
Pool Resources: 1,000|3468 ms|118.25 ms|35.5 ms|lightning is **%233** faster than generic
lightning is **%9669** faster than advanced| -|Total requests: 100,000
Test loops: 2
Pool Resources: 1,000|43703 ms|1669 ms|505.5 ms|lightning is **%230** faster than generic
lightning is **%8545** faster than advanced| - - -```bash -### Starting Test- 1 ### -- Total requests: 1000 -- Test loops: 1 -- Pool Resources: 10 -- Acquiring Time: 0 ms -- Release After: 1 ms -> lightning-pool : Avg 331 ms -> generic-pool : Avg 501 ms -Result: lightning is % 51 faster than generic - -### Starting Test- 2 ### -- Total requests: 10000 -- Test loops: 1 -- Pool Resources: 10 -- Acquiring Time: 0 ms -- Release After: 1 ms -> lightning-pool : Avg 3034 ms -> generic-pool : Avg 4566 ms -Result: lightning is % 50 faster than generic - -### Starting Test- 3 ### -- Total requests: 10000 -- Test loops: 1 -- Pool Resources: 100 -- Acquiring Time: 0 ms -- Release After: 1 ms -> lightning-pool : Avg 365 ms -> generic-pool : Avg 561 ms -Result: lightning is % 54 faster than generic - -### Starting Test- 4 ### -- Total requests: 10000 -- Test loops: 1 -- Pool Resources: 1000 -- Acquiring Time: 0 ms -- Release After: 1 ms -> lightning-pool : Avg 71 ms -> generic-pool : Avg 119 ms -Result: lightning is % 68 faster than generic - -### Starting Test- 5 ### -- Total requests: 100000 -- Test loops: 1 -- Pool Resources: 1000 -- Acquiring Time: 0 ms -- Release After: 1 ms -> lightning-pool : Avg 753 ms -> generic-pool : Avg 1420 ms -Result: lightning is % 89 faster than generic - -****************** -All tests complete - -``` diff --git a/README.md b/README.md index 3dd042c..4881848 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ High performance resource pool written with TypeScript. - - The fastest Resource Pool implementation for JavaScript ever! Check out [benchmark](BANCHMARK.md) results + - A fast Resource Pool implementation for JavaScript - see [doc/BENCHMARKS.md](doc/BENCHMARKS.md) for the numbers against [generic-pool](https://github.com/coopernurse/node-pool), or [benchmark/README.md](benchmark/README.md) to reproduce them yourself - Advanced configuration options, suits for enterprise level applications - Configuration can be changed while pool running - Promise based factory supported @@ -65,7 +65,7 @@ const client = await pool.acquire(); // Use resource await client.query("select * from foo"); // return object back to pool -await pool.release(client); +await pool.releaseAsync(client); /** * Step 4 - Shutdown pool (optional) @@ -78,295 +78,8 @@ await pool.close(5000); ## Documentation -### Creating a `Pool` instance - -lightning-pool module exports createPool() method and Pool class. Both can be used to instantiate a Pool. - -```ts -import {createPool} from 'lightning-pool'; -const pool = createPool(factory, options); -``` - -```ts -import {Pool} from 'lightning-pool'; -const pool = new Pool(factory, options); -``` - -#### factory - -Can be any object/instance with the following properties: - -- `create` : The function that the `Pool` will call when it needs a new resource. It should return a `Promise`. -- `destroy` : The function that the `Pool` will call when it wants to destroy a `resource`. It should accept first argument as `resource`, where `resource` is whatever factory.create made. It should return a `Promise<>`. -- `reset` (optional) : The function that the `Pool` will call before any `resource` back to the `Pool`. It should accept first argument as `resource`, where `resource` is whatever factory.create made. It should return a `Promise<>`. `Pool` will destroy and remove the resource from the `Pool` on any error. -- `validate` (optional) : The function that the `Pool` will call when any resource needs to be validated. It should accept first argument as `resource`, where `resource` is whatever factory.create made. It should return a `Promise<>`. `Pool` will destroy and remove the resource from the `Pool` on any error. - -#### options -- `acquireMaxRetries`: Maximum number that `Pool` will try to create a resource before returning the error. (Default 0) -- `acquireRetryWait`: Time in millis that `Pool` will wait after each tries. (Default 2000) -- `acquireTimeoutMillis`: Time in millis an acquire call will wait for a resource before timing out. (Default 0 - no limit) -- `fifo`: If true resources will be allocated first-in-first-out order. resources will be allocated last-in-first-out order. (Default true) -- `idleTimeoutMillis`: The minimum amount of time in millis that an `resource` may sit idle in the `Pool`. (Default 30000) -- `houseKeepInterval`: Time period in millis that `Pool` will make a cleanup. (Default 1000) -- `min`: Minimum number of resources that `Pool` will keep. (Default 0) -- `minIdle`: Minimum number of resources that `Pool` will keep in idle state. (Default 0) -- `max`: Maximum number of resources that `Pool` will create. (Default 10) -- `maxQueue`: Maximum number of request that `Pool` will accept. (Default 1000) -- `resetOnReturn`: If true `Pool` will call `reset()` function of factory before moving it idle state. (Default true) -- `validation`: If true `Pool` will call `validation()` function of factory when it needs it. If false, `validation()` never been called. (Default true) - -### Methods - -#### Pool.prototype.acquire() - -Acquires a `resource` from the `Pool` or create a new one. - -##### Usage - -`pool.acquire(): Promise` - -`pool.acquire(factoryCreateOptions: any): Promise` - -`pool.acquire(callback:Callback): Promise` - -`pool.acquire(factoryCreateOptions?: any, callback:Callback): Promise` - -- *Returns*: A Promise - - -```js -var promise = pool.acquire(); -promise.then(resource => { - // Do what ever you want with resource -}).catch(err =>{ - // Handle Error -}); -``` - -#### Pool.prototype.isAcquired() - -Returns if a resource has been acquired from the `Pool` and not yet released or destroyed. - -##### Usage - -`pool.isAcquired(resource)` - -- `resource`: A previously acquired resource -- *Returns*: True if the resource is acquired, else False - -```js -if (pool.isAcquired(resource)) { - // Do any thing -} -``` - - - -#### Pool.prototype.includes() - -Returns if the `Pool` contains a resource - -##### Usage - -`pool.includes(resource)` - -- `resource`: A resource object -- *Returns*: True if the resource is in the `Pool`, else False - -```js -if (pool.includes(resource)) { - // Do any thing -} -``` - - - -#### Pool.prototype.release() - -Releases an allocated `resource` and let it back to pool. - -##### Usage - -`pool.release(resource)` - -- `resource`: A previously acquired resource -- *Returns*: undefined - -```js -pool.release(resource); -``` - - - - -#### Pool.prototype.destroy() - -Releases, destroys and removes any `resource` from `Pool`. - -##### Usage - -`pool.destroy(resource)` - -- `resource`: A previously acquired resource -- *Returns*: undefined - -```js -pool.destroy(resource); -``` - - - - -#### Pool.prototype.start() - -Starts the `Pool` and begins creating of resources, starts house keeping and any other internal logic. - -*Note: This method is not need to be called. `Pool` instance will automatically be started when acquire() method is called* - -##### Usage - -`pool.start()` - -- *Returns*: undefined - - -```js -pool.start(); -``` - - - -#### Pool.prototype.close() - -Shuts down the `Pool` and destroys all resources. - -##### Usage - -`close(callback: Callback): void;` -`close(terminateWait: number, callback?: Callback): Promise;` -`close(force: boolean, callback?: Callback): void;` - -- `force`: If true, `Pool` will immediately destroy resources instead of waiting to be released -- `terminateWait`: If specified, `Pool` will wait for active resources to release -- `callback`: If specified, callback will be called after close. If not specified a promise returns. - - -```js -var promise = pool.close(); -promise.then(() => { - console.log('Pool has been shut down') -}).catch(err => { - console.error(err); -}); -``` - -### Properties - - -- `acquired` (Number): Returns number of acquired resources. -- `available` (Number): Returns number of idle resources. -- `creating` (Number): Returns number of resources currently been created. -- `pending` (Number): Returns number of acquire request waits in the `Pool` queue. -- `size` (Number): Returns number of total resources. -- `state` (PoolState): Returns current state of the `Pool`. -- `options` (PoolOptions): Returns object instance that holds configuration properties - - `acquireMaxRetries` (Get/Set): Maximum number that `Pool` will try to create a resource before returning the error. (Default 0) - - `acquireRetryWait` (Get/Set): Time in millis that `Pool` will wait after each tries. (Default 2000) - - `acquireTimeoutMillis` (Get/Set): Time in millis an acquire call will wait for a resource before timing out. (Default 0 - no limit) - - `fifo` (Get/Set): If true resources will be allocated first-in-first-out order. resources will be allocated last-in-first-out order. (Default true) - - `idleTimeoutMillis` (Get/Set): The minimum amount of time in millis that an `resource` may sit idle in the `Pool`. (Default 30000) - - `houseKeepInterval` (Get/Set): Time period in millis that `Pool` will make a cleanup. (Default 1000) - - `min` (Get/Set): Minimum number of resources that `Pool` will keep. (Default 0) - - `minIdle` (Get/Set): Minimum number of resources that `Pool` will keep in idle state. (Default 0) - - `max` (Get/Set): Maximum number of resources that `Pool` will create. (Default 10) - - `maxQueue` (Get/Set): Maximum number of request that `Pool` will acceps. (Default 1000) - - `resetOnReturn` (Get/Set): If true `Pool` will call `reset()` function of factory before moving it idle state. (Default true) - - `validation` (Get/Set): If true `Pool` will call `validation()` function of factory when it needs it. If false, `validation()` never been called. (Default true) - -### Events - -Pool derives from EventEmitter and produce the following events: - -- `acquire`: Emitted when a resource acquired. - -```js -pool.on('acquire', function(resource){ - //.... -}) -``` - -- `create`: Emitted when a new resource is added to the `Pool`. -```js -pool.on('create', function(resource){ - //.... -}) -``` - -- `create-error`: Emitted when a factory.create informs any error. -```js -pool.on('create-error', function(error){ - //Log stuff maybe -}) -``` - -- `destroy`: Emitted when a resource is destroyed and removed from the `Pool`. -- `destroy-error`: Emitted when a factory.destroy informs any error. -```js -pool.on('destroy-error', function(error, resource){ - //Log stuff maybe -}) -``` - -- `return`: Emitted when an acquired resource returns to the `Pool`. -```js -pool.on('start', function(resource){ - //... -}) -``` - - -- `start`: Emitted when the `Pool` started. -```js -pool.on('start', function(){ - //... -}) -``` - -- `closing`: Emitted when before closing the `Pool`. -```js -pool.on('closing', function(){ - //... -}) -``` - - -- `close`: Emitted when after closing the `Pool`. -```js -pool.on('close', function(){ - //... -}) -``` - -- `validate-error`: Emitted when a factory.validate informs any error. -```js -pool.on('validate-error', function(error, resource){ - //Log stuff maybe -}) -``` - -## PoolState enum - -Pool.PoolState (Number): - -- IDLE: 0, // Pool has not been started - -- STARTED: 1, // Pool has been started - -- CLOSING: 2, // Pool shutdown in progress - -- CLOSED: 3 // Pool has been shut down - - +See [doc/API.md](doc/API.md) for the full API reference - factory/options shape, every method and property, +emitted events, and the `PoolState`/`ResourceState` enums. ## Node Compatibility diff --git a/doc/API.md b/doc/API.md new file mode 100644 index 0000000..17e7d5e --- /dev/null +++ b/doc/API.md @@ -0,0 +1,223 @@ +# lightning-pool API Reference + +For an overview, installation, and a quick example, see the [README](../README.md). + +## Creating a `Pool` instance + +`lightning-pool` exports both a `createPool()` factory function and the `Pool` class itself. Either can be used to +instantiate a pool. + +```ts +import { createPool } from 'lightning-pool'; +const pool = createPool(factory, options); +``` + +```ts +import { Pool } from 'lightning-pool'; +const pool = new Pool(factory, options); +``` + +### `factory` + +Any object with the following properties: + +- `create(info?: {tries: number, maxRetries: number})`: Called when the `Pool` needs a new resource. May return the + resource directly or a `Promise` that resolves to it. `info` is populated by the `Pool` itself on retry attempts + (see `acquireMaxRetries`) - it is not something a caller of `acquire()` passes in. +- `destroy(resource)`: Called when the `Pool` wants to destroy a `resource` (where `resource` is whatever + `factory.create` returned). May return `void` or a `Promise`. +- `reset(resource)` (optional): Called before a `resource` is returned to the idle pool. May return `void` or a + `Promise`. If it throws or rejects, the `Pool` destroys and removes the resource instead of returning it to + the idle pool. +- `validate(resource)` (optional): Called to validate a `resource` before handing it out (see the `validation` + option). May return `void`, a `boolean`, or a `Promise` of either. If it throws, rejects, or resolves to `false`, + the `Pool` destroys and removes the resource and tries the next one instead. + +### `options` + +- `acquireMaxRetries`: Maximum number of times the `Pool` will retry creating a resource before giving up and + returning the error to the caller. (Default `0` - fail on the first error, no retries) +- `acquireRetryWait`: Time in milliseconds the `Pool` waits between retry attempts. (Default `2000`) +- `acquireTimeoutMillis`: Time in milliseconds an `acquire()` call will wait for a resource before failing with a + timeout error. (Default `0` - no timeout) +- `fifo`: If `true`, idle resources are handed out in first-in-first-out order (the longest-idle resource first). If + `false`, last-in-first-out (the most recently released resource first). (Default `true`) +- `idleTimeoutMillis`: The minimum amount of time in milliseconds a resource may sit idle in the `Pool` before the + housekeeper is allowed to destroy it (subject to `min`/`minIdle`). (Default `30000`) +- `houseKeepInterval`: Time in milliseconds between housekeeping passes, which enforce `idleTimeoutMillis` and + `min`/`minIdle`. (Default `1000`) +- `min`: Minimum number of resources the `Pool` tries to keep alive in total. (Default `0`) +- `minIdle`: Minimum number of resources the `Pool` tries to keep idle (immediately available). (Default `0`) +- `max`: Maximum number of resources the `Pool` will create. (Default `10`) +- `maxQueue`: Maximum number of `acquire()` requests that may be queued/pending at once; further requests fail + immediately with an error instead of waiting. (Default `1000`) +- `validation`: If `true`, the `Pool` calls `factory.validate()` on a resource before handing it out (when the + factory provides one). If `false`, `validate()` is never called. (Default `true`) + +All options can also be read/written after construction via `pool.options.` - see [Properties](#properties). + +## Methods + +### `pool.acquire()` + +Acquires a resource from the `Pool`, or creates a new one if none is idle. + +```ts +acquire(): Promise; +acquire(callback: Callback): void; +``` + +```js +const resource = await pool.acquire(); +// or, callback style: +pool.acquire((err, resource) => { + if (err) { + /* handle error */ + } +}); +``` + +### `pool.release()` / `pool.releaseAsync()` + +Releases an acquired resource back to the `Pool` so it can be reused. + +```ts +release(resource: T, callback?: Callback): void; +releaseAsync(resource: T): Promise; +``` + +`release()` always returns immediately (`undefined`) - it does not wait for `factory.reset()` (if any) to finish. Use +`releaseAsync()` (or pass a `callback`) if you need to know when the release has actually completed. + +```js +pool.release(resource); +// or, to wait for completion: +await pool.releaseAsync(resource); +``` + +### `pool.destroy()` / `pool.destroyAsync()` + +Releases, destroys, and removes a resource from the `Pool` entirely (it will not be reused). + +```ts +destroy(resource: T, callback?: Callback): void; +destroyAsync(resource: T): Promise; +``` + +```js +pool.destroy(resource); +// or, to wait for completion: +await pool.destroyAsync(resource); +``` + +### `pool.isAcquired()` + +Returns whether a resource is currently acquired (not yet released or destroyed). + +```ts +isAcquired(resource: T): boolean; +``` + +### `pool.includes()` + +Returns whether a resource belongs to this `Pool` (acquired, idle, or otherwise tracked - not yet destroyed). + +```ts +includes(resource: T): boolean; +``` + +### `pool.start()` + +Starts the `Pool`: begins creating resources to satisfy `min`/`minIdle` and starts the housekeeper. + +*Note: calling this explicitly is optional - the `Pool` starts itself automatically the first time `acquire()` is +called.* + +```ts +start(): void; +``` + +### `pool.close()` / `pool.closeAsync()` + +Shuts down the `Pool` and destroys all of its resources. Any `acquire()` call still queued at the time `close()` is +invoked is rejected with an error. + +```ts +close(): Promise; +close(callback: Callback): void; +close(terminateWait: number, callback?: Callback): void; +close(force: boolean, callback?: Callback): void; + +closeAsync(): Promise; +closeAsync(terminateWait?: number): Promise; +closeAsync(force?: boolean): Promise; +``` + +- `terminateWait` (number): How long, in milliseconds, to wait for acquired resources to be released before forcibly + destroying them anyway. Omit (or pass no argument) to wait indefinitely. +- `force` (boolean): `true` is shorthand for `terminateWait: 0` (destroy acquired resources immediately, without + waiting); `false` is shorthand for waiting indefinitely. +- `callback`: If provided, it is called once the `Pool` has fully closed. If omitted, `close()`/`closeAsync()` + returns a `Promise` instead. + +```js +await pool.close(5000); // wait up to 5s for active resources, then force-close +await pool.close(0); // close immediately, destroying acquired resources without waiting +await pool.close(); // wait indefinitely for active resources to be released +``` + +## Properties + +- `acquired` (`number`): Number of resources currently acquired. +- `available` (`number`): Number of idle resources. +- `creating` (`number`): Number of resources currently being created. +- `pending` (`number`): Number of `acquire()` requests currently queued/being processed. +- `size` (`number`): Total number of resources tracked by the `Pool` (acquired + idle + being created). +- `state` (`PoolState`): Current lifecycle state of the `Pool` - see [`PoolState`](#poolstate) below. +- `options` (`PoolOptions`): A live, mutable options object - every option above is a get/set property on it, and + changes take effect immediately (e.g. `pool.options.max = 20`). + +## Events + +`Pool` extends `EventEmitter` and emits the following events: + +- `start`: The `Pool` has started. +- `closing`: The `Pool` has begun shutting down (emitted at the start of `close()`). +- `close`: The `Pool` has finished shutting down and all resources have been destroyed. +- `terminate`: Emitted when `close()`'s `terminateWait` elapses and acquired resources are force-released instead of + waited for further. +- `create(resource)`: A new resource was created and added to the `Pool`. +- `error(err, info)`: `factory.create()` failed. `info` is `{ requestTime, tries, maxRetries }`. +- `acquire(resource)`: A resource was handed out to a caller. +- `return(resource)`: A previously-acquired resource was released back to the idle pool. +- `destroy(resource)`: A resource was destroyed and removed from the `Pool`. +- `destroy-error(err, resource)`: `factory.destroy()` failed while destroying a resource. +- `validate-error(err, resource)`: `factory.validate()` failed (or returned `false`) while validating a resource on + borrow; the resource is destroyed and the `Pool` tries the next one. +- `request-timeout`: An `acquire()` call timed out (see `acquireTimeoutMillis`). + +```js +pool.on('acquire', resource => { + /* ... */ +}); +pool.on('destroy-error', (err, resource) => { + /* log it */ +}); +``` + +## `PoolState` + +The `state` property (and the `PoolState` enum exported from the package): + +- `IDLE` (`0`): The `Pool` has not been started yet. +- `STARTED` (`1`): The `Pool` is running. +- `CLOSING` (`2`): Shutdown is in progress. +- `CLOSED` (`3`): The `Pool` has fully shut down. Calling `start()` again brings it back to `STARTED`. + +## `ResourceState` + +Internal per-resource state, also exported as an enum (mostly useful when inspecting events or writing tests): + +- `IDLE` (`0`): The resource is idle and available for `acquire()`. +- `ACQUIRED` (`1`): The resource is currently acquired by a caller. +- `VALIDATION` (`2`): The resource is being validated (see the `validation` option) before being handed out. diff --git a/doc/BENCHMARKS.md b/doc/BENCHMARKS.md new file mode 100644 index 0000000..73d7247 --- /dev/null +++ b/doc/BENCHMARKS.md @@ -0,0 +1,364 @@ +# lightning-pool Benchmarks + +_Generated automatically by `npm run bench` (or standalone via `npm run bench:report`). Do not hand-edit — re-run one of those instead._ + +## Contents + +- [Methodology](#methodology) + - [Disclosed asymmetries](#disclosed-asymmetries) +- [Environment](#environment) +- [Sequential Acquire/Release](#sequential-acquirerelease) +- [Concurrent Acquire/Release](#concurrent-acquirerelease) +- [Queue Contention](#queue-contention) +- [Create/Destroy Churn](#createdestroy-churn) +- [Validate on Borrow](#validate-on-borrow) +- [Raw data](#raw-data) + +## Methodology + +These numbers are produced by `benchmark/` (run via `npm run bench`), comparing lightning-pool against [generic-pool](https://github.com/coopernurse/node-pool) driving an identical simulated resource (see `benchmark/resource.ts`) - no real database or socket involved, so the numbers isolate each pool's own bookkeeping and scheduling overhead rather than any backend's latency. See [benchmark/README.md](../benchmark/README.md) for how to reproduce them. + +Each scenario is implemented once per library, using that library's own idiomatic API (lightning-pool's `acquire`/`releaseAsync`/`destroyAsync` vs. generic-pool's `acquire`/`release`/`destroy`), while both read the exact same pool-size/concurrency knobs from `benchmark/scenarios/*.ts`. Only the pooling mechanism varies, not the workload. + +Each `(library, scenario)` pair runs in its own child process, spawned sequentially (never in parallel), to avoid CPU contention skewing numbers and to get clean, uncontaminated V8 JIT warm-up per run. The default matrix runs each pair `--repeats=3` times; the tables below report the **median across repeats**, with intra-run p75/p99 latency and ops/sec from tinybench's own sample statistics. + +Each table also reports **GC (ms/op)** and **Peak Heap (KB)** - allocation pressure, not just wall-clock speed. GC (ms/op) is the total time spent in garbage collection during the run (observed via `node:perf_hooks`, every GC pause regardless of cause), divided by the number of timed samples. Peak Heap (KB) isn't a per-call figure: each worker process is started with `--expose-gc`, forces a clean GC immediately before the run to get a baseline `heapUsed`, then tracks the highest `heapUsed` seen at any point during the run - the most the heap ever grew above that baseline while running the whole scenario. Both include tinybench's own warmup iterations (it doesn't expose a hook at the boundary between warmup and the timed run). + +By default every simulated resource's `create()`/`destroy()` resolves immediately (0ms) so the numbers measure pure pool overhead. Set `BENCH_CREATE_DELAY_MS`/`BENCH_DESTROY_DELAY_MS` to approximate a real backend's connection cost instead: `BENCH_CREATE_DELAY_MS=5 npm run bench`. + +**Why GC/heap sampling needs a forced yield here.** Neither scenario does real I/O, so a pool whose hot path resolves entirely through chained `Promise`s (no timer/socket in between) can run its whole scenario without Node's event loop ever reaching a macrotask turn - and both `node:perf_hooks`' `'gc'` performance entries and a plain polling timer are only delivered/fire on such a turn. Left unpatched, this reads back as `gcCount=0`/`peakHeap=0` for whichever library's task happens to chain purely through microtasks, indistinguishable from genuinely zero allocation (confirmed directly: a raw, uninstrumented 2,000,000-iteration acquire/release loop reports zero for *both* libraries here, even though real GCs are demonstrably happening - inserting a periodic `setImmediate` yield in that same raw loop immediately surfaces hundreds of real GC events and tens of MB of real heap growth). `benchmark/runner/worker.ts`'s `installPeriodicYield()` fixes this at the source: it patches `bench.add()` to insert a real `setImmediate` yield (plus a heap sample) into the task's `afterEach` hook every 250 samples. tinybench's own time budget only accumulates the timed `fn()` duration, not hook time (see its `Task` internals), so this yield doesn't shrink the sample count or skew Mean/p75/p99/ops-per-sec - it only makes the whole run take a little longer in real wall-clock time, which is what makes GC (ms/op) and Peak Heap (KB) trustworthy enough to compare between libraries at all. + +### Disclosed asymmetries + +1. **Validation** - lightning-pool's `validation` option and generic-pool's `testOnBorrow` option are conceptually equivalent (both call `factory.validate()` before handing a resource out) but are each library's own native mechanism, not a shared shim. +2. **Queue depth** - generic-pool's `maxWaitingClients` and lightning-pool's `maxQueue` are each set to (at least) the scenario's concurrency so neither library ever rejects a request for being over capacity; the numbers measure queueing/scheduling cost, not admission-control behaviour. +3. **Resource shape** - both libraries pool the exact same plain `{ id, destroyed }` object (see `benchmark/resource.ts`), so no library gains or loses time doing work specific to a real resource type. + +## Environment + +- Run date: 2026-09-08T13:35:32.707Z +- Node.js: v24.15.0 +- OS: Darwin 25.6.0 (darwin/arm64) +- CPU: Apple M1 Pro (10 logical cores) +- RAM: 16.0 GB total +- Library versions (installed, not this repo's semver range): lightning-pool 4.13.0, generic-pool 3.9.0 + +## Sequential Acquire/Release + +A single `acquire()` followed by a `release()`, one at a +time, against a pool of max size 10 that has +already warmed up (the resource is always idle and immediately reusable). +The baseline "warm path" cost of the pool's own bookkeeping, with no +contention and no resource creation in the timed path. (poolSize=10) + +| Library | Mean (ms) | p75 (ms) | p99 (ms) | ops/sec | vs. slowest | GC (ms/op) | Peak Heap (KB) | +|---|---:|---:|---:|---:|---:|---:|---:| +| lightning-pool (4.13.0) | ***0.0005*** | ***0.0005*** | ***0.0009*** | ***2295293.7*** | ***1.14x*** | ***0.0000*** | ***46427.88*** | +| generic-pool (3.9.0) | 0.0005 | 0.0005 | 0.0011 | 2023524.5 | 1.00x | 0.0000 | 72265.55 | + +
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Mean latency (ms, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms" 0.0000 --> 0.0008 + bar [0.0005, 0.0005] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}, 'themeVariables': {'xyChart': {'plotColorPalette': '#f2a900'}}}}%% +xychart-beta + title "Throughput (ops/sec, higher is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ops/sec" 0.0000 --> 3442940.5965 + bar [2295293.7310, 2023524.5148] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "GC time (ms/op, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms/op" 0.0000 --> 0.0001 + bar [0.0000, 0.0000] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Peak heap growth (KB, max memory reached)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "KB" 0.0000 --> 108398.3203 + bar [46427.8750, 72265.5469] +``` + +
+ +## Concurrent Acquire/Release + +50 `acquire()` calls +fired at once via `Promise.all`, each released immediately after, against a +pool sized exactly to the concurrency (max=50) +so every call is satisfied from the idle pool with no queueing. Measures the +pool's concurrent-safety bookkeeping in isolation from queue contention. (poolSize=50, concurrency=50) + +| Library | Mean (ms) | p75 (ms) | p99 (ms) | ops/sec | vs. slowest | GC (ms/op) | Peak Heap (KB) | +|---|---:|---:|---:|---:|---:|---:|---:| +| lightning-pool (4.13.0) | ***0.0239*** | ***0.0224*** | ***0.0739*** | ***44168.6*** | ***1.14x*** | ***0.0017*** | ***29785.13*** | +| generic-pool (3.9.0) | 0.0272 | 0.0255 | 0.0879 | 39166.2 | 1.00x | 0.0019 | 40802.12 | + +
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Mean latency (ms, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms" 0.0000 --> 0.0407 + bar [0.0239, 0.0272] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}, 'themeVariables': {'xyChart': {'plotColorPalette': '#f2a900'}}}}%% +xychart-beta + title "Throughput (ops/sec, higher is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ops/sec" 0.0000 --> 66252.8749 + bar [44168.5833, 39166.1973] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "GC time (ms/op, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms/op" 0.0000 --> 0.0029 + bar [0.0017, 0.0019] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Peak heap growth (KB, max memory reached)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "KB" 0.0000 --> 61203.1758 + bar [29785.1328, 40802.1172] +``` + +
+ +## Queue Contention + +500 `acquire()` calls fired at +once via `Promise.all` against a pool of max size +10 - the overwhelming majority must queue and wait +for a resource to be released back before they can be served. Each holder +releases its resource right after acquiring it, so the queue continuously +drains. This is the scenario the pool's request queue and scheduling logic +matter most for. (poolSize=10, concurrency=500) + +| Library | Mean (ms) | p75 (ms) | p99 (ms) | ops/sec | vs. slowest | GC (ms/op) | Peak Heap (KB) | +|---|---:|---:|---:|---:|---:|---:|---:| +| lightning-pool (4.13.0) | ***0.2337*** | ***0.2213*** | ***0.5334*** | ***4474.3*** | ***1.15x*** | 0.0166 | 32565.05 | +| generic-pool (3.9.0) | 0.2682 | 0.2607 | 0.5640 | 3822.1 | 1.00x | ***0.0130*** | ***20323.23*** | + +
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Mean latency (ms, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms" 0.0000 --> 0.4022 + bar [0.2337, 0.2682] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}, 'themeVariables': {'xyChart': {'plotColorPalette': '#f2a900'}}}}%% +xychart-beta + title "Throughput (ops/sec, higher is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ops/sec" 0.0000 --> 6711.4042 + bar [4474.2695, 3822.0629] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "GC time (ms/op, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms/op" 0.0000 --> 0.0250 + bar [0.0166, 0.0130] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Peak heap growth (KB, max memory reached)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "KB" 0.0000 --> 48847.5820 + bar [32565.0547, 20323.2344] +``` + +
+ +## Create/Destroy Churn + +10 concurrent `acquire()` ++ `destroy()` cycles per iteration - the resource is destroyed instead of +released, so it never returns to the idle list and a brand-new one must be +created for every single acquire (pool max=10). +Isolates the factory create/destroy pipeline overhead from the idle-reuse +path the other scenarios mostly exercise. Set `BENCH_CREATE_DELAY_MS`/ +`BENCH_DESTROY_DELAY_MS` to approximate a real backend's connection cost. (poolSize=10, concurrency=10) + +| Library | Mean (ms) | p75 (ms) | p99 (ms) | ops/sec | vs. slowest | GC (ms/op) | Peak Heap (KB) | +|---|---:|---:|---:|---:|---:|---:|---:| +| lightning-pool (4.13.0) | ***0.0091*** | ***0.0086*** | ***0.0314*** | ***121418.4*** | ***1.43x*** | ***0.0011*** | ***112372.29*** | +| generic-pool (3.9.0) | 0.0130 | 0.0120 | 0.0408 | 84909.0 | 1.00x | 0.0018 | 118058.28 | + +
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Mean latency (ms, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms" 0.0000 --> 0.0195 + bar [0.0091, 0.0130] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}, 'themeVariables': {'xyChart': {'plotColorPalette': '#f2a900'}}}}%% +xychart-beta + title "Throughput (ops/sec, higher is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ops/sec" 0.0000 --> 182127.5988 + bar [121418.3992, 84909.0282] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "GC time (ms/op, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms/op" 0.0000 --> 0.0027 + bar [0.0011, 0.0018] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Peak heap growth (KB, max memory reached)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "KB" 0.0000 --> 177087.4219 + bar [112372.2891, 118058.2813] +``` + +
+ +## Validate on Borrow + +Same shape as Concurrent Acquire/Release +(50 concurrent acquire+release cycles, +pool max=50), but with each library's +borrow-time validation hook enabled (lightning-pool's `validation: true` + +`factory.validate`, generic-pool's `testOnBorrow: true` + +`factory.validate`) - isolates the added cost of validating a resource +before handing it out. (poolSize=50, concurrency=50) + +| Library | Mean (ms) | p75 (ms) | p99 (ms) | ops/sec | vs. slowest | GC (ms/op) | Peak Heap (KB) | +|---|---:|---:|---:|---:|---:|---:|---:| +| lightning-pool (4.13.0) | ***0.0276*** | ***0.0249*** | ***0.0998*** | ***39526.5*** | ***1.42x*** | 0.0022 | 46813.63 | +| generic-pool (3.9.0) | 0.0391 | 0.0374 | 0.1250 | 26761.0 | 1.00x | ***0.0021*** | ***16179.91*** | + +
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Mean latency (ms, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms" 0.0000 --> 0.0586 + bar [0.0276, 0.0391] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}, 'themeVariables': {'xyChart': {'plotColorPalette': '#f2a900'}}}}%% +xychart-beta + title "Throughput (ops/sec, higher is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ops/sec" 0.0000 --> 59289.7031 + bar [39526.4687, 26760.9701] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "GC time (ms/op, lower is better)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "ms/op" 0.0000 --> 0.0033 + bar [0.0022, 0.0021] +``` + +
+
+ +```mermaid +%%{init: {'xyChart': {'width': 600, 'height': 300, 'chartOrientation': 'horizontal'}}}%% +xychart-beta + title "Peak heap growth (KB, max memory reached)" + x-axis ["lightning-pool", "generic-pool"] + y-axis "KB" 0.0000 --> 70220.4375 + bar [46813.6250, 16179.9063] +``` + +
+ +## Raw data + +Backing raw data for the numbers above lives in `benchmark/results/*.json` (gitignored; regenerate with `npm run bench`). From 749554dff10064394e53fbd97e17980fe011bad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Hano=C4=9Flu?= Date: Tue, 8 Sep 2026 17:54:59 +0300 Subject: [PATCH 4/5] docs: Add CLAUDE.md with graphify usage guidelines --- CLAUDE.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..417efeb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,9 @@ +## graphify + +This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. + +Rules: +- For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. +- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. +- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. +- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost). From ba8a0f739b8c049632e4c95013fc83a421d9fc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eray=20Hano=C4=9Flu?= Date: Tue, 8 Sep 2026 17:55:07 +0300 Subject: [PATCH 5/5] 4.14.0 --- CHANGELOG.md | 17 ++++++++++++++++- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c11eb..f015f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ ## Changelog -### [v4.13.0](https://github.com/panates/lightning-pool/compare/v4.12.0...v4.13.0) - +### [v4.14.0](https://github.com/panates/lightning-pool/compare/v4.13.0...v4.14.0) - + +#### 🚀 New Features + +- feat: Replace benchmark-tests with a proper benchmark suite @Eray Hanoğlu + +#### 🪲 Fixes + +- fix: Correct release/close/ensureMin bugs and cut hot-path allocations in Pool @Eray Hanoğlu + +#### 📖 Documentation Changes + +- docs: Split API reference into doc/API.md, add generated doc/BENCHMARKS.md @Eray Hanoğlu +- docs: Add CLAUDE.md with graphify usage guidelines @Eray Hanoğlu + +### [v4.13.0](https://github.com/panates/lightning-pool/compare/v4.12.0...v4.13.0) - 27 August 2026 ### [v4.12.0](https://github.com/panates/lightning-pool/compare/v4.11.1...v4.12.0) - 4 December 2025 diff --git a/package-lock.json b/package-lock.json index ab144e7..0c22430 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lightning-pool", - "version": "4.13.0", + "version": "4.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lightning-pool", - "version": "4.13.0", + "version": "4.14.0", "license": "MIT", "dependencies": { "doublylinked": "^2.5.6", diff --git a/package.json b/package.json index 8ce8213..8dd0cfa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lightning-pool", "description": "Fastest generic Pool written with TypeScript", - "version": "4.13.0", + "version": "4.14.0", "author": "Panates", "license": "MIT", "dependencies": {