From cb652ece6caf4f5c168571cb2733b7559f4aec1f Mon Sep 17 00:00:00 2001 From: KaKa <23028015+climba03003@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:29:25 +0800 Subject: [PATCH] feat: worker pool (#22) * feat: worker pool * fix: unit test or duplicate ts-node registration --- .github/workflows/ci-worker-pool.yml | 115 +++++++++ package.json | 4 +- packages/worker-pool/.eslintrc | 24 ++ packages/worker-pool/lib/index.ts | 2 + packages/worker-pool/lib/mjs/package.json | 3 + packages/worker-pool/lib/pool.ts | 190 ++++++++++++++ packages/worker-pool/lib/utils.ts | 72 ++++++ packages/worker-pool/lib/worker.ts | 132 ++++++++++ packages/worker-pool/package.json | 47 ++++ packages/worker-pool/test/pingpong.worker.ts | 5 + packages/worker-pool/test/worker.test.ts | 50 ++++ packages/worker-pool/tsconfig.cjs.json | 7 + packages/worker-pool/tsconfig.json | 26 ++ packages/worker-pool/tsconfig.mjs.json | 11 + pnpm-lock.yaml | 257 +++++++++++++++++++ 15 files changed, 944 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci-worker-pool.yml create mode 100644 packages/worker-pool/.eslintrc create mode 100644 packages/worker-pool/lib/index.ts create mode 100644 packages/worker-pool/lib/mjs/package.json create mode 100644 packages/worker-pool/lib/pool.ts create mode 100644 packages/worker-pool/lib/utils.ts create mode 100644 packages/worker-pool/lib/worker.ts create mode 100644 packages/worker-pool/package.json create mode 100644 packages/worker-pool/test/pingpong.worker.ts create mode 100644 packages/worker-pool/test/worker.test.ts create mode 100644 packages/worker-pool/tsconfig.cjs.json create mode 100644 packages/worker-pool/tsconfig.json create mode 100644 packages/worker-pool/tsconfig.mjs.json diff --git a/.github/workflows/ci-worker-pool.yml b/.github/workflows/ci-worker-pool.yml new file mode 100644 index 00000000..3642be4d --- /dev/null +++ b/.github/workflows/ci-worker-pool.yml @@ -0,0 +1,115 @@ +name: Continuous Integration - Worker Pool + +on: + push: + paths: + - ".github/workflows/ci-worker-pool.yml" + - "packages/worker-pool/**" + pull_request: + paths: + - ".github/workflows/ci-worker-pool.yml" + - "packages/worker-pool/**" + +jobs: + linter: + name: Lint Code + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out repo + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Lint code + run: pnpm --filter "./packages/worker-pool" run lint + + test: + name: Test + runs-on: ${{ matrix.os }} + permissions: + contents: read + strategy: + matrix: + node-version: [18, 20] + os: [macos-latest, ubuntu-latest, windows-latest] + steps: + - name: Check out repo + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-${{ matrix.node-version }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.node-version }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Run tests + run: pnpm --filter "./packages/worker-pool" run test + + automerge: + name: Automerge Dependabot PRs + if: > + github.event_name == 'pull_request' && + github.event.pull_request.user.login == 'dependabot[bot]' + needs: test + permissions: + pull-requests: write + contents: write + runs-on: ubuntu-latest + steps: + - uses: fastify/github-action-merge-dependabot@v3 + with: + exclude: ${{ inputs.auto-merge-exclude }} + github-token: ${{ secrets.GITHUB_TOKEN }} + target: major diff --git a/package.json b/package.json index 887be61d..13d2a430 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "scripts": { "unit": "pnpm -r --workspace-concurrency=1 unit", "test": "pnpm -r --workspace-concurrency=1 test", - "lint": "pnpm -r --workspace-concurrency=1 lint" + "lint": "pnpm -r --workspace-concurrency=1 lint", + "build": "pnpm -r --workspace-concurrency=1 build", + "clean": "pnpm -r --workspace-concurrency=1 clean" } } diff --git a/packages/worker-pool/.eslintrc b/packages/worker-pool/.eslintrc new file mode 100644 index 00000000..ea67ea7d --- /dev/null +++ b/packages/worker-pool/.eslintrc @@ -0,0 +1,24 @@ +{ + "extends": "standard-with-typescript", + "parserOptions": { + "project": "./tsconfig.json" + }, + "rules": { + // conflict between standard and standard-typescript + "no-void": ["error", { "allowAsStatement": true }] + }, + "overrides": [ + { + "files": ["**/*.test.ts"], + "rules": { + "@typescript-eslint/no-floating-promises": "off" + } + }, + { + "files": ["scripts/*.mjs"], + "rules": { + "@typescript-eslint/explicit-function-return-type": "off" + } + } + ] +} diff --git a/packages/worker-pool/lib/index.ts b/packages/worker-pool/lib/index.ts new file mode 100644 index 00000000..f55e86fe --- /dev/null +++ b/packages/worker-pool/lib/index.ts @@ -0,0 +1,2 @@ +export * from './pool' +export * from './worker' diff --git a/packages/worker-pool/lib/mjs/package.json b/packages/worker-pool/lib/mjs/package.json new file mode 100644 index 00000000..3dbc1ca5 --- /dev/null +++ b/packages/worker-pool/lib/mjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/packages/worker-pool/lib/pool.ts b/packages/worker-pool/lib/pool.ts new file mode 100644 index 00000000..783c1d29 --- /dev/null +++ b/packages/worker-pool/lib/pool.ts @@ -0,0 +1,190 @@ +import { randomUUID } from 'node:crypto' +import EventEmitter from 'node:events' +import { cpus } from 'node:os' +import { Worker, type TransferListItem, type WorkerOptions } from 'node:worker_threads' +import { TYPESCRIPT_WORKER, filenameFixture, handleMaybePromise, isTSNode, optionsFixture } from './utils' + +export interface WorkerMessage { + messageId: string + action: string + value: unknown + transferList?: TransferListItem[] +} + +export interface WorkerPoolOptions extends WorkerOptions { + minWorker?: number + maxWorker?: number + + maxQueueSize?: number +} + +export class WorkerPool extends EventEmitter { + // public variables + minWorker: number + maxWorker: number + maxQueueSize: number + // private variables + readonly #filename: string + readonly #options: WorkerOptions + readonly #queue: WorkerMessage[] + readonly #workers: Map + readonly #idleWorkers: Set + #terminated: boolean + + get workerCount (): number { + return this.#workers.size + } + + get queueSize (): number { + return this.#queue.length + } + + // WorkerPool provide similar interface of + // Worker to ease the usage + constructor (filename: string | URL, options?: WorkerPoolOptions) { + super() + this.#filename = filenameFixture(filename) + // use spread operator to extract worker options + const { minWorker, maxWorker, maxQueueSize, ...workerOptions } = options ?? {} + this.minWorker = minWorker ?? 1 + this.maxWorker = maxWorker ?? Math.max(1, cpus().length - 1) + this.maxQueueSize = maxQueueSize ?? Infinity + + this.#options = optionsFixture(workerOptions) + if (isTSNode()) { + this.#options.workerData.__filename = this.#filename + this.#filename = TYPESCRIPT_WORKER + } + + // unbounded array + this.#queue = [] + this.#workers = new Map() + this.#idleWorkers = new Set() + this.#terminated = false + + // spwan worker until minWorker + this.#spwan() + } + + postMessage (value: unknown, transferList?: TransferListItem[]): void { + if (this.#terminated) return + this.#pushBuffer('message', value, transferList) + this.#distribute() + } + + terminate (): void { + if (this.#terminated) return + this.#terminated = true + for (const [, worker] of this.#workers) { + worker.postMessage({ messageId: randomUUID(), action: 'close' }) + } + } + + #pushBuffer (action: string, value: unknown, transferList?: TransferListItem[]): void { + if (this.queueSize >= this.maxQueueSize) throw Error('exceed queue size') + this.#queue.push({ + messageId: randomUUID(), + action, + value, + transferList: transferList ?? [] + }) + } + + #spwan (): void { + if (this.#terminated) return + for (let i = this.workerCount; i < this.minWorker; i++) { + this.#spwanOnce() + } + } + + #spwanOnce (): number | undefined { + if (this.#terminated) return + if (this.#workers.size >= this.maxWorker) return + const worker = new Worker(this.#filename, this.#options) + // thread id will be negative number after exit + // so, we cache it first + const threadId = worker.threadId + this.#workers.set(threadId, worker) + let terminationTimeout: NodeJS.Timeout | null = null + + worker.on('online', () => { + this.emit('worker:online', worker) + }) + worker.on('message', (value) => { + if (typeof value === 'object') { + switch (value.action) { + case 'idle': { + this.#idleWorkers.add(threadId) + this.emit('worker:idle', worker) + this.#distribute() + break + } + case 'busy': { + this.#idleWorkers.delete(threadId) + this.emit('worker:busy', worker) + break + } + case 'terminate': { + this.#workers.delete(threadId) + this.#idleWorkers.delete(threadId) + // we provide 30 seconds to cleanup + terminationTimeout = setTimeout(() => { + handleMaybePromise(async () => { + await worker.terminate() + }, () => { + terminationTimeout = null + this.emit('worker:terminate', worker) + this.#spwan() + }) + }, 30e3) + break + } + default: { + // do not handle something we don't understand + this.emit('worker:message', worker, value?.value ?? value) + break + } + } + } else { + // do not handle something we don't understand + this.emit('worker:message', worker, value) + } + }) + worker.on('exit', () => { + // when the worker is properly exit + this.#idleWorkers.delete(threadId) + this.#workers.delete(threadId) + terminationTimeout !== null && clearTimeout(terminationTimeout) + this.emit('worker:exit', worker) + this.#spwan() + + if (this.#terminated && this.workerCount === 0) { + this.emit('terminated') + } + }) + + return worker.threadId + } + + #distribute (): void { + if (this.#idleWorkers.size === 0) { + // we increase worker until max + this.#spwanOnce() + } + + if (this.#queue.length === 0 && this.#idleWorkers.size === 0) { + return + } + + for (const threadId of this.#idleWorkers) { + const worker = this.#workers.get(threadId) + if (worker === undefined) continue + // FIFO + const message = this.#queue.shift() + if (message === undefined) continue + const { transferList, ...value } = message + worker.postMessage(value, transferList) + this.#idleWorkers.delete(threadId) + } + } +} diff --git a/packages/worker-pool/lib/utils.ts b/packages/worker-pool/lib/utils.ts new file mode 100644 index 00000000..17a1921c --- /dev/null +++ b/packages/worker-pool/lib/utils.ts @@ -0,0 +1,72 @@ +import { extname, normalize } from 'node:path' +import { fileURLToPath } from 'node:url' +import { type WorkerOptions } from 'node:worker_threads' + +export const TYPESCRIPT_WORKER = ` +const workerData = require('worker_threads').workerData +// we need to escape in unit test environment +// prevent duplicate ts-node registration +if(!process[Symbol.for('ts-node.register.instance')]) + require('ts-node').register(workerData.__tsNodeOptions) +require(workerData.__filename) +` + +export function isTSNode (): boolean { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + return !!(process as any)[Symbol.for('ts-node.register.instance')] +} + +const defaultExtension = isTSNode() ? '.ts' : '.js' + +export function filenameFixture (filename: string | URL): string { + if (filename instanceof URL) { + filename = fileURLToPath(filename) + } + + const extension = extname(filename) + switch (extension) { + case '': { + // provide missing extension + filename += defaultExtension + break + } + case '.js': + case '.mjs': { + // replace .[m]js to .ts + isTSNode() && (filename = filename.replace(extension, '.ts')) + break + } + } + + return normalize(filename) +} + +export function optionsFixture (options?: WorkerOptions): WorkerOptions { + options ??= {} + + if (isTSNode()) { + options.eval = true + options.workerData ??= {} + options.workerData.__filename = '' + options.workerData.__tsNodeOptions ??= {} + } + + return options +} + +export function isThenable (value: unknown): value is Promise { + return typeof value === 'object' && value !== null && typeof (value as any).then === 'function' +} + +export function handleMaybePromise (exec: () => unknown, done: (err: Error | null, value?: unknown) => void): void { + try { + const result = exec() + if (isThenable(result)) { + result.then((value) => { done(null, value) }, done) + } else { + done(null) + } + } catch (err) { + done(err as Error) + } +} diff --git a/packages/worker-pool/lib/worker.ts b/packages/worker-pool/lib/worker.ts new file mode 100644 index 00000000..6b3ec073 --- /dev/null +++ b/packages/worker-pool/lib/worker.ts @@ -0,0 +1,132 @@ +import { randomUUID } from 'node:crypto' +import { isMainThread, parentPort, type MessagePort, type TransferListItem } from 'node:worker_threads' +import { handleMaybePromise } from './utils' + +export type WorkerHandle = (...args: unknown[]) => unknown +export type WorkerCleanup = () => unknown +export type WorkerPrepare = () => unknown + +interface WorkerOptions { + prepare: WorkerPrepare + cleanup: WorkerCleanup +} + +// Worker class that is designed to use inside the worker_threads only +export class Worker { + channel: MessagePort + readonly #handle: WorkerHandle + readonly #cleanup: WorkerCleanup + readonly #prepare: WorkerPrepare + // state + #idle: boolean + terminated: boolean + + get idle (): boolean { + return this.#idle + } + + set idle (idle: boolean) { + this.#idle = idle + if (idle) { + this.#send('idle') + } else { + this.#send('busy') + } + } + + constructor (handle: WorkerHandle, options?: WorkerOptions) { + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + this.channel = parentPort as MessagePort + this.#handle = handle + this.#cleanup = options?.cleanup ?? (() => {}) + this.#prepare = options?.prepare ?? (() => {}) + + // state + this.#idle = false + this.terminated = false + + // auto launch + this.launch() + } + + launch (): void { + if (isMainThread) { + // we expect to run inside worker threads only + process.exit(1) + } + + // when process is disconnect + // which means the main thread accidentially exit + process.on('disconnect', () => { + this.terminate() + }) + + this.channel.on('message', (value) => { + if (typeof value === 'object') { + switch (value.action) { + case 'close': { + this.terminate() + break + } + default: { + this.handle(value?.value ?? value) + break + } + } + } else { + // do not handle something we don't understand + this.handle(value) + } + }) + this.channel.on('close', () => { + this.terminate() + }) + + handleMaybePromise(() => { + return this.#prepare() + }, () => { + this.idle = true + }) + } + + handle (value: unknown): void { + this.idle = false + handleMaybePromise(() => { + return this.#handle(value) + }, (err) => { + this.idle = true + err !== null && this.channel.postMessage(err) + }) + } + + postMessage (value: unknown, transferList?: TransferListItem[]): void { + this.#send('message', value, transferList) + } + + #send (action: string, value?: unknown, transferList?: TransferListItem[]): void { + this.channel.postMessage({ + messageId: randomUUID(), + action, + value + }, transferList) + } + + terminate (): void { + // prevent duplicate call + if (this.terminated) return + this.terminated = true + this.#send('terminate') + + handleMaybePromise(() => { + return this.#cleanup() + }, (err) => { + process.exit(err !== null ? 1 : 0) + }) + } + + run (): void { + // run method emit idle when worker is not closed + if (this.terminated) return + this.idle = true + } +} diff --git a/packages/worker-pool/package.json b/packages/worker-pool/package.json new file mode 100644 index 00000000..60d520bf --- /dev/null +++ b/packages/worker-pool/package.json @@ -0,0 +1,47 @@ +{ + "name": "@kakang/worker-pool", + "version": "0.0.1", + "description": "", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib" + }, + "scripts": { + "clean": "node ../../scripts/build.mjs --clean", + "lint": "eslint --ext .ts lib test", + "lint:fix": "npm run lint -- --fix", + "build": "node ../../scripts/build.mjs --build=\"all\"", + "build:cjs": "node ../../scripts/build.mjs --build='cjs'", + "build:mjs": "node ../../scripts/build.mjs --build='mjs'", + "unit": "node --require ts-node/register ../../scripts/test.mjs", + "test": "npm run lint && npm run unit", + "coverage": "c8 node --require ts-node/register ./test/run.ts", + "prepublishOnly": "npm run build", + "postpublish": "npm run clean" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/kaka-ng/nodejs.git" + }, + "author": "KaKa ", + "license": "GPL-3.0", + "devDependencies": { + "@types/node": "^20.11.16", + "@typescript-eslint/eslint-plugin": "6.20.0", + "@typescript-eslint/parser": "6.20.0", + "eslint": "^8.56.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.1.1", + "rimraf": "^5.0.5", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.8", + "typescript": "~5.3.3" + } +} diff --git a/packages/worker-pool/test/pingpong.worker.ts b/packages/worker-pool/test/pingpong.worker.ts new file mode 100644 index 00000000..5c6218eb --- /dev/null +++ b/packages/worker-pool/test/pingpong.worker.ts @@ -0,0 +1,5 @@ +import { Worker } from '../lib' + +const worker = new Worker(function () { + worker.postMessage('pong') +}) diff --git a/packages/worker-pool/test/worker.test.ts b/packages/worker-pool/test/worker.test.ts new file mode 100644 index 00000000..78db1deb --- /dev/null +++ b/packages/worker-pool/test/worker.test.ts @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict' +import { once } from 'node:events' +import { join } from 'node:path' +import t from 'node:test' +import { WorkerPool } from '../lib' + +t.test('events', async function (t) { + const worker = new WorkerPool(join(__dirname, 'pingpong.worker.ts'), { + minWorker: 1, + maxWorker: 1 + }) + const events = { + online: 0, + idle: 0, + busy: 0, + terminate: 0, + message: 0, + exit: 0 + } + function inc (event: keyof typeof events): () => void { + return function () { + events[event]++ + } + } + worker.on('worker:online', inc('online')) + worker.on('worker:idle', inc('idle')) + worker.on('worker:busy', inc('busy')) + worker.on('worker:terminate', inc('terminate')) + worker.on('worker:message', inc('message')) + worker.on('worker:exit', inc('exit')) + + await once(worker, 'worker:online') + assert.equal(worker.workerCount, 1) + + worker.postMessage('ping') + + await once(worker, 'worker:message') + worker.terminate() + + await once(worker, 'terminated') + + assert.deepEqual(events, { + online: 1, + idle: 2, // 1: startup, 2: handle job + busy: 1, + terminate: 0, + message: 1, + exit: 1 + }) +}) diff --git a/packages/worker-pool/tsconfig.cjs.json b/packages/worker-pool/tsconfig.cjs.json new file mode 100644 index 00000000..97d519dc --- /dev/null +++ b/packages/worker-pool/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": ["lib/**/*"] +} diff --git a/packages/worker-pool/tsconfig.json b/packages/worker-pool/tsconfig.json new file mode 100644 index 00000000..dc5c37ac --- /dev/null +++ b/packages/worker-pool/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "CommonJS", + "target": "ES2018", + "moduleResolution": "Node", + + "resolveJsonModule": true, + + "removeComments": true, + "preserveConstEnums": true, + + "sourceMap": true, + + "declaration": true, + + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["lib/**/*", "test/**/*"], + "exclude": ["node_modules"] +} diff --git a/packages/worker-pool/tsconfig.mjs.json b/packages/worker-pool/tsconfig.mjs.json new file mode 100644 index 00000000..c119ce6b --- /dev/null +++ b/packages/worker-pool/tsconfig.mjs.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "outDir": "lib/mjs" + }, + "include": ["lib/**/*"], + "tsc-alias": { + "resolveFullPaths": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7d36d86..94d8d965 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -168,6 +168,45 @@ importers: specifier: ~5.3.3 version: 5.3.3 + packages/worker-pool: + devDependencies: + '@types/node': + specifier: ^20.11.16 + version: 20.11.16 + '@typescript-eslint/eslint-plugin': + specifier: 6.20.0 + version: 6.20.0(@typescript-eslint/parser@6.20.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': + specifier: 6.20.0 + version: 6.20.0(eslint@8.56.0)(typescript@5.3.3) + eslint: + specifier: ^8.56.0 + version: 8.56.0 + eslint-config-standard-with-typescript: + specifier: ^43.0.1 + version: 43.0.1(@typescript-eslint/eslint-plugin@6.20.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3) + eslint-plugin-import: + specifier: ^2.29.1 + version: 2.29.1(@typescript-eslint/parser@6.20.0)(eslint@8.56.0) + eslint-plugin-n: + specifier: ^16.6.2 + version: 16.6.2(eslint@8.56.0) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.1.1(eslint@8.56.0) + rimraf: + specifier: ^5.0.5 + version: 5.0.5 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.11.16)(typescript@5.3.3) + tsc-alias: + specifier: ^1.8.8 + version: 1.8.8 + typescript: + specifier: ~5.3.3 + version: 5.3.3 + packages: /@aashutoshrathi/word-wrap@1.2.6: @@ -329,6 +368,35 @@ packages: resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} dev: true + /@typescript-eslint/eslint-plugin@6.20.0(@typescript-eslint/parser@6.20.0)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-fTwGQUnjhoYHeSF6m5pWNkzmDDdsKELYrOBxhjMrofPqCkoC2k3B2wvGHFxa1CTIqkEn88nlW1HVMztjo2K8Hg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.20.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.20.0 + '@typescript-eslint/type-utils': 6.20.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.20.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.20.0 + debug: 4.3.4 + eslint: 8.56.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} @@ -358,6 +426,27 @@ packages: - supports-color dev: true + /@typescript-eslint/parser@6.20.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.20.0 + '@typescript-eslint/types': 6.20.0 + '@typescript-eslint/typescript-estree': 6.20.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.20.0 + debug: 4.3.4 + eslint: 8.56.0 + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -379,6 +468,14 @@ packages: - supports-color dev: true + /@typescript-eslint/scope-manager@6.20.0: + resolution: {integrity: sha512-p4rvHQRDTI1tGGMDFQm+GtxP1ZHyAh64WANVoyEcNMpaTFn3ox/3CcgtIlELnRfKzSs/DwYlDccJEtr3O6qBvA==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.20.0 + '@typescript-eslint/visitor-keys': 6.20.0 + dev: true + /@typescript-eslint/scope-manager@6.21.0: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -387,6 +484,26 @@ packages: '@typescript-eslint/visitor-keys': 6.21.0 dev: true + /@typescript-eslint/type-utils@6.20.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-qnSobiJQb1F5JjN0YDRPHruQTrX7ICsmltXhkV536mp4idGAYrIyr47zF/JmkJtEcAVnIz4gUYJ7gOZa6SmN4g==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.20.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.20.0(eslint@8.56.0)(typescript@5.3.3) + debug: 4.3.4 + eslint: 8.56.0 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/type-utils@6.21.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} @@ -407,11 +524,38 @@ packages: - supports-color dev: true + /@typescript-eslint/types@6.20.0: + resolution: {integrity: sha512-MM9mfZMAhiN4cOEcUOEx+0HmuaW3WBfukBZPCfwSqFnQy0grXYtngKCqpQN339X3RrwtzspWJrpbrupKYUSBXQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/typescript-estree@6.20.0(typescript@5.3.3): + resolution: {integrity: sha512-RnRya9q5m6YYSpBN7IzKu9FmLcYtErkDkc8/dKv81I9QiLLtVBHrjz+Ev/crAqgMNW2FCsoZF4g2QUylMnJz+g==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.20.0 + '@typescript-eslint/visitor-keys': 6.20.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.3.3) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -434,6 +578,25 @@ packages: - supports-color dev: true + /@typescript-eslint/utils@6.20.0(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-/EKuw+kRu2vAqCoDwDCBtDRU6CTKbUmwwI7SH7AashZ+W+7o8eiyy6V2cdOqN49KsTcASWsC5QeghYuRDTyOOg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.20.0 + '@typescript-eslint/types': 6.20.0 + '@typescript-eslint/typescript-estree': 6.20.0(typescript@5.3.3) + eslint: 8.56.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/utils@6.21.0(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -453,6 +616,14 @@ packages: - typescript dev: true + /@typescript-eslint/visitor-keys@6.20.0: + resolution: {integrity: sha512-E8Cp98kRe4gKHjJD4NExXKz/zOJ1A2hhZc+IMVD6i7w4yjIvh6VyuRI0gRtxAsXtoC35uGMaQ9rjI2zJaXDEAw==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.20.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -878,6 +1049,28 @@ packages: eslint: 8.56.0 dev: true + /eslint-config-standard-with-typescript@43.0.1(@typescript-eslint/eslint-plugin@6.20.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-WfZ986+qzIzX6dcr4yGUyVb/l9N3Z8wPXCc5z/70fljs3UbWhhV+WxrfgsqMToRzuuyX9MqZ974pq2UPhDTOcA==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.4.0 + eslint: ^8.0.1 + eslint-plugin-import: ^2.25.2 + eslint-plugin-n: '^15.0.0 || ^16.0.0 ' + eslint-plugin-promise: ^6.0.0 + typescript: '*' + dependencies: + '@typescript-eslint/eslint-plugin': 6.20.0(@typescript-eslint/parser@6.20.0)(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) + eslint: 8.56.0 + eslint-config-standard: 17.1.0(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.20.0)(eslint@8.56.0) + eslint-plugin-n: 16.6.2(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + typescript: 5.3.3 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-config-standard-with-typescript@43.0.1(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@16.6.2)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3): resolution: {integrity: sha512-WfZ986+qzIzX6dcr4yGUyVb/l9N3Z8wPXCc5z/70fljs3UbWhhV+WxrfgsqMToRzuuyX9MqZ974pq2UPhDTOcA==} peerDependencies: @@ -925,6 +1118,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.20.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.20.0(eslint@8.56.0)(typescript@5.3.3) + debug: 3.2.7 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + dev: true + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -966,6 +1188,41 @@ packages: eslint-compat-utils: 0.1.2(eslint@8.56.0) dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.20.0)(eslint@8.56.0): + resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.20.0(eslint@8.56.0)(typescript@5.3.3) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.56.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.20.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.56.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'}