From 6a1fd2a1b72faf68e05ead5d8faad8b814400c7f Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 11:49:04 +0200 Subject: [PATCH 01/25] docs(workflow): add incremental commit and PR workflow instructions --- .github/copilot-instructions.md | 17 +- AGENTS.md | 17 +- ai-docs/implementation-plan.md | 2142 +++++++++++++++++++++++++++++++ 3 files changed, 2166 insertions(+), 10 deletions(-) create mode 100644 ai-docs/implementation-plan.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0279cc7..63ebd51 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -55,15 +55,22 @@ npm run lint:fix # Auto-fix linting issues ## Important Notes for AI Agents -1. **Remove Demo Code**: The `logistics/` folder and `seeder.ts` are examples. Replace with actual cleaner implementation. +1. **Incremental Commits & PRs**: Always implement changes in small commits grouped into reviewable PRs. After implementing each part: + - Stage files with `git add` + - Stop and wait for user confirmation before committing + - Only commit after explicit user approval + - Use conventional commits: `(): ` + - When enough commits are accumulated (3-5 related commits), STOP and tell user to create PR before continuing -2. **Package Metadata**: Update `package.json` name from `jobnik-worker-boilerplate` to `cleaner`. +2. **Remove Demo Code**: The `logistics/` folder and `seeder.ts` are examples. Replace with actual cleaner implementation. -3. **Type Safety**: Define job/stage types in `src/cleaner/types.ts` and update `worker.ts` with correct generic parameters. +3. **Package Metadata**: Update `package.json` name from `jobnik-worker-boilerplate` to `cleaner`. -4. **Path Aliases**: Use `@common/` for imports from `src/common/`. +4. **Type Safety**: Define job/stage types in `src/cleaner/types.ts` and update `worker.ts` with correct generic parameters. -5. **Endpoints**: `/metrics` (Prometheus), `/liveness` (health check). +5. **Path Aliases**: Use `@common/` for imports from `src/common/`. + +6. **Endpoints**: `/metrics` (Prometheus), `/liveness` (health check). ## Common Tasks diff --git a/AGENTS.md b/AGENTS.md index eef4db8..14dee47 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,15 +55,22 @@ npm run lint:fix # Auto-fix linting issues ## Important Notes for AI Agents -1. **Remove Demo Code**: The `logistics/` folder and `seeder.ts` are examples. Replace with actual cleaner implementation. +1. **Incremental Commits & PRs**: Always implement changes in small commits grouped into reviewable PRs. After implementing each part: + - Stage files with `git add` + - Stop and wait for user confirmation before committing + - Only commit after explicit user approval + - Use conventional commits: `(): ` + - When enough commits are accumulated (3-5 related commits), STOP and tell user to create PR before continuing -2. **Package Metadata**: Update `package.json` name from `jobnik-worker-boilerplate` to `cleaner`. +2. **Remove Demo Code**: The `logistics/` folder and `seeder.ts` are examples. Replace with actual cleaner implementation. -3. **Type Safety**: Define job/stage types in `src/cleaner/types.ts` and update `worker.ts` with correct generic parameters. +3. **Package Metadata**: Update `package.json` name from `jobnik-worker-boilerplate` to `cleaner`. -4. **Path Aliases**: Use `@common/` for imports from `src/common/`. +4. **Type Safety**: Define job/stage types in `src/cleaner/types.ts` and update `worker.ts` with correct generic parameters. -5. **Endpoints**: `/metrics` (Prometheus), `/liveness` (health check). +5. **Path Aliases**: Use `@common/` for imports from `src/common/`. + +6. **Endpoints**: `/metrics` (Prometheus), `/liveness` (health check). ## Common Tasks diff --git a/ai-docs/implementation-plan.md b/ai-docs/implementation-plan.md new file mode 100644 index 0000000..991185b --- /dev/null +++ b/ai-docs/implementation-plan.md @@ -0,0 +1,2142 @@ +# Cleaner Worker Implementation Plan + +## Incremental Commit & PR Workflow + +**IMPORTANT**: This implementation should be done incrementally with small commits grouped into reviewable PRs. + +### Workflow Instructions + +1. **Small Commits**: Implement one logical piece at a time +2. **Stage Files**: After implementing each part, stage the files using `git add` +3. **Review Checkpoint**: Stop and wait for user confirmation before committing +4. **Commit on Approval**: Only commit changes after user explicitly approves +5. **Follow Conventional Commits**: Use the format `(): ` (see `ai-docs/git-workflow.md`) +6. **PR Creation**: When enough commits are accumulated (usually 3-5 related commits), STOP and tell the user it's time to create a PR. Wait for the user to create PR and start a new branch before continuing. + +### Suggested Commit & PR Breakdown + +This plan contains 15 steps. Suggested grouping: + +**PR 1: Foundation** (Steps 1-3) + +- Commit 1: Install dependencies +- Commit 2: Update configuration +- Commit 3: Update constants + +**PR 2: Core Types** (Steps 4-5) + +- Commit 1: Create error classes and tests +- Commit 2: Create types + +**PR 3: Validation Layer** (Step 6) + +- Commit 1: Create validation schemas and validator with tests + +**PR 4: Strategy Pattern** (Step 7) + +- Commit 1: Create strategy interface and tiles deletion strategy +- Commit 2: Create strategy factory +- Commit 3: Add tests for strategies + +**PR 5: Error Handling** (Step 8) + +- Commit 1: Create error handler with tests + +**PR 6: Task Polling** (Step 9) + +- Commit 1: Create TaskPoller with tests + +**PR 7: Integration** (Steps 10-12) + +- Commit 1: Update container config +- Commit 2: Update index.ts +- Commit 3: Remove demo code + +**PR 8: Final Cleanup** (Steps 13-15) + +- Commit 1: Run verification commands +- Commit 2: Fix any issues found + +Or follow the user's preferred breakdown. + +## Overview + +This plan implements a task polling system for the Cleaner worker service using `@map-colonies/mc-priority-queue` instead of `@map-colonies/jobnik-sdk`. The jobnik-sdk code will be commented out (not deleted) for future migration. + +### Architecture Summary + +- **TaskPoller**: Main polling loop - polls job/task pairs from config +- **TaskValidator**: Validates task parameters with Zod schemas +- **StrategyFactory**: Resolves strategy classes by taskType +- **ITaskStrategy**: Interface for task execution strategies +- **TilesDeletionStrategy**: Concrete strategy for tiles-deletion tasks +- **ErrorHandler**: Centralized error handling with metrics/logging +- **Custom Errors**: RecoverableError, UnrecoverableError + +### Design Decisions + +| Aspect | Decision | +| ---------------------- | ------------------------------------ | +| Polling | Round-robin through configured pairs | +| Strategy mapping | By taskType only | +| Strategy instantiation | Factory using tsyringe | +| Validation failure | Reject unrecoverable | +| Error categories | 2 (Recoverable, Unrecoverable) | +| Ack/Reject | Handled in TaskPoller | +| Max attempts | Per-pair in config | + +--- + +## Prerequisites + +- Node.js >= 24.0.0 +- npm >= 10.x + +--- + +## Step 1: Install Dependencies + +Run the following command: + +```bash +npm install @map-colonies/mc-priority-queue @map-colonies/mc-utils zod --legacy-peer-deps +``` + +--- + +## Step 2: Update Configuration + +### File: `config/default.json` + +Replace the entire file with: + +```json +{ + "telemetry": { + "metrics": {}, + "tracing": { + "isEnabled": false + }, + "shared": {}, + "logger": { + "level": "info", + "prettyPrint": false + } + }, + "server": { + "port": 8080 + }, + "queue": { + "jobManagerBaseUrl": "http://job-manager:8080", + "heartbeatBaseUrl": "http://heartbeat:8080", + "heartbeatIntervalMs": 1000 + }, + "polling": { + "dequeueIntervalMs": 3000, + "pairs": [ + { + "jobType": "Ingestion_Update", + "taskType": "tiles-deletion", + "maxAttempts": 3 + } + ] + }, + "httpRetry": { + "attempts": 3, + "delay": "exponential", + "shouldResetTimeout": true + } +} +``` + +--- + +## Step 3: Update Constants + +### File: `src/common/constants.ts` + +Replace the entire file with: + +```typescript +import { readPackageJsonSync } from '@map-colonies/read-pkg'; + +export const SERVICE_NAME = readPackageJsonSync().name ?? 'unknown_worker'; +export const DEFAULT_SERVER_PORT = 8080; + +export const IGNORED_OUTGOING_TRACE_ROUTES = [/^.*\/v1\/metrics.*$/]; +export const IGNORED_INCOMING_TRACE_ROUTES = [/^.*\/docs.*$/]; + +/* eslint-disable @typescript-eslint/naming-convention */ +export const SERVICES = { + LOGGER: Symbol('Logger'), + CONFIG: Symbol('Config'), + TRACER: Symbol('Tracer'), + METRICS: Symbol('METRICS'), + QUEUE_CLIENT: Symbol('QueueClient'), + TASK_POLLER: Symbol('TaskPoller'), + STRATEGY_FACTORY: Symbol('StrategyFactory'), + TASK_VALIDATOR: Symbol('TaskValidator'), + ERROR_HANDLER: Symbol('ErrorHandler'), + // ============================================================================= + // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk + // The tokens below are kept for future migration. + // ============================================================================= + JOBNIK_SDK: Symbol('JobnikSDK'), + WORKER: Symbol('Worker'), +} satisfies Record; +/* eslint-enable @typescript-eslint/naming-convention */ + +// Strategy tokens - add new strategies here +/* eslint-disable @typescript-eslint/naming-convention */ +export const STRATEGY_TOKENS = { + TILES_DELETION: Symbol('TilesDeletionStrategy'), +} satisfies Record; +/* eslint-enable @typescript-eslint/naming-convention */ +``` + +--- + +## Step 4: Create Error Classes + +### File: `src/cleaner/errors/errors.ts` + +Create directory `src/cleaner/errors/` and create file: + +```typescript +/** + * Base class for recoverable errors. + * Tasks that fail with this error will be retried (if attempts < maxAttempts). + */ +export class RecoverableError extends Error { + public readonly isRecoverable = true; + + public constructor( + message: string, + public readonly cause?: Error + ) { + super(message); + this.name = 'RecoverableError'; + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * Base class for unrecoverable errors. + * Tasks that fail with this error will NOT be retried. + */ +export class UnrecoverableError extends Error { + public readonly isRecoverable = false; + + public constructor( + message: string, + public readonly cause?: Error + ) { + super(message); + this.name = 'UnrecoverableError'; + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * Thrown when task parameter validation fails. + * Always unrecoverable - bad data won't fix itself. + */ +export class ValidationError extends UnrecoverableError { + public constructor( + message: string, + public readonly validationErrors?: unknown + ) { + super(message); + this.name = 'ValidationError'; + } +} + +/** + * Thrown when no strategy is found for a task type. + * Always unrecoverable - configuration issue. + */ +export class StrategyNotFoundError extends UnrecoverableError { + public constructor(taskType: string) { + super(`No strategy found for task type: ${taskType}`); + this.name = 'StrategyNotFoundError'; + } +} + +/** + * Type guard to check if an error is recoverable. + */ +export function isRecoverableError(error: unknown): error is RecoverableError { + return error instanceof Error && 'isRecoverable' in error && error.isRecoverable === true; +} + +/** + * Type guard to check if an error is unrecoverable. + */ +export function isUnrecoverableError(error: unknown): error is UnrecoverableError { + return error instanceof Error && 'isRecoverable' in error && error.isRecoverable === false; +} +``` + +### File: `src/cleaner/errors/index.ts` + +```typescript +export { RecoverableError, UnrecoverableError, ValidationError, StrategyNotFoundError, isRecoverableError, isUnrecoverableError } from './errors'; +``` + +### Test File: `tests/errors.spec.ts` + +```typescript +import { describe, it, expect } from 'vitest'; +import { + RecoverableError, + UnrecoverableError, + ValidationError, + StrategyNotFoundError, + isRecoverableError, + isUnrecoverableError, +} from '@src/cleaner/errors'; + +describe('Error Classes', () => { + describe('RecoverableError', () => { + it('should create error with message', () => { + const error = new RecoverableError('Something went wrong'); + + expect(error.message).toBe('Something went wrong'); + expect(error.name).toBe('RecoverableError'); + expect(error.isRecoverable).toBe(true); + }); + + it('should create error with cause', () => { + const cause = new Error('Original error'); + const error = new RecoverableError('Wrapped error', cause); + + expect(error.cause).toBe(cause); + }); + + it('should be instance of Error', () => { + const error = new RecoverableError('Test'); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(RecoverableError); + }); + }); + + describe('UnrecoverableError', () => { + it('should create error with message', () => { + const error = new UnrecoverableError('Fatal error'); + + expect(error.message).toBe('Fatal error'); + expect(error.name).toBe('UnrecoverableError'); + expect(error.isRecoverable).toBe(false); + }); + + it('should create error with cause', () => { + const cause = new Error('Original error'); + const error = new UnrecoverableError('Wrapped error', cause); + + expect(error.cause).toBe(cause); + }); + }); + + describe('ValidationError', () => { + it('should create error with message', () => { + const error = new ValidationError('Invalid parameters'); + + expect(error.message).toBe('Invalid parameters'); + expect(error.name).toBe('ValidationError'); + expect(error.isRecoverable).toBe(false); + }); + + it('should create error with validation errors', () => { + const validationErrors = [{ field: 'path', message: 'required' }]; + const error = new ValidationError('Invalid parameters', validationErrors); + + expect(error.validationErrors).toEqual(validationErrors); + }); + + it('should be instance of UnrecoverableError', () => { + const error = new ValidationError('Test'); + + expect(error).toBeInstanceOf(UnrecoverableError); + }); + }); + + describe('StrategyNotFoundError', () => { + it('should create error with task type in message', () => { + const error = new StrategyNotFoundError('unknown-task'); + + expect(error.message).toBe('No strategy found for task type: unknown-task'); + expect(error.name).toBe('StrategyNotFoundError'); + expect(error.isRecoverable).toBe(false); + }); + }); + + describe('isRecoverableError', () => { + it('should return true for RecoverableError', () => { + const error = new RecoverableError('Test'); + + expect(isRecoverableError(error)).toBe(true); + }); + + it('should return false for UnrecoverableError', () => { + const error = new UnrecoverableError('Test'); + + expect(isRecoverableError(error)).toBe(false); + }); + + it('should return false for regular Error', () => { + const error = new Error('Test'); + + expect(isRecoverableError(error)).toBe(false); + }); + + it('should return false for non-error values', () => { + expect(isRecoverableError(null)).toBe(false); + expect(isRecoverableError(undefined)).toBe(false); + expect(isRecoverableError('string')).toBe(false); + }); + }); + + describe('isUnrecoverableError', () => { + it('should return true for UnrecoverableError', () => { + const error = new UnrecoverableError('Test'); + + expect(isUnrecoverableError(error)).toBe(true); + }); + + it('should return true for ValidationError', () => { + const error = new ValidationError('Test'); + + expect(isUnrecoverableError(error)).toBe(true); + }); + + it('should return false for RecoverableError', () => { + const error = new RecoverableError('Test'); + + expect(isUnrecoverableError(error)).toBe(false); + }); + + it('should return false for regular Error', () => { + const error = new Error('Test'); + + expect(isUnrecoverableError(error)).toBe(false); + }); + }); +}); +``` + +--- + +## Step 5: Create Types + +### File: `src/cleaner/types.ts` + +```typescript +import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; + +/** + * Configuration for a polling job/task pair. + */ +export interface PollingPairConfig { + jobType: string; + taskType: string; + maxAttempts: number; +} + +/** + * Configuration for the polling system. + */ +export interface PollingConfig { + dequeueIntervalMs: number; + pairs: PollingPairConfig[]; +} + +/** + * Configuration for the queue client. + */ +export interface QueueConfig { + jobManagerBaseUrl: string; + heartbeatBaseUrl: string; + heartbeatIntervalMs: number; +} + +/** + * HTTP retry configuration. + */ +export interface HttpRetryConfig { + attempts: number; + delay: 'exponential' | number; + shouldResetTimeout: boolean; +} + +/** + * Context passed to error handler. + */ +export interface ErrorContext { + jobType: string; + taskType: string; + taskId: string; + jobId: string; + attempts: number; + maxAttempts: number; +} + +/** + * Decision made by error handler. + */ +export interface ErrorDecision { + isRecoverable: boolean; + reason: string; + shouldLog: boolean; + logLevel: 'error' | 'warn' | 'info'; +} + +/** + * Generic task response type alias for convenience. + */ +export type TaskResponse = ITaskResponse; +``` + +--- + +## Step 6: Create Validation + +### File: `src/cleaner/validation/schemas.ts` + +```typescript +import { z } from 'zod'; + +/** + * Schema registry - maps taskType to its Zod schema. + * Add new schemas here as new task types are added. + */ +export const taskSchemas: Record = { + // Placeholder schema for tiles-deletion - to be defined later + 'tiles-deletion': z.object({}).passthrough(), +}; + +/** + * Get schema for a task type. + * Returns a passthrough schema if no specific schema is defined. + */ +export function getSchemaForTaskType(taskType: string): z.ZodSchema { + return taskSchemas[taskType] ?? z.object({}).passthrough(); +} +``` + +### File: `src/cleaner/validation/index.ts` + +```typescript +import { injectable } from 'tsyringe'; +import type { z } from 'zod'; +import { ValidationError } from '../errors'; +import { getSchemaForTaskType } from './schemas'; + +@injectable() +export class TaskValidator { + /** + * Validates task parameters against the schema for the given task type. + * @throws {ValidationError} if validation fails + */ + public validate(taskType: string, parameters: unknown): T { + const schema = getSchemaForTaskType(taskType); + + const result = schema.safeParse(parameters); + + if (!result.success) { + throw new ValidationError(`Validation failed for task type "${taskType}": ${result.error.message}`, result.error.errors); + } + + return result.data as T; + } + + /** + * Checks if a schema exists for the given task type. + */ + public hasSchema(taskType: string): boolean { + return taskType in getSchemaForTaskType(taskType); + } + + /** + * Gets the schema for a task type. + */ + public getSchema(taskType: string): z.ZodSchema { + return getSchemaForTaskType(taskType); + } +} + +export { getSchemaForTaskType, taskSchemas } from './schemas'; +``` + +### Test File: `tests/taskValidator.spec.ts` + +```typescript +import { describe, it, expect, beforeEach } from 'vitest'; +import { z } from 'zod'; +import { TaskValidator, taskSchemas } from '@src/cleaner/validation'; +import { ValidationError } from '@src/cleaner/errors'; + +describe('TaskValidator', () => { + let validator: TaskValidator; + + beforeEach(() => { + validator = new TaskValidator(); + }); + + describe('validate', () => { + it('should pass validation for valid parameters with passthrough schema', () => { + const parameters = { anyField: 'anyValue', number: 123 }; + + const result = validator.validate('tiles-deletion', parameters); + + expect(result).toEqual(parameters); + }); + + it('should pass validation for empty object with passthrough schema', () => { + const parameters = {}; + + const result = validator.validate('tiles-deletion', parameters); + + expect(result).toEqual(parameters); + }); + + it('should pass validation for unknown task type with passthrough schema', () => { + const parameters = { foo: 'bar' }; + + const result = validator.validate('unknown-task', parameters); + + expect(result).toEqual(parameters); + }); + + it('should throw ValidationError when validation fails', () => { + // Temporarily add a strict schema for testing + const originalSchema = taskSchemas['tiles-deletion']; + taskSchemas['tiles-deletion'] = z.object({ + requiredField: z.string(), + }); + + try { + expect(() => validator.validate('tiles-deletion', {})).toThrow(ValidationError); + } finally { + // Restore original schema + taskSchemas['tiles-deletion'] = originalSchema; + } + }); + + it('should include validation errors in thrown ValidationError', () => { + const originalSchema = taskSchemas['tiles-deletion']; + taskSchemas['tiles-deletion'] = z.object({ + requiredField: z.string(), + }); + + try { + expect(() => validator.validate('tiles-deletion', {})).toThrow( + expect.objectContaining({ + name: 'ValidationError', + validationErrors: expect.any(Array), + }) + ); + } finally { + taskSchemas['tiles-deletion'] = originalSchema; + } + }); + }); + + describe('getSchema', () => { + it('should return schema for known task type', () => { + const schema = validator.getSchema('tiles-deletion'); + + expect(schema).toBeDefined(); + expect(typeof schema.safeParse).toBe('function'); + }); + + it('should return passthrough schema for unknown task type', () => { + const schema = validator.getSchema('unknown-task'); + + expect(schema).toBeDefined(); + const result = schema.safeParse({ any: 'data' }); + expect(result.success).toBe(true); + }); + }); +}); +``` + +--- + +## Step 7: Create Strategy Interface and TilesDeletionStrategy + +### File: `src/cleaner/strategies/taskStrategy.ts` + +```typescript +import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; + +/** + * Interface for task execution strategies. + * Each task type should have a corresponding strategy implementation. + */ +export interface ITaskStrategy { + /** + * Executes the task logic. + * @param task - The task to execute + * @throws {RecoverableError} for transient failures that should be retried + * @throws {UnrecoverableError} for permanent failures that should not be retried + */ + execute(task: ITaskResponse): Promise; +} +``` + +### File: `src/cleaner/strategies/tilesDeletionStrategy.ts` + +```typescript +import type { Logger } from '@map-colonies/js-logger'; +import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; +import { inject, injectable } from 'tsyringe'; +import { SERVICES } from '@common/constants'; +import type { ITaskStrategy } from './taskStrategy'; + +/** + * Strategy for handling tiles-deletion tasks. + * Deletes map tiles before updating to lower resolution. + */ +@injectable() +export class TilesDeletionStrategy implements ITaskStrategy { + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} + + /** + * Executes the tiles deletion task. + * @param task - The task containing deletion parameters + */ + public async execute(task: ITaskResponse): Promise { + this.logger.info({ + msg: 'Executing tiles deletion strategy', + taskId: task.id, + jobId: task.jobId, + parameters: task.parameters, + }); + + // TODO: Implement actual tiles deletion logic + // 1. Parse and validate tile coordinates/paths from task.parameters + // 2. Connect to tile storage + // 3. Delete tiles + // 4. Verify deletion + + this.logger.info({ + msg: 'Tiles deletion completed', + taskId: task.id, + jobId: task.jobId, + }); + } +} +``` + +### File: `src/cleaner/strategies/index.ts` + +```typescript +import type { DependencyContainer } from 'tsyringe'; +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { SERVICES, STRATEGY_TOKENS } from '@common/constants'; +import { StrategyNotFoundError } from '../errors'; +import type { ITaskStrategy } from './taskStrategy'; +import { TilesDeletionStrategy } from './tilesDeletionStrategy'; + +/** + * Maps task types to their strategy tokens. + * Add new mappings here when adding new strategies. + */ +const TASK_TYPE_TO_STRATEGY: Record = { + 'tiles-deletion': STRATEGY_TOKENS.TILES_DELETION, +}; + +/** + * Factory for resolving task strategies by task type. + */ +@injectable() +export class StrategyFactory { + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject('container') private readonly container: DependencyContainer + ) {} + + /** + * Gets the strategy for a given task type. + * @param taskType - The type of task + * @returns The strategy instance + * @throws {StrategyNotFoundError} if no strategy is registered for the task type + */ + public getStrategy(taskType: string): ITaskStrategy { + const strategyToken = TASK_TYPE_TO_STRATEGY[taskType]; + + if (!strategyToken) { + this.logger.error({ msg: 'No strategy found for task type', taskType }); + throw new StrategyNotFoundError(taskType); + } + + return this.container.resolve(strategyToken); + } + + /** + * Checks if a strategy exists for the given task type. + */ + public hasStrategy(taskType: string): boolean { + return taskType in TASK_TYPE_TO_STRATEGY; + } + + /** + * Gets all registered task types. + */ + public getRegisteredTaskTypes(): string[] { + return Object.keys(TASK_TYPE_TO_STRATEGY); + } +} + +export { ITaskStrategy } from './taskStrategy'; +export { TilesDeletionStrategy } from './tilesDeletionStrategy'; +``` + +### Test File: `tests/tilesDeletionStrategy.spec.ts` + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; +import { TilesDeletionStrategy } from '@src/cleaner/strategies/tilesDeletionStrategy'; + +describe('TilesDeletionStrategy', () => { + let strategy: TilesDeletionStrategy; + let mockLogger: { + info: ReturnType; + error: ReturnType; + warn: ReturnType; + debug: ReturnType; + }; + + beforeEach(() => { + mockLogger = { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }; + strategy = new TilesDeletionStrategy(mockLogger as never); + }); + + describe('execute', () => { + it('should log start and completion of task', async () => { + const task: ITaskResponse = { + id: 'task-123', + jobId: 'job-456', + parameters: { path: '/tiles/layer1' }, + type: 'tiles-deletion', + description: 'Delete tiles', + created: new Date().toISOString(), + updated: new Date().toISOString(), + status: 'In-Progress', + attempts: 0, + reason: '', + resettable: true, + }; + + await strategy.execute(task); + + expect(mockLogger.info).toHaveBeenCalledTimes(2); + expect(mockLogger.info).toHaveBeenCalledWith( + expect.objectContaining({ + msg: 'Executing tiles deletion strategy', + taskId: 'task-123', + jobId: 'job-456', + }) + ); + expect(mockLogger.info).toHaveBeenCalledWith( + expect.objectContaining({ + msg: 'Tiles deletion completed', + taskId: 'task-123', + jobId: 'job-456', + }) + ); + }); + + it('should complete without throwing for valid task', async () => { + const task: ITaskResponse = { + id: 'task-123', + jobId: 'job-456', + parameters: {}, + type: 'tiles-deletion', + description: 'Delete tiles', + created: new Date().toISOString(), + updated: new Date().toISOString(), + status: 'In-Progress', + attempts: 0, + reason: '', + resettable: true, + }; + + await expect(strategy.execute(task)).resolves.toBeUndefined(); + }); + }); +}); +``` + +### Test File: `tests/strategyFactory.spec.ts` + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import type { DependencyContainer } from 'tsyringe'; +import { STRATEGY_TOKENS } from '@src/common/constants'; +import { StrategyFactory } from '@src/cleaner/strategies'; +import { StrategyNotFoundError } from '@src/cleaner/errors'; +import { TilesDeletionStrategy } from '@src/cleaner/strategies/tilesDeletionStrategy'; + +describe('StrategyFactory', () => { + let factory: StrategyFactory; + let mockLogger: { + info: ReturnType; + error: ReturnType; + warn: ReturnType; + debug: ReturnType; + }; + let mockContainer: { + resolve: ReturnType; + }; + let mockStrategy: TilesDeletionStrategy; + + beforeEach(() => { + mockLogger = { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }; + + mockStrategy = new TilesDeletionStrategy(mockLogger as never); + + mockContainer = { + resolve: vi.fn().mockReturnValue(mockStrategy), + }; + + factory = new StrategyFactory(mockLogger as never, mockContainer as unknown as DependencyContainer); + }); + + describe('getStrategy', () => { + it('should return strategy for tiles-deletion task type', () => { + const strategy = factory.getStrategy('tiles-deletion'); + + expect(strategy).toBe(mockStrategy); + expect(mockContainer.resolve).toHaveBeenCalledWith(STRATEGY_TOKENS.TILES_DELETION); + }); + + it('should throw StrategyNotFoundError for unknown task type', () => { + expect(() => factory.getStrategy('unknown-task')).toThrow(StrategyNotFoundError); + expect(() => factory.getStrategy('unknown-task')).toThrow('No strategy found for task type: unknown-task'); + }); + + it('should log error when strategy not found', () => { + try { + factory.getStrategy('unknown-task'); + } catch { + // Expected to throw + } + + expect(mockLogger.error).toHaveBeenCalledWith( + expect.objectContaining({ + msg: 'No strategy found for task type', + taskType: 'unknown-task', + }) + ); + }); + }); + + describe('hasStrategy', () => { + it('should return true for tiles-deletion', () => { + expect(factory.hasStrategy('tiles-deletion')).toBe(true); + }); + + it('should return false for unknown task type', () => { + expect(factory.hasStrategy('unknown-task')).toBe(false); + }); + }); + + describe('getRegisteredTaskTypes', () => { + it('should return array containing tiles-deletion', () => { + const taskTypes = factory.getRegisteredTaskTypes(); + + expect(taskTypes).toContain('tiles-deletion'); + }); + }); +}); +``` + +--- + +## Step 8: Create Error Handler + +### File: `src/cleaner/errors/errorHandler.ts` + +```typescript +import type { Logger } from '@map-colonies/js-logger'; +import type { Registry, Counter } from 'prom-client'; +import { inject, injectable } from 'tsyringe'; +import { SERVICES } from '@common/constants'; +import type { ErrorContext, ErrorDecision } from '../types'; +import { isRecoverableError, isUnrecoverableError } from './errors'; + +@injectable() +export class ErrorHandler { + private taskFailuresCounter?: Counter<'job_type' | 'task_type' | 'recoverable'>; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry + ) { + this.initializeMetrics(); + } + + /** + * Handles an error and returns a decision on how to proceed. + */ + public handle(error: unknown, context: ErrorContext): ErrorDecision { + const isRecoverable = this.determineRecoverability(error, context); + const reason = this.formatReason(error); + + const decision: ErrorDecision = { + isRecoverable, + reason, + shouldLog: true, + logLevel: isRecoverable ? 'warn' : 'error', + }; + + this.logError(error, context, decision); + this.recordMetrics(context, isRecoverable); + + return decision; + } + + /** + * Determines if an error is recoverable. + */ + private determineRecoverability(error: unknown, context: ErrorContext): boolean { + // If it's explicitly marked, use that + if (isRecoverableError(error)) { + // But only if we haven't exceeded max attempts + return context.attempts < context.maxAttempts; + } + + if (isUnrecoverableError(error)) { + return false; + } + + // For unknown errors, treat as recoverable if under max attempts + return context.attempts < context.maxAttempts; + } + + /** + * Formats an error into a reason string. + */ + private formatReason(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); + } + + /** + * Logs the error with appropriate context. + */ + private logError(error: unknown, context: ErrorContext, decision: ErrorDecision): void { + const logData = { + msg: 'Task processing failed', + taskId: context.taskId, + jobId: context.jobId, + jobType: context.jobType, + taskType: context.taskType, + attempts: context.attempts, + maxAttempts: context.maxAttempts, + isRecoverable: decision.isRecoverable, + error: error instanceof Error ? { message: error.message, name: error.name, stack: error.stack } : error, + }; + + if (decision.logLevel === 'error') { + this.logger.error(logData); + } else { + this.logger.warn(logData); + } + } + + /** + * Records failure metrics. + */ + private recordMetrics(context: ErrorContext, isRecoverable: boolean): void { + this.taskFailuresCounter?.inc({ + job_type: context.jobType, + task_type: context.taskType, + recoverable: String(isRecoverable), + }); + } + + /** + * Initializes Prometheus metrics. + */ + private initializeMetrics(): void { + try { + this.taskFailuresCounter = new (require('prom-client').Counter)({ + name: 'cleaner_task_failures_total', + help: 'Total number of task failures', + labelNames: ['job_type', 'task_type', 'recoverable'], + registers: [this.metricsRegistry], + }); + } catch { + // Metrics may already be registered in tests + } + } +} + +export { ErrorHandler }; +``` + +### Update: `src/cleaner/errors/index.ts` + +Replace with: + +```typescript +export { RecoverableError, UnrecoverableError, ValidationError, StrategyNotFoundError, isRecoverableError, isUnrecoverableError } from './errors'; +export { ErrorHandler } from './errorHandler'; +``` + +### Test File: `tests/errorHandler.spec.ts` + +```typescript +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { Registry } from 'prom-client'; +import { ErrorHandler } from '@src/cleaner/errors/errorHandler'; +import { RecoverableError, UnrecoverableError, ValidationError } from '@src/cleaner/errors'; +import type { ErrorContext } from '@src/cleaner/types'; + +describe('ErrorHandler', () => { + let errorHandler: ErrorHandler; + let mockLogger: { + info: ReturnType; + error: ReturnType; + warn: ReturnType; + debug: ReturnType; + }; + let metricsRegistry: Registry; + + const createContext = (overrides: Partial = {}): ErrorContext => ({ + jobType: 'Ingestion_Update', + taskType: 'tiles-deletion', + taskId: 'task-123', + jobId: 'job-456', + attempts: 1, + maxAttempts: 3, + ...overrides, + }); + + beforeEach(() => { + mockLogger = { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }; + metricsRegistry = new Registry(); + errorHandler = new ErrorHandler(mockLogger as never, metricsRegistry); + }); + + describe('handle', () => { + describe('RecoverableError', () => { + it('should return recoverable decision when attempts < maxAttempts', () => { + const error = new RecoverableError('Transient failure'); + const context = createContext({ attempts: 1, maxAttempts: 3 }); + + const decision = errorHandler.handle(error, context); + + expect(decision.isRecoverable).toBe(true); + expect(decision.reason).toBe('Transient failure'); + expect(decision.logLevel).toBe('warn'); + }); + + it('should return unrecoverable decision when attempts >= maxAttempts', () => { + const error = new RecoverableError('Transient failure'); + const context = createContext({ attempts: 3, maxAttempts: 3 }); + + const decision = errorHandler.handle(error, context); + + expect(decision.isRecoverable).toBe(false); + }); + }); + + describe('UnrecoverableError', () => { + it('should always return unrecoverable decision', () => { + const error = new UnrecoverableError('Fatal failure'); + const context = createContext({ attempts: 1, maxAttempts: 3 }); + + const decision = errorHandler.handle(error, context); + + expect(decision.isRecoverable).toBe(false); + expect(decision.reason).toBe('Fatal failure'); + expect(decision.logLevel).toBe('error'); + }); + }); + + describe('ValidationError', () => { + it('should return unrecoverable decision', () => { + const error = new ValidationError('Invalid parameters'); + const context = createContext(); + + const decision = errorHandler.handle(error, context); + + expect(decision.isRecoverable).toBe(false); + expect(decision.reason).toBe('Invalid parameters'); + }); + }); + + describe('Regular Error', () => { + it('should return recoverable when under max attempts', () => { + const error = new Error('Unknown error'); + const context = createContext({ attempts: 1, maxAttempts: 3 }); + + const decision = errorHandler.handle(error, context); + + expect(decision.isRecoverable).toBe(true); + }); + + it('should return unrecoverable when at max attempts', () => { + const error = new Error('Unknown error'); + const context = createContext({ attempts: 3, maxAttempts: 3 }); + + const decision = errorHandler.handle(error, context); + + expect(decision.isRecoverable).toBe(false); + }); + }); + + describe('Logging', () => { + it('should log warning for recoverable errors', () => { + const error = new RecoverableError('Transient'); + const context = createContext(); + + errorHandler.handle(error, context); + + expect(mockLogger.warn).toHaveBeenCalled(); + expect(mockLogger.error).not.toHaveBeenCalled(); + }); + + it('should log error for unrecoverable errors', () => { + const error = new UnrecoverableError('Fatal'); + const context = createContext(); + + errorHandler.handle(error, context); + + expect(mockLogger.error).toHaveBeenCalled(); + expect(mockLogger.warn).not.toHaveBeenCalled(); + }); + + it('should include context in log', () => { + const error = new Error('Test'); + const context = createContext(); + + errorHandler.handle(error, context); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + taskId: context.taskId, + jobId: context.jobId, + jobType: context.jobType, + taskType: context.taskType, + attempts: context.attempts, + maxAttempts: context.maxAttempts, + }) + ); + }); + }); + + describe('Non-Error values', () => { + it('should handle string errors', () => { + const context = createContext(); + + const decision = errorHandler.handle('String error', context); + + expect(decision.reason).toBe('String error'); + }); + + it('should handle null errors', () => { + const context = createContext(); + + const decision = errorHandler.handle(null, context); + + expect(decision.reason).toBe('null'); + }); + }); + }); +}); +``` + +--- + +## Step 9: Create TaskPoller + +### File: `src/cleaner/taskPoller.ts` + +```typescript +import { setTimeout as setTimeoutPromise } from 'node:timers/promises'; +import type { Logger } from '@map-colonies/js-logger'; +import type { TaskHandler as QueueClient, ITaskResponse } from '@map-colonies/mc-priority-queue'; +import { inject, injectable } from 'tsyringe'; +import { SERVICES } from '@common/constants'; +import type { ConfigType } from '@common/config'; +import type { PollingConfig, PollingPairConfig, ErrorContext } from './types'; +import { TaskValidator } from './validation'; +import { StrategyFactory } from './strategies'; +import { ErrorHandler, ValidationError } from './errors'; + +@injectable() +export class TaskPoller { + private isRunning = false; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.QUEUE_CLIENT) private readonly queueClient: QueueClient, + private readonly taskValidator: TaskValidator, + private readonly strategyFactory: StrategyFactory, + private readonly errorHandler: ErrorHandler + ) {} + + /** + * Starts the polling loop. + * Polls all configured job/task pairs in round-robin fashion. + */ + public async start(): Promise { + const pollingConfig = this.config.get('polling'); + const { pairs, dequeueIntervalMs } = pollingConfig; + + this.logger.info({ + msg: 'Starting task poller', + pairs: pairs.map((p) => `${p.jobType}:${p.taskType}`), + dequeueIntervalMs, + }); + + this.isRunning = true; + + while (this.isRunning) { + await this.pollAllPairs(pairs, dequeueIntervalMs); + } + + this.logger.info({ msg: 'Task poller stopped' }); + } + + /** + * Stops the polling loop gracefully. + */ + public stop(): void { + this.logger.info({ msg: 'Stopping task poller' }); + this.isRunning = false; + } + + /** + * Returns whether the poller is currently running. + */ + public getIsRunning(): boolean { + return this.isRunning; + } + + /** + * Polls all configured pairs in round-robin fashion. + */ + private async pollAllPairs(pairs: PollingPairConfig[], dequeueIntervalMs: number): Promise { + let taskProcessed = false; + + for (const pair of pairs) { + if (!this.isRunning) { + break; + } + + const processed = await this.pollPair(pair); + if (processed) { + taskProcessed = true; + } + } + + // If no task was processed in this round, wait before next round + if (!taskProcessed) { + await setTimeoutPromise(dequeueIntervalMs); + } + } + + /** + * Polls a single job/task pair. + * @returns true if a task was processed, false otherwise + */ + private async pollPair(pair: PollingPairConfig): Promise { + const { jobType, taskType, maxAttempts } = pair; + + try { + const task = await this.queueClient.dequeue(jobType, taskType); + + if (!task) { + return false; + } + + await this.processTask(task, pair); + return true; + } catch (error) { + // Log polling errors but don't crash the loop + this.logger.error({ + msg: 'Error polling for tasks', + jobType, + taskType, + error: error instanceof Error ? error.message : error, + }); + return false; + } + } + + /** + * Processes a single task. + */ + private async processTask(task: ITaskResponse, pair: PollingPairConfig): Promise { + const { jobType, taskType, maxAttempts } = pair; + const { id: taskId, jobId, attempts } = task; + + this.logger.info({ + msg: 'Processing task', + taskId, + jobId, + jobType, + taskType, + attempts, + }); + + try { + // Validate task parameters + const validatedParams = this.taskValidator.validate(taskType, task.parameters); + + // Get and execute strategy + const strategy = this.strategyFactory.getStrategy(taskType); + await strategy.execute({ ...task, parameters: validatedParams }); + + // Acknowledge successful completion + await this.queueClient.ack(jobId, taskId); + + this.logger.info({ + msg: 'Task completed successfully', + taskId, + jobId, + jobType, + taskType, + }); + } catch (error) { + await this.handleTaskError(error, task, pair); + } + } + + /** + * Handles task processing errors. + */ + private async handleTaskError(error: unknown, task: ITaskResponse, pair: PollingPairConfig): Promise { + const { jobType, taskType, maxAttempts } = pair; + const { id: taskId, jobId, attempts } = task; + + const context: ErrorContext = { + jobType, + taskType, + taskId, + jobId, + attempts, + maxAttempts, + }; + + const decision = this.errorHandler.handle(error, context); + + await this.queueClient.reject(jobId, taskId, decision.isRecoverable, decision.reason); + } +} +``` + +### Test File: `tests/taskPoller.spec.ts` + +```typescript +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import type { TaskHandler as QueueClient, ITaskResponse } from '@map-colonies/mc-priority-queue'; +import { TaskPoller } from '@src/cleaner/taskPoller'; +import { TaskValidator } from '@src/cleaner/validation'; +import { StrategyFactory, type ITaskStrategy } from '@src/cleaner/strategies'; +import { ErrorHandler, ValidationError, StrategyNotFoundError } from '@src/cleaner/errors'; +import type { PollingConfig } from '@src/cleaner/types'; + +describe('TaskPoller', () => { + let taskPoller: TaskPoller; + let mockLogger: { + info: ReturnType; + error: ReturnType; + warn: ReturnType; + debug: ReturnType; + }; + let mockConfig: { + get: ReturnType; + }; + let mockQueueClient: { + dequeue: ReturnType; + ack: ReturnType; + reject: ReturnType; + }; + let mockTaskValidator: { + validate: ReturnType; + }; + let mockStrategyFactory: { + getStrategy: ReturnType; + }; + let mockErrorHandler: { + handle: ReturnType; + }; + let mockStrategy: { + execute: ReturnType; + }; + + const pollingConfig: PollingConfig = { + dequeueIntervalMs: 100, + pairs: [{ jobType: 'Ingestion_Update', taskType: 'tiles-deletion', maxAttempts: 3 }], + }; + + const createMockTask = (overrides: Partial> = {}): ITaskResponse => ({ + id: 'task-123', + jobId: 'job-456', + parameters: { path: '/tiles' }, + type: 'tiles-deletion', + description: 'Delete tiles', + created: new Date().toISOString(), + updated: new Date().toISOString(), + status: 'In-Progress', + attempts: 1, + reason: '', + resettable: true, + ...overrides, + }); + + beforeEach(() => { + mockLogger = { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + }; + + mockConfig = { + get: vi.fn().mockReturnValue(pollingConfig), + }; + + mockQueueClient = { + dequeue: vi.fn(), + ack: vi.fn(), + reject: vi.fn(), + }; + + mockTaskValidator = { + validate: vi.fn().mockImplementation((_, params) => params), + }; + + mockStrategy = { + execute: vi.fn().mockResolvedValue(undefined), + }; + + mockStrategyFactory = { + getStrategy: vi.fn().mockReturnValue(mockStrategy), + }; + + mockErrorHandler = { + handle: vi.fn().mockReturnValue({ + isRecoverable: true, + reason: 'Error message', + shouldLog: true, + logLevel: 'warn', + }), + }; + + taskPoller = new TaskPoller( + mockLogger as never, + mockConfig as never, + mockQueueClient as unknown as QueueClient, + mockTaskValidator as unknown as TaskValidator, + mockStrategyFactory as unknown as StrategyFactory, + mockErrorHandler as unknown as ErrorHandler + ); + }); + + afterEach(() => { + taskPoller.stop(); + }); + + describe('start', () => { + it('should log start message with configured pairs', async () => { + mockQueueClient.dequeue.mockResolvedValue(null); + + const startPromise = taskPoller.start(); + // Let one iteration run + await vi.waitFor(() => expect(mockQueueClient.dequeue).toHaveBeenCalled()); + taskPoller.stop(); + await startPromise; + + expect(mockLogger.info).toHaveBeenCalledWith( + expect.objectContaining({ + msg: 'Starting task poller', + pairs: ['Ingestion_Update:tiles-deletion'], + }) + ); + }); + + it('should poll configured pairs', async () => { + mockQueueClient.dequeue.mockResolvedValue(null); + + const startPromise = taskPoller.start(); + await vi.waitFor(() => expect(mockQueueClient.dequeue).toHaveBeenCalled()); + taskPoller.stop(); + await startPromise; + + expect(mockQueueClient.dequeue).toHaveBeenCalledWith('Ingestion_Update', 'tiles-deletion'); + }); + }); + + describe('stop', () => { + it('should stop the polling loop', async () => { + mockQueueClient.dequeue.mockResolvedValue(null); + + const startPromise = taskPoller.start(); + await vi.waitFor(() => expect(taskPoller.getIsRunning()).toBe(true)); + + taskPoller.stop(); + await startPromise; + + expect(taskPoller.getIsRunning()).toBe(false); + expect(mockLogger.info).toHaveBeenCalledWith(expect.objectContaining({ msg: 'Task poller stopped' })); + }); + }); + + describe('task processing', () => { + it('should process task successfully', async () => { + const task = createMockTask(); + mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); + + const startPromise = taskPoller.start(); + await vi.waitFor(() => expect(mockQueueClient.ack).toHaveBeenCalled()); + taskPoller.stop(); + await startPromise; + + expect(mockTaskValidator.validate).toHaveBeenCalledWith('tiles-deletion', task.parameters); + expect(mockStrategyFactory.getStrategy).toHaveBeenCalledWith('tiles-deletion'); + expect(mockStrategy.execute).toHaveBeenCalled(); + expect(mockQueueClient.ack).toHaveBeenCalledWith('job-456', 'task-123'); + }); + + it('should reject task on validation error', async () => { + const task = createMockTask(); + mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); + mockTaskValidator.validate.mockImplementation(() => { + throw new ValidationError('Invalid params'); + }); + mockErrorHandler.handle.mockReturnValue({ + isRecoverable: false, + reason: 'Invalid params', + shouldLog: true, + logLevel: 'error', + }); + + const startPromise = taskPoller.start(); + await vi.waitFor(() => expect(mockQueueClient.reject).toHaveBeenCalled()); + taskPoller.stop(); + await startPromise; + + expect(mockQueueClient.reject).toHaveBeenCalledWith('job-456', 'task-123', false, 'Invalid params'); + }); + + it('should reject task on strategy error', async () => { + const task = createMockTask(); + mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); + mockStrategy.execute.mockRejectedValue(new Error('Strategy failed')); + + const startPromise = taskPoller.start(); + await vi.waitFor(() => expect(mockQueueClient.reject).toHaveBeenCalled()); + taskPoller.stop(); + await startPromise; + + expect(mockErrorHandler.handle).toHaveBeenCalled(); + expect(mockQueueClient.reject).toHaveBeenCalled(); + }); + + it('should reject task on strategy not found', async () => { + const task = createMockTask(); + mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); + mockStrategyFactory.getStrategy.mockImplementation(() => { + throw new StrategyNotFoundError('unknown'); + }); + + const startPromise = taskPoller.start(); + await vi.waitFor(() => expect(mockQueueClient.reject).toHaveBeenCalled()); + taskPoller.stop(); + await startPromise; + + expect(mockErrorHandler.handle).toHaveBeenCalled(); + }); + }); + + describe('getIsRunning', () => { + it('should return false initially', () => { + expect(taskPoller.getIsRunning()).toBe(false); + }); + + it('should return true when running', async () => { + mockQueueClient.dequeue.mockResolvedValue(null); + + const startPromise = taskPoller.start(); + await vi.waitFor(() => expect(taskPoller.getIsRunning()).toBe(true)); + + taskPoller.stop(); + await startPromise; + }); + }); +}); +``` + +--- + +## Step 10: Update containerConfig.ts + +### File: `src/containerConfig.ts` + +Replace the entire file with: + +```typescript +import { getOtelMixin } from '@map-colonies/telemetry'; +import { trace } from '@opentelemetry/api'; +import { Registry } from 'prom-client'; +import { container, instancePerContainerCachingFactory } from 'tsyringe'; +import type { DependencyContainer } from 'tsyringe/dist/typings/types'; +import jsLogger, { type Logger } from '@map-colonies/js-logger'; +import { TaskHandler as QueueClient } from '@map-colonies/mc-priority-queue'; +import { InjectionObject, registerDependencies } from '@common/dependencyRegistration'; +import { SERVICES, SERVICE_NAME, STRATEGY_TOKENS } from '@common/constants'; +import { getTracing } from '@common/tracing'; +import { type ConfigType, getConfig } from './common/config'; +import type { QueueConfig, HttpRetryConfig } from './cleaner/types'; +import { TaskPoller } from './cleaner/taskPoller'; +import { TaskValidator } from './cleaner/validation'; +import { StrategyFactory, TilesDeletionStrategy } from './cleaner/strategies'; +import { ErrorHandler } from './cleaner/errors'; + +// ============================================================================= +// TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk +// The imports below are commented out until the migration is complete. +// ============================================================================= +// import { IWorker, JobnikSDK } from '@map-colonies/jobnik-sdk'; +// import { workerBuilder } from './worker'; +// import { LogisticJobTypes, LogisticStageTypes } from './logistics/types'; + +export interface RegisterOptions { + override?: InjectionObject[]; + useChild?: boolean; +} + +export const registerExternalValues = async (options?: RegisterOptions): Promise => { + const configInstance = getConfig(); + + const loggerConfig = configInstance.get('telemetry.logger'); + + const logger = jsLogger({ ...loggerConfig, prettyPrint: loggerConfig.prettyPrint, mixin: getOtelMixin() }); + + const tracer = trace.getTracer(SERVICE_NAME); + const metricsRegistry = new Registry(); + configInstance.initializeMetrics(metricsRegistry); + + const dependencies: InjectionObject[] = [ + { token: SERVICES.CONFIG, provider: { useValue: configInstance } }, + { token: SERVICES.LOGGER, provider: { useValue: logger } }, + { token: SERVICES.TRACER, provider: { useValue: tracer } }, + { token: SERVICES.METRICS, provider: { useValue: metricsRegistry } }, + + // Queue Client (mc-priority-queue) + { + token: SERVICES.QUEUE_CLIENT, + provider: { + useFactory: instancePerContainerCachingFactory((depContainer) => { + const containerLogger = depContainer.resolve(SERVICES.LOGGER); + const config = depContainer.resolve(SERVICES.CONFIG); + const queueConfig = config.get('queue'); + const httpRetryConfig = config.get('httpRetry'); + + return new QueueClient( + containerLogger, + queueConfig.jobManagerBaseUrl, + queueConfig.heartbeatBaseUrl, + config.get('polling.dequeueIntervalMs'), + queueConfig.heartbeatIntervalMs, + httpRetryConfig + ); + }), + }, + }, + + // Container reference for strategy factory + { token: 'container', provider: { useValue: container } }, + + // Strategies + { token: STRATEGY_TOKENS.TILES_DELETION, provider: { useClass: TilesDeletionStrategy } }, + + // Services + { token: SERVICES.TASK_VALIDATOR, provider: { useClass: TaskValidator } }, + { token: SERVICES.STRATEGY_FACTORY, provider: { useClass: StrategyFactory } }, + { token: SERVICES.ERROR_HANDLER, provider: { useClass: ErrorHandler } }, + { token: SERVICES.TASK_POLLER, provider: { useClass: TaskPoller } }, + + // Graceful shutdown + { + token: 'onSignal', + provider: { + useFactory: (depContainer) => { + const taskPoller = depContainer.resolve(SERVICES.TASK_POLLER); + return async (): Promise => { + taskPoller.stop(); + await getTracing().stop(); + }; + }, + }, + }, + + // ============================================================================= + // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk + // The registrations below are commented out until the migration is complete. + // ============================================================================= + /* + { + token: SERVICES.JOBNIK_SDK, + provider: { + useFactory: instancePerContainerCachingFactory((depContainer) => { + const containerLogger = depContainer.resolve(SERVICES.LOGGER); + const config = depContainer.resolve(SERVICES.CONFIG); + const containerMetricsRegistry = depContainer.resolve(SERVICES.METRICS); + return new JobnikSDK({ + ...config.get('jobnik.sdk'), + logger: containerLogger, + metricsRegistry: containerMetricsRegistry, + }); + }), + }, + }, + { + token: SERVICES.WORKER, + provider: { + useFactory: instancePerContainerCachingFactory(workerBuilder), + }, + }, + */ + ]; + + return Promise.resolve(registerDependencies(dependencies, options?.override, options?.useChild)); +}; +``` + +--- + +## Step 11: Update index.ts + +### File: `src/index.ts` + +Replace the entire file with: + +```typescript +// this import must be called before the first import of tsyringe +import 'reflect-metadata'; +import { createServer } from 'node:http'; +import express from 'express'; +import { metricsMiddleware } from '@map-colonies/telemetry/prom-metrics'; +import { createTerminus } from '@godaddy/terminus'; +import type { Logger } from '@map-colonies/js-logger'; +import { SERVICES } from '@common/constants'; +import type { ConfigType } from '@common/config'; +import { registerExternalValues } from './containerConfig'; +import { TaskPoller } from './cleaner/taskPoller'; + +// ============================================================================= +// TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk +// The imports below are commented out until the migration is complete. +// ============================================================================= +// import type { IWorker } from '@map-colonies/jobnik-sdk'; +// import { LogisticsSDK } from './logistics/types'; +// import { seedData } from './seeder'; + +void registerExternalValues() + .then(async (container) => { + const logger = container.resolve(SERVICES.LOGGER); + const config = container.resolve(SERVICES.CONFIG); + const taskPoller = container.resolve(SERVICES.TASK_POLLER); + + // ============================================================================= + // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk + // The code below is commented out until the migration is complete. + // ============================================================================= + // const worker = container.resolve(SERVICES.WORKER); + // const sdk = container.resolve(SERVICES.JOBNIK_SDK); + // await seedData(sdk.getProducer()); + + const port = config.get('server.port'); + const stubHealthCheck = async (): Promise => Promise.resolve(); + + const app = express(); + + app.use(metricsMiddleware(container.resolve(SERVICES.METRICS), true)); + const server = createTerminus(createServer(app), { + healthChecks: { '/liveness': stubHealthCheck }, + onSignal: container.resolve('onSignal'), + }); + server.listen(port, () => { + logger.info(`app started on port ${port}`); + }); + + // Start the task poller (replaces worker.start() from jobnik-sdk) + await taskPoller.start(); + + // ============================================================================= + // TODO: When we move to the new job-manager, replace taskPoller.start() with: + // await worker.start(); + // ============================================================================= + }) + .catch((error: Error) => { + console.error('failed initializing the worker'); + console.error(error); + process.exit(1); + }); +``` + +--- + +## Step 12: Comment out worker.ts + +### File: `src/worker.ts` + +Replace the entire file with: + +```typescript +// ============================================================================= +// TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk +// The code below is commented out until the migration is complete. +// Uncomment and update when migrating to the new job-manager. +// ============================================================================= + +/* +import type { IWorker } from '@map-colonies/jobnik-sdk'; +import type { Logger } from '@map-colonies/js-logger'; +import type { DependencyContainer, FactoryFunction } from 'tsyringe'; +import { SERVICES } from './common/constants'; +import { LogisticsManager } from './logistics/manager'; +import { LogisticsSDK } from './logistics/types'; +import { ConfigType } from './common/config'; + +export const workerBuilder: FactoryFunction = (container: DependencyContainer) => { + const sdk = container.resolve(SERVICES.JOBNIK_SDK); + const logger = container.resolve(SERVICES.LOGGER); + const config = container.resolve(SERVICES.CONFIG); + + const logisticsManager = container.resolve(LogisticsManager); + + const worker = sdk.createWorker<'hazmatTransport', 'delivery'>( + 'delivery', + logisticsManager.handleDeliveryTask.bind(logisticsManager), + config.get('jobnik.worker') + ); + + worker.on('error', (err) => { + logger.error({ msg: 'Worker encountered an error:', err }); + }); + + return worker; +}; +*/ + +export {}; +``` + +--- + +## Step 13: Delete Old Files + +Delete the following files and directories: + +```bash +rm -rf src/logistics +rm -f src/seeder.ts +rm -f tests/logistics.spec.ts +``` + +--- + +## Step 14: Update Test Helpers + +### File: `tests/helpers/fakes.ts` + +Replace the entire file with: + +```typescript +import { simpleFaker } from '@faker-js/faker'; +import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; +import { vitest } from 'vitest'; +import jsLogger from '@map-colonies/js-logger'; + +/** + * Creates a mock task response for testing. + */ +export function createMockTaskResponse(overrides: Partial> = {}): ITaskResponse { + return { + id: simpleFaker.string.uuid(), + jobId: simpleFaker.string.uuid(), + type: 'tiles-deletion', + description: 'Test task', + parameters: {} as T, + created: new Date().toISOString(), + updated: new Date().toISOString(), + status: 'In-Progress', + attempts: 0, + reason: '', + resettable: true, + ...overrides, + }; +} + +/** + * Creates a mock logger for testing. + */ +export function createMockLogger() { + return { + info: vitest.fn(), + error: vitest.fn(), + warn: vitest.fn(), + debug: vitest.fn(), + trace: vitest.fn(), + fatal: vitest.fn(), + }; +} + +/** + * Creates a mock queue client for testing. + */ +export function createMockQueueClient() { + return { + dequeue: vitest.fn(), + ack: vitest.fn(), + reject: vitest.fn(), + updateProgress: vitest.fn(), + }; +} + +/** + * Creates a disabled logger for testing. + */ +export function createDisabledLogger() { + return jsLogger({ enabled: false }); +} +``` + +--- + +## Step 15: Create Directory Structure + +Ensure the following directory structure exists: + +``` +src/ + cleaner/ + errors/ + errors.ts + errorHandler.ts + index.ts + strategies/ + index.ts + taskStrategy.ts + tilesDeletionStrategy.ts + validation/ + index.ts + schemas.ts + taskPoller.ts + types.ts +``` + +Create directories: + +```bash +mkdir -p src/cleaner/errors +mkdir -p src/cleaner/strategies +mkdir -p src/cleaner/validation +``` + +--- + +## Verification Steps + +After implementing all steps, run these commands to verify: + +### 1. Install dependencies + +```bash +npm install --legacy-peer-deps +``` + +### 2. Run linting + +```bash +npm run lint +``` + +If there are lint errors, run: + +```bash +npm run lint:fix +``` + +### 3. Run tests + +```bash +npm test +``` + +All tests should pass with 80%+ coverage. + +### 4. Run build + +```bash +npm run build +``` + +Build should complete without errors. + +### 5. Verify file structure + +```bash +find src/cleaner -type f -name "*.ts" | sort +``` + +Expected output: + +``` +src/cleaner/errors/errorHandler.ts +src/cleaner/errors/errors.ts +src/cleaner/errors/index.ts +src/cleaner/strategies/index.ts +src/cleaner/strategies/taskStrategy.ts +src/cleaner/strategies/tilesDeletionStrategy.ts +src/cleaner/taskPoller.ts +src/cleaner/types.ts +src/cleaner/validation/index.ts +src/cleaner/validation/schemas.ts +``` + +--- + +## Summary + +This implementation provides: + +1. **TaskPoller** - Round-robin polling of configured job/task pairs +2. **TaskValidator** - Zod-based validation with registry pattern +3. **StrategyFactory** - tsyringe-based strategy resolution +4. **TilesDeletionStrategy** - Skeleton strategy for tiles deletion +5. **ErrorHandler** - Centralized error handling with metrics +6. **Custom Errors** - RecoverableError, UnrecoverableError, ValidationError +7. **Full test coverage** - Unit tests for all components + +The jobnik-sdk code is preserved in comments for future migration. From 969b7e24cf903976bddff8fd7c96f4b942609164 Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 14:28:39 +0200 Subject: [PATCH 02/25] feat(config): add queue, polling, and HTTP retry configuration - Add configuration sync across all deployment levels (default.json, env vars, Helm) - Create complete custom-environment-variables.json with all config mappings - Add helm/local.yaml for local development overrides - Update Helm chart values and ConfigMap template - Add config/local.json example for local development (gitignored) - Document configuration sync requirement in common-patterns.md - Update gitignore to exclude local.json --- .gitignore | 3 + ai-docs/common-patterns.md | 78 ++++++++++++++++++++++-- config/custom-environment-variables.json | 27 ++++++++ config/default.json | 20 ++++++ helm/local.yaml | 14 +++++ helm/templates/configmap.yaml | 13 ++++ helm/values.yaml | 10 +++ 7 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 config/custom-environment-variables.json create mode 100644 helm/local.yaml diff --git a/.gitignore b/.gitignore index 606515f..085e231 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,9 @@ typings/ .env.test config.json +# Local configuration overrides +config/local.json + # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/ai-docs/common-patterns.md b/ai-docs/common-patterns.md index 84008af..a052aea 100644 --- a/ai-docs/common-patterns.md +++ b/ai-docs/common-patterns.md @@ -170,13 +170,81 @@ export class MyService { ``` config/ - default.json # Base config (always loaded) - development.json # Merged when NODE_ENV=development - production.json # Merged when NODE_ENV=production - test.json # Merged when NODE_ENV=test - local.json # Local overrides (gitignored) + default.json # Base config (always loaded) + development.json # Merged when NODE_ENV=development + production.json # Merged when NODE_ENV=production + test.json # Merged when NODE_ENV=test + local.json # Local overrides (gitignored) + custom-environment-variables.json # Environment variable mappings +helm/ + values.yaml # Helm chart default values + local.yaml # Local Helm overrides (gitignored) + templates/configmap.yaml # ConfigMap template ``` +### Adding New Configuration Values + +**IMPORTANT**: When adding new configuration values, you MUST update ALL configuration files to maintain sync across all deployment levels: + +1. **`config/default.json`** - Add the configuration with default values +2. **`config/custom-environment-variables.json`** - Add environment variable mappings +3. **`helm/values.yaml`** - Add Helm chart values +4. **`helm/values-local.yaml`** - Add local development values (if needed) +5. **`helm/templates/configmap.yaml`** - Add to ConfigMap template + +**Example:** + +Adding a new `queue.jobManagerBaseUrl` configuration: + +**1. config/default.json** + +```json +{ + "queue": { + "jobManagerBaseUrl": "http://job-manager:8080" + } +} +``` + +**2. config/custom-environment-variables.json** + +```json +{ + "queue": { + "jobManagerBaseUrl": "QUEUE_JOB_MANAGER_BASE_URL" + } +} +``` + +**3. helm/values.yaml** + +```yaml +env: + queue: + jobManagerBaseUrl: 'http://job-manager:8080' +``` + +**4. helm/local.yaml** (for local development) + +```yaml +env: + queue: + jobManagerBaseUrl: 'http://localhost:8080' +``` + +**5. helm/templates/configmap.yaml** + +```yaml +data: + QUEUE_JOB_MANAGER_BASE_URL: { { .Values.env.queue.jobManagerBaseUrl | quote } } +``` + +This ensures configuration works correctly in: + +- Local development (default.json, local.json) +- CI/CD environments (environment variables) +- Kubernetes deployments (Helm charts) + ## Error Handling ### In Task Handlers diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json new file mode 100644 index 0000000..9e92c20 --- /dev/null +++ b/config/custom-environment-variables.json @@ -0,0 +1,27 @@ +{ + "telemetry": { + "tracing": { + "isEnabled": "TELEMETRY_TRACING_ENABLED" + }, + "logger": { + "level": "LOG_LEVEL", + "prettyPrint": "LOG_PRETTY_PRINT_ENABLED" + } + }, + "server": { + "port": "SERVER_PORT" + }, + "queue": { + "jobManagerBaseUrl": "QUEUE_JOB_MANAGER_BASE_URL", + "heartbeatBaseUrl": "QUEUE_HEARTBEAT_BASE_URL", + "heartbeatIntervalMs": "QUEUE_HEARTBEAT_INTERVAL_MS" + }, + "polling": { + "dequeueIntervalMs": "POLLING_DEQUEUE_INTERVAL_MS" + }, + "httpRetry": { + "attempts": "HTTP_RETRY_ATTEMPTS", + "delay": "HTTP_RETRY_DELAY", + "shouldResetTimeout": "HTTP_RETRY_SHOULD_RESET_TIMEOUT" + } +} diff --git a/config/default.json b/config/default.json index 3ed9f26..1c261d5 100644 --- a/config/default.json +++ b/config/default.json @@ -12,5 +12,25 @@ }, "server": { "port": 8080 + }, + "queue": { + "jobManagerBaseUrl": "http://job-manager:8080", + "heartbeatBaseUrl": "http://heartbeat:8080", + "heartbeatIntervalMs": 1000 + }, + "polling": { + "dequeueIntervalMs": 3000, + "pairs": [ + { + "jobType": "Ingestion_Update", + "taskType": "tiles-deletion", + "maxAttempts": 3 + } + ] + }, + "httpRetry": { + "attempts": 3, + "delay": "exponential", + "shouldResetTimeout": true } } diff --git a/helm/local.yaml b/helm/local.yaml new file mode 100644 index 0000000..3145a25 --- /dev/null +++ b/helm/local.yaml @@ -0,0 +1,14 @@ +# Local development overrides +# This file is gitignored and serves as an example for local Helm values + +env: + queue: + jobManagerBaseUrl: "http://localhost:8080" + heartbeatBaseUrl: "http://localhost:8081" + heartbeatIntervalMs: 1000 + polling: + dequeueIntervalMs: 3000 + httpRetry: + attempts: 3 + delay: "exponential" + shouldResetTimeout: true diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 6d3d439..dcb0575 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -18,5 +18,18 @@ data: CONFIG_VERSION: {{ .version | quote }} CONFIG_OFFLINE_MODE: {{ .offlineMode | quote }} CONFIG_SERVER_URL: {{ .serverUrl | quote }} + {{- end }} + {{- with $.Values.env.queue }} + QUEUE_JOB_MANAGER_BASE_URL: {{ .jobManagerBaseUrl | quote }} + QUEUE_HEARTBEAT_BASE_URL: {{ .heartbeatBaseUrl | quote }} + QUEUE_HEARTBEAT_INTERVAL_MS: {{ .heartbeatIntervalMs | quote }} + {{- end }} + {{- with $.Values.env.polling }} + POLLING_DEQUEUE_INTERVAL_MS: {{ .dequeueIntervalMs | quote }} + {{- end }} + {{- with $.Values.env.httpRetry }} + HTTP_RETRY_ATTEMPTS: {{ .attempts | quote }} + HTTP_RETRY_DELAY: {{ .delay | quote }} + HTTP_RETRY_SHOULD_RESET_TIMEOUT: {{ .shouldResetTimeout | quote }} {{- end -}} {{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 6b910b2..1afebc2 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -76,6 +76,16 @@ env: tracing: enabled: false url: http://localhost:55681/v1/trace + queue: + jobManagerBaseUrl: "http://job-manager:8080" + heartbeatBaseUrl: "http://heartbeat:8080" + heartbeatIntervalMs: 1000 + polling: + dequeueIntervalMs: 3000 + httpRetry: + attempts: 3 + delay: "exponential" + shouldResetTimeout: true resources: enabled: true From b419a000dd4f8a2a0a4aa5290721af2a0cee2aea Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 14:33:33 +0200 Subject: [PATCH 03/25] chore(docs): remove implementation-plan.md from git tracking - Add implementation-plan.md to gitignore for local use only - File can be regenerated as needed and doesn't need version control --- .gitignore | 3 + ai-docs/implementation-plan.md | 2142 -------------------------------- 2 files changed, 3 insertions(+), 2142 deletions(-) delete mode 100644 ai-docs/implementation-plan.md diff --git a/.gitignore b/.gitignore index 085e231..9b3797e 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,9 @@ config.json # Local configuration overrides config/local.json +# Local documentation (can be regenerated) +ai-docs/implementation-plan.md + # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/ai-docs/implementation-plan.md b/ai-docs/implementation-plan.md deleted file mode 100644 index 991185b..0000000 --- a/ai-docs/implementation-plan.md +++ /dev/null @@ -1,2142 +0,0 @@ -# Cleaner Worker Implementation Plan - -## Incremental Commit & PR Workflow - -**IMPORTANT**: This implementation should be done incrementally with small commits grouped into reviewable PRs. - -### Workflow Instructions - -1. **Small Commits**: Implement one logical piece at a time -2. **Stage Files**: After implementing each part, stage the files using `git add` -3. **Review Checkpoint**: Stop and wait for user confirmation before committing -4. **Commit on Approval**: Only commit changes after user explicitly approves -5. **Follow Conventional Commits**: Use the format `(): ` (see `ai-docs/git-workflow.md`) -6. **PR Creation**: When enough commits are accumulated (usually 3-5 related commits), STOP and tell the user it's time to create a PR. Wait for the user to create PR and start a new branch before continuing. - -### Suggested Commit & PR Breakdown - -This plan contains 15 steps. Suggested grouping: - -**PR 1: Foundation** (Steps 1-3) - -- Commit 1: Install dependencies -- Commit 2: Update configuration -- Commit 3: Update constants - -**PR 2: Core Types** (Steps 4-5) - -- Commit 1: Create error classes and tests -- Commit 2: Create types - -**PR 3: Validation Layer** (Step 6) - -- Commit 1: Create validation schemas and validator with tests - -**PR 4: Strategy Pattern** (Step 7) - -- Commit 1: Create strategy interface and tiles deletion strategy -- Commit 2: Create strategy factory -- Commit 3: Add tests for strategies - -**PR 5: Error Handling** (Step 8) - -- Commit 1: Create error handler with tests - -**PR 6: Task Polling** (Step 9) - -- Commit 1: Create TaskPoller with tests - -**PR 7: Integration** (Steps 10-12) - -- Commit 1: Update container config -- Commit 2: Update index.ts -- Commit 3: Remove demo code - -**PR 8: Final Cleanup** (Steps 13-15) - -- Commit 1: Run verification commands -- Commit 2: Fix any issues found - -Or follow the user's preferred breakdown. - -## Overview - -This plan implements a task polling system for the Cleaner worker service using `@map-colonies/mc-priority-queue` instead of `@map-colonies/jobnik-sdk`. The jobnik-sdk code will be commented out (not deleted) for future migration. - -### Architecture Summary - -- **TaskPoller**: Main polling loop - polls job/task pairs from config -- **TaskValidator**: Validates task parameters with Zod schemas -- **StrategyFactory**: Resolves strategy classes by taskType -- **ITaskStrategy**: Interface for task execution strategies -- **TilesDeletionStrategy**: Concrete strategy for tiles-deletion tasks -- **ErrorHandler**: Centralized error handling with metrics/logging -- **Custom Errors**: RecoverableError, UnrecoverableError - -### Design Decisions - -| Aspect | Decision | -| ---------------------- | ------------------------------------ | -| Polling | Round-robin through configured pairs | -| Strategy mapping | By taskType only | -| Strategy instantiation | Factory using tsyringe | -| Validation failure | Reject unrecoverable | -| Error categories | 2 (Recoverable, Unrecoverable) | -| Ack/Reject | Handled in TaskPoller | -| Max attempts | Per-pair in config | - ---- - -## Prerequisites - -- Node.js >= 24.0.0 -- npm >= 10.x - ---- - -## Step 1: Install Dependencies - -Run the following command: - -```bash -npm install @map-colonies/mc-priority-queue @map-colonies/mc-utils zod --legacy-peer-deps -``` - ---- - -## Step 2: Update Configuration - -### File: `config/default.json` - -Replace the entire file with: - -```json -{ - "telemetry": { - "metrics": {}, - "tracing": { - "isEnabled": false - }, - "shared": {}, - "logger": { - "level": "info", - "prettyPrint": false - } - }, - "server": { - "port": 8080 - }, - "queue": { - "jobManagerBaseUrl": "http://job-manager:8080", - "heartbeatBaseUrl": "http://heartbeat:8080", - "heartbeatIntervalMs": 1000 - }, - "polling": { - "dequeueIntervalMs": 3000, - "pairs": [ - { - "jobType": "Ingestion_Update", - "taskType": "tiles-deletion", - "maxAttempts": 3 - } - ] - }, - "httpRetry": { - "attempts": 3, - "delay": "exponential", - "shouldResetTimeout": true - } -} -``` - ---- - -## Step 3: Update Constants - -### File: `src/common/constants.ts` - -Replace the entire file with: - -```typescript -import { readPackageJsonSync } from '@map-colonies/read-pkg'; - -export const SERVICE_NAME = readPackageJsonSync().name ?? 'unknown_worker'; -export const DEFAULT_SERVER_PORT = 8080; - -export const IGNORED_OUTGOING_TRACE_ROUTES = [/^.*\/v1\/metrics.*$/]; -export const IGNORED_INCOMING_TRACE_ROUTES = [/^.*\/docs.*$/]; - -/* eslint-disable @typescript-eslint/naming-convention */ -export const SERVICES = { - LOGGER: Symbol('Logger'), - CONFIG: Symbol('Config'), - TRACER: Symbol('Tracer'), - METRICS: Symbol('METRICS'), - QUEUE_CLIENT: Symbol('QueueClient'), - TASK_POLLER: Symbol('TaskPoller'), - STRATEGY_FACTORY: Symbol('StrategyFactory'), - TASK_VALIDATOR: Symbol('TaskValidator'), - ERROR_HANDLER: Symbol('ErrorHandler'), - // ============================================================================= - // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk - // The tokens below are kept for future migration. - // ============================================================================= - JOBNIK_SDK: Symbol('JobnikSDK'), - WORKER: Symbol('Worker'), -} satisfies Record; -/* eslint-enable @typescript-eslint/naming-convention */ - -// Strategy tokens - add new strategies here -/* eslint-disable @typescript-eslint/naming-convention */ -export const STRATEGY_TOKENS = { - TILES_DELETION: Symbol('TilesDeletionStrategy'), -} satisfies Record; -/* eslint-enable @typescript-eslint/naming-convention */ -``` - ---- - -## Step 4: Create Error Classes - -### File: `src/cleaner/errors/errors.ts` - -Create directory `src/cleaner/errors/` and create file: - -```typescript -/** - * Base class for recoverable errors. - * Tasks that fail with this error will be retried (if attempts < maxAttempts). - */ -export class RecoverableError extends Error { - public readonly isRecoverable = true; - - public constructor( - message: string, - public readonly cause?: Error - ) { - super(message); - this.name = 'RecoverableError'; - Error.captureStackTrace(this, this.constructor); - } -} - -/** - * Base class for unrecoverable errors. - * Tasks that fail with this error will NOT be retried. - */ -export class UnrecoverableError extends Error { - public readonly isRecoverable = false; - - public constructor( - message: string, - public readonly cause?: Error - ) { - super(message); - this.name = 'UnrecoverableError'; - Error.captureStackTrace(this, this.constructor); - } -} - -/** - * Thrown when task parameter validation fails. - * Always unrecoverable - bad data won't fix itself. - */ -export class ValidationError extends UnrecoverableError { - public constructor( - message: string, - public readonly validationErrors?: unknown - ) { - super(message); - this.name = 'ValidationError'; - } -} - -/** - * Thrown when no strategy is found for a task type. - * Always unrecoverable - configuration issue. - */ -export class StrategyNotFoundError extends UnrecoverableError { - public constructor(taskType: string) { - super(`No strategy found for task type: ${taskType}`); - this.name = 'StrategyNotFoundError'; - } -} - -/** - * Type guard to check if an error is recoverable. - */ -export function isRecoverableError(error: unknown): error is RecoverableError { - return error instanceof Error && 'isRecoverable' in error && error.isRecoverable === true; -} - -/** - * Type guard to check if an error is unrecoverable. - */ -export function isUnrecoverableError(error: unknown): error is UnrecoverableError { - return error instanceof Error && 'isRecoverable' in error && error.isRecoverable === false; -} -``` - -### File: `src/cleaner/errors/index.ts` - -```typescript -export { RecoverableError, UnrecoverableError, ValidationError, StrategyNotFoundError, isRecoverableError, isUnrecoverableError } from './errors'; -``` - -### Test File: `tests/errors.spec.ts` - -```typescript -import { describe, it, expect } from 'vitest'; -import { - RecoverableError, - UnrecoverableError, - ValidationError, - StrategyNotFoundError, - isRecoverableError, - isUnrecoverableError, -} from '@src/cleaner/errors'; - -describe('Error Classes', () => { - describe('RecoverableError', () => { - it('should create error with message', () => { - const error = new RecoverableError('Something went wrong'); - - expect(error.message).toBe('Something went wrong'); - expect(error.name).toBe('RecoverableError'); - expect(error.isRecoverable).toBe(true); - }); - - it('should create error with cause', () => { - const cause = new Error('Original error'); - const error = new RecoverableError('Wrapped error', cause); - - expect(error.cause).toBe(cause); - }); - - it('should be instance of Error', () => { - const error = new RecoverableError('Test'); - - expect(error).toBeInstanceOf(Error); - expect(error).toBeInstanceOf(RecoverableError); - }); - }); - - describe('UnrecoverableError', () => { - it('should create error with message', () => { - const error = new UnrecoverableError('Fatal error'); - - expect(error.message).toBe('Fatal error'); - expect(error.name).toBe('UnrecoverableError'); - expect(error.isRecoverable).toBe(false); - }); - - it('should create error with cause', () => { - const cause = new Error('Original error'); - const error = new UnrecoverableError('Wrapped error', cause); - - expect(error.cause).toBe(cause); - }); - }); - - describe('ValidationError', () => { - it('should create error with message', () => { - const error = new ValidationError('Invalid parameters'); - - expect(error.message).toBe('Invalid parameters'); - expect(error.name).toBe('ValidationError'); - expect(error.isRecoverable).toBe(false); - }); - - it('should create error with validation errors', () => { - const validationErrors = [{ field: 'path', message: 'required' }]; - const error = new ValidationError('Invalid parameters', validationErrors); - - expect(error.validationErrors).toEqual(validationErrors); - }); - - it('should be instance of UnrecoverableError', () => { - const error = new ValidationError('Test'); - - expect(error).toBeInstanceOf(UnrecoverableError); - }); - }); - - describe('StrategyNotFoundError', () => { - it('should create error with task type in message', () => { - const error = new StrategyNotFoundError('unknown-task'); - - expect(error.message).toBe('No strategy found for task type: unknown-task'); - expect(error.name).toBe('StrategyNotFoundError'); - expect(error.isRecoverable).toBe(false); - }); - }); - - describe('isRecoverableError', () => { - it('should return true for RecoverableError', () => { - const error = new RecoverableError('Test'); - - expect(isRecoverableError(error)).toBe(true); - }); - - it('should return false for UnrecoverableError', () => { - const error = new UnrecoverableError('Test'); - - expect(isRecoverableError(error)).toBe(false); - }); - - it('should return false for regular Error', () => { - const error = new Error('Test'); - - expect(isRecoverableError(error)).toBe(false); - }); - - it('should return false for non-error values', () => { - expect(isRecoverableError(null)).toBe(false); - expect(isRecoverableError(undefined)).toBe(false); - expect(isRecoverableError('string')).toBe(false); - }); - }); - - describe('isUnrecoverableError', () => { - it('should return true for UnrecoverableError', () => { - const error = new UnrecoverableError('Test'); - - expect(isUnrecoverableError(error)).toBe(true); - }); - - it('should return true for ValidationError', () => { - const error = new ValidationError('Test'); - - expect(isUnrecoverableError(error)).toBe(true); - }); - - it('should return false for RecoverableError', () => { - const error = new RecoverableError('Test'); - - expect(isUnrecoverableError(error)).toBe(false); - }); - - it('should return false for regular Error', () => { - const error = new Error('Test'); - - expect(isUnrecoverableError(error)).toBe(false); - }); - }); -}); -``` - ---- - -## Step 5: Create Types - -### File: `src/cleaner/types.ts` - -```typescript -import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; - -/** - * Configuration for a polling job/task pair. - */ -export interface PollingPairConfig { - jobType: string; - taskType: string; - maxAttempts: number; -} - -/** - * Configuration for the polling system. - */ -export interface PollingConfig { - dequeueIntervalMs: number; - pairs: PollingPairConfig[]; -} - -/** - * Configuration for the queue client. - */ -export interface QueueConfig { - jobManagerBaseUrl: string; - heartbeatBaseUrl: string; - heartbeatIntervalMs: number; -} - -/** - * HTTP retry configuration. - */ -export interface HttpRetryConfig { - attempts: number; - delay: 'exponential' | number; - shouldResetTimeout: boolean; -} - -/** - * Context passed to error handler. - */ -export interface ErrorContext { - jobType: string; - taskType: string; - taskId: string; - jobId: string; - attempts: number; - maxAttempts: number; -} - -/** - * Decision made by error handler. - */ -export interface ErrorDecision { - isRecoverable: boolean; - reason: string; - shouldLog: boolean; - logLevel: 'error' | 'warn' | 'info'; -} - -/** - * Generic task response type alias for convenience. - */ -export type TaskResponse = ITaskResponse; -``` - ---- - -## Step 6: Create Validation - -### File: `src/cleaner/validation/schemas.ts` - -```typescript -import { z } from 'zod'; - -/** - * Schema registry - maps taskType to its Zod schema. - * Add new schemas here as new task types are added. - */ -export const taskSchemas: Record = { - // Placeholder schema for tiles-deletion - to be defined later - 'tiles-deletion': z.object({}).passthrough(), -}; - -/** - * Get schema for a task type. - * Returns a passthrough schema if no specific schema is defined. - */ -export function getSchemaForTaskType(taskType: string): z.ZodSchema { - return taskSchemas[taskType] ?? z.object({}).passthrough(); -} -``` - -### File: `src/cleaner/validation/index.ts` - -```typescript -import { injectable } from 'tsyringe'; -import type { z } from 'zod'; -import { ValidationError } from '../errors'; -import { getSchemaForTaskType } from './schemas'; - -@injectable() -export class TaskValidator { - /** - * Validates task parameters against the schema for the given task type. - * @throws {ValidationError} if validation fails - */ - public validate(taskType: string, parameters: unknown): T { - const schema = getSchemaForTaskType(taskType); - - const result = schema.safeParse(parameters); - - if (!result.success) { - throw new ValidationError(`Validation failed for task type "${taskType}": ${result.error.message}`, result.error.errors); - } - - return result.data as T; - } - - /** - * Checks if a schema exists for the given task type. - */ - public hasSchema(taskType: string): boolean { - return taskType in getSchemaForTaskType(taskType); - } - - /** - * Gets the schema for a task type. - */ - public getSchema(taskType: string): z.ZodSchema { - return getSchemaForTaskType(taskType); - } -} - -export { getSchemaForTaskType, taskSchemas } from './schemas'; -``` - -### Test File: `tests/taskValidator.spec.ts` - -```typescript -import { describe, it, expect, beforeEach } from 'vitest'; -import { z } from 'zod'; -import { TaskValidator, taskSchemas } from '@src/cleaner/validation'; -import { ValidationError } from '@src/cleaner/errors'; - -describe('TaskValidator', () => { - let validator: TaskValidator; - - beforeEach(() => { - validator = new TaskValidator(); - }); - - describe('validate', () => { - it('should pass validation for valid parameters with passthrough schema', () => { - const parameters = { anyField: 'anyValue', number: 123 }; - - const result = validator.validate('tiles-deletion', parameters); - - expect(result).toEqual(parameters); - }); - - it('should pass validation for empty object with passthrough schema', () => { - const parameters = {}; - - const result = validator.validate('tiles-deletion', parameters); - - expect(result).toEqual(parameters); - }); - - it('should pass validation for unknown task type with passthrough schema', () => { - const parameters = { foo: 'bar' }; - - const result = validator.validate('unknown-task', parameters); - - expect(result).toEqual(parameters); - }); - - it('should throw ValidationError when validation fails', () => { - // Temporarily add a strict schema for testing - const originalSchema = taskSchemas['tiles-deletion']; - taskSchemas['tiles-deletion'] = z.object({ - requiredField: z.string(), - }); - - try { - expect(() => validator.validate('tiles-deletion', {})).toThrow(ValidationError); - } finally { - // Restore original schema - taskSchemas['tiles-deletion'] = originalSchema; - } - }); - - it('should include validation errors in thrown ValidationError', () => { - const originalSchema = taskSchemas['tiles-deletion']; - taskSchemas['tiles-deletion'] = z.object({ - requiredField: z.string(), - }); - - try { - expect(() => validator.validate('tiles-deletion', {})).toThrow( - expect.objectContaining({ - name: 'ValidationError', - validationErrors: expect.any(Array), - }) - ); - } finally { - taskSchemas['tiles-deletion'] = originalSchema; - } - }); - }); - - describe('getSchema', () => { - it('should return schema for known task type', () => { - const schema = validator.getSchema('tiles-deletion'); - - expect(schema).toBeDefined(); - expect(typeof schema.safeParse).toBe('function'); - }); - - it('should return passthrough schema for unknown task type', () => { - const schema = validator.getSchema('unknown-task'); - - expect(schema).toBeDefined(); - const result = schema.safeParse({ any: 'data' }); - expect(result.success).toBe(true); - }); - }); -}); -``` - ---- - -## Step 7: Create Strategy Interface and TilesDeletionStrategy - -### File: `src/cleaner/strategies/taskStrategy.ts` - -```typescript -import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; - -/** - * Interface for task execution strategies. - * Each task type should have a corresponding strategy implementation. - */ -export interface ITaskStrategy { - /** - * Executes the task logic. - * @param task - The task to execute - * @throws {RecoverableError} for transient failures that should be retried - * @throws {UnrecoverableError} for permanent failures that should not be retried - */ - execute(task: ITaskResponse): Promise; -} -``` - -### File: `src/cleaner/strategies/tilesDeletionStrategy.ts` - -```typescript -import type { Logger } from '@map-colonies/js-logger'; -import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; -import { inject, injectable } from 'tsyringe'; -import { SERVICES } from '@common/constants'; -import type { ITaskStrategy } from './taskStrategy'; - -/** - * Strategy for handling tiles-deletion tasks. - * Deletes map tiles before updating to lower resolution. - */ -@injectable() -export class TilesDeletionStrategy implements ITaskStrategy { - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} - - /** - * Executes the tiles deletion task. - * @param task - The task containing deletion parameters - */ - public async execute(task: ITaskResponse): Promise { - this.logger.info({ - msg: 'Executing tiles deletion strategy', - taskId: task.id, - jobId: task.jobId, - parameters: task.parameters, - }); - - // TODO: Implement actual tiles deletion logic - // 1. Parse and validate tile coordinates/paths from task.parameters - // 2. Connect to tile storage - // 3. Delete tiles - // 4. Verify deletion - - this.logger.info({ - msg: 'Tiles deletion completed', - taskId: task.id, - jobId: task.jobId, - }); - } -} -``` - -### File: `src/cleaner/strategies/index.ts` - -```typescript -import type { DependencyContainer } from 'tsyringe'; -import { inject, injectable } from 'tsyringe'; -import type { Logger } from '@map-colonies/js-logger'; -import { SERVICES, STRATEGY_TOKENS } from '@common/constants'; -import { StrategyNotFoundError } from '../errors'; -import type { ITaskStrategy } from './taskStrategy'; -import { TilesDeletionStrategy } from './tilesDeletionStrategy'; - -/** - * Maps task types to their strategy tokens. - * Add new mappings here when adding new strategies. - */ -const TASK_TYPE_TO_STRATEGY: Record = { - 'tiles-deletion': STRATEGY_TOKENS.TILES_DELETION, -}; - -/** - * Factory for resolving task strategies by task type. - */ -@injectable() -export class StrategyFactory { - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject('container') private readonly container: DependencyContainer - ) {} - - /** - * Gets the strategy for a given task type. - * @param taskType - The type of task - * @returns The strategy instance - * @throws {StrategyNotFoundError} if no strategy is registered for the task type - */ - public getStrategy(taskType: string): ITaskStrategy { - const strategyToken = TASK_TYPE_TO_STRATEGY[taskType]; - - if (!strategyToken) { - this.logger.error({ msg: 'No strategy found for task type', taskType }); - throw new StrategyNotFoundError(taskType); - } - - return this.container.resolve(strategyToken); - } - - /** - * Checks if a strategy exists for the given task type. - */ - public hasStrategy(taskType: string): boolean { - return taskType in TASK_TYPE_TO_STRATEGY; - } - - /** - * Gets all registered task types. - */ - public getRegisteredTaskTypes(): string[] { - return Object.keys(TASK_TYPE_TO_STRATEGY); - } -} - -export { ITaskStrategy } from './taskStrategy'; -export { TilesDeletionStrategy } from './tilesDeletionStrategy'; -``` - -### Test File: `tests/tilesDeletionStrategy.spec.ts` - -```typescript -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; -import { TilesDeletionStrategy } from '@src/cleaner/strategies/tilesDeletionStrategy'; - -describe('TilesDeletionStrategy', () => { - let strategy: TilesDeletionStrategy; - let mockLogger: { - info: ReturnType; - error: ReturnType; - warn: ReturnType; - debug: ReturnType; - }; - - beforeEach(() => { - mockLogger = { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - }; - strategy = new TilesDeletionStrategy(mockLogger as never); - }); - - describe('execute', () => { - it('should log start and completion of task', async () => { - const task: ITaskResponse = { - id: 'task-123', - jobId: 'job-456', - parameters: { path: '/tiles/layer1' }, - type: 'tiles-deletion', - description: 'Delete tiles', - created: new Date().toISOString(), - updated: new Date().toISOString(), - status: 'In-Progress', - attempts: 0, - reason: '', - resettable: true, - }; - - await strategy.execute(task); - - expect(mockLogger.info).toHaveBeenCalledTimes(2); - expect(mockLogger.info).toHaveBeenCalledWith( - expect.objectContaining({ - msg: 'Executing tiles deletion strategy', - taskId: 'task-123', - jobId: 'job-456', - }) - ); - expect(mockLogger.info).toHaveBeenCalledWith( - expect.objectContaining({ - msg: 'Tiles deletion completed', - taskId: 'task-123', - jobId: 'job-456', - }) - ); - }); - - it('should complete without throwing for valid task', async () => { - const task: ITaskResponse = { - id: 'task-123', - jobId: 'job-456', - parameters: {}, - type: 'tiles-deletion', - description: 'Delete tiles', - created: new Date().toISOString(), - updated: new Date().toISOString(), - status: 'In-Progress', - attempts: 0, - reason: '', - resettable: true, - }; - - await expect(strategy.execute(task)).resolves.toBeUndefined(); - }); - }); -}); -``` - -### Test File: `tests/strategyFactory.spec.ts` - -```typescript -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import type { DependencyContainer } from 'tsyringe'; -import { STRATEGY_TOKENS } from '@src/common/constants'; -import { StrategyFactory } from '@src/cleaner/strategies'; -import { StrategyNotFoundError } from '@src/cleaner/errors'; -import { TilesDeletionStrategy } from '@src/cleaner/strategies/tilesDeletionStrategy'; - -describe('StrategyFactory', () => { - let factory: StrategyFactory; - let mockLogger: { - info: ReturnType; - error: ReturnType; - warn: ReturnType; - debug: ReturnType; - }; - let mockContainer: { - resolve: ReturnType; - }; - let mockStrategy: TilesDeletionStrategy; - - beforeEach(() => { - mockLogger = { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - }; - - mockStrategy = new TilesDeletionStrategy(mockLogger as never); - - mockContainer = { - resolve: vi.fn().mockReturnValue(mockStrategy), - }; - - factory = new StrategyFactory(mockLogger as never, mockContainer as unknown as DependencyContainer); - }); - - describe('getStrategy', () => { - it('should return strategy for tiles-deletion task type', () => { - const strategy = factory.getStrategy('tiles-deletion'); - - expect(strategy).toBe(mockStrategy); - expect(mockContainer.resolve).toHaveBeenCalledWith(STRATEGY_TOKENS.TILES_DELETION); - }); - - it('should throw StrategyNotFoundError for unknown task type', () => { - expect(() => factory.getStrategy('unknown-task')).toThrow(StrategyNotFoundError); - expect(() => factory.getStrategy('unknown-task')).toThrow('No strategy found for task type: unknown-task'); - }); - - it('should log error when strategy not found', () => { - try { - factory.getStrategy('unknown-task'); - } catch { - // Expected to throw - } - - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - msg: 'No strategy found for task type', - taskType: 'unknown-task', - }) - ); - }); - }); - - describe('hasStrategy', () => { - it('should return true for tiles-deletion', () => { - expect(factory.hasStrategy('tiles-deletion')).toBe(true); - }); - - it('should return false for unknown task type', () => { - expect(factory.hasStrategy('unknown-task')).toBe(false); - }); - }); - - describe('getRegisteredTaskTypes', () => { - it('should return array containing tiles-deletion', () => { - const taskTypes = factory.getRegisteredTaskTypes(); - - expect(taskTypes).toContain('tiles-deletion'); - }); - }); -}); -``` - ---- - -## Step 8: Create Error Handler - -### File: `src/cleaner/errors/errorHandler.ts` - -```typescript -import type { Logger } from '@map-colonies/js-logger'; -import type { Registry, Counter } from 'prom-client'; -import { inject, injectable } from 'tsyringe'; -import { SERVICES } from '@common/constants'; -import type { ErrorContext, ErrorDecision } from '../types'; -import { isRecoverableError, isUnrecoverableError } from './errors'; - -@injectable() -export class ErrorHandler { - private taskFailuresCounter?: Counter<'job_type' | 'task_type' | 'recoverable'>; - - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry - ) { - this.initializeMetrics(); - } - - /** - * Handles an error and returns a decision on how to proceed. - */ - public handle(error: unknown, context: ErrorContext): ErrorDecision { - const isRecoverable = this.determineRecoverability(error, context); - const reason = this.formatReason(error); - - const decision: ErrorDecision = { - isRecoverable, - reason, - shouldLog: true, - logLevel: isRecoverable ? 'warn' : 'error', - }; - - this.logError(error, context, decision); - this.recordMetrics(context, isRecoverable); - - return decision; - } - - /** - * Determines if an error is recoverable. - */ - private determineRecoverability(error: unknown, context: ErrorContext): boolean { - // If it's explicitly marked, use that - if (isRecoverableError(error)) { - // But only if we haven't exceeded max attempts - return context.attempts < context.maxAttempts; - } - - if (isUnrecoverableError(error)) { - return false; - } - - // For unknown errors, treat as recoverable if under max attempts - return context.attempts < context.maxAttempts; - } - - /** - * Formats an error into a reason string. - */ - private formatReason(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - return String(error); - } - - /** - * Logs the error with appropriate context. - */ - private logError(error: unknown, context: ErrorContext, decision: ErrorDecision): void { - const logData = { - msg: 'Task processing failed', - taskId: context.taskId, - jobId: context.jobId, - jobType: context.jobType, - taskType: context.taskType, - attempts: context.attempts, - maxAttempts: context.maxAttempts, - isRecoverable: decision.isRecoverable, - error: error instanceof Error ? { message: error.message, name: error.name, stack: error.stack } : error, - }; - - if (decision.logLevel === 'error') { - this.logger.error(logData); - } else { - this.logger.warn(logData); - } - } - - /** - * Records failure metrics. - */ - private recordMetrics(context: ErrorContext, isRecoverable: boolean): void { - this.taskFailuresCounter?.inc({ - job_type: context.jobType, - task_type: context.taskType, - recoverable: String(isRecoverable), - }); - } - - /** - * Initializes Prometheus metrics. - */ - private initializeMetrics(): void { - try { - this.taskFailuresCounter = new (require('prom-client').Counter)({ - name: 'cleaner_task_failures_total', - help: 'Total number of task failures', - labelNames: ['job_type', 'task_type', 'recoverable'], - registers: [this.metricsRegistry], - }); - } catch { - // Metrics may already be registered in tests - } - } -} - -export { ErrorHandler }; -``` - -### Update: `src/cleaner/errors/index.ts` - -Replace with: - -```typescript -export { RecoverableError, UnrecoverableError, ValidationError, StrategyNotFoundError, isRecoverableError, isUnrecoverableError } from './errors'; -export { ErrorHandler } from './errorHandler'; -``` - -### Test File: `tests/errorHandler.spec.ts` - -```typescript -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { Registry } from 'prom-client'; -import { ErrorHandler } from '@src/cleaner/errors/errorHandler'; -import { RecoverableError, UnrecoverableError, ValidationError } from '@src/cleaner/errors'; -import type { ErrorContext } from '@src/cleaner/types'; - -describe('ErrorHandler', () => { - let errorHandler: ErrorHandler; - let mockLogger: { - info: ReturnType; - error: ReturnType; - warn: ReturnType; - debug: ReturnType; - }; - let metricsRegistry: Registry; - - const createContext = (overrides: Partial = {}): ErrorContext => ({ - jobType: 'Ingestion_Update', - taskType: 'tiles-deletion', - taskId: 'task-123', - jobId: 'job-456', - attempts: 1, - maxAttempts: 3, - ...overrides, - }); - - beforeEach(() => { - mockLogger = { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - }; - metricsRegistry = new Registry(); - errorHandler = new ErrorHandler(mockLogger as never, metricsRegistry); - }); - - describe('handle', () => { - describe('RecoverableError', () => { - it('should return recoverable decision when attempts < maxAttempts', () => { - const error = new RecoverableError('Transient failure'); - const context = createContext({ attempts: 1, maxAttempts: 3 }); - - const decision = errorHandler.handle(error, context); - - expect(decision.isRecoverable).toBe(true); - expect(decision.reason).toBe('Transient failure'); - expect(decision.logLevel).toBe('warn'); - }); - - it('should return unrecoverable decision when attempts >= maxAttempts', () => { - const error = new RecoverableError('Transient failure'); - const context = createContext({ attempts: 3, maxAttempts: 3 }); - - const decision = errorHandler.handle(error, context); - - expect(decision.isRecoverable).toBe(false); - }); - }); - - describe('UnrecoverableError', () => { - it('should always return unrecoverable decision', () => { - const error = new UnrecoverableError('Fatal failure'); - const context = createContext({ attempts: 1, maxAttempts: 3 }); - - const decision = errorHandler.handle(error, context); - - expect(decision.isRecoverable).toBe(false); - expect(decision.reason).toBe('Fatal failure'); - expect(decision.logLevel).toBe('error'); - }); - }); - - describe('ValidationError', () => { - it('should return unrecoverable decision', () => { - const error = new ValidationError('Invalid parameters'); - const context = createContext(); - - const decision = errorHandler.handle(error, context); - - expect(decision.isRecoverable).toBe(false); - expect(decision.reason).toBe('Invalid parameters'); - }); - }); - - describe('Regular Error', () => { - it('should return recoverable when under max attempts', () => { - const error = new Error('Unknown error'); - const context = createContext({ attempts: 1, maxAttempts: 3 }); - - const decision = errorHandler.handle(error, context); - - expect(decision.isRecoverable).toBe(true); - }); - - it('should return unrecoverable when at max attempts', () => { - const error = new Error('Unknown error'); - const context = createContext({ attempts: 3, maxAttempts: 3 }); - - const decision = errorHandler.handle(error, context); - - expect(decision.isRecoverable).toBe(false); - }); - }); - - describe('Logging', () => { - it('should log warning for recoverable errors', () => { - const error = new RecoverableError('Transient'); - const context = createContext(); - - errorHandler.handle(error, context); - - expect(mockLogger.warn).toHaveBeenCalled(); - expect(mockLogger.error).not.toHaveBeenCalled(); - }); - - it('should log error for unrecoverable errors', () => { - const error = new UnrecoverableError('Fatal'); - const context = createContext(); - - errorHandler.handle(error, context); - - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); - - it('should include context in log', () => { - const error = new Error('Test'); - const context = createContext(); - - errorHandler.handle(error, context); - - expect(mockLogger.warn).toHaveBeenCalledWith( - expect.objectContaining({ - taskId: context.taskId, - jobId: context.jobId, - jobType: context.jobType, - taskType: context.taskType, - attempts: context.attempts, - maxAttempts: context.maxAttempts, - }) - ); - }); - }); - - describe('Non-Error values', () => { - it('should handle string errors', () => { - const context = createContext(); - - const decision = errorHandler.handle('String error', context); - - expect(decision.reason).toBe('String error'); - }); - - it('should handle null errors', () => { - const context = createContext(); - - const decision = errorHandler.handle(null, context); - - expect(decision.reason).toBe('null'); - }); - }); - }); -}); -``` - ---- - -## Step 9: Create TaskPoller - -### File: `src/cleaner/taskPoller.ts` - -```typescript -import { setTimeout as setTimeoutPromise } from 'node:timers/promises'; -import type { Logger } from '@map-colonies/js-logger'; -import type { TaskHandler as QueueClient, ITaskResponse } from '@map-colonies/mc-priority-queue'; -import { inject, injectable } from 'tsyringe'; -import { SERVICES } from '@common/constants'; -import type { ConfigType } from '@common/config'; -import type { PollingConfig, PollingPairConfig, ErrorContext } from './types'; -import { TaskValidator } from './validation'; -import { StrategyFactory } from './strategies'; -import { ErrorHandler, ValidationError } from './errors'; - -@injectable() -export class TaskPoller { - private isRunning = false; - - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(SERVICES.CONFIG) private readonly config: ConfigType, - @inject(SERVICES.QUEUE_CLIENT) private readonly queueClient: QueueClient, - private readonly taskValidator: TaskValidator, - private readonly strategyFactory: StrategyFactory, - private readonly errorHandler: ErrorHandler - ) {} - - /** - * Starts the polling loop. - * Polls all configured job/task pairs in round-robin fashion. - */ - public async start(): Promise { - const pollingConfig = this.config.get('polling'); - const { pairs, dequeueIntervalMs } = pollingConfig; - - this.logger.info({ - msg: 'Starting task poller', - pairs: pairs.map((p) => `${p.jobType}:${p.taskType}`), - dequeueIntervalMs, - }); - - this.isRunning = true; - - while (this.isRunning) { - await this.pollAllPairs(pairs, dequeueIntervalMs); - } - - this.logger.info({ msg: 'Task poller stopped' }); - } - - /** - * Stops the polling loop gracefully. - */ - public stop(): void { - this.logger.info({ msg: 'Stopping task poller' }); - this.isRunning = false; - } - - /** - * Returns whether the poller is currently running. - */ - public getIsRunning(): boolean { - return this.isRunning; - } - - /** - * Polls all configured pairs in round-robin fashion. - */ - private async pollAllPairs(pairs: PollingPairConfig[], dequeueIntervalMs: number): Promise { - let taskProcessed = false; - - for (const pair of pairs) { - if (!this.isRunning) { - break; - } - - const processed = await this.pollPair(pair); - if (processed) { - taskProcessed = true; - } - } - - // If no task was processed in this round, wait before next round - if (!taskProcessed) { - await setTimeoutPromise(dequeueIntervalMs); - } - } - - /** - * Polls a single job/task pair. - * @returns true if a task was processed, false otherwise - */ - private async pollPair(pair: PollingPairConfig): Promise { - const { jobType, taskType, maxAttempts } = pair; - - try { - const task = await this.queueClient.dequeue(jobType, taskType); - - if (!task) { - return false; - } - - await this.processTask(task, pair); - return true; - } catch (error) { - // Log polling errors but don't crash the loop - this.logger.error({ - msg: 'Error polling for tasks', - jobType, - taskType, - error: error instanceof Error ? error.message : error, - }); - return false; - } - } - - /** - * Processes a single task. - */ - private async processTask(task: ITaskResponse, pair: PollingPairConfig): Promise { - const { jobType, taskType, maxAttempts } = pair; - const { id: taskId, jobId, attempts } = task; - - this.logger.info({ - msg: 'Processing task', - taskId, - jobId, - jobType, - taskType, - attempts, - }); - - try { - // Validate task parameters - const validatedParams = this.taskValidator.validate(taskType, task.parameters); - - // Get and execute strategy - const strategy = this.strategyFactory.getStrategy(taskType); - await strategy.execute({ ...task, parameters: validatedParams }); - - // Acknowledge successful completion - await this.queueClient.ack(jobId, taskId); - - this.logger.info({ - msg: 'Task completed successfully', - taskId, - jobId, - jobType, - taskType, - }); - } catch (error) { - await this.handleTaskError(error, task, pair); - } - } - - /** - * Handles task processing errors. - */ - private async handleTaskError(error: unknown, task: ITaskResponse, pair: PollingPairConfig): Promise { - const { jobType, taskType, maxAttempts } = pair; - const { id: taskId, jobId, attempts } = task; - - const context: ErrorContext = { - jobType, - taskType, - taskId, - jobId, - attempts, - maxAttempts, - }; - - const decision = this.errorHandler.handle(error, context); - - await this.queueClient.reject(jobId, taskId, decision.isRecoverable, decision.reason); - } -} -``` - -### Test File: `tests/taskPoller.spec.ts` - -```typescript -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import type { TaskHandler as QueueClient, ITaskResponse } from '@map-colonies/mc-priority-queue'; -import { TaskPoller } from '@src/cleaner/taskPoller'; -import { TaskValidator } from '@src/cleaner/validation'; -import { StrategyFactory, type ITaskStrategy } from '@src/cleaner/strategies'; -import { ErrorHandler, ValidationError, StrategyNotFoundError } from '@src/cleaner/errors'; -import type { PollingConfig } from '@src/cleaner/types'; - -describe('TaskPoller', () => { - let taskPoller: TaskPoller; - let mockLogger: { - info: ReturnType; - error: ReturnType; - warn: ReturnType; - debug: ReturnType; - }; - let mockConfig: { - get: ReturnType; - }; - let mockQueueClient: { - dequeue: ReturnType; - ack: ReturnType; - reject: ReturnType; - }; - let mockTaskValidator: { - validate: ReturnType; - }; - let mockStrategyFactory: { - getStrategy: ReturnType; - }; - let mockErrorHandler: { - handle: ReturnType; - }; - let mockStrategy: { - execute: ReturnType; - }; - - const pollingConfig: PollingConfig = { - dequeueIntervalMs: 100, - pairs: [{ jobType: 'Ingestion_Update', taskType: 'tiles-deletion', maxAttempts: 3 }], - }; - - const createMockTask = (overrides: Partial> = {}): ITaskResponse => ({ - id: 'task-123', - jobId: 'job-456', - parameters: { path: '/tiles' }, - type: 'tiles-deletion', - description: 'Delete tiles', - created: new Date().toISOString(), - updated: new Date().toISOString(), - status: 'In-Progress', - attempts: 1, - reason: '', - resettable: true, - ...overrides, - }); - - beforeEach(() => { - mockLogger = { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), - }; - - mockConfig = { - get: vi.fn().mockReturnValue(pollingConfig), - }; - - mockQueueClient = { - dequeue: vi.fn(), - ack: vi.fn(), - reject: vi.fn(), - }; - - mockTaskValidator = { - validate: vi.fn().mockImplementation((_, params) => params), - }; - - mockStrategy = { - execute: vi.fn().mockResolvedValue(undefined), - }; - - mockStrategyFactory = { - getStrategy: vi.fn().mockReturnValue(mockStrategy), - }; - - mockErrorHandler = { - handle: vi.fn().mockReturnValue({ - isRecoverable: true, - reason: 'Error message', - shouldLog: true, - logLevel: 'warn', - }), - }; - - taskPoller = new TaskPoller( - mockLogger as never, - mockConfig as never, - mockQueueClient as unknown as QueueClient, - mockTaskValidator as unknown as TaskValidator, - mockStrategyFactory as unknown as StrategyFactory, - mockErrorHandler as unknown as ErrorHandler - ); - }); - - afterEach(() => { - taskPoller.stop(); - }); - - describe('start', () => { - it('should log start message with configured pairs', async () => { - mockQueueClient.dequeue.mockResolvedValue(null); - - const startPromise = taskPoller.start(); - // Let one iteration run - await vi.waitFor(() => expect(mockQueueClient.dequeue).toHaveBeenCalled()); - taskPoller.stop(); - await startPromise; - - expect(mockLogger.info).toHaveBeenCalledWith( - expect.objectContaining({ - msg: 'Starting task poller', - pairs: ['Ingestion_Update:tiles-deletion'], - }) - ); - }); - - it('should poll configured pairs', async () => { - mockQueueClient.dequeue.mockResolvedValue(null); - - const startPromise = taskPoller.start(); - await vi.waitFor(() => expect(mockQueueClient.dequeue).toHaveBeenCalled()); - taskPoller.stop(); - await startPromise; - - expect(mockQueueClient.dequeue).toHaveBeenCalledWith('Ingestion_Update', 'tiles-deletion'); - }); - }); - - describe('stop', () => { - it('should stop the polling loop', async () => { - mockQueueClient.dequeue.mockResolvedValue(null); - - const startPromise = taskPoller.start(); - await vi.waitFor(() => expect(taskPoller.getIsRunning()).toBe(true)); - - taskPoller.stop(); - await startPromise; - - expect(taskPoller.getIsRunning()).toBe(false); - expect(mockLogger.info).toHaveBeenCalledWith(expect.objectContaining({ msg: 'Task poller stopped' })); - }); - }); - - describe('task processing', () => { - it('should process task successfully', async () => { - const task = createMockTask(); - mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); - - const startPromise = taskPoller.start(); - await vi.waitFor(() => expect(mockQueueClient.ack).toHaveBeenCalled()); - taskPoller.stop(); - await startPromise; - - expect(mockTaskValidator.validate).toHaveBeenCalledWith('tiles-deletion', task.parameters); - expect(mockStrategyFactory.getStrategy).toHaveBeenCalledWith('tiles-deletion'); - expect(mockStrategy.execute).toHaveBeenCalled(); - expect(mockQueueClient.ack).toHaveBeenCalledWith('job-456', 'task-123'); - }); - - it('should reject task on validation error', async () => { - const task = createMockTask(); - mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); - mockTaskValidator.validate.mockImplementation(() => { - throw new ValidationError('Invalid params'); - }); - mockErrorHandler.handle.mockReturnValue({ - isRecoverable: false, - reason: 'Invalid params', - shouldLog: true, - logLevel: 'error', - }); - - const startPromise = taskPoller.start(); - await vi.waitFor(() => expect(mockQueueClient.reject).toHaveBeenCalled()); - taskPoller.stop(); - await startPromise; - - expect(mockQueueClient.reject).toHaveBeenCalledWith('job-456', 'task-123', false, 'Invalid params'); - }); - - it('should reject task on strategy error', async () => { - const task = createMockTask(); - mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); - mockStrategy.execute.mockRejectedValue(new Error('Strategy failed')); - - const startPromise = taskPoller.start(); - await vi.waitFor(() => expect(mockQueueClient.reject).toHaveBeenCalled()); - taskPoller.stop(); - await startPromise; - - expect(mockErrorHandler.handle).toHaveBeenCalled(); - expect(mockQueueClient.reject).toHaveBeenCalled(); - }); - - it('should reject task on strategy not found', async () => { - const task = createMockTask(); - mockQueueClient.dequeue.mockResolvedValueOnce(task).mockResolvedValue(null); - mockStrategyFactory.getStrategy.mockImplementation(() => { - throw new StrategyNotFoundError('unknown'); - }); - - const startPromise = taskPoller.start(); - await vi.waitFor(() => expect(mockQueueClient.reject).toHaveBeenCalled()); - taskPoller.stop(); - await startPromise; - - expect(mockErrorHandler.handle).toHaveBeenCalled(); - }); - }); - - describe('getIsRunning', () => { - it('should return false initially', () => { - expect(taskPoller.getIsRunning()).toBe(false); - }); - - it('should return true when running', async () => { - mockQueueClient.dequeue.mockResolvedValue(null); - - const startPromise = taskPoller.start(); - await vi.waitFor(() => expect(taskPoller.getIsRunning()).toBe(true)); - - taskPoller.stop(); - await startPromise; - }); - }); -}); -``` - ---- - -## Step 10: Update containerConfig.ts - -### File: `src/containerConfig.ts` - -Replace the entire file with: - -```typescript -import { getOtelMixin } from '@map-colonies/telemetry'; -import { trace } from '@opentelemetry/api'; -import { Registry } from 'prom-client'; -import { container, instancePerContainerCachingFactory } from 'tsyringe'; -import type { DependencyContainer } from 'tsyringe/dist/typings/types'; -import jsLogger, { type Logger } from '@map-colonies/js-logger'; -import { TaskHandler as QueueClient } from '@map-colonies/mc-priority-queue'; -import { InjectionObject, registerDependencies } from '@common/dependencyRegistration'; -import { SERVICES, SERVICE_NAME, STRATEGY_TOKENS } from '@common/constants'; -import { getTracing } from '@common/tracing'; -import { type ConfigType, getConfig } from './common/config'; -import type { QueueConfig, HttpRetryConfig } from './cleaner/types'; -import { TaskPoller } from './cleaner/taskPoller'; -import { TaskValidator } from './cleaner/validation'; -import { StrategyFactory, TilesDeletionStrategy } from './cleaner/strategies'; -import { ErrorHandler } from './cleaner/errors'; - -// ============================================================================= -// TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk -// The imports below are commented out until the migration is complete. -// ============================================================================= -// import { IWorker, JobnikSDK } from '@map-colonies/jobnik-sdk'; -// import { workerBuilder } from './worker'; -// import { LogisticJobTypes, LogisticStageTypes } from './logistics/types'; - -export interface RegisterOptions { - override?: InjectionObject[]; - useChild?: boolean; -} - -export const registerExternalValues = async (options?: RegisterOptions): Promise => { - const configInstance = getConfig(); - - const loggerConfig = configInstance.get('telemetry.logger'); - - const logger = jsLogger({ ...loggerConfig, prettyPrint: loggerConfig.prettyPrint, mixin: getOtelMixin() }); - - const tracer = trace.getTracer(SERVICE_NAME); - const metricsRegistry = new Registry(); - configInstance.initializeMetrics(metricsRegistry); - - const dependencies: InjectionObject[] = [ - { token: SERVICES.CONFIG, provider: { useValue: configInstance } }, - { token: SERVICES.LOGGER, provider: { useValue: logger } }, - { token: SERVICES.TRACER, provider: { useValue: tracer } }, - { token: SERVICES.METRICS, provider: { useValue: metricsRegistry } }, - - // Queue Client (mc-priority-queue) - { - token: SERVICES.QUEUE_CLIENT, - provider: { - useFactory: instancePerContainerCachingFactory((depContainer) => { - const containerLogger = depContainer.resolve(SERVICES.LOGGER); - const config = depContainer.resolve(SERVICES.CONFIG); - const queueConfig = config.get('queue'); - const httpRetryConfig = config.get('httpRetry'); - - return new QueueClient( - containerLogger, - queueConfig.jobManagerBaseUrl, - queueConfig.heartbeatBaseUrl, - config.get('polling.dequeueIntervalMs'), - queueConfig.heartbeatIntervalMs, - httpRetryConfig - ); - }), - }, - }, - - // Container reference for strategy factory - { token: 'container', provider: { useValue: container } }, - - // Strategies - { token: STRATEGY_TOKENS.TILES_DELETION, provider: { useClass: TilesDeletionStrategy } }, - - // Services - { token: SERVICES.TASK_VALIDATOR, provider: { useClass: TaskValidator } }, - { token: SERVICES.STRATEGY_FACTORY, provider: { useClass: StrategyFactory } }, - { token: SERVICES.ERROR_HANDLER, provider: { useClass: ErrorHandler } }, - { token: SERVICES.TASK_POLLER, provider: { useClass: TaskPoller } }, - - // Graceful shutdown - { - token: 'onSignal', - provider: { - useFactory: (depContainer) => { - const taskPoller = depContainer.resolve(SERVICES.TASK_POLLER); - return async (): Promise => { - taskPoller.stop(); - await getTracing().stop(); - }; - }, - }, - }, - - // ============================================================================= - // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk - // The registrations below are commented out until the migration is complete. - // ============================================================================= - /* - { - token: SERVICES.JOBNIK_SDK, - provider: { - useFactory: instancePerContainerCachingFactory((depContainer) => { - const containerLogger = depContainer.resolve(SERVICES.LOGGER); - const config = depContainer.resolve(SERVICES.CONFIG); - const containerMetricsRegistry = depContainer.resolve(SERVICES.METRICS); - return new JobnikSDK({ - ...config.get('jobnik.sdk'), - logger: containerLogger, - metricsRegistry: containerMetricsRegistry, - }); - }), - }, - }, - { - token: SERVICES.WORKER, - provider: { - useFactory: instancePerContainerCachingFactory(workerBuilder), - }, - }, - */ - ]; - - return Promise.resolve(registerDependencies(dependencies, options?.override, options?.useChild)); -}; -``` - ---- - -## Step 11: Update index.ts - -### File: `src/index.ts` - -Replace the entire file with: - -```typescript -// this import must be called before the first import of tsyringe -import 'reflect-metadata'; -import { createServer } from 'node:http'; -import express from 'express'; -import { metricsMiddleware } from '@map-colonies/telemetry/prom-metrics'; -import { createTerminus } from '@godaddy/terminus'; -import type { Logger } from '@map-colonies/js-logger'; -import { SERVICES } from '@common/constants'; -import type { ConfigType } from '@common/config'; -import { registerExternalValues } from './containerConfig'; -import { TaskPoller } from './cleaner/taskPoller'; - -// ============================================================================= -// TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk -// The imports below are commented out until the migration is complete. -// ============================================================================= -// import type { IWorker } from '@map-colonies/jobnik-sdk'; -// import { LogisticsSDK } from './logistics/types'; -// import { seedData } from './seeder'; - -void registerExternalValues() - .then(async (container) => { - const logger = container.resolve(SERVICES.LOGGER); - const config = container.resolve(SERVICES.CONFIG); - const taskPoller = container.resolve(SERVICES.TASK_POLLER); - - // ============================================================================= - // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk - // The code below is commented out until the migration is complete. - // ============================================================================= - // const worker = container.resolve(SERVICES.WORKER); - // const sdk = container.resolve(SERVICES.JOBNIK_SDK); - // await seedData(sdk.getProducer()); - - const port = config.get('server.port'); - const stubHealthCheck = async (): Promise => Promise.resolve(); - - const app = express(); - - app.use(metricsMiddleware(container.resolve(SERVICES.METRICS), true)); - const server = createTerminus(createServer(app), { - healthChecks: { '/liveness': stubHealthCheck }, - onSignal: container.resolve('onSignal'), - }); - server.listen(port, () => { - logger.info(`app started on port ${port}`); - }); - - // Start the task poller (replaces worker.start() from jobnik-sdk) - await taskPoller.start(); - - // ============================================================================= - // TODO: When we move to the new job-manager, replace taskPoller.start() with: - // await worker.start(); - // ============================================================================= - }) - .catch((error: Error) => { - console.error('failed initializing the worker'); - console.error(error); - process.exit(1); - }); -``` - ---- - -## Step 12: Comment out worker.ts - -### File: `src/worker.ts` - -Replace the entire file with: - -```typescript -// ============================================================================= -// TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk -// The code below is commented out until the migration is complete. -// Uncomment and update when migrating to the new job-manager. -// ============================================================================= - -/* -import type { IWorker } from '@map-colonies/jobnik-sdk'; -import type { Logger } from '@map-colonies/js-logger'; -import type { DependencyContainer, FactoryFunction } from 'tsyringe'; -import { SERVICES } from './common/constants'; -import { LogisticsManager } from './logistics/manager'; -import { LogisticsSDK } from './logistics/types'; -import { ConfigType } from './common/config'; - -export const workerBuilder: FactoryFunction = (container: DependencyContainer) => { - const sdk = container.resolve(SERVICES.JOBNIK_SDK); - const logger = container.resolve(SERVICES.LOGGER); - const config = container.resolve(SERVICES.CONFIG); - - const logisticsManager = container.resolve(LogisticsManager); - - const worker = sdk.createWorker<'hazmatTransport', 'delivery'>( - 'delivery', - logisticsManager.handleDeliveryTask.bind(logisticsManager), - config.get('jobnik.worker') - ); - - worker.on('error', (err) => { - logger.error({ msg: 'Worker encountered an error:', err }); - }); - - return worker; -}; -*/ - -export {}; -``` - ---- - -## Step 13: Delete Old Files - -Delete the following files and directories: - -```bash -rm -rf src/logistics -rm -f src/seeder.ts -rm -f tests/logistics.spec.ts -``` - ---- - -## Step 14: Update Test Helpers - -### File: `tests/helpers/fakes.ts` - -Replace the entire file with: - -```typescript -import { simpleFaker } from '@faker-js/faker'; -import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; -import { vitest } from 'vitest'; -import jsLogger from '@map-colonies/js-logger'; - -/** - * Creates a mock task response for testing. - */ -export function createMockTaskResponse(overrides: Partial> = {}): ITaskResponse { - return { - id: simpleFaker.string.uuid(), - jobId: simpleFaker.string.uuid(), - type: 'tiles-deletion', - description: 'Test task', - parameters: {} as T, - created: new Date().toISOString(), - updated: new Date().toISOString(), - status: 'In-Progress', - attempts: 0, - reason: '', - resettable: true, - ...overrides, - }; -} - -/** - * Creates a mock logger for testing. - */ -export function createMockLogger() { - return { - info: vitest.fn(), - error: vitest.fn(), - warn: vitest.fn(), - debug: vitest.fn(), - trace: vitest.fn(), - fatal: vitest.fn(), - }; -} - -/** - * Creates a mock queue client for testing. - */ -export function createMockQueueClient() { - return { - dequeue: vitest.fn(), - ack: vitest.fn(), - reject: vitest.fn(), - updateProgress: vitest.fn(), - }; -} - -/** - * Creates a disabled logger for testing. - */ -export function createDisabledLogger() { - return jsLogger({ enabled: false }); -} -``` - ---- - -## Step 15: Create Directory Structure - -Ensure the following directory structure exists: - -``` -src/ - cleaner/ - errors/ - errors.ts - errorHandler.ts - index.ts - strategies/ - index.ts - taskStrategy.ts - tilesDeletionStrategy.ts - validation/ - index.ts - schemas.ts - taskPoller.ts - types.ts -``` - -Create directories: - -```bash -mkdir -p src/cleaner/errors -mkdir -p src/cleaner/strategies -mkdir -p src/cleaner/validation -``` - ---- - -## Verification Steps - -After implementing all steps, run these commands to verify: - -### 1. Install dependencies - -```bash -npm install --legacy-peer-deps -``` - -### 2. Run linting - -```bash -npm run lint -``` - -If there are lint errors, run: - -```bash -npm run lint:fix -``` - -### 3. Run tests - -```bash -npm test -``` - -All tests should pass with 80%+ coverage. - -### 4. Run build - -```bash -npm run build -``` - -Build should complete without errors. - -### 5. Verify file structure - -```bash -find src/cleaner -type f -name "*.ts" | sort -``` - -Expected output: - -``` -src/cleaner/errors/errorHandler.ts -src/cleaner/errors/errors.ts -src/cleaner/errors/index.ts -src/cleaner/strategies/index.ts -src/cleaner/strategies/taskStrategy.ts -src/cleaner/strategies/tilesDeletionStrategy.ts -src/cleaner/taskPoller.ts -src/cleaner/types.ts -src/cleaner/validation/index.ts -src/cleaner/validation/schemas.ts -``` - ---- - -## Summary - -This implementation provides: - -1. **TaskPoller** - Round-robin polling of configured job/task pairs -2. **TaskValidator** - Zod-based validation with registry pattern -3. **StrategyFactory** - tsyringe-based strategy resolution -4. **TilesDeletionStrategy** - Skeleton strategy for tiles deletion -5. **ErrorHandler** - Centralized error handling with metrics -6. **Custom Errors** - RecoverableError, UnrecoverableError, ValidationError -7. **Full test coverage** - Unit tests for all components - -The jobnik-sdk code is preserved in comments for future migration. From 4e63eb5b5d89999ba97508f81e870b2ad6f802e1 Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 14:39:47 +0200 Subject: [PATCH 04/25] feat(constants): add service tokens for cleaner implementation - Add QUEUE_CLIENT, TASK_POLLER, STRATEGY_FACTORY, TASK_VALIDATOR, ERROR_HANDLER tokens - Add migration note for jobnik-sdk tokens - Document that strategies use task type strings instead of Symbol tokens - Update common-patterns.md with correct Symbol syntax --- ai-docs/common-patterns.md | 18 ++++++++++-------- src/common/constants.ts | 9 +++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/ai-docs/common-patterns.md b/ai-docs/common-patterns.md index a052aea..4a49399 100644 --- a/ai-docs/common-patterns.md +++ b/ai-docs/common-patterns.md @@ -54,17 +54,19 @@ In `common/constants.ts`: ```typescript export const SERVICES = { - CONFIG: Symbol.for('Config'), - LOGGER: Symbol.for('Logger'), - TRACER: Symbol.for('Tracer'), - METRICS: Symbol.for('Metrics'), - JOBNIK_SDK: Symbol.for('JobnikSDK'), - WORKER: Symbol.for('Worker'), + CONFIG: Symbol('Config'), + LOGGER: Symbol('Logger'), + TRACER: Symbol('Tracer'), + METRICS: Symbol('Metrics'), + JOBNIK_SDK: Symbol('JobnikSDK'), + WORKER: Symbol('Worker'), // Add new tokens here - CLEANER_SERVICE: Symbol.for('CleanerService'), -} as const; + CLEANER_SERVICE: Symbol('CleanerService'), +} satisfies Record; ``` +**Note**: Strategy classes are registered using their task type string directly (e.g., `'tiles-deletion'`) rather than Symbol tokens. This eliminates the need for an additional mapping layer. + ## Task Handling ### Task Handler Structure diff --git a/src/common/constants.ts b/src/common/constants.ts index 908ace3..98bb821 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -12,6 +12,15 @@ export const SERVICES = { CONFIG: Symbol('Config'), TRACER: Symbol('Tracer'), METRICS: Symbol('METRICS'), + QUEUE_CLIENT: Symbol('QueueClient'), + TASK_POLLER: Symbol('TaskPoller'), + STRATEGY_FACTORY: Symbol('StrategyFactory'), + TASK_VALIDATOR: Symbol('TaskValidator'), + ERROR_HANDLER: Symbol('ErrorHandler'), + // ============================================================================= + // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk + // The tokens below are kept for future migration. + // ============================================================================= JOBNIK_SDK: Symbol('JobnikSDK'), WORKER: Symbol('Worker'), } satisfies Record; From e45bebca40b8c491798e3c4ea5af54782450a32d Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 15:02:48 +0200 Subject: [PATCH 05/25] refactor(config): combine polling config into queue section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move polling.dequeueIntervalMs to queue.dequeueIntervalMs - Move polling.pairs to queue.pairs - Update env var name: POLLING_DEQUEUE_INTERVAL_MS → QUEUE_DEQUEUE_INTERVAL_MS - Simplifies config structure - all queue-related settings in one place - Update all config levels: default.json, env vars, Helm values/templates --- config/custom-environment-variables.json | 6 ++---- config/default.json | 4 +--- helm/local.yaml | 1 - helm/templates/configmap.yaml | 4 +--- helm/values.yaml | 1 - 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 9e92c20..41a0d4d 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -14,10 +14,8 @@ "queue": { "jobManagerBaseUrl": "QUEUE_JOB_MANAGER_BASE_URL", "heartbeatBaseUrl": "QUEUE_HEARTBEAT_BASE_URL", - "heartbeatIntervalMs": "QUEUE_HEARTBEAT_INTERVAL_MS" - }, - "polling": { - "dequeueIntervalMs": "POLLING_DEQUEUE_INTERVAL_MS" + "heartbeatIntervalMs": "QUEUE_HEARTBEAT_INTERVAL_MS", + "dequeueIntervalMs": "QUEUE_DEQUEUE_INTERVAL_MS" }, "httpRetry": { "attempts": "HTTP_RETRY_ATTEMPTS", diff --git a/config/default.json b/config/default.json index 1c261d5..65ec561 100644 --- a/config/default.json +++ b/config/default.json @@ -16,9 +16,7 @@ "queue": { "jobManagerBaseUrl": "http://job-manager:8080", "heartbeatBaseUrl": "http://heartbeat:8080", - "heartbeatIntervalMs": 1000 - }, - "polling": { + "heartbeatIntervalMs": 1000, "dequeueIntervalMs": 3000, "pairs": [ { diff --git a/helm/local.yaml b/helm/local.yaml index 3145a25..fb369f0 100644 --- a/helm/local.yaml +++ b/helm/local.yaml @@ -6,7 +6,6 @@ env: jobManagerBaseUrl: "http://localhost:8080" heartbeatBaseUrl: "http://localhost:8081" heartbeatIntervalMs: 1000 - polling: dequeueIntervalMs: 3000 httpRetry: attempts: 3 diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index dcb0575..67e3602 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -23,9 +23,7 @@ data: QUEUE_JOB_MANAGER_BASE_URL: {{ .jobManagerBaseUrl | quote }} QUEUE_HEARTBEAT_BASE_URL: {{ .heartbeatBaseUrl | quote }} QUEUE_HEARTBEAT_INTERVAL_MS: {{ .heartbeatIntervalMs | quote }} - {{- end }} - {{- with $.Values.env.polling }} - POLLING_DEQUEUE_INTERVAL_MS: {{ .dequeueIntervalMs | quote }} + QUEUE_DEQUEUE_INTERVAL_MS: {{ .dequeueIntervalMs | quote }} {{- end }} {{- with $.Values.env.httpRetry }} HTTP_RETRY_ATTEMPTS: {{ .attempts | quote }} diff --git a/helm/values.yaml b/helm/values.yaml index 1afebc2..cfaa261 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -80,7 +80,6 @@ env: jobManagerBaseUrl: "http://job-manager:8080" heartbeatBaseUrl: "http://heartbeat:8080" heartbeatIntervalMs: 1000 - polling: dequeueIntervalMs: 3000 httpRetry: attempts: 3 From 3bae874d42098ec3e7c839af29ee2bd4b2cf7120 Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 15:40:33 +0200 Subject: [PATCH 06/25] feat(errors): add custom error classes and core types - Add RecoverableError for retryable failures (network, timeouts) - Add UnrecoverableError for permanent failures (validation, config) - Add ValidationError for schema validation failures - Add StrategyNotFoundError for missing task strategies - Add core type definitions: PollingPairConfig, QueueConfig, HttpRetryConfig, ErrorContext, ErrorDecision, TaskResponse --- src/cleaner/errors/errors.ts | 50 ++++++++++++++++++++++++++++++++++++ src/cleaner/errors/index.ts | 1 + src/cleaner/types.ts | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/cleaner/errors/errors.ts create mode 100644 src/cleaner/errors/index.ts create mode 100644 src/cleaner/types.ts diff --git a/src/cleaner/errors/errors.ts b/src/cleaner/errors/errors.ts new file mode 100644 index 0000000..7723b83 --- /dev/null +++ b/src/cleaner/errors/errors.ts @@ -0,0 +1,50 @@ +/** + * Base class for recoverable errors that can be retried. + * Task will be retried if attempts < maxAttempts. + */ +export class RecoverableError extends Error { + public constructor(message: string, cause?: Error) { + super(message); + this.name = RecoverableError.name; + this.cause = cause; + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * Base class for unrecoverable errors that should not be retried. + * Task will be rejected immediately. + */ +export class UnrecoverableError extends Error { + public constructor(message: string, cause?: Error) { + super(message); + this.name = UnrecoverableError.name; + this.cause = cause; + Error.captureStackTrace(this, this.constructor); + } +} + +/** + * Validation error - task parameters failed schema validation. + * This is always unrecoverable since invalid parameters won't become valid on retry. + */ +export class ValidationError extends UnrecoverableError { + public constructor( + message: string, + public readonly validationDetails?: unknown + ) { + super(message); + this.name = ValidationError.name; + } +} + +/** + * Strategy not found error - no strategy registered for the task type. + * This is always unrecoverable since the strategy won't appear on retry. + */ +export class StrategyNotFoundError extends UnrecoverableError { + public constructor(taskType: string) { + super(`No strategy registered for task type: ${taskType}`); + this.name = 'StrategyNotFoundError'; + } +} diff --git a/src/cleaner/errors/index.ts b/src/cleaner/errors/index.ts new file mode 100644 index 0000000..ae7d662 --- /dev/null +++ b/src/cleaner/errors/index.ts @@ -0,0 +1 @@ +export { RecoverableError, UnrecoverableError, ValidationError, StrategyNotFoundError } from './errors'; diff --git a/src/cleaner/types.ts b/src/cleaner/types.ts new file mode 100644 index 0000000..e9d10c5 --- /dev/null +++ b/src/cleaner/types.ts @@ -0,0 +1,45 @@ +import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; + +/** + * Job/Task pair configuration for polling. + * Defines which job types and task types to poll from the queue. + */ +export interface PollingPairConfig { + jobType: string; + taskType: string; +} + +/** + * Queue configuration including polling pairs. + */ +export interface QueueConfig { + jobManagerBaseUrl: string; + heartbeatBaseUrl: string; + heartbeatIntervalMs: number; + dequeueIntervalMs: number; + pairs: PollingPairConfig[]; +} + +/** + * Context information for error handling decisions. + */ +export interface ErrorContext { + jobId: string; + taskId: string; + attemptNumber: number; + maxAttempts: number; + error: Error; +} + +/** + * Decision made by the error handler. + */ +export interface ErrorDecision { + shouldRetry: boolean; + reason: string; +} + +/** + * Task response helper type for strategy implementations. + */ +export type TaskResponse = ITaskResponse>; From 82ab7afcf657f04df65bc714be2f99604c52e968 Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 16:16:05 +0200 Subject: [PATCH 07/25] feat(validation): add task parameter validation infrastructure - Add createMockLogger() test helper for reusable logger mocking - Add TaskValidator class for type-safe parameter validation - Add taskSchemas map for registering task-specific schemas - Add placeholder tilesDeletionParamsSchema (TODO implementation) - Validation failures throw ValidationError (unrecoverable) - Add basic test coverage for validation infrastructure --- src/cleaner/validation/index.ts | 2 ++ src/cleaner/validation/schemas.ts | 13 +++++++++ src/cleaner/validation/taskValidator.ts | 37 +++++++++++++++++++++++ tests/helpers/mocks.ts | 17 +++++++++++ tests/validation.spec.ts | 39 +++++++++++++++++++++++++ 5 files changed, 108 insertions(+) create mode 100644 src/cleaner/validation/index.ts create mode 100644 src/cleaner/validation/schemas.ts create mode 100644 src/cleaner/validation/taskValidator.ts create mode 100644 tests/helpers/mocks.ts create mode 100644 tests/validation.spec.ts diff --git a/src/cleaner/validation/index.ts b/src/cleaner/validation/index.ts new file mode 100644 index 0000000..f62dfde --- /dev/null +++ b/src/cleaner/validation/index.ts @@ -0,0 +1,2 @@ +export { TaskValidator } from './taskValidator'; +export { taskSchemas, type TaskType } from './schemas'; diff --git a/src/cleaner/validation/schemas.ts b/src/cleaner/validation/schemas.ts new file mode 100644 index 0000000..f3af4f1 --- /dev/null +++ b/src/cleaner/validation/schemas.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +export const tilesDeletionParamsSchema = z.object({ + //TODO: implement tiles deletion params schema(will be imported from @map-colonies/raster-shared) +}); + +export type TilesDeletionParams = z.infer; + +export const taskSchemas = { + 'tiles-deletion': tilesDeletionParamsSchema, +} as const; + +export type TaskType = keyof typeof taskSchemas; diff --git a/src/cleaner/validation/taskValidator.ts b/src/cleaner/validation/taskValidator.ts new file mode 100644 index 0000000..32422cb --- /dev/null +++ b/src/cleaner/validation/taskValidator.ts @@ -0,0 +1,37 @@ +import { inject, injectable } from 'tsyringe'; +import { type Logger } from '@map-colonies/js-logger'; +import { type z } from 'zod'; +import { SERVICES } from '@common/constants'; +import { ValidationError } from '../errors'; +import { taskSchemas, type TaskType } from './schemas'; + +/** + * TaskValidator is responsible for validating task parameters against Zod schemas. + * Validation failures result in ValidationError (unrecoverable). + */ +@injectable() +export class TaskValidator { + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} + + /** + * Validates task parameters against the schema for the given task type. + * @param taskType - The task type (e.g., 'tiles-deletion') + * @param params - The task parameters to validate + * @returns The validated and typed parameters + * @throws ValidationError if validation fails or schema not found + */ + public validate(taskType: T, params: unknown): z.infer<(typeof taskSchemas)[T]> { + const schema = taskSchemas[taskType]; + + const result = schema.safeParse(params); + + if (!result.success) { + const message = `Task parameter validation failed for task type: ${taskType}`; + this.logger.error({ msg: message, errors: result.error.errors }); + throw new ValidationError(message, result.error.errors); + } + + this.logger.debug({ msg: 'Task parameters validated successfully', taskType }); + return result.data as z.infer<(typeof taskSchemas)[T]>; + } +} diff --git a/tests/helpers/mocks.ts b/tests/helpers/mocks.ts new file mode 100644 index 0000000..41e069b --- /dev/null +++ b/tests/helpers/mocks.ts @@ -0,0 +1,17 @@ +import { vi } from 'vitest'; +import type { Logger } from '@map-colonies/js-logger'; + +/** + * Creates a mock Logger instance for testing. + * All logger methods are vi.fn() spies that can be asserted against. + */ +export function createMockLogger(): Logger { + return { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + fatal: vi.fn(), + trace: vi.fn(), + } as unknown as Logger; +} diff --git a/tests/validation.spec.ts b/tests/validation.spec.ts new file mode 100644 index 0000000..a416a7e --- /dev/null +++ b/tests/validation.spec.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import type { Logger } from '@map-colonies/js-logger'; +import { TaskValidator } from '../src/cleaner/validation'; +import { createMockLogger } from './helpers/mocks'; + +describe('TaskValidator', () => { + let taskValidator: TaskValidator; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = createMockLogger(); + taskValidator = new TaskValidator(mockLogger); + }); + + describe('validate', () => { + it('should validate valid tiles-deletion parameters', () => { + //TODO: implement valid parameters for tiles-deletion task once the schema is defined + const validParams = {}; + + const result = taskValidator.validate('tiles-deletion', validParams); + + expect(result).toEqual(validParams); + expect(mockLogger.debug).toHaveBeenCalledWith({ + msg: 'Task parameters validated successfully', + taskType: 'tiles-deletion', + }); + }); + + it('should return validated data on success', () => { + const params = { someField: 'value' }; + const result = taskValidator.validate('tiles-deletion', params); + + expect(result).toBeDefined(); + expect(mockLogger.debug).toHaveBeenCalled(); + }); + + //TODO: implement test for invalid parameters once the schema is defined + }); +}); From d5dd37431a6680a2d1313c44d227e6770a09e9e3 Mon Sep 17 00:00:00 2001 From: almog8k Date: Sun, 8 Feb 2026 16:21:11 +0200 Subject: [PATCH 08/25] chore(deps): add zod as direct dependency - Add zod@^3.25.76 to match version from mc-utils - Required for validation schema definitions - Ensures single version across dependencies --- package-lock.json | 7396 ++++++++++++++++++++++++++++++++------------- package.json | 4 +- 2 files changed, 5279 insertions(+), 2121 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3488f7d..f4e7e4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@map-colonies/config": "^3.0.0", "@map-colonies/jobnik-sdk": "^0.1.0", "@map-colonies/js-logger": "^3.0.2", + "@map-colonies/mc-priority-queue": "^9.0.2", "@map-colonies/read-pkg": "^1.0.0", "@map-colonies/schemas": "^1.13.0", "@map-colonies/telemetry": "^10.0.1", @@ -21,7 +22,8 @@ "express": "^4.21.2", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.8.0" + "tsyringe": "^4.8.0", + "zod": "^3.25.76" }, "devDependencies": { "@commitlint/cli": "^19.8.0", @@ -1217,17 +1219,15 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, "license": "MIT", "engines": { "node": "20 || >=22" } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "license": "MIT", "dependencies": { "@isaacs/balanced-match": "^4.0.1" @@ -1339,6 +1339,18 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1471,6 +1483,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@map-colonies/error-express-handler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@map-colonies/error-express-handler/-/error-express-handler-2.1.0.tgz", + "integrity": "sha512-8qcyePq5JVrbEw7rioZ7nQfYavVw8OiFGwfAJolDmq045ppm82IEKBFMgzLC4p4dbRj+wDzwcuRkcv5yGE0IZA==", + "license": "ISC", + "dependencies": { + "http-status-codes": "^2.1.4" + } + }, + "node_modules/@map-colonies/error-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@map-colonies/error-types/-/error-types-1.3.1.tgz", + "integrity": "sha512-ZcXiCYcjk4SBhAxO6JGJZ9cmiCInBULpisrnTViPsdxtfk+1a6XG/sKXop5U5se6xQZ77L43ZEUhiwvE7FsaPA==", + "license": "ISC", + "peer": true, + "dependencies": { + "@map-colonies/error-express-handler": "^2.0.0", + "express": "^4.17.1", + "http-status-codes": "^2.1.4" + } + }, "node_modules/@map-colonies/eslint-config": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@map-colonies/eslint-config/-/eslint-config-6.0.0.tgz", @@ -1546,6 +1579,7 @@ "resolved": "https://registry.npmjs.org/@map-colonies/js-logger/-/js-logger-3.0.2.tgz", "integrity": "sha512-ZF4hYGkrVNvT9DVPvn9Ha5PZ16zSmg2f9VLtbev4QfS2MwcnuN0N9AIaGz+8avgNYucSwAw8PSyZEWJmnUf39Q==", "license": "ISC", + "peer": true, "dependencies": { "pino": "^9.7.0", "pino-caller": "^4.0.0", @@ -1555,6 +1589,43 @@ "node": ">=22" } }, + "node_modules/@map-colonies/mc-priority-queue": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@map-colonies/mc-priority-queue/-/mc-priority-queue-9.0.2.tgz", + "integrity": "sha512-oJXjMzPoX94S/+zP2GHAUM/Ykh5S1mrfwEbGViK+mFj2pq+MObMx8ckGsPtr27JB82v4bWv307Xe53yVfPWamA==", + "license": "ISC", + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@map-colonies/js-logger": "^3.0.2", + "@map-colonies/mc-utils": "^4.0.4" + } + }, + "node_modules/@map-colonies/mc-utils": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@map-colonies/mc-utils/-/mc-utils-4.0.4.tgz", + "integrity": "sha512-8H8geqdgYuu+nedveufokCoo/H9mrGieYFvG/Zqoqh7YAZ2KOKANlwpMbLEKS/7+SwzU8ZTxy4YWt0iziEJkqw==", + "license": "ISC", + "peer": true, + "dependencies": { + "@turf/turf": "^6.5.0", + "axios": "^1.10.0", + "axios-retry": "^4.5.0", + "gdal-async": "^3.12.1", + "http-status-codes": "^2.3.0", + "lodash": "^4.17.21", + "ngeohash": "^0.6.3", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=24.0.0" + }, + "peerDependencies": { + "@map-colonies/error-types": "^1.3.1", + "@map-colonies/js-logger": "^3.0.2" + } + }, "node_modules/@map-colonies/prettier-config": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@map-colonies/prettier-config/-/prettier-config-0.0.1.tgz", @@ -1714,6 +1785,82 @@ "node": ">=12.4.0" } }, + "node_modules/@npmcli/agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.0.tgz", + "integrity": "sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==", + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oozcitak/dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz", + "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/url": "^3.0.0", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/infra": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz", + "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==", + "license": "MIT", + "dependencies": { + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz", + "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==", + "license": "MIT", + "dependencies": { + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@oozcitak/util": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz", + "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==", + "license": "MIT", + "engines": { + "node": ">=20.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -3105,6 +3252,12 @@ "@opentelemetry/api": "^1.1.0" } }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "license": "MIT" + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -3579,707 +3732,2384 @@ "win32" ] }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, + "node_modules/@turf/along": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/along/-/along-6.5.0.tgz", + "integrity": "sha512-LLyWQ0AARqJCmMcIEAXF4GEu8usmd4Kbz3qk1Oy5HoRNpZX47+i5exQtmIWKdqJ1MMhW26fCTXgpsEs5zgJ5gw==", "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@turf/bearing": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/aws-lambda": { - "version": "8.10.147", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", - "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==", - "license": "MIT" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, + "node_modules/@turf/angle": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/angle/-/angle-6.5.0.tgz", + "integrity": "sha512-4pXMbWhFofJJAOvTMCns6N4C8CMd5Ih4O2jSAG9b3dDHakj3O4yN1+Zbm+NUei+eVEZ9gFeVp9svE3aMDenIkw==", "license": "MIT", "dependencies": { - "@types/connect": "*", - "@types/node": "*" + "@turf/bearing": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/rhumb-bearing": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/bunyan": { - "version": "1.8.11", - "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", - "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", + "node_modules/@turf/area": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-6.5.0.tgz", + "integrity": "sha512-xCZdiuojokLbQ+29qR6qoMD89hv+JAgWjLrwSEWL+3JV8IXKeNFl6XkEJz9HGkVpnXvQKJoRz4/liT+8ZZ5Jyg==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, + "node_modules/@turf/bbox": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-6.5.0.tgz", + "integrity": "sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw==", "license": "MIT", "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@turf/bbox-clip": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bbox-clip/-/bbox-clip-6.5.0.tgz", + "integrity": "sha512-F6PaIRF8WMp8EmgU/Ke5B1Y6/pia14UAYB5TiBC668w5rVVjy5L8rTm/m2lEkkDMHlzoP9vNY4pxpNthE7rLcQ==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/conventional-commits-parser": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", - "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", - "dev": true, + "node_modules/@turf/bbox-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bbox-polygon/-/bbox-polygon-6.5.0.tgz", + "integrity": "sha512-+/r0NyL1lOG3zKZmmf6L8ommU07HliP4dgYToMoTxqzsWzyLjaj/OzgQ8rBmv703WJX+aS6yCmLuIhYqyufyuw==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, + "node_modules/@turf/bearing": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bearing/-/bearing-6.5.0.tgz", + "integrity": "sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A==", "license": "MIT", "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", - "dev": true, + "node_modules/@turf/bezier-spline": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/bezier-spline/-/bezier-spline-6.5.0.tgz", + "integrity": "sha512-vokPaurTd4PF96rRgGVm6zYYC5r1u98ZsG+wZEv9y3kJTuJRX/O3xIY2QnTGTdbVmAJN1ouOsD0RoZYaVoXORQ==", "license": "MIT", "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", - "license": "MIT" - }, - "node_modules/@types/memcached": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", - "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==", + "node_modules/@turf/boolean-clockwise": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-6.5.0.tgz", + "integrity": "sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mysql": { - "version": "2.15.26", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", - "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", + "node_modules/@turf/boolean-contains": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz", + "integrity": "sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "node_modules/@turf/boolean-crosses": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-crosses/-/boolean-crosses-6.5.0.tgz", + "integrity": "sha512-gvshbTPhAHporTlQwBJqyfW+2yV8q/mOTxG6PzRVl6ARsqNoqYQWkd4MLug7OmAqVyBzLK3201uAeBjxbGw0Ng==", "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~7.16.0" + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/polygon-to-line": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/pg": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", - "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "node_modules/@turf/boolean-disjoint": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-disjoint/-/boolean-disjoint-6.5.0.tgz", + "integrity": "sha512-rZ2ozlrRLIAGo2bjQ/ZUu4oZ/+ZjGvLkN5CKXSKBcu6xFO6k2bgqeM8a1836tAW+Pqp/ZFsTA5fZHsJZvP2D5g==", "license": "MIT", "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/polygon-to-line": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/pg-pool": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", - "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "node_modules/@turf/boolean-equal": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-equal/-/boolean-equal-6.5.0.tgz", + "integrity": "sha512-cY0M3yoLC26mhAnjv1gyYNQjn7wxIXmL2hBmI/qs8g5uKuC2hRWi13ydufE3k4x0aNRjFGlg41fjoYLwaVF+9Q==", "license": "MIT", "dependencies": { - "@types/pg": "*" + "@turf/clean-coords": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "geojson-equality": "0.1.6" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, + "node_modules/@turf/boolean-intersects": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-intersects/-/boolean-intersects-6.5.0.tgz", + "integrity": "sha512-nIxkizjRdjKCYFQMnml6cjPsDOBCThrt+nkqtSEcxkKMhAQj5OO7o2CecioNTaX8EayqwMGVKcsz27oP4mKPTw==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@turf/boolean-disjoint": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, + "node_modules/@turf/boolean-overlap": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-overlap/-/boolean-overlap-6.5.0.tgz", + "integrity": "sha512-8btMIdnbXVWUa1M7D4shyaSGxLRw6NjMcqKBcsTXcZdnaixl22k7ar7BvIzkaRYN3SFECk9VGXfLncNS3ckQUw==", "license": "MIT", "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/line-overlap": "^6.5.0", + "@turf/meta": "^6.5.0", + "geojson-equality": "0.1.6" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, + "node_modules/@turf/boolean-parallel": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-parallel/-/boolean-parallel-6.5.0.tgz", + "integrity": "sha512-aSHJsr1nq9e5TthZGZ9CZYeXklJyRgR5kCLm5X4urz7+MotMOp/LsGOsvKvK9NeUl9+8OUmfMn8EFTT8LkcvIQ==", "license": "MIT", "dependencies": { - "@types/mime": "^1", - "@types/node": "*" + "@turf/clean-coords": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/rhumb-bearing": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", - "license": "MIT" - }, - "node_modules/@types/tedious": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", - "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "node_modules/@turf/boolean-point-in-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", + "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", "license": "MIT", "dependencies": { - "@types/node": "*" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", - "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", - "dev": true, + "node_modules/@turf/boolean-point-on-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz", + "integrity": "sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ==", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/type-utils": "8.47.0", - "@typescript-eslint/utils": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.47.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, + "node_modules/@turf/boolean-within": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-within/-/boolean-within-6.5.0.tgz", + "integrity": "sha512-YQB3oU18Inx35C/LU930D36RAVe7LDXk1kWsQ8mLmuqYn9YdPsDQTMTkLJMhoQ8EbN7QTdy333xRQ4MYgToteQ==", "license": "MIT", - "engines": { - "node": ">= 4" + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", - "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", - "dev": true, + "node_modules/@turf/buffer": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/buffer/-/buffer-6.5.0.tgz", + "integrity": "sha512-qeX4N6+PPWbKqp1AVkBVWFerGjMYMUyencwfnkCesoznU6qvfugFHNAngNqIBVnJjZ5n8IFyOf+akcxnrt9sNg==", "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@turf/bbox": "^6.5.0", + "@turf/center": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/projection": "^6.5.0", + "d3-geo": "1.7.1", + "turf-jsts": "*" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", - "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", - "dev": true, + "node_modules/@turf/center": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/center/-/center-6.5.0.tgz", + "integrity": "sha512-T8KtMTfSATWcAX088rEDKjyvQCBkUsLnK/Txb6/8WUXIeOZyHu42G7MkdkHRoHtwieLdduDdmPLFyTdG5/e7ZQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.47.0", - "@typescript-eslint/types": "^8.47.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@turf/bbox": "^6.5.0", + "@turf/helpers": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", - "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", - "dev": true, + "node_modules/@turf/center-mean": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/center-mean/-/center-mean-6.5.0.tgz", + "integrity": "sha512-AAX6f4bVn12pTVrMUiB9KrnV94BgeBKpyg3YpfnEbBpkN/znfVhL8dG8IxMAxAoSZ61Zt9WLY34HfENveuOZ7Q==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@turf/bbox": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", - "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", - "dev": true, + "node_modules/@turf/center-median": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/center-median/-/center-median-6.5.0.tgz", + "integrity": "sha512-dT8Ndu5CiZkPrj15PBvslpuf01ky41DEYEPxS01LOxp5HOUHXp1oJxsPxvc+i/wK4BwccPNzU1vzJ0S4emd1KQ==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@turf/center-mean": "^6.5.0", + "@turf/centroid": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", - "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", - "dev": true, + "node_modules/@turf/center-of-mass": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/center-of-mass/-/center-of-mass-6.5.0.tgz", + "integrity": "sha512-EWrriU6LraOfPN7m1jZi+1NLTKNkuIsGLZc2+Y8zbGruvUW+QV7K0nhf7iZWutlxHXTBqEXHbKue/o79IumAsQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@turf/centroid": "^6.5.0", + "@turf/convex": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", - "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", - "dev": true, + "node_modules/@turf/centroid": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-6.5.0.tgz", + "integrity": "sha512-MwE1oq5E3isewPprEClbfU5pXljIK/GUOMbn22UM3IFPDJX0KeoyLNwghszkdmFp/qMGL/M13MMWvU+GNLXP/A==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", - "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", - "dev": true, + "node_modules/@turf/circle": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/circle/-/circle-6.5.0.tgz", + "integrity": "sha512-oU1+Kq9DgRnoSbWFHKnnUdTmtcRUMmHoV9DjTXu9vOLNV5OWtAAh1VZ+mzsioGGzoDNT/V5igbFOkMfBQc0B6A==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.47.0", - "@typescript-eslint/tsconfig-utils": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@turf/destination": "^6.5.0", + "@turf/helpers": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, + "node_modules/@turf/clean-coords": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-6.5.0.tgz", + "integrity": "sha512-EMX7gyZz0WTH/ET7xV8MyrExywfm9qUi0/MY89yNffzGIEHuFfqwhcCqZ8O00rZIPZHUTxpmsxQSTfzJJA1CPw==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", + "node_modules/@turf/clone": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-6.5.0.tgz", + "integrity": "sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw==", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "@turf/helpers": "^6.5.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", - "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", - "dev": true, + "node_modules/@turf/clusters": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clusters/-/clusters-6.5.0.tgz", + "integrity": "sha512-Y6gfnTJzQ1hdLfCsyd5zApNbfLIxYEpmDibHUqR5z03Lpe02pa78JtgrgUNt1seeO/aJ4TG1NLN8V5gOrHk04g==", "license": "MIT", - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/turf" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.47.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", - "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", - "dev": true, + "node_modules/@turf/clusters-dbscan": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clusters-dbscan/-/clusters-dbscan-6.5.0.tgz", + "integrity": "sha512-SxZEE4kADU9DqLRiT53QZBBhu8EP9skviSyl+FGj08Y01xfICM/RR9ACUdM0aEQimhpu+ZpRVcUK+2jtiCGrYQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "eslint-visitor-keys": "^4.2.1" + "@turf/clone": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0", + "density-clustering": "1.3.0" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clusters-kmeans": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/clusters-kmeans/-/clusters-kmeans-6.5.0.tgz", + "integrity": "sha512-DwacD5+YO8kwDPKaXwT9DV46tMBVNsbi1IzdajZu1JDSWoN7yc7N9Qt88oi+p30583O0UPVkAK+A10WAQv4mUw==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "skmeans": "0.9.7" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/turf" } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@turf/collect": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/collect/-/collect-6.5.0.tgz", + "integrity": "sha512-4dN/T6LNnRg099m97BJeOcTA5fSI8cu87Ydgfibewd2KQwBexO69AnjEFqfPX3Wj+Zvisj1uAVIZbPmSSrZkjg==", "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "rbush": "2.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@turf/combine": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/combine/-/combine-6.5.0.tgz", + "integrity": "sha512-Q8EIC4OtAcHiJB3C4R+FpB4LANiT90t17uOd851qkM2/o6m39bfN5Mv0PWqMZIHWrrosZqRqoY9dJnzz/rJxYQ==", "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@turf/concave": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/concave/-/concave-6.5.0.tgz", + "integrity": "sha512-I/sUmUC8TC5h/E2vPwxVht+nRt+TnXIPRoztDFvS8/Y0+cBDple9inLSo9nnPXMXidrBlGXZ9vQx/BjZUJgsRQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@turf/clone": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/tin": "^6.5.0", + "topojson-client": "3.x", + "topojson-server": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@turf/convex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/convex/-/convex-6.5.0.tgz", + "integrity": "sha512-x7ZwC5z7PJB0SBwNh7JCeCNx7Iu+QSrH7fYgK0RhhNop13TqUlvHMirMLRgf2db1DqUetrAO2qHJeIuasquUWg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0", + "concaveman": "*" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@turf/destination": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/destination/-/destination-6.5.0.tgz", + "integrity": "sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@turf/difference": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/difference/-/difference-6.5.0.tgz", + "integrity": "sha512-l8iR5uJqvI+5Fs6leNbhPY5t/a3vipUF/3AeVLpwPQcgmedNXyheYuy07PcMGH5Jdpi5gItOiTqwiU/bUH4b3A==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "polygon-clipping": "^0.15.3" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@turf/dissolve": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/dissolve/-/dissolve-6.5.0.tgz", + "integrity": "sha512-WBVbpm9zLTp0Bl9CE35NomTaOL1c4TQCtEoO43YaAhNEWJOOIhZMFJyr8mbvYruKl817KinT3x7aYjjCMjTAsQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "polygon-clipping": "^0.15.3" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@turf/distance": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.5.0.tgz", + "integrity": "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@turf/distance-weight": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/distance-weight/-/distance-weight-6.5.0.tgz", + "integrity": "sha512-a8qBKkgVNvPKBfZfEJZnC3DV7dfIsC3UIdpRci/iap/wZLH41EmS90nM+BokAJflUHYy8PqE44wySGWHN1FXrQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/centroid": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@turf/ellipse": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/ellipse/-/ellipse-6.5.0.tgz", + "integrity": "sha512-kuXtwFviw/JqnyJXF1mrR/cb496zDTSbGKtSiolWMNImYzGGkbsAsFTjwJYgD7+4FixHjp0uQPzo70KDf3AIBw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/rhumb-destination": "^6.5.0", + "@turf/transform-rotate": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@turf/envelope": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/envelope/-/envelope-6.5.0.tgz", + "integrity": "sha512-9Z+FnBWvOGOU4X+fMZxYFs1HjFlkKqsddLuMknRaqcJd6t+NIv5DWvPtDL8ATD2GEExYDiFLwMdckfr1yqJgHA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/bbox-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, + "node_modules/@turf/explode": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/explode/-/explode-6.5.0.tgz", + "integrity": "sha512-6cSvMrnHm2qAsace6pw9cDmK2buAlw8+tjeJVXMfMyY+w7ZUi1rprWMsY92J7s2Dar63Bv09n56/1V7+tcj52Q==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/@turf/flatten": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/flatten/-/flatten-6.5.0.tgz", + "integrity": "sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@turf/flip": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/flip/-/flip-6.5.0.tgz", + "integrity": "sha512-oyikJFNjt2LmIXQqgOGLvt70RgE2lyzPMloYWM7OR5oIFGRiBvqVD2hA6MNw6JewIm30fWZ8DQJw1NHXJTJPbg==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", + "node_modules/@turf/great-circle": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/great-circle/-/great-circle-6.5.0.tgz", + "integrity": "sha512-7ovyi3HaKOXdFyN7yy1yOMa8IyOvV46RC1QOQTT+RYUN8ke10eyqExwBpL9RFUPvlpoTzoYbM/+lWPogQlFncg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", + "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "license": "MIT", + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/hex-grid": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/hex-grid/-/hex-grid-6.5.0.tgz", + "integrity": "sha512-Ln3tc2tgZT8etDOldgc6e741Smg1CsMKAz1/Mlel+MEL5Ynv2mhx3m0q4J9IB1F3a4MNjDeVvm8drAaf9SF33g==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/intersect": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/interpolate": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/interpolate/-/interpolate-6.5.0.tgz", + "integrity": "sha512-LSH5fMeiGyuDZ4WrDJNgh81d2DnNDUVJtuFryJFup8PV8jbs46lQGfI3r1DJ2p1IlEJIz3pmAZYeTfMMoeeohw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/centroid": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/hex-grid": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/point-grid": "^6.5.0", + "@turf/square-grid": "^6.5.0", + "@turf/triangle-grid": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/intersect": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/intersect/-/intersect-6.5.0.tgz", + "integrity": "sha512-2legGJeKrfFkzntcd4GouPugoqPUjexPZnOvfez+3SfIMrHvulw8qV8u7pfVyn2Yqs53yoVCEjS5sEpvQ5YRQg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "polygon-clipping": "^0.15.3" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", + "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/isobands": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/isobands/-/isobands-6.5.0.tgz", + "integrity": "sha512-4h6sjBPhRwMVuFaVBv70YB7eGz+iw0bhPRnp+8JBdX1UPJSXhoi/ZF2rACemRUr0HkdVB/a1r9gC32vn5IAEkw==", + "license": "MIT", + "dependencies": { + "@turf/area": "^6.5.0", + "@turf/bbox": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/explode": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "object-assign": "*" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/isolines": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/isolines/-/isolines-6.5.0.tgz", + "integrity": "sha512-6ElhiLCopxWlv4tPoxiCzASWt/jMRvmp6mRYrpzOm3EUl75OhHKa/Pu6Y9nWtCMmVC/RcWtiiweUocbPLZLm0A==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "object-assign": "*" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/kinks": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/kinks/-/kinks-6.5.0.tgz", + "integrity": "sha512-ViCngdPt1eEL7hYUHR2eHR662GvCgTc35ZJFaNR6kRtr6D8plLaDju0FILeFFWSc+o8e3fwxZEJKmFj9IzPiIQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/length": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/length/-/length-6.5.0.tgz", + "integrity": "sha512-5pL5/pnw52fck3oRsHDcSGrj9HibvtlrZ0QNy2OcW8qBFDNgZ4jtl6U7eATVoyWPKBHszW3dWETW+iLV7UARig==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-arc": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-arc/-/line-arc-6.5.0.tgz", + "integrity": "sha512-I6c+V6mIyEwbtg9P9zSFF89T7QPe1DPTG3MJJ6Cm1MrAY0MdejwQKOpsvNl8LDU2ekHOlz2kHpPVR7VJsoMllA==", + "license": "MIT", + "dependencies": { + "@turf/circle": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-chunk": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-chunk/-/line-chunk-6.5.0.tgz", + "integrity": "sha512-i1FGE6YJaaYa+IJesTfyRRQZP31QouS+wh/pa6O3CC0q4T7LtHigyBSYjrbjSLfn2EVPYGlPCMFEqNWCOkC6zg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/length": "^6.5.0", + "@turf/line-slice-along": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-intersect": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-6.5.0.tgz", + "integrity": "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "geojson-rbush": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-offset": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-offset/-/line-offset-6.5.0.tgz", + "integrity": "sha512-CEXZbKgyz8r72qRvPchK0dxqsq8IQBdH275FE6o4MrBkzMcoZsfSjghtXzKaz9vvro+HfIXal0sTk2mqV1lQTw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-overlap": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-overlap/-/line-overlap-6.5.0.tgz", + "integrity": "sha512-xHOaWLd0hkaC/1OLcStCpfq55lPHpPNadZySDXYiYjEz5HXr1oKmtMYpn0wGizsLwrOixRdEp+j7bL8dPt4ojQ==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/nearest-point-on-line": "^6.5.0", + "deep-equal": "1.x", + "geojson-rbush": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-segment": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-6.5.0.tgz", + "integrity": "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-slice": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-slice/-/line-slice-6.5.0.tgz", + "integrity": "sha512-vDqJxve9tBHhOaVVFXqVjF5qDzGtKWviyjbyi2QnSnxyFAmLlLnBfMX8TLQCAf2GxHibB95RO5FBE6I2KVPRuw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/nearest-point-on-line": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-slice-along": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-slice-along/-/line-slice-along-6.5.0.tgz", + "integrity": "sha512-KHJRU6KpHrAj+BTgTNqby6VCTnDzG6a1sJx/I3hNvqMBLvWVA2IrkR9L9DtsQsVY63IBwVdQDqiwCuZLDQh4Ng==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-split": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-split/-/line-split-6.5.0.tgz", + "integrity": "sha512-/rwUMVr9OI2ccJjw7/6eTN53URtGThNSD5I0GgxyFXMtxWiloRJ9MTff8jBbtPWrRka/Sh2GkwucVRAEakx9Sw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/nearest-point-on-line": "^6.5.0", + "@turf/square": "^6.5.0", + "@turf/truncate": "^6.5.0", + "geojson-rbush": "3.x" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-to-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/line-to-polygon/-/line-to-polygon-6.5.0.tgz", + "integrity": "sha512-qYBuRCJJL8Gx27OwCD1TMijM/9XjRgXH/m/TyuND4OXedBpIWlK5VbTIO2gJ8OCfznBBddpjiObLBrkuxTpN4Q==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/mask": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/mask/-/mask-6.5.0.tgz", + "integrity": "sha512-RQha4aU8LpBrmrkH8CPaaoAfk0Egj5OuXtv6HuCQnHeGNOQt3TQVibTA3Sh4iduq4EPxnZfDjgsOeKtrCA19lg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "polygon-clipping": "^0.15.3" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.5.0.tgz", + "integrity": "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/midpoint": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/midpoint/-/midpoint-6.5.0.tgz", + "integrity": "sha512-MyTzV44IwmVI6ec9fB2OgZ53JGNlgOpaYl9ArKoF49rXpL84F9rNATndbe0+MQIhdkw8IlzA6xVP4lZzfMNVCw==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/moran-index": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/moran-index/-/moran-index-6.5.0.tgz", + "integrity": "sha512-ItsnhrU2XYtTtTudrM8so4afBCYWNaB0Mfy28NZwLjB5jWuAsvyV+YW+J88+neK/ougKMTawkmjQqodNJaBeLQ==", + "license": "MIT", + "dependencies": { + "@turf/distance-weight": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point/-/nearest-point-6.5.0.tgz", + "integrity": "sha512-fguV09QxilZv/p94s8SMsXILIAMiaXI5PATq9d7YWijLxWUj6Q/r43kxyoi78Zmwwh1Zfqz9w+bCYUAxZ5+euA==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-on-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz", + "integrity": "sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-to-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-to-line/-/nearest-point-to-line-6.5.0.tgz", + "integrity": "sha512-PXV7cN0BVzUZdjj6oeb/ESnzXSfWmEMrsfZSDRgqyZ9ytdiIj/eRsnOXLR13LkTdXVOJYDBuf7xt1mLhM4p6+Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/point-to-line-distance": "^6.5.0", + "object-assign": "*" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/planepoint": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/planepoint/-/planepoint-6.5.0.tgz", + "integrity": "sha512-R3AahA6DUvtFbka1kcJHqZ7DMHmPXDEQpbU5WaglNn7NaCQg9HB0XM0ZfqWcd5u92YXV+Gg8QhC8x5XojfcM4Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-grid": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/point-grid/-/point-grid-6.5.0.tgz", + "integrity": "sha512-Iq38lFokNNtQJnOj/RBKmyt6dlof0yhaHEDELaWHuECm1lIZLY3ZbVMwbs+nXkwTAHjKfS/OtMheUBkw+ee49w==", + "license": "MIT", + "dependencies": { + "@turf/boolean-within": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-on-feature": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/point-on-feature/-/point-on-feature-6.5.0.tgz", + "integrity": "sha512-bDpuIlvugJhfcF/0awAQ+QI6Om1Y1FFYE8Y/YdxGRongivix850dTeXCo0mDylFdWFPGDo7Mmh9Vo4VxNwW/TA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/center": "^6.5.0", + "@turf/explode": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/nearest-point": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-to-line-distance": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/point-to-line-distance/-/point-to-line-distance-6.5.0.tgz", + "integrity": "sha512-opHVQ4vjUhNBly1bob6RWy+F+hsZDH9SA0UW36pIRzfpu27qipU18xup0XXEePfY6+wvhF6yL/WgCO2IbrLqEA==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/projection": "^6.5.0", + "@turf/rhumb-bearing": "^6.5.0", + "@turf/rhumb-distance": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/points-within-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/points-within-polygon/-/points-within-polygon-6.5.0.tgz", + "integrity": "sha512-YyuheKqjliDsBDt3Ho73QVZk1VXX1+zIA2gwWvuz8bR1HXOkcuwk/1J76HuFMOQI3WK78wyAi+xbkx268PkQzQ==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-smooth": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-smooth/-/polygon-smooth-6.5.0.tgz", + "integrity": "sha512-LO/X/5hfh/Rk4EfkDBpLlVwt3i6IXdtQccDT9rMjXEP32tRgy0VMFmdkNaXoGlSSKf/1mGqLl4y4wHd86DqKbg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-tangents": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-tangents/-/polygon-tangents-6.5.0.tgz", + "integrity": "sha512-sB4/IUqJMYRQH9jVBwqS/XDitkEfbyqRy+EH/cMRJURTg78eHunvJ708x5r6umXsbiUyQU4eqgPzEylWEQiunw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/boolean-within": "^6.5.0", + "@turf/explode": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/nearest-point": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-to-line": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-to-line/-/polygon-to-line-6.5.0.tgz", + "integrity": "sha512-5p4n/ij97EIttAq+ewSnKt0ruvuM+LIDzuczSzuHTpq4oS7Oq8yqg5TQ4nzMVuK41r/tALCk7nAoBuw3Su4Gcw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygonize": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/polygonize/-/polygonize-6.5.0.tgz", + "integrity": "sha512-a/3GzHRaCyzg7tVYHo43QUChCspa99oK4yPqooVIwTC61npFzdrmnywMv0S+WZjHZwK37BrFJGFrZGf6ocmY5w==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/envelope": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/projection": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/projection/-/projection-6.5.0.tgz", + "integrity": "sha512-/Pgh9mDvQWWu8HRxqpM+tKz8OzgauV+DiOcr3FCjD6ubDnrrmMJlsf6fFJmggw93mtVPrZRL6yyi9aYCQBOIvg==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/random": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/random/-/random-6.5.0.tgz", + "integrity": "sha512-8Q25gQ/XbA7HJAe+eXp4UhcXM9aOOJFaxZ02+XSNwMvY8gtWSCBLVqRcW4OhqilgZ8PeuQDWgBxeo+BIqqFWFQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rectangle-grid": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/rectangle-grid/-/rectangle-grid-6.5.0.tgz", + "integrity": "sha512-yQZ/1vbW68O2KsSB3OZYK+72aWz/Adnf7m2CMKcC+aq6TwjxZjAvlbCOsNUnMAuldRUVN1ph6RXMG4e9KEvKvg==", + "license": "MIT", + "dependencies": { + "@turf/boolean-intersects": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rewind": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-6.5.0.tgz", + "integrity": "sha512-IoUAMcHWotBWYwSYuYypw/LlqZmO+wcBpn8ysrBNbazkFNkLf3btSDZMkKJO/bvOzl55imr/Xj4fi3DdsLsbzQ==", + "license": "MIT", + "dependencies": { + "@turf/boolean-clockwise": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-bearing": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-bearing/-/rhumb-bearing-6.5.0.tgz", + "integrity": "sha512-jMyqiMRK4hzREjQmnLXmkJ+VTNTx1ii8vuqRwJPcTlKbNWfjDz/5JqJlb5NaFDcdMpftWovkW5GevfnuzHnOYA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-destination": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-destination/-/rhumb-destination-6.5.0.tgz", + "integrity": "sha512-RHNP1Oy+7xTTdRrTt375jOZeHceFbjwohPHlr9Hf68VdHHPMAWgAKqiX2YgSWDcvECVmiGaBKWus1Df+N7eE4Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-distance": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-distance/-/rhumb-distance-6.5.0.tgz", + "integrity": "sha512-oKp8KFE8E4huC2Z1a1KNcFwjVOqa99isxNOwfo4g3SUABQ6NezjKDDrnvC4yI5YZ3/huDjULLBvhed45xdCrzg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/sample": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/sample/-/sample-6.5.0.tgz", + "integrity": "sha512-kSdCwY7el15xQjnXYW520heKUrHwRvnzx8ka4eYxX9NFeOxaFITLW2G7UtXb6LJK8mmPXI8Aexv23F2ERqzGFg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/sector": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/sector/-/sector-6.5.0.tgz", + "integrity": "sha512-cYUOkgCTWqa23SOJBqxoFAc/yGCUsPRdn/ovbRTn1zNTm/Spmk6hVB84LCKOgHqvSF25i0d2kWqpZDzLDdAPbw==", + "license": "MIT", + "dependencies": { + "@turf/circle": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/line-arc": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/shortest-path": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/shortest-path/-/shortest-path-6.5.0.tgz", + "integrity": "sha512-4de5+G7+P4hgSoPwn+SO9QSi9HY5NEV/xRJ+cmoFVRwv2CDsuOPDheHKeuIAhKyeKDvPvPt04XYWbac4insJMg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/bbox-polygon": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/clean-coords": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/transform-scale": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/simplify": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/simplify/-/simplify-6.5.0.tgz", + "integrity": "sha512-USas3QqffPHUY184dwQdP8qsvcVH/PWBYdXY5am7YTBACaQOMAlf6AKJs9FT8jiO6fQpxfgxuEtwmox+pBtlOg==", + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/square": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/square/-/square-6.5.0.tgz", + "integrity": "sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/square-grid": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/square-grid/-/square-grid-6.5.0.tgz", + "integrity": "sha512-mlR0ayUdA+L4c9h7p4k3pX6gPWHNGuZkt2c5II1TJRmhLkW2557d6b/Vjfd1z9OVaajb1HinIs1FMSAPXuuUrA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/rectangle-grid": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/standard-deviational-ellipse": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/standard-deviational-ellipse/-/standard-deviational-ellipse-6.5.0.tgz", + "integrity": "sha512-02CAlz8POvGPFK2BKK8uHGUk/LXb0MK459JVjKxLC2yJYieOBTqEbjP0qaWhiBhGzIxSMaqe8WxZ0KvqdnstHA==", + "license": "MIT", + "dependencies": { + "@turf/center-mean": "^6.5.0", + "@turf/ellipse": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/points-within-polygon": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tag": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/tag/-/tag-6.5.0.tgz", + "integrity": "sha512-XwlBvrOV38CQsrNfrxvBaAPBQgXMljeU0DV8ExOyGM7/hvuGHJw3y8kKnQ4lmEQcmcrycjDQhP7JqoRv8vFssg==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tesselate": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/tesselate/-/tesselate-6.5.0.tgz", + "integrity": "sha512-M1HXuyZFCfEIIKkglh/r5L9H3c5QTEsnMBoZOFQiRnGPGmJWcaBissGb7mTFX2+DKE7FNWXh4TDnZlaLABB0dQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "earcut": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tin": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/tin/-/tin-6.5.0.tgz", + "integrity": "sha512-YLYikRzKisfwj7+F+Tmyy/LE3d2H7D4kajajIfc9mlik2+esG7IolsX/+oUz1biguDYsG0DUA8kVYXDkobukfg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-rotate": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/transform-rotate/-/transform-rotate-6.5.0.tgz", + "integrity": "sha512-A2Ip1v4246ZmpssxpcL0hhiVBEf4L8lGnSPWTgSv5bWBEoya2fa/0SnFX9xJgP40rMP+ZzRaCN37vLHbv1Guag==", + "license": "MIT", + "dependencies": { + "@turf/centroid": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/rhumb-bearing": "^6.5.0", + "@turf/rhumb-destination": "^6.5.0", + "@turf/rhumb-distance": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-scale": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/transform-scale/-/transform-scale-6.5.0.tgz", + "integrity": "sha512-VsATGXC9rYM8qTjbQJ/P7BswKWXHdnSJ35JlV4OsZyHBMxJQHftvmZJsFbOqVtQnIQIzf2OAly6rfzVV9QLr7g==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^6.5.0", + "@turf/center": "^6.5.0", + "@turf/centroid": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/rhumb-bearing": "^6.5.0", + "@turf/rhumb-destination": "^6.5.0", + "@turf/rhumb-distance": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-translate": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/transform-translate/-/transform-translate-6.5.0.tgz", + "integrity": "sha512-NABLw5VdtJt/9vSstChp93pc6oel4qXEos56RBMsPlYB8hzNTEKYtC146XJvyF4twJeeYS8RVe1u7KhoFwEM5w==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/rhumb-destination": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/triangle-grid": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/triangle-grid/-/triangle-grid-6.5.0.tgz", + "integrity": "sha512-2jToUSAS1R1htq4TyLQYPTIsoy6wg3e3BQXjm2rANzw4wPQCXGOxrur1Fy9RtzwqwljlC7DF4tg0OnWr8RjmfA==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/intersect": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/truncate": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/truncate/-/truncate-6.5.0.tgz", + "integrity": "sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/turf": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/turf/-/turf-6.5.0.tgz", + "integrity": "sha512-ipMCPnhu59bh92MNt8+pr1VZQhHVuTMHklciQURo54heoxRzt1neNYZOBR6jdL+hNsbDGAECMuIpAutX+a3Y+w==", + "license": "MIT", + "dependencies": { + "@turf/along": "^6.5.0", + "@turf/angle": "^6.5.0", + "@turf/area": "^6.5.0", + "@turf/bbox": "^6.5.0", + "@turf/bbox-clip": "^6.5.0", + "@turf/bbox-polygon": "^6.5.0", + "@turf/bearing": "^6.5.0", + "@turf/bezier-spline": "^6.5.0", + "@turf/boolean-clockwise": "^6.5.0", + "@turf/boolean-contains": "^6.5.0", + "@turf/boolean-crosses": "^6.5.0", + "@turf/boolean-disjoint": "^6.5.0", + "@turf/boolean-equal": "^6.5.0", + "@turf/boolean-intersects": "^6.5.0", + "@turf/boolean-overlap": "^6.5.0", + "@turf/boolean-parallel": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/boolean-point-on-line": "^6.5.0", + "@turf/boolean-within": "^6.5.0", + "@turf/buffer": "^6.5.0", + "@turf/center": "^6.5.0", + "@turf/center-mean": "^6.5.0", + "@turf/center-median": "^6.5.0", + "@turf/center-of-mass": "^6.5.0", + "@turf/centroid": "^6.5.0", + "@turf/circle": "^6.5.0", + "@turf/clean-coords": "^6.5.0", + "@turf/clone": "^6.5.0", + "@turf/clusters": "^6.5.0", + "@turf/clusters-dbscan": "^6.5.0", + "@turf/clusters-kmeans": "^6.5.0", + "@turf/collect": "^6.5.0", + "@turf/combine": "^6.5.0", + "@turf/concave": "^6.5.0", + "@turf/convex": "^6.5.0", + "@turf/destination": "^6.5.0", + "@turf/difference": "^6.5.0", + "@turf/dissolve": "^6.5.0", + "@turf/distance": "^6.5.0", + "@turf/distance-weight": "^6.5.0", + "@turf/ellipse": "^6.5.0", + "@turf/envelope": "^6.5.0", + "@turf/explode": "^6.5.0", + "@turf/flatten": "^6.5.0", + "@turf/flip": "^6.5.0", + "@turf/great-circle": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/hex-grid": "^6.5.0", + "@turf/interpolate": "^6.5.0", + "@turf/intersect": "^6.5.0", + "@turf/invariant": "^6.5.0", + "@turf/isobands": "^6.5.0", + "@turf/isolines": "^6.5.0", + "@turf/kinks": "^6.5.0", + "@turf/length": "^6.5.0", + "@turf/line-arc": "^6.5.0", + "@turf/line-chunk": "^6.5.0", + "@turf/line-intersect": "^6.5.0", + "@turf/line-offset": "^6.5.0", + "@turf/line-overlap": "^6.5.0", + "@turf/line-segment": "^6.5.0", + "@turf/line-slice": "^6.5.0", + "@turf/line-slice-along": "^6.5.0", + "@turf/line-split": "^6.5.0", + "@turf/line-to-polygon": "^6.5.0", + "@turf/mask": "^6.5.0", + "@turf/meta": "^6.5.0", + "@turf/midpoint": "^6.5.0", + "@turf/moran-index": "^6.5.0", + "@turf/nearest-point": "^6.5.0", + "@turf/nearest-point-on-line": "^6.5.0", + "@turf/nearest-point-to-line": "^6.5.0", + "@turf/planepoint": "^6.5.0", + "@turf/point-grid": "^6.5.0", + "@turf/point-on-feature": "^6.5.0", + "@turf/point-to-line-distance": "^6.5.0", + "@turf/points-within-polygon": "^6.5.0", + "@turf/polygon-smooth": "^6.5.0", + "@turf/polygon-tangents": "^6.5.0", + "@turf/polygon-to-line": "^6.5.0", + "@turf/polygonize": "^6.5.0", + "@turf/projection": "^6.5.0", + "@turf/random": "^6.5.0", + "@turf/rewind": "^6.5.0", + "@turf/rhumb-bearing": "^6.5.0", + "@turf/rhumb-destination": "^6.5.0", + "@turf/rhumb-distance": "^6.5.0", + "@turf/sample": "^6.5.0", + "@turf/sector": "^6.5.0", + "@turf/shortest-path": "^6.5.0", + "@turf/simplify": "^6.5.0", + "@turf/square": "^6.5.0", + "@turf/square-grid": "^6.5.0", + "@turf/standard-deviational-ellipse": "^6.5.0", + "@turf/tag": "^6.5.0", + "@turf/tesselate": "^6.5.0", + "@turf/tin": "^6.5.0", + "@turf/transform-rotate": "^6.5.0", + "@turf/transform-scale": "^6.5.0", + "@turf/transform-translate": "^6.5.0", + "@turf/triangle-grid": "^6.5.0", + "@turf/truncate": "^6.5.0", + "@turf/union": "^6.5.0", + "@turf/unkink-polygon": "^6.5.0", + "@turf/voronoi": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/union": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/union/-/union-6.5.0.tgz", + "integrity": "sha512-igYWCwP/f0RFHIlC2c0SKDuM/ObBaqSljI3IdV/x71805QbIvY/BYGcJdyNcgEA6cylIGl/0VSlIbpJHZ9ldhw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "polygon-clipping": "^0.15.3" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/unkink-polygon": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/unkink-polygon/-/unkink-polygon-6.5.0.tgz", + "integrity": "sha512-8QswkzC0UqKmN1DT6HpA9upfa1HdAA5n6bbuzHy8NJOX8oVizVAqfEPY0wqqTgboDjmBR4yyImsdPGUl3gZ8JQ==", + "license": "MIT", + "dependencies": { + "@turf/area": "^6.5.0", + "@turf/boolean-point-in-polygon": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/meta": "^6.5.0", + "rbush": "^2.0.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/voronoi": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/voronoi/-/voronoi-6.5.0.tgz", + "integrity": "sha512-C/xUsywYX+7h1UyNqnydHXiun4UPjK88VDghtoRypR9cLlb7qozkiLRphQxxsCM0KxyxpVPHBVQXdAL3+Yurow==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^6.5.0", + "@turf/invariant": "^6.5.0", + "d3-voronoi": "1.1.2" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.147", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", + "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bunyan": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.8", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.8.tgz", + "integrity": "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/memcached": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@types/memcached/-/memcached-2.2.10.tgz", + "integrity": "sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/pg": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", + "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" + }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", + "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/type-utils": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.47.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz", + "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", + "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.47.0", + "@typescript-eslint/types": "^8.47.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", + "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", + "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", + "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0", + "@typescript-eslint/utils": "8.47.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz", + "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", + "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.47.0", + "@typescript-eslint/tsconfig-utils": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/visitor-keys": "8.47.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz", + "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.47.0", + "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/typescript-estree": "8.47.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", + "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.47.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", "cpu": [ @@ -4287,1355 +6117,2004 @@ ], "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", + "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", "engines": { - "node": ">=14.0.0" + "node": "*" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.3.tgz", + "integrity": "sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==", + "license": "ISC", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0", + "unique-filename": "^5.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.1.tgz", + "integrity": "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.1.2", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" }, - "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" + "engines": { + "node": "20 || >=22" }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "license": "MIT", + "node_modules/cacache/node_modules/minimatch": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", + "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", + "license": "BlueOak-1.0.0", "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@isaacs/brace-expansion": "^5.0.1" + }, + "engines": { + "node": "20 || >=22" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 0.4" }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "engines": { + "node": ">= 0.4" } }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^4.0.3" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/@vitest/ui": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", - "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", - "peer": true, + "license": "ISC", "dependencies": { - "@vitest/utils": "3.2.4", - "fflate": "^0.8.2", - "flatted": "^3.3.3", - "pathe": "^2.0.3", - "sirv": "^3.0.1", - "tinyglobby": "^0.2.14", - "tinyrainbow": "^2.0.0" + "is-glob": "^4.0.1" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, - "peerDependencies": { - "vitest": "3.2.4" + "engines": { + "node": ">=12" } }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "color-name": "~1.1.4" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=7.0.0" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": "^12.20.0 || >=14" } }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" + "node": ">= 12.0.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, "engines": { - "node": ">= 14" + "node": ">= 0.6" } }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "license": "MIT", - "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/ajv-draft-04": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", - "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peerDependencies": { - "ajv": "^8.5.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concaveman": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concaveman/-/concaveman-2.0.0.tgz", + "integrity": "sha512-3a9C//4G44/boNehBPZMRh8XxrwBvTXlhENUim+GMm207WoDie/Vq89U5lkhLn3kKA+vxwmwfdQPWIRwjQWoLA==", + "license": "ISC", + "dependencies": { + "point-in-polygon": "^1.1.0", + "rbush": "^4.0.1", + "robust-predicates": "^3.0.2", + "tinyqueue": "^3.0.0" + } + }, + "node_modules/concaveman/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/concaveman/node_modules/rbush": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", "license": "MIT", "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "quickselect": "^3.0.0" } }, - "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/config": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.12.tgz", + "integrity": "sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==", "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + }, "engines": { - "node": ">=8" + "node": ">= 10.0.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "safe-buffer": "5.2.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">= 0.6" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", "dev": true, "license": "ISC", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "compare-func": "^2.0.0" }, "engines": { - "node": ">= 8" + "node": ">=16" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/array-ify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", "dev": true, - "license": "MIT" + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", "dev": true, "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, "engines": { - "node": ">=8" + "node": ">=16" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.6" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz", - "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==", + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/copyfiles": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", + "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" + }, + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" } }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "node_modules/copyfiles/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/copyfiles/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, - "license": "MIT" - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, "engines": { - "node": "*" + "node": ">=10" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/copyfiles/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10" } }, - "node_modules/bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", + "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">= 8" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node_modules/d3-geo": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.7.1.tgz", + "integrity": "sha512-O4AempWAr+P5qbk2bC2FuN/sDW4z+dN2wDf9QV3bxQt4M5HfOEeXLgJ/UKQW0+o1Dj8BE+L5kiDbdWUMjsmQpw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/d3-voronoi": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", + "integrity": "sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw==", + "license": "BSD-3-Clause" + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": "*" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", "license": "MIT", "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" }, "engines": { - "node": ">=18" - } - }, - "node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "license": "MIT", "engines": { - "node": ">= 16" + "node": ">=0.10.0" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 0.4" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "license": "MIT" - }, - "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==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "node": ">= 0.4" }, - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, "engines": { - "node": ">=7.0.0" + "node": ">=0.4.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "node_modules/density-clustering": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/density-clustering/-/density-clustering-1.3.0.tgz", + "integrity": "sha512-icpmBubVTwLnsaor9qH/4tG5+7+f61VcqMN3V3pm9sxxSCt2Jcs0zWOgwZW9ARJYaKD3FumIgHiMOcIMRRAzFQ==", "license": "MIT" }, - "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/comment-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/compare-func": { + "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", - "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-ify": "^1.0.0", - "dot-prop": "^5.1.0" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "license": "MIT" - }, - "node_modules/config": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/config/-/config-3.3.12.tgz", - "integrity": "sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==", "license": "MIT", "dependencies": { - "json5": "^2.2.3" + "path-type": "^4.0.0" }, "engines": { - "node": ">= 10.0.0" + "node": ">=8" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "is-obj": "^2.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/conventional-changelog-angular": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", - "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "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": "ISC", - "dependencies": { - "compare-func": "^2.0.0" - }, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">= 0.8" } }, - "node_modules/conventional-changelog-conventionalcommits": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", - "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", - "dev": true, - "license": "ISC", + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", "dependencies": { - "compare-func": "^2.0.0" - }, + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=6" } }, - "node_modules/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" - }, - "bin": { - "conventional-commits-parser": "cli.mjs" - }, + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">= 0.4" } }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, "license": "MIT" }, - "node_modules/copyfiles": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", - "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" + "es-errors": "^1.3.0" }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" + "engines": { + "node": ">= 0.4" } }, - "node_modules/copyfiles/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "license": "ISC", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/copyfiles/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/copyfiles/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, - "license": "ISC", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=6" } }, - "node_modules/core-util-is": { + "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=14" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" + "url": "https://eslint.org/donate" }, "peerDependencies": { - "typescript": ">=4.9.5" + "jiti": "*" }, "peerDependenciesMeta": { - "typescript": { + "jiti": { "optional": true } } }, - "node_modules/cosmiconfig-typescript-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", - "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "dependencies": { - "jiti": "^2.6.1" + "bin": { + "eslint-config-prettier": "bin/cli.js" }, - "engines": { - "node": ">=v18" + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" }, "peerDependencies": { - "@types/node": "*", - "cosmiconfig": ">=9", - "typescript": ">=5" + "eslint": ">=7.0.0" } }, - "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", "dev": true, "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" }, "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/dargs": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", - "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/date-fns": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", - "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "url": "https://opencollective.com/eslint-import-resolver-typescript" }, - "engines": { - "node": ">=6.0" + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" }, "peerDependenciesMeta": { - "supports-color": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { "optional": true } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/eslint-plugin-import-x": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.1.tgz", + "integrity": "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==", "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "^8.35.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.0.1", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-import-x" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/eslint-plugin-import-x/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "path-type": "^4.0.0" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=8" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "node_modules/eslint-plugin-jest": { + "version": "28.14.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", + "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "is-obj": "^2.0.0" + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "engines": { - "node": ">=8" + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">= 0.8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", "dependencies": { - "once": "^1.4.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "p-limit": "^3.0.2" }, "engines": { - "node": ">=18" + "node": ">=10" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + }, + "node_modules/eslint/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -5645,852 +8124,823 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "MIT", - "peer": true, + "license": "BSD-2-Clause", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" }, - "peerDependencies": { - "eslint": ">=7.0.0" + "engines": { + "node": ">=0.10" } }, - "node_modules/eslint-import-context": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", - "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "get-tsconfig": "^4.10.1", - "stable-hash-x": "^0.2.0" + "estraverse": "^5.2.0" }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-context" - }, - "peerDependencies": { - "unrs-resolver": "^1.0.0" - }, - "peerDependenciesMeta": { - "unrs-resolver": { - "optional": true - } + "node": ">=4.0" } }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "ISC", - "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, + "license": "BSD-2-Clause", "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } + "node": ">=4.0" } }, - "node_modules/eslint-plugin-import-x": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.1.tgz", - "integrity": "sha512-vPZZsiOKaBAIATpFE2uMI4w5IRwdv/FpQ+qZZMR4E+PeOcM4OeoEbqxRMnywdxP19TyB/3h6QBB0EWon7letSQ==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/types": "^8.35.0", - "comment-parser": "^1.4.1", - "debug": "^4.4.1", - "eslint-import-context": "^0.1.9", - "is-glob": "^4.0.3", - "minimatch": "^9.0.3 || ^10.0.1", - "semver": "^7.7.2", - "stable-hash-x": "^0.2.0", - "unrs-resolver": "^1.9.2" - }, + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-import-x" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", - "eslint-import-resolver-node": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/utils": { - "optional": true - }, - "eslint-import-resolver-node": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import-x/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": "20 || >=22" + "node": ">= 0.10.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/eslint-plugin-jest": { - "version": "28.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", - "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", - "dev": true, + "node_modules/express-prom-bundle": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.6.0.tgz", + "integrity": "sha512-tZh2P2p5a8/yxQ5VbRav011Poa4R0mHqdFwn9Swe/obXDe5F0jY9wtRAfNYnqk4LXY7akyvR/nrvAHxQPWUjsQ==", "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + "on-finished": "^2.3.0", + "url-value-parser": "^2.0.0" }, "engines": { - "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + "node": ">=10" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", - "jest": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } + "prom-client": ">=12.0.0" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8.6.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">= 6" } }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "reusify": "^1.0.4" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=16.0.0" } }, - "node_modules/eslint/node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16" } }, - "node_modules/eslint/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "ISC" }, - "node_modules/eslint/node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "license": "MIT", "engines": { - "node": ">=10" + "node": ">=4.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "license": "BSD-2-Clause", + "license": "ISC", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=14" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" }, "engines": { - "node": ">=0.10" + "node": ">= 6" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 0.6" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">= 0.6" } }, - "node_modules/estree-walker": { + "node_modules/fs-minipass": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", "dependencies": { - "@types/estree": "^1.0.0" + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "BSD-2-Clause", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=0.10.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", - "dev": true, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, "engines": { - "node": ">=12.0.0" + "node": ">=14" } }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" }, "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=14" } - }, - "node_modules/express-prom-bundle": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/express-prom-bundle/-/express-prom-bundle-6.6.0.tgz", - "integrity": "sha512-tZh2P2p5a8/yxQ5VbRav011Poa4R0mHqdFwn9Swe/obXDe5F0jY9wtRAfNYnqk4LXY7akyvR/nrvAHxQPWUjsQ==", - "license": "MIT", + }, + "node_modules/gdal-async": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/gdal-async/-/gdal-async-3.12.1.tgz", + "integrity": "sha512-KmxY5Vk571aBcoxzbSwPnXcJvYfzyEIiMMADpOuZlnXp4v7LaTlOYSqXvdzyH3xwcO8N/+265rTFv7ArPJL8Fg==", + "bundleDependencies": [ + "@mapbox/node-pre-gyp" + ], + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "on-finished": "^2.3.0", - "url-value-parser": "^2.0.0" + "@mapbox/node-pre-gyp": "^2.0.0", + "@petamoriken/float16": "^3.9.2", + "nan": "^2.23.0", + "node-gyp": "^12.1.0", + "xmlbuilder2": "^4.0.0", + "yatag": "^1.2.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, - "peerDependencies": { - "prom-client": ">=12.0.0" + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/mmomtchev" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", + "node_modules/gdal-async/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", "dependencies": { - "ms": "2.0.0" + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/express/node_modules/ms": { + "node_modules/gdal-async/node_modules/@mapbox/node-pre-gyp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-copy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", - "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", + "inBundle": true, + "license": "BSD-3-Clause", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" }, "engines": { - "node": ">=8.6.0" + "node": ">=18" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, + "node_modules/gdal-async/node_modules/abbrev": { + "version": "3.0.1", + "inBundle": true, "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, "engines": { - "node": ">= 6" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "node_modules/gdal-async/node_modules/agent-base": { + "version": "7.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "node_modules/gdal-async/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "dev": true, - "license": "MIT" + "node_modules/gdal-async/node_modules/consola": { + "version": "3.4.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, + "node_modules/gdal-async/node_modules/debug": { + "version": "4.4.1", + "inBundle": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=16.0.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "node_modules/gdal-async/node_modules/detect-libc": { + "version": "2.0.4", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/gdal-async/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">=8" + "node": ">= 14" } }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "node_modules/gdal-async/node_modules/minipass": { + "version": "7.1.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/gdal-async/node_modules/minizlib": { + "version": "3.0.2", + "inBundle": true, "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "minipass": "^7.1.2" }, "engines": { - "node": ">= 0.8" + "node": ">= 18" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/gdal-async/node_modules/mkdirp": { + "version": "3.0.1", + "inBundle": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "node_modules/gdal-async/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, "license": "MIT" }, - "node_modules/find-up": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", - "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", - "dev": true, + "node_modules/gdal-async/node_modules/node-fetch": { + "version": "2.7.0", + "inBundle": true, "license": "MIT", "dependencies": { - "locate-path": "^7.2.0", - "path-exists": "^5.0.0", - "unicorn-magic": "^0.1.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "4.x || >=6.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", + "node_modules/gdal-async/node_modules/nopt": { + "version": "8.1.0", + "inBundle": true, + "license": "ISC", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": ">=16" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" + "node_modules/gdal-async/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, + "node_modules/gdal-async/node_modules/tar": { + "version": "7.4.3", + "inBundle": true, "license": "ISC", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", + "node_modules/gdal-async/node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "node_modules/gdal-async/node_modules/tr46": { + "version": "0.0.3", + "inBundle": true, "license": "MIT" }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" + "node_modules/gdal-async/node_modules/webidl-conversions": { + "version": "3.0.1", + "inBundle": true, + "license": "BSD-2-Clause" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, + "node_modules/gdal-async/node_modules/whatwg-url": { + "version": "5.0.0", + "inBundle": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/geojson-equality": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/geojson-equality/-/geojson-equality-0.1.6.tgz", + "integrity": "sha512-TqG8YbqizP3EfwP5Uw4aLu6pKkg6JQK9uq/XZ1lXQntvTHD1BBKJWhNpJ2M0ax6TuWMP3oyx6Oq7FCIfznrgpQ==", "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "deep-equal": "^1.0.0" } }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", + "node_modules/geojson-rbush": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/geojson-rbush/-/geojson-rbush-3.2.0.tgz", + "integrity": "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w==", + "license": "MIT", "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" + "@turf/bbox": "*", + "@turf/helpers": "6.x", + "@turf/meta": "6.x", + "@types/geojson": "7946.0.8", + "rbush": "^3.0.1" } }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", + "node_modules/geojson-rbush/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/geojson-rbush/node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" + "quickselect": "^2.0.0" } }, "node_modules/get-caller-file": { @@ -6575,7 +9025,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -6663,6 +9112,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -6680,6 +9135,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -6692,6 +9159,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6717,6 +9199,12 @@ "dev": true, "license": "MIT" }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -6733,6 +9221,19 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-status-codes": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", @@ -6844,7 +9345,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -6855,7 +9355,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -6878,6 +9377,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6887,6 +9395,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -6932,6 +9456,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6984,6 +9524,36 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7361,7 +9931,6 @@ "version": "11.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "dev": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -7405,6 +9974,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-fetch-happen": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz", + "integrity": "sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==", + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7524,7 +10124,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -7546,10 +10145,122 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.1.tgz", + "integrity": "sha512-yHK8pb0iCGat0lDrs/D6RZmCdaBT64tULXjdxjSMAqoDi18Q3qKEUTHypHQZQd9+FYpIS+lkvpq6C/R6SbUeRw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" } }, "node_modules/mkdirp": { @@ -7611,6 +10322,12 @@ "url": "https://github.com/sponsors/raouldeheer" } }, + "node_modules/nan": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", + "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -7662,6 +10379,15 @@ "node": ">= 0.6" } }, + "node_modules/ngeohash": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/ngeohash/-/ngeohash-0.6.3.tgz", + "integrity": "sha512-kltF0cOxgx1AbmVzKxYZaoB0aj7mOxZeHaerEtQV0YaqnkXNq26WWqMmJ6lTqShYxVRWZ/mwvvTrNeOwdslWiw==", + "license": "MIT", + "engines": { + "node": ">=v0.2.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -7682,6 +10408,54 @@ } } }, + "node_modules/node-gyp": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", + "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^15.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.0.tgz", + "integrity": "sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/noms": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", @@ -7693,6 +10467,21 @@ "readable-stream": "~1.0.31" } }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7703,6 +10492,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -7715,6 +10513,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -7835,6 +10658,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -7897,7 +10732,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -7923,7 +10757,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -8117,6 +10950,22 @@ "node": ">=12" } }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, + "node_modules/polygon-clipping": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz", + "integrity": "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==", + "license": "MIT", + "dependencies": { + "robust-predicates": "^3.0.2", + "splaytree": "^3.1.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -8270,6 +11119,15 @@ "dev": true, "license": "MIT" }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -8307,6 +11165,19 @@ "node": "^16 || ^18 || >=20" } }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -8344,6 +11215,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -8416,6 +11293,12 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/quickselect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-1.1.1.tgz", + "integrity": "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ==", + "license": "ISC" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -8440,6 +11323,15 @@ "node": ">= 0.8" } }, + "node_modules/rbush": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-2.0.2.tgz", + "integrity": "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==", + "license": "MIT", + "dependencies": { + "quickselect": "^1.0.1" + } + }, "node_modules/readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", @@ -8481,6 +11373,26 @@ "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "license": "Apache-2.0" }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -8553,6 +11465,15 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -8618,6 +11539,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -8810,6 +11737,38 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -8952,6 +11911,12 @@ "node": ">=18" } }, + "node_modules/skmeans": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", + "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==", + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8962,6 +11927,44 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/sonic-boom": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", @@ -9000,6 +12003,15 @@ "source-map": "^0.6.0" } }, + "node_modules/splaytree": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.2.3.tgz", + "integrity": "sha512-7OXrNWzy6CK+r7Ch9OLPBDTKfB6XlWHjX4P0RU5B3IgFuWPeYN0XtRtlexGRjgbQxpfaUve6jTAwBGWuGntz/w==", + "license": "MIT", + "engines": { + "node": ">=18.20 || >=20" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -9009,6 +12021,18 @@ "node": ">= 10.x" } }, + "node_modules/ssri": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.0.tgz", + "integrity": "sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -9193,6 +12217,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tar": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -9389,7 +12429,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -9406,7 +12445,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -9424,7 +12462,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -9444,6 +12481,12 @@ "node": "^18.0.0 || >=20.0.0" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/tinyrainbow": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", @@ -9486,6 +12529,44 @@ "node": ">=0.6" } }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/topojson-server": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/topojson-server/-/topojson-server-3.0.1.tgz", + "integrity": "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "geo2topo": "bin/geo2topo" + } + }, + "node_modules/topojson-server/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -9562,6 +12643,12 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/turf-jsts": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/turf-jsts/-/turf-jsts-1.2.3.tgz", + "integrity": "sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==", + "license": "(EDL-1.0 OR EPL-1.0)" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9671,6 +12758,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-filename": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", + "integrity": "sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==", + "license": "ISC", + "dependencies": { + "unique-slug": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/unique-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-6.0.0.tgz", + "integrity": "sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -10108,6 +13219,21 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xmlbuilder2": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz", + "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==", + "license": "MIT", + "dependencies": { + "@oozcitak/dom": "^2.0.2", + "@oozcitak/infra": "^2.0.2", + "@oozcitak/util": "^10.0.0", + "js-yaml": "^4.1.1" + }, + "engines": { + "node": ">=20.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -10126,6 +13252,15 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", @@ -10165,6 +13300,18 @@ "node": ">=12" } }, + "node_modules/yatag": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/yatag/-/yatag-1.3.0.tgz", + "integrity": "sha512-kkdqNFmWCWdz2FkGYnnkyhKlpJbd9zlZuMTtcKpDJZ3ZgfuX0beGhuZseI6npm120Ti5iMI+53JOK2EUgUVziw==", + "license": "ISC", + "dependencies": { + "glob": "^7.2.3" + }, + "bin": { + "yatag": "yatag.js" + } + }, "node_modules/yocto-queue": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", @@ -10177,6 +13324,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index e167795..d0e8686 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@map-colonies/config": "^3.0.0", "@map-colonies/jobnik-sdk": "^0.1.0", "@map-colonies/js-logger": "^3.0.2", + "@map-colonies/mc-priority-queue": "^9.0.2", "@map-colonies/read-pkg": "^1.0.0", "@map-colonies/schemas": "^1.13.0", "@map-colonies/telemetry": "^10.0.1", @@ -42,7 +43,8 @@ "express": "^4.21.2", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.8.0" + "tsyringe": "^4.8.0", + "zod": "^3.25.76" }, "devDependencies": { "@commitlint/cli": "^19.8.0", From c83baac39a516502c71cbab92df1e7788fc0ffd3 Mon Sep 17 00:00:00 2001 From: almog8k Date: Mon, 9 Feb 2026 15:04:34 +0200 Subject: [PATCH 09/25] feat(errors): add ErrorHandler for centralized error handling - Add ErrorHandler class to decide retry vs reject for task errors - Handle RecoverableError: retry if attempts < maxAttempts - Handle UnrecoverableError: never retry (validation, config errors) - Handle unknown errors: treat as recoverable with retry fallback - Log errors with full context (job, task, attempts, error details) - Add comprehensive test coverage (100% for ErrorHandler) --- src/cleaner/errors/errorHandler.ts | 81 +++++++++++++++++++++ src/cleaner/errors/index.ts | 1 + tests/errorHandler.spec.ts | 113 +++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 src/cleaner/errors/errorHandler.ts create mode 100644 tests/errorHandler.spec.ts diff --git a/src/cleaner/errors/errorHandler.ts b/src/cleaner/errors/errorHandler.ts new file mode 100644 index 0000000..d679ff8 --- /dev/null +++ b/src/cleaner/errors/errorHandler.ts @@ -0,0 +1,81 @@ +import { inject, injectable } from 'tsyringe'; +import { type Logger } from '@map-colonies/js-logger'; +import { SERVICES } from '@common/constants'; +import type { ErrorContext, ErrorDecision } from '../types'; +import { RecoverableError, UnrecoverableError } from './errors'; + +/** + * ErrorHandler centralizes error handling logic for task processing. + * Determines whether errors should trigger retry or rejection. + */ +@injectable() +export class ErrorHandler { + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} + + /** + * Handles an error that occurred during task processing. + * + * @param context - Error context including job, task, attempts, and error + * @returns Decision object indicating whether to retry and the reason + */ + public handleError(context: ErrorContext): ErrorDecision { + const { jobId, taskId, attemptNumber, maxAttempts, error } = context; + this.logger.error({ + msg: 'Task processing error', + jobId, + taskId, + attemptNumber, + maxAttempts, + err: error, + }); + + const decision = this.evaluateError(context); + + this.logDecision(context, decision); + + return decision; + } + + /** + * Evaluates the error and determines the retry decision. + * + * - UnrecoverableError: never retry + * - RecoverableError: retry if attempts remain + * - Unknown errors: treated as recoverable, retry if attempts remain + */ + private evaluateError(context: ErrorContext): ErrorDecision { + const { attemptNumber, maxAttempts, error } = context; + + if (error instanceof UnrecoverableError) { + return { shouldRetry: false, reason: `Unrecoverable error-${error.name}: ${error.message}` }; + } + + const shouldRetry = attemptNumber < maxAttempts; + const errorLabel = error instanceof RecoverableError ? error.name : 'Unknown'; + const errorMessage = `${errorLabel} error: ${error.message}`; + + const reason = shouldRetry + ? `${errorMessage}, attempt ${attemptNumber}/${maxAttempts}` + : `Max attempts (${maxAttempts}) reached for ${errorMessage}`; + + return { shouldRetry, reason }; + } + + private logDecision(context: ErrorContext, decision: ErrorDecision): void { + const { jobId, taskId, error } = context; + const isKnownError = error instanceof RecoverableError || error instanceof UnrecoverableError; + + const logData = { + msg: decision.shouldRetry ? 'Task will be retried' : 'Task will be rejected', + jobId, + taskId, + reason: decision.reason, + }; + + if (decision.shouldRetry && isKnownError) { + this.logger.info(logData); + } else { + this.logger.warn(logData); + } + } +} diff --git a/src/cleaner/errors/index.ts b/src/cleaner/errors/index.ts index ae7d662..c8d0da2 100644 --- a/src/cleaner/errors/index.ts +++ b/src/cleaner/errors/index.ts @@ -1 +1,2 @@ export { RecoverableError, UnrecoverableError, ValidationError, StrategyNotFoundError } from './errors'; +export { ErrorHandler } from './errorHandler'; diff --git a/tests/errorHandler.spec.ts b/tests/errorHandler.spec.ts new file mode 100644 index 0000000..b3a776f --- /dev/null +++ b/tests/errorHandler.spec.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { faker } from '@faker-js/faker'; +import type { Logger } from '@map-colonies/js-logger'; +import { ErrorHandler, RecoverableError, UnrecoverableError, ValidationError } from '../src/cleaner/errors'; +import type { ErrorContext } from '../src/cleaner/types'; +import { createMockLogger } from './helpers/mocks'; + +describe('ErrorHandler', () => { + let errorHandler: ErrorHandler; + let mockLogger: Logger; + + beforeEach(() => { + mockLogger = createMockLogger(); + errorHandler = new ErrorHandler(mockLogger); + }); + + describe('handleError', () => { + const baseContext: Omit = { + jobId: faker.string.uuid(), + taskId: faker.string.uuid(), + attemptNumber: 1, + maxAttempts: 3, + }; + + describe('UnrecoverableError handling', () => { + it('should not retry for unrecoverable errors regardless of attempts', () => { + const error = new ValidationError('Invalid schema'); + const context: ErrorContext = { ...baseContext, attemptNumber: 1, error }; + + const decision = errorHandler.handleError(context); + + expect(decision.shouldRetry).toBe(false); + expect(decision.reason).toContain('Unrecoverable error'); + expect(mockLogger.error).toHaveBeenCalled(); + expect(mockLogger.warn).toHaveBeenCalled(); + }); + + it('should not retry even on first attempt', () => { + const error = new UnrecoverableError('Fatal error'); + const context: ErrorContext = { ...baseContext, attemptNumber: 1, error }; + + const decision = errorHandler.handleError(context); + + expect(decision.shouldRetry).toBe(false); + }); + }); + + describe('RecoverableError handling', () => { + it('should retry when attempts remain', () => { + const error = new RecoverableError('Network timeout'); + const context: ErrorContext = { ...baseContext, attemptNumber: 1, error }; + + const decision = errorHandler.handleError(context); + + expect(decision.shouldRetry).toBe(true); + expect(decision.reason).toContain('RecoverableError'); + expect(decision.reason).toContain('Network timeout'); + expect(decision.reason).toContain('1/3'); + expect(mockLogger.info).toHaveBeenCalled(); + }); + + it('should not retry when max attempts reached', () => { + const error = new RecoverableError('Network timeout'); + const context: ErrorContext = { ...baseContext, attemptNumber: 3, error }; + + const decision = errorHandler.handleError(context); + + expect(decision.shouldRetry).toBe(false); + expect(decision.reason).toContain('Max attempts'); + expect(decision.reason).toContain('3'); + expect(decision.reason).toContain('Network timeout'); + expect(mockLogger.warn).toHaveBeenCalled(); + }); + + it('should retry on second attempt if max is 3', () => { + const error = new RecoverableError('Temporary failure'); + const context: ErrorContext = { ...baseContext, attemptNumber: 2, maxAttempts: 3, error }; + + const decision = errorHandler.handleError(context); + + expect(decision.shouldRetry).toBe(true); + expect(decision.reason).toContain('2/3'); + }); + }); + + describe('Unknown error handling', () => { + it('should treat unknown errors as recoverable and retry', () => { + const error = new Error('Unknown error type'); + const context: ErrorContext = { ...baseContext, attemptNumber: 1, error }; + + const decision = errorHandler.handleError(context); + + expect(decision.shouldRetry).toBe(true); + expect(decision.reason).toContain('Unknown error'); + expect(decision.reason).toContain('Unknown error type'); + expect(decision.reason).toContain('1/3'); + expect(mockLogger.warn).toHaveBeenCalled(); + }); + + it('should not retry unknown errors when max attempts reached', () => { + const error = new Error('Unknown error'); + const context: ErrorContext = { ...baseContext, attemptNumber: 3, error }; + + const decision = errorHandler.handleError(context); + + expect(decision.shouldRetry).toBe(false); + expect(decision.reason).toContain('Max attempts'); + expect(decision.reason).toContain('Unknown error'); + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); + }); +}); From a53b01126060b7528d297de23428874c141963c0 Mon Sep 17 00:00:00 2001 From: almog8k Date: Mon, 9 Feb 2026 16:07:11 +0200 Subject: [PATCH 10/25] docs(testing): update testing guide with hybrid architecture - Replace generic examples with actual project patterns - Add hybrid philosophy (unit *.spec.ts + integration *.integration.spec.ts) - Document testing decision guide (when to use unit vs integration) - Add real examples from codebase (ErrorHandler, createMockLogger) - Include faker usage examples for test data generation - Add troubleshooting section for common issues - Document selective test running commands - Remove outdated boilerplate references - Keep guide concise and practical (~220 lines) --- ai-docs/testing.md | 322 ++++++++++++++++++++++++++++----------------- 1 file changed, 200 insertions(+), 122 deletions(-) diff --git a/ai-docs/testing.md b/ai-docs/testing.md index 0fd7bee..b1dc2bd 100644 --- a/ai-docs/testing.md +++ b/ai-docs/testing.md @@ -1,199 +1,277 @@ # Testing -## Framework +## Overview -The project uses [Vitest](https://vitest.dev/) for testing with V8 coverage. +The project uses [Vitest](https://vitest.dev/) with V8 coverage for testing. We follow a **hybrid testing approach** combining unit and integration tests. ## Commands -| Command | Description | -| -------------------- | --------------------------- | -| `npm test` | Run all tests with coverage | -| `npm run test:watch` | Watch mode for development | -| `npm run test:ui` | Open Vitest UI dashboard | +| Command | Description | +| -------------------------------------------- | --------------------------- | +| `npm test` | Run all tests with coverage | +| `npm run test:watch` | Watch mode for development | +| `npm run test:ui` | Open Vitest UI dashboard | +| `npx vitest run tests/file.spec.ts` | Run specific test file | +| `npx vitest run -t "test name"` | Run tests matching pattern | +| `npx vitest run tests/*.spec.ts` | Run only unit tests | +| `npx vitest run tests/*.integration.spec.ts` | Run only integration tests | -## Configuration +## Testing Philosophy -Test configuration is in `vitest.config.mts`: +We use a **hybrid approach**: -```typescript -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - coverage: { - provider: 'v8', - reporter: ['text', 'html', 'lcov'], - }, - }, -}); -``` +- **Unit tests** (`*.spec.ts`) — Test individual classes/functions in isolation with mocked dependencies +- **Integration tests** (`*.integration.spec.ts`) — Test component interactions with real dependencies + +### When to Use Unit Tests + +- Testing business logic (error handling, validation, decision logic) +- Fast feedback during development +- Testing edge cases and error paths +- When dependencies are complex or slow (HTTP clients, file I/O) + +### When to Use Integration Tests + +- Testing component wiring and dependency injection +- Verifying end-to-end workflows (polling → validation → strategy → ack/reject) +- Testing configuration loading and service initialization +- When interactions between real components are critical ## File Structure ``` tests/ - *.spec.ts # Test files - helpers/ # Test utilities (if needed) - mocks/ # Mock implementations (if needed) + errorHandler.spec.ts # Unit test + validation.spec.ts # Unit test + taskPoller.integration.spec.ts # Integration test + helpers/ + mocks.ts # Reusable mock factories + fakes.ts # Fake data generators + setup/ + vite.setup.ts # Vitest global setup ``` -## Writing Tests +## Writing Unit Tests -### Basic Test Structure +### Basic Structure ```typescript -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { CleanerManager } from '../src/cleaner/manager'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { ErrorHandler } from '../src/cleaner/errors'; +import { createMockLogger } from './helpers/mocks'; -describe('CleanerManager', () => { - let manager: CleanerManager; +describe('ErrorHandler', () => { + let errorHandler: ErrorHandler; + let mockLogger: Logger; beforeEach(() => { - manager = new CleanerManager(); + mockLogger = createMockLogger(); + errorHandler = new ErrorHandler(mockLogger); }); - describe('handleCleanupTask', () => { - it('should process valid cleanup task', async () => { - const task = createMockTask({ resourcePath: '/tmp/test' }); - const context = createMockContext(); - - await manager.handleCleanupTask(task, context); + it('should retry recoverable errors when attempts remain', () => { + const error = new RecoverableError('Network timeout'); + const context = { jobId: '123', taskId: '456', attemptNumber: 1, maxAttempts: 3, error }; - expect(context.updateStageUserMetadata).toHaveBeenCalled(); - }); + const decision = errorHandler.handleError(context); - it('should throw error for invalid resource path', async () => { - const task = createMockTask({ resourcePath: '' }); - const context = createMockContext(); - - await expect(manager.handleCleanupTask(task, context)).rejects.toThrow('Resource path is required'); - }); + expect(decision.shouldRetry).toBe(true); + expect(decision.reason).toContain('1/3'); }); }); ``` -### Mocking Dependencies +### Using Mock Helpers + +Use `createMockLogger()` from `tests/helpers/mocks.ts` for consistent logger mocking: ```typescript -import { vi } from 'vitest'; +import { createMockLogger } from './helpers/mocks'; -// Mock logger -const mockLogger = { - info: vi.fn(), - error: vi.fn(), - warn: vi.fn(), - debug: vi.fn(), -}; +const mockLogger = createMockLogger(); +// All logger methods are vi.fn() spies: debug, info, warn, error, fatal, trace +expect(mockLogger.error).toHaveBeenCalled(); +``` -// Mock task context -const createMockContext = () => ({ - logger: mockLogger, - job: { - id: 'job-123', - userMetadata: { initiatedBy: 'test' }, - }, - updateStageUserMetadata: vi.fn(), -}); +### Generating Test Data with Faker -// Mock task -const createMockTask = (data: Partial = {}) => ({ - id: 'task-123', - data: { - resourcePath: '/default/path', - expirationDate: new Date().toISOString(), - ...data, +```typescript +import { faker } from '@faker-js/faker'; + +const taskId = faker.string.uuid(); +const jobId = faker.string.uuid(); +const errorMessage = faker.lorem.sentence(); +const resourcePath = faker.system.filePath(); +const timestamp = faker.date.past().toISOString(); + +// For complex objects +const mockTask = { + id: faker.string.uuid(), + type: faker.helpers.arrayElement(['tiles-deletion', 'cleanup']), + parameters: { + layerPath: faker.system.directoryPath(), + extent: faker.location.latitude(), }, -}); +}; ``` +## Writing Integration Tests + ### Testing with DI Container ```typescript +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { container } from 'tsyringe'; -import { beforeEach, afterEach } from 'vitest'; +import { SERVICES } from '../src/common/constants'; +import { StrategyFactory } from '../src/cleaner/strategies'; -describe('Integration Tests', () => { - beforeEach(() => { - // Create child container for isolation - const childContainer = container.createChildContainer(); +describe('StrategyFactory Integration', () => { + let childContainer: DependencyContainer; - // Register mocks - childContainer.register(SERVICES.LOGGER, { useValue: mockLogger }); + beforeEach(() => { + childContainer = container.createChildContainer(); + // Register real or mock dependencies + childContainer.register(SERVICES.LOGGER, { useValue: createMockLogger() }); }); afterEach(() => { - container.clearInstances(); + childContainer.clearInstances(); }); -}); -``` - -### Testing Async Operations - -```typescript -import { vi } from 'vitest'; -it('should handle async cleanup', async () => { - // Mock async operation - const cleanupFn = vi.fn().mockResolvedValue(undefined); + it('should resolve registered strategy from container', () => { + childContainer.register('tiles-deletion', { useClass: TilesDeletionStrategy }); + const factory = new StrategyFactory(childContainer.resolve(SERVICES.LOGGER), childContainer); - await manager.cleanup(cleanupFn); + const strategy = factory.resolve('tiles-deletion'); - expect(cleanupFn).toHaveBeenCalledTimes(1); -}); - -it('should handle async errors', async () => { - const cleanupFn = vi.fn().mockRejectedValue(new Error('Cleanup failed')); - - await expect(manager.cleanup(cleanupFn)).rejects.toThrow('Cleanup failed'); + expect(strategy).toBeInstanceOf(TilesDeletionStrategy); + }); }); ``` ## Test Utilities -### Using Faker for Test Data +### Reusable Mocks (`tests/helpers/mocks.ts`) ```typescript -import { faker } from '@faker-js/faker'; +import { vi } from 'vitest'; +import type { Logger } from '@map-colonies/js-logger'; + +export function createMockLogger(): Logger { + return { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + fatal: vi.fn(), + trace: vi.fn(), + } as unknown as Logger; +} +``` -const createRandomTask = () => ({ - id: faker.string.uuid(), - data: { - resourcePath: faker.system.filePath(), - expirationDate: faker.date.past().toISOString(), - }, -}); +### Custom Mock Factories + +Create reusable factories in `tests/helpers/mocks.ts`: + +```typescript +export function createMockTaskResponse(overrides = {}) { + return { + jobId: faker.string.uuid(), + taskId: faker.string.uuid(), + ack: vi.fn(), + reject: vi.fn(), + ...overrides, + }; +} ``` ## Coverage -Coverage reports are generated in multiple formats: +Coverage reports are generated in three formats: -- `text` - Terminal output -- `html` - Browse at `coverage/index.html` -- `lcov` - For CI integration +- **text** — Terminal output during test run +- **html** — Browse at `coverage/index.html` +- **json/json-summary** — For CI integration ### Coverage Thresholds -Configure in `vitest.config.mts` if needed: +Configured in `vitest.config.mts` at **80%** for all metrics: ```typescript coverage: { thresholds: { - lines: 80, - functions: 80, - branches: 80, - statements: 80, + global: { + statements: 80, + branches: 80, + functions: 80, + lines: 80, + }, }, } ``` +Tests will fail if coverage drops below these thresholds. + +## Troubleshooting + +### Tests Pass Locally But Fail in CI + +- Check Node version matches (`>=24.0.0` required) +- Verify environment variables are set correctly +- Clear coverage cache: `rm -rf coverage .vitest` + +### Mock Not Being Called + +```typescript +// Ensure mock is called with exact arguments +expect(mockLogger.error).toHaveBeenCalledWith(expect.objectContaining({ msg: 'Task failed' })); + +// Check if called at all +expect(mockLogger.error).toHaveBeenCalled(); + +// Verify call count +expect(mockLogger.error).toHaveBeenCalledTimes(1); +``` + +### Type Errors with Mocks + +```typescript +// Use 'as unknown as Type' for complex mocks +const mockLogger = { + debug: vi.fn(), + // ... other methods +} as unknown as Logger; +``` + +### Async Test Timeout + +```typescript +// Increase timeout for slow tests (default: 5000ms) +it('should handle long operation', async () => { + // ... +}, 10000); // 10 second timeout +``` + +### DI Container State Leaking Between Tests + +```typescript +// Always use child containers in tests +beforeEach(() => { + childContainer = container.createChildContainer(); +}); + +afterEach(() => { + childContainer.clearInstances(); +}); +``` + ## Best Practices -1. **Unit test business logic** - Focus on managers and services -2. **Mock external dependencies** - SDK, logger, config -3. **Use descriptive test names** - `should when ` -4. **Keep tests isolated** - No shared state between tests -5. **Test error cases** - Validate error handling paths -6. **Use beforeEach for setup** - Reset mocks and create fresh instances +1. **Descriptive test names** — Use `should when ` format +2. **Arrange-Act-Assert** — Structure tests clearly: setup, execute, verify +3. **One assertion per test** — Focus each test on a single behavior +4. **Mock external dependencies** — Logger, HTTP clients, file system, queue clients +5. **Use faker for test data** — Avoid hardcoded strings, generate realistic data +6. **Test error paths** — Validate error handling, not just happy paths +7. **Isolate tests** — No shared state, use `beforeEach` for fresh instances +8. **Use helper functions** — Extract common setup into `tests/helpers/` From 920cd42fec89983e0f9d9db39c90a08faedd0c35 Mon Sep 17 00:00:00 2001 From: almog8k Date: Mon, 9 Feb 2026 16:41:46 +0200 Subject: [PATCH 11/25] feat(strategies): add strategy pattern with factory and tiles-deletion stub - Add ITaskStrategy interface with generic params for type-safe execution - Strategy receives only validated params, no context duplication - Add StrategyFactory using DI container to resolve strategies by task type - Add TilesDeletionStrategy stub implementing ITaskStrategy - Factory resolves strategies from DI container via string tokens - Factory throws StrategyNotFoundError for unregistered task types - Add comprehensive unit tests for StrategyFactory (5 test cases) - TilesDeletionStrategy implementation deferred (TODO with placeholder) - Strategies focus on business logic, orchestration layer handles context --- src/cleaner/strategies/index.ts | 3 + src/cleaner/strategies/strategyFactory.ts | 38 +++++++++ src/cleaner/strategies/taskStrategy.ts | 16 ++++ .../strategies/tilesDeletionStrategy.ts | 35 ++++++++ tests/strategyFactory.spec.ts | 85 +++++++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 src/cleaner/strategies/index.ts create mode 100644 src/cleaner/strategies/strategyFactory.ts create mode 100644 src/cleaner/strategies/taskStrategy.ts create mode 100644 src/cleaner/strategies/tilesDeletionStrategy.ts create mode 100644 tests/strategyFactory.spec.ts diff --git a/src/cleaner/strategies/index.ts b/src/cleaner/strategies/index.ts new file mode 100644 index 0000000..3f2da61 --- /dev/null +++ b/src/cleaner/strategies/index.ts @@ -0,0 +1,3 @@ +export { type ITaskStrategy } from './taskStrategy'; +export { StrategyFactory } from './strategyFactory'; +export { TilesDeletionStrategy } from './tilesDeletionStrategy'; diff --git a/src/cleaner/strategies/strategyFactory.ts b/src/cleaner/strategies/strategyFactory.ts new file mode 100644 index 0000000..208d854 --- /dev/null +++ b/src/cleaner/strategies/strategyFactory.ts @@ -0,0 +1,38 @@ +import { inject } from 'tsyringe'; +import type { DependencyContainer } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { SERVICES } from '@common/constants'; +import { StrategyNotFoundError } from '../errors'; +import type { ITaskStrategy } from './taskStrategy'; + +/** + * Factory for resolving task strategies by task type. + * Strategies are registered in the DI container with their task type as the token. + */ +export class StrategyFactory { + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + private readonly container: DependencyContainer + ) {} + + /** + * Resolves a strategy for the given task type. + * + * @param taskType - The task type to resolve (e.g., 'tiles-deletion') + * @returns The registered strategy instance + * @throws StrategyNotFoundError if no strategy is registered for the task type + */ + public resolve(taskType: string): ITaskStrategy { + this.logger.debug({ msg: 'Resolving strategy', taskType }); + + if (!this.container.isRegistered(taskType)) { + throw new StrategyNotFoundError(taskType); + } + + const strategy = this.container.resolve(taskType); + + this.logger.debug({ msg: 'Strategy resolved successfully', taskType }); + + return strategy; + } +} diff --git a/src/cleaner/strategies/taskStrategy.ts b/src/cleaner/strategies/taskStrategy.ts new file mode 100644 index 0000000..9208cff --- /dev/null +++ b/src/cleaner/strategies/taskStrategy.ts @@ -0,0 +1,16 @@ +/** + * Strategy interface for task execution. + * Concrete strategies implement task-specific business logic. + * + * @template T - The type of validated task parameters (defaults to generic object) + */ +export interface ITaskStrategy> { + /** + * Executes the task with validated parameters. + * + * @param params - Validated task parameters (validated via Zod schema) + * @throws RecoverableError for transient failures (will be retried) + * @throws UnrecoverableError for permanent failures (will be rejected) + */ + execute: (params: T) => Promise; +} diff --git a/src/cleaner/strategies/tilesDeletionStrategy.ts b/src/cleaner/strategies/tilesDeletionStrategy.ts new file mode 100644 index 0000000..2730255 --- /dev/null +++ b/src/cleaner/strategies/tilesDeletionStrategy.ts @@ -0,0 +1,35 @@ +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { SERVICES } from '@common/constants'; +import type { TilesDeletionParams } from '../validation/schemas'; +import type { ITaskStrategy } from './taskStrategy'; + +/** + * Strategy for handling tiles-deletion tasks. + * Deletes tiles from storage before updating to lower resolution. + */ +@injectable() +export class TilesDeletionStrategy implements ITaskStrategy { + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} + + /** + * Executes tiles deletion task. + * + * @param params - Validated tiles deletion parameters + */ + public async execute(params: TilesDeletionParams): Promise { + this.logger.info({ + msg: 'Executing tiles deletion task', + params, + }); + + // TODO: Implement tiles deletion logic + // 1. Extract tile coordinates/ranges from params + // 2. Connect to tile storage (S3, filesystem, etc.) + // 3. Delete tiles in the specified range + // 4. Handle partial failures (retry logic) + await Promise.resolve(); // Placeholder for async implementation + + this.logger.info({ msg: 'Tiles deletion task completed' }); + } +} diff --git a/tests/strategyFactory.spec.ts b/tests/strategyFactory.spec.ts new file mode 100644 index 0000000..f2d4437 --- /dev/null +++ b/tests/strategyFactory.spec.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { container, DependencyContainer } from 'tsyringe'; +import { faker } from '@faker-js/faker'; +import type { Logger } from '@map-colonies/js-logger'; +import { SERVICES } from '../src/common/constants'; +import { StrategyFactory, TilesDeletionStrategy, type ITaskStrategy } from '../src/cleaner/strategies'; +import { StrategyNotFoundError } from '../src/cleaner/errors'; +import { createMockLogger } from './helpers/mocks'; + +// Mock strategy for testing +class MockStrategy implements ITaskStrategy { + public async execute(): Promise { + // Mock implementation + } +} + +describe('StrategyFactory', () => { + let strategyFactory: StrategyFactory; + let mockLogger: Logger; + let childContainer: DependencyContainer; + + beforeEach(() => { + mockLogger = createMockLogger(); + childContainer = container.createChildContainer(); + childContainer.register(SERVICES.LOGGER, { useValue: mockLogger }); + + // Create factory with child container + strategyFactory = new StrategyFactory(mockLogger, childContainer); + }); + + afterEach(() => { + childContainer.clearInstances(); + }); + + describe('resolve', () => { + it('should resolve registered strategy from container', () => { + const taskType = 'tiles-deletion'; + childContainer.register(taskType, { useClass: TilesDeletionStrategy }); + + const strategy = strategyFactory.resolve(taskType); + + expect(strategy).toBeInstanceOf(TilesDeletionStrategy); + }); + + it('should throw StrategyNotFoundError for unregistered task type', () => { + const taskType = faker.string.alpha(10); + + expect(() => strategyFactory.resolve(taskType)).toThrow(StrategyNotFoundError); + }); + + it('should resolve different strategies for different task types', () => { + const taskType1 = 'tiles-deletion'; + const taskType2 = 'files-deletion'; + + childContainer.register(taskType1, { useClass: TilesDeletionStrategy }); + childContainer.register(taskType2, { useClass: MockStrategy }); + + const strategy1 = strategyFactory.resolve(taskType1); + const strategy2 = strategyFactory.resolve(taskType2); + + expect(strategy1).toBeInstanceOf(TilesDeletionStrategy); + expect(strategy2).toBeInstanceOf(MockStrategy); + expect(strategy1).not.toBe(strategy2); + }); + + it('should resolve same strategy instance for multiple calls (singleton behavior)', () => { + const taskType = 'tiles-deletion'; + childContainer.registerSingleton(taskType, TilesDeletionStrategy); + + const strategy1 = strategyFactory.resolve(taskType); + const strategy2 = strategyFactory.resolve(taskType); + + expect(strategy1).toBe(strategy2); + }); + + it('should handle special characters in task type', () => { + const taskType = 'task-with-special_chars.v2'; + childContainer.register(taskType, { useClass: MockStrategy }); + + const strategy = strategyFactory.resolve(taskType); + + expect(strategy).toBeInstanceOf(MockStrategy); + }); + }); +}); From 9ba7105812d3ee6a43685a46ca488557e40ddc5f Mon Sep 17 00:00:00 2001 From: almog8k Date: Mon, 9 Feb 2026 16:47:02 +0200 Subject: [PATCH 12/25] chore: remove demo code (logistics, seeder) - Remove src/logistics/ directory (demo manager and types) - Remove src/seeder.ts (demo job/task seeder) - Remove tests/logistics.spec.ts (demo integration tests) - Update containerConfig.ts to remove logistics types - Update index.ts to remove seedData demo call - Replace worker.ts with stub pending TaskPoller implementation - Reduces test count from 16 to 14 (removed 2 demo tests) --- src/containerConfig.ts | 4 +-- src/index.ts | 7 ---- src/logistics/manager.ts | 28 ---------------- src/logistics/types.ts | 48 --------------------------- src/seeder.ts | 33 ------------------ src/worker.ts | 37 +++++++-------------- tests/logistics.spec.ts | 72 ---------------------------------------- 7 files changed, 14 insertions(+), 215 deletions(-) delete mode 100644 src/logistics/manager.ts delete mode 100644 src/logistics/types.ts delete mode 100644 src/seeder.ts delete mode 100644 tests/logistics.spec.ts diff --git a/src/containerConfig.ts b/src/containerConfig.ts index 9c71f00..243dac7 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -10,7 +10,6 @@ import { SERVICES, SERVICE_NAME } from '@common/constants'; import { getTracing } from '@common/tracing'; import { ConfigType, getConfig } from './common/config'; import { workerBuilder } from './worker'; -import { LogisticJobTypes, LogisticStageTypes } from './logistics/types'; export interface RegisterOptions { override?: InjectionObject[]; @@ -40,7 +39,8 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise const logger = container.resolve(SERVICES.LOGGER); const config = container.resolve(SERVICES.CONFIG); const metricsRegistry = container.resolve(SERVICES.METRICS); - return new JobnikSDK({ + // TODO: Replace with actual job/stage types once TaskPoller is implemented + return new JobnikSDK({ ...config.get('jobnik.sdk'), logger, metricsRegistry, diff --git a/src/index.ts b/src/index.ts index 62dba4c..c4dedb5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,6 @@ import type { IWorker } from '@map-colonies/jobnik-sdk'; import { SERVICES } from '@common/constants'; import { ConfigType } from '@common/config'; import { registerExternalValues } from './containerConfig'; -import { LogisticsSDK } from './logistics/types'; -import { seedData } from './seeder'; void registerExternalValues() .then(async (container) => { @@ -18,11 +16,6 @@ void registerExternalValues() const config = container.resolve(SERVICES.CONFIG); const worker = container.resolve(SERVICES.WORKER); - // REMOVE THIS IN PRODUCTION - for demo purposes only - const sdk = container.resolve(SERVICES.JOBNIK_SDK); - await seedData(sdk.getProducer()); - // END REMOVE THIS IN PRODUCTION - for demo purposes only - const port = config.get('server.port'); const stubHealthCheck = async (): Promise => Promise.resolve(); diff --git a/src/logistics/manager.ts b/src/logistics/manager.ts deleted file mode 100644 index 1986d6e..0000000 --- a/src/logistics/manager.ts +++ /dev/null @@ -1,28 +0,0 @@ -// import { setTimeout as sleep } from 'node:timers/promises'; -import type { Task, TaskHandlerContext } from '@map-colonies/jobnik-sdk'; -import { injectable } from 'tsyringe'; -import type { LogisticJobTypes, LogisticStageTypes } from './types'; - -async function pickupPackage(): Promise { - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - const randomDelay = Math.floor(Math.random() * 120000); - await new Promise((resolve) => setTimeout(resolve, randomDelay)); -} - -@injectable() -export class LogisticsManager { - public async handleDeliveryTask( - task: Task, - context: TaskHandlerContext - ): Promise { - context.logger.info({ msg: 'Handling pickup task for quantity:', quantity: task.data.quantity }); - const casualtyCount = context.job.userMetadata?.casualtyCount ?? 0; - if (casualtyCount > task.data.quantity) { - throw new Error('Casualty count exceeds quantity to be picked up'); - } - - await pickupPackage(); - - await context.updateStageUserMetadata({ signaturedBy: 'John Doe' }); - } -} diff --git a/src/logistics/types.ts b/src/logistics/types.ts deleted file mode 100644 index 3aa45bb..0000000 --- a/src/logistics/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { IJobnikSDK } from '@map-colonies/jobnik-sdk'; - -export interface LogisticJobTypes { - hazmatTransport: { - data: { unNumber: string; hazardClass: string }; - userMetadata: { leaksCount: number; casualtyCount: number }; - }; - standardTransport: { - data: { - condition: 'clear' | 'rain' | 'snow'; - visibility: number; // in meters - temperature: number; // in celsius - windSpeed: number; // in km/h - }; - userMetadata: { fugitivesCount: number }; - }; -} - -export interface LogisticStageTypes { - pickup: { - data: { - location: string; - timeWindowStart: string; // ISO 8601 date string - timeWindowEnd: string; // ISO 8601 date string - }; - userMetadata: { notes?: string }; - task: { data: { itemId: string; quantity: number }; userMetadata: { pickedBy: string } }; - }; - drive: { - data: { - routeId: string; - estimatedDuration: number; // in minutes - }; - userMetadata: { driverId: string; vehicleId: string }; - task: { data: { distance: number }; userMetadata: { startedAt: string; endedAt?: string } }; - }; - delivery: { - data: { - location: string; - timeWindowStart: string; // ISO 8601 date string - timeWindowEnd: string; // ISO 8601 date string - }; - userMetadata: { signaturedBy: string }; - task: { data: { itemId: string; quantity: number }; userMetadata: { deliveredBy: string } }; - }; -} - -export type LogisticsSDK = IJobnikSDK; diff --git a/src/seeder.ts b/src/seeder.ts deleted file mode 100644 index 32c00d3..0000000 --- a/src/seeder.ts +++ /dev/null @@ -1,33 +0,0 @@ -// THIS FILE IS FOR DEMONSTRATION PURPOSES ONLY. IT SHOWS HOW TO USE THE PRODUCER TO SEED DATA INTO THE JOBNIK SYSTEM. -// REMOVE THIS FILE IN PRODUCTION. -import { IProducer } from '@map-colonies/jobnik-sdk'; -import { LogisticJobTypes, LogisticStageTypes } from './logistics/types'; - -export async function seedData(producer: IProducer): Promise { - const job = await producer.createJob<'hazmatTransport'>({ - name: 'Hazmat Transport Job', - priority: 'HIGH', - data: { unNumber: 'UN1993', hazardClass: '3' }, - userMetadata: { leaksCount: 0, casualtyCount: 2 }, - }); - - const stage = await producer.createStage<'delivery'>(job.id, { - type: 'delivery', - data: { - location: 'Warehouse 42', - timeWindowStart: new Date().toISOString(), - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - timeWindowEnd: new Date(Date.now() + 3600 * 1000).toISOString(), - }, - userMetadata: { signaturedBy: '' }, - }); - - await producer.createTasks<'delivery'>(stage.id, 'delivery', [ - { - data: { itemId: 'ITEM123', quantity: 5 }, - }, - { - data: { itemId: 'ITEM456', quantity: 3 }, - }, - ]); -} diff --git a/src/worker.ts b/src/worker.ts index a61d1df..38f47f8 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,27 +1,14 @@ import type { IWorker } from '@map-colonies/jobnik-sdk'; -import type { Logger } from '@map-colonies/js-logger'; -import type { DependencyContainer, FactoryFunction } from 'tsyringe'; -import { SERVICES } from './common/constants'; -import { LogisticsManager } from './logistics/manager'; -import { LogisticsSDK } from './logistics/types'; -import { ConfigType } from './common/config'; - -export const workerBuilder: FactoryFunction = (container: DependencyContainer) => { - const sdk = container.resolve(SERVICES.JOBNIK_SDK); - const logger = container.resolve(SERVICES.LOGGER); - const config = container.resolve(SERVICES.CONFIG); - - const logisticsManager = container.resolve(LogisticsManager); - - const worker = sdk.createWorker<'hazmatTransport', 'delivery'>( - 'delivery', - logisticsManager.handleDeliveryTask.bind(logisticsManager), - config.get('jobnik.worker') - ); - - worker.on('error', (err) => { - logger.error({ msg: 'Worker encountered an error:', err }); - }); - - return worker; +import type { FactoryFunction } from 'tsyringe'; + +/** + * Worker factory function. + * TODO: Implement worker creation once TaskPoller is implemented. + * For now, this is a stub to satisfy the containerConfig registration. + */ +export const workerBuilder: FactoryFunction = () => { + // TODO: Replace with actual worker implementation + // When TaskPoller is ready, this will create and configure the worker + // For the skeleton, we return a minimal stub + throw new Error('Worker not implemented - TaskPoller integration pending'); }; diff --git a/tests/logistics.spec.ts b/tests/logistics.spec.ts deleted file mode 100644 index 92c1bec..0000000 --- a/tests/logistics.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; -import jsLogger from '@map-colonies/js-logger'; -import { trace } from '@opentelemetry/api'; -import { initConfig } from '@src/common/config'; -import { SERVICES } from '@src/common/constants'; -import { registerExternalValues } from '@src/containerConfig'; -import { LogisticsManager } from '@src/logistics/manager'; -import { createFakeJobTaskChain, createFakeTaskHandlerContext } from './helpers/fakes'; - -describe('Logistics', function () { - let taskHandler: LogisticsManager['handleDeliveryTask']; - - beforeAll(async function () { - try { - await initConfig(true); - } catch (error) { - console.error('Failed to initialize config in tests:', error); - throw error; - } - }); - - beforeEach(async function () { - const container = await registerExternalValues({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - - vi.useFakeTimers(); - - const logisticsManager = container.resolve(LogisticsManager); - taskHandler = logisticsManager.handleDeliveryTask.bind(logisticsManager); - }); - - describe('Happy Path', function () { - it('should handle pickup task successfully', async function () { - const { - job: fakeJob, - stage: fakeStage, - task: fakeTask, - } = createFakeJobTaskChain({ userMetadata: { casualtyCount: 0 } }, {}, { data: { quantity: 10 } }); - - const context = createFakeTaskHandlerContext(fakeJob, fakeStage); - context.updateStageUserMetadata.mockResolvedValue(undefined); - - const result = taskHandler(fakeTask, context); - - vi.runAllTimers(); - - await expect(result).resolves.toBeUndefined(); - }); - }); - - describe('Sad Path', function () { - it('should throw an error when casualty count exceeds quantity', async function () { - const { - job: fakeJob, - stage: fakeStage, - task: fakeTask, - } = createFakeJobTaskChain({ userMetadata: { casualtyCount: 15 } }, {}, { data: { quantity: 10 } }); - - const context = createFakeTaskHandlerContext(fakeJob, fakeStage); - context.updateStageUserMetadata.mockResolvedValue(undefined); - - const result = taskHandler(fakeTask, context); - - await expect(result).rejects.toThrow('Casualty count exceeds quantity to be picked up'); - }); - }); -}); From 44fd6a96d23875c7e22f9d80684350b536b42f6b Mon Sep 17 00:00:00 2001 From: almog8k Date: Tue, 10 Feb 2026 10:00:12 +0200 Subject: [PATCH 13/25] chore: add helm/local.yaml to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9b3797e..a768d3e 100644 --- a/.gitignore +++ b/.gitignore @@ -79,6 +79,7 @@ config.json # Local configuration overrides config/local.json +helm/local.yaml # Local documentation (can be regenerated) ai-docs/implementation-plan.md From 8289f6c055f245bc8fb0a58a6654cd102d895b52 Mon Sep 17 00:00:00 2001 From: almog8k Date: Tue, 10 Feb 2026 10:03:20 +0200 Subject: [PATCH 14/25] feat: update project references from jobnik-worker-boilerplate to cleaner --- .github/copilot-instructions.md | 4 +-- AGENTS.md | 4 +-- README.md | 2 +- ai-docs/common-patterns.md | 4 +-- catalog-info.yaml | 4 +-- config/custom-environment-variables.json | 40 +++++++++++++++++++----- config/default.json | 4 +-- helm/Chart.yaml | 4 +-- helm/local.yaml | 11 +++++++ helm/templates/_helpers.tpl | 28 ++++++++--------- helm/templates/configmap.yaml | 10 +++--- helm/templates/deployment.yaml | 20 ++++++------ helm/values.yaml | 25 +++++++++++---- package-lock.json | 4 +-- package.json | 2 +- 15 files changed, 107 insertions(+), 59 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 63ebd51..8068ad8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ **Cleaner** is a TypeScript-based worker service built on the MapColonies Jobnik SDK framework. It is a distributed task processing worker that handles cleanup operations as part of the raster processing pipeline. -This project is based on the `jobnik-worker-boilerplate` template and follows the MapColonies ecosystem conventions for observability, configuration, and deployment. +This project is based on the `cleaner` template and follows the MapColonies ecosystem conventions for observability, configuration, and deployment. ## Quick Reference @@ -64,7 +64,7 @@ npm run lint:fix # Auto-fix linting issues 2. **Remove Demo Code**: The `logistics/` folder and `seeder.ts` are examples. Replace with actual cleaner implementation. -3. **Package Metadata**: Update `package.json` name from `jobnik-worker-boilerplate` to `cleaner`. +3. **Package Metadata**: Update `package.json` name from `cleaner` to `cleaner`. 4. **Type Safety**: Define job/stage types in `src/cleaner/types.ts` and update `worker.ts` with correct generic parameters. diff --git a/AGENTS.md b/AGENTS.md index 14dee47..f63ec72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ **Cleaner** is a TypeScript-based worker service built on the MapColonies Jobnik SDK framework. It is a distributed task processing worker that handles cleanup operations as part of the raster processing pipeline. -This project is based on the `jobnik-worker-boilerplate` template and follows the MapColonies ecosystem conventions for observability, configuration, and deployment. +This project is based on the `cleaner` template and follows the MapColonies ecosystem conventions for observability, configuration, and deployment. ## Quick Reference @@ -64,7 +64,7 @@ npm run lint:fix # Auto-fix linting issues 2. **Remove Demo Code**: The `logistics/` folder and `seeder.ts` are examples. Replace with actual cleaner implementation. -3. **Package Metadata**: Update `package.json` name from `jobnik-worker-boilerplate` to `cleaner`. +3. **Package Metadata**: Update `package.json` name from `cleaner` to `cleaner`. 4. **Type Safety**: Define job/stage types in `src/cleaner/types.ts` and update `worker.ts` with correct generic parameters. diff --git a/README.md b/README.md index 9792d8b..f237cca 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ export const workerBuilder: FactoryFunction = (container: DependencyCon ### 5. Rename Throughout the Project -Update references to `jobnik-worker-boilerplate` in: +Update references to `cleaner` in: - `package.json` - name, description, author - `helm/Chart.yaml` - name, description - `helm/values.yaml` - mclabels, configManagement.name, image.repository diff --git a/ai-docs/common-patterns.md b/ai-docs/common-patterns.md index 4a49399..e5e5cf6 100644 --- a/ai-docs/common-patterns.md +++ b/ai-docs/common-patterns.md @@ -203,7 +203,7 @@ Adding a new `queue.jobManagerBaseUrl` configuration: ```json { "queue": { - "jobManagerBaseUrl": "http://job-manager:8080" + "jobManagerBaseUrl": "http//localhost:8080" } } ``` @@ -223,7 +223,7 @@ Adding a new `queue.jobManagerBaseUrl` configuration: ```yaml env: queue: - jobManagerBaseUrl: 'http://job-manager:8080' + jobManagerBaseUrl: 'http//localhost:8080' ``` **4. helm/local.yaml** (for local development) diff --git a/catalog-info.yaml b/catalog-info.yaml index 9454e21..8a44c88 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -1,10 +1,10 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: jobnik-worker-boilerplate + name: cleaner description: A boilerplate github repo for a nodejs worker service annotations: - github.com/project-slug: MapColonies/jobnik-worker-boilerplate + github.com/project-slug: MapColonies/cleaner tags: - nodejs - typescript diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 41a0d4d..4328058 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -1,25 +1,49 @@ { "telemetry": { "tracing": { - "isEnabled": "TELEMETRY_TRACING_ENABLED" + "isEnabled": { + "__name": "TELEMETRY_TRACING_ENABLED", + "__format": "boolean" + } }, "logger": { "level": "LOG_LEVEL", - "prettyPrint": "LOG_PRETTY_PRINT_ENABLED" + "prettyPrint": { + "__name": "LOG_PRETTY_PRINT_ENABLED", + "__format": "boolean" + } } }, "server": { - "port": "SERVER_PORT" + "port": { + "__name": "SERVER_PORT", + "__format": "number" + } }, "queue": { "jobManagerBaseUrl": "QUEUE_JOB_MANAGER_BASE_URL", "heartbeatBaseUrl": "QUEUE_HEARTBEAT_BASE_URL", - "heartbeatIntervalMs": "QUEUE_HEARTBEAT_INTERVAL_MS", - "dequeueIntervalMs": "QUEUE_DEQUEUE_INTERVAL_MS" + "heartbeatIntervalMs": { + "__name": "QUEUE_HEARTBEAT_INTERVAL_MS", + "__format": "number" + }, + "dequeueIntervalMs": { + "__name": "QUEUE_DEQUEUE_INTERVAL_MS", + "__format": "number" + } }, "httpRetry": { - "attempts": "HTTP_RETRY_ATTEMPTS", - "delay": "HTTP_RETRY_DELAY", - "shouldResetTimeout": "HTTP_RETRY_SHOULD_RESET_TIMEOUT" + "attempts": { + "__name": "HTTP_RETRY_ATTEMPTS", + "__format": "number" + }, + "delay": { + "__name": "HTTP_RETRY_DELAY", + "__format": "number" + }, + "shouldResetTimeout": { + "__name": "HTTP_RETRY_SHOULD_RESET_TIMEOUT", + "__format": "boolean" + } } } diff --git a/config/default.json b/config/default.json index 65ec561..582c0ec 100644 --- a/config/default.json +++ b/config/default.json @@ -14,8 +14,8 @@ "port": 8080 }, "queue": { - "jobManagerBaseUrl": "http://job-manager:8080", - "heartbeatBaseUrl": "http://heartbeat:8080", + "jobManagerBaseUrl": "http//localhost:8080", + "heartbeatBaseUrl": "http//localhost:8081", "heartbeatIntervalMs": 1000, "dequeueIntervalMs": 3000, "pairs": [ diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 0f80ed2..4525fc5 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: jobnik-worker-boilerplate -description: A Helm chart for jobnik-worker-boilerplate service +name: cleaner +description: A Helm chart for cleaner service type: application version: 1.0.0 appVersion: 1.0.0 diff --git a/helm/local.yaml b/helm/local.yaml index fb369f0..cb93657 100644 --- a/helm/local.yaml +++ b/helm/local.yaml @@ -1,6 +1,17 @@ # Local development overrides # This file is gitignored and serves as an example for local Helm values +jobDefinitions: + jobs: + update: + type: "" + swapUpdate: + type: "" + tasks: + tiles-deletion: + type: "tiles-deletion" + maxAttempts: 3 + env: queue: jobManagerBaseUrl: "http://localhost:8080" diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index 93b6095..5ef3b1b 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "jobnik-worker-boilerplate.name" -}} +{{- define "cleaner.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} @@ -10,7 +10,7 @@ Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} -{{- define "jobnik-worker-boilerplate.fullname" -}} +{{- define "cleaner.fullname" -}} {{- if .Values.fullnameOverride -}} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} {{- else -}} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "jobnik-worker-boilerplate.chart" -}} +{{- define "cleaner.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "jobnik-worker-boilerplate.labels" -}} -helm.sh/chart: {{ include "jobnik-worker-boilerplate.chart" . }} -{{ include "jobnik-worker-boilerplate.selectorLabels" . }} +{{- define "cleaner.labels" -}} +helm.sh/chart: {{ include "cleaner.chart" . }} +{{ include "cleaner.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -46,15 +46,15 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Returns the tag of the chart. */}} -{{- define "jobnik-worker-boilerplate.tag" -}} +{{- define "cleaner.tag" -}} {{- default (printf "v%s" .Chart.AppVersion) .Values.image.tag }} {{- end }} {{/* Selector labels */}} -{{- define "jobnik-worker-boilerplate.selectorLabels" -}} -app.kubernetes.io/name: {{ include "jobnik-worker-boilerplate.name" . }} +{{- define "cleaner.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cleaner.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{ include "mclabels.selectorLabels" . }} {{- end }} @@ -62,7 +62,7 @@ app.kubernetes.io/instance: {{ .Release.Name }} {{/* Returns the cloud provider name from global if exists or from the chart's values, defaults to minikube */}} -{{- define "jobnik-worker-boilerplate.cloudProviderFlavor" -}} +{{- define "cleaner.cloudProviderFlavor" -}} {{- if .Values.global.cloudProvider.flavor }} {{- .Values.global.cloudProvider.flavor -}} {{- else if .Values.cloudProvider -}} @@ -75,7 +75,7 @@ Returns the cloud provider name from global if exists or from the chart's values {{/* Returns the cloud provider docker registry url from global if exists or from the chart's values */}} -{{- define "jobnik-worker-boilerplate.cloudProviderDockerRegistryUrl" -}} +{{- define "cleaner.cloudProviderDockerRegistryUrl" -}} {{- if .Values.global.cloudProvider.dockerRegistryUrl }} {{- printf "%s/" .Values.global.cloudProvider.dockerRegistryUrl -}} {{- else if .Values.cloudProvider.dockerRegistryUrl -}} @@ -87,7 +87,7 @@ Returns the cloud provider docker registry url from global if exists or from the {{/* Returns the cloud provider image pull secret name from global if exists or from the chart's values */}} -{{- define "jobnik-worker-boilerplate.cloudProviderImagePullSecretName" -}} +{{- define "cleaner.cloudProviderImagePullSecretName" -}} {{- if .Values.global.cloudProvider.imagePullSecretName }} {{- .Values.global.cloudProvider.imagePullSecretName -}} {{- else if .Values.cloudProvider.imagePullSecretName -}} @@ -98,7 +98,7 @@ Returns the cloud provider image pull secret name from global if exists or from {{/* Returns the tracing url from global if exists or from the chart's values */}} -{{- define "jobnik-worker-boilerplate.tracingUrl" -}} +{{- define "cleaner.tracingUrl" -}} {{- if .Values.global.tracing.url }} {{- .Values.global.tracing.url -}} {{- else if .Values.cloudProvider -}} @@ -109,7 +109,7 @@ Returns the tracing url from global if exists or from the chart's values {{/* Returns the tracing url from global if exists or from the chart's values */}} -{{- define "jobnik-worker-boilerplate.metricsUrl" -}} +{{- define "cleaner.metricsUrl" -}} {{- if .Values.global.metrics.url }} {{- .Values.global.metrics.url -}} {{- else -}} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 67e3602..5933157 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -1,10 +1,10 @@ -{{- $tracingUrl := include "jobnik-worker-boilerplate.tracingUrl" . -}} -{{- $metricsUrl := include "jobnik-worker-boilerplate.metricsUrl" . -}} +{{- $tracingUrl := include "cleaner.tracingUrl" . -}} +{{- $metricsUrl := include "cleaner.metricsUrl" . -}} {{- if .Values.enabled -}} apiVersion: v1 kind: ConfigMap metadata: - name: {{ printf "%s-configmap" (include "jobnik-worker-boilerplate.fullname" .) }} + name: {{ printf "%s-configmap" (include "cleaner.fullname" .) }} data: LOG_LEVEL: {{ .Values.env.logLevel | quote }} LOG_PRETTY_PRINT_ENABLED: {{ .Values.env.logPrettyPrintEnabled | quote }} @@ -19,13 +19,13 @@ data: CONFIG_OFFLINE_MODE: {{ .offlineMode | quote }} CONFIG_SERVER_URL: {{ .serverUrl | quote }} {{- end }} - {{- with $.Values.env.queue }} + {{- with .Values.env.queue }} QUEUE_JOB_MANAGER_BASE_URL: {{ .jobManagerBaseUrl | quote }} QUEUE_HEARTBEAT_BASE_URL: {{ .heartbeatBaseUrl | quote }} QUEUE_HEARTBEAT_INTERVAL_MS: {{ .heartbeatIntervalMs | quote }} QUEUE_DEQUEUE_INTERVAL_MS: {{ .dequeueIntervalMs | quote }} {{- end }} - {{- with $.Values.env.httpRetry }} + {{- with .Values.env.httpRetry }} HTTP_RETRY_ATTEMPTS: {{ .attempts | quote }} HTTP_RETRY_DELAY: {{ .delay | quote }} HTTP_RETRY_SHOULD_RESET_TIMEOUT: {{ .shouldResetTimeout | quote }} diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index d9e70a2..3efbe18 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -1,19 +1,19 @@ {{- $releaseName := .Release.Name -}} -{{- $chartName := include "jobnik-worker-boilerplate.name" . -}} -{{- $cloudProviderFlavor := include "jobnik-worker-boilerplate.cloudProviderFlavor" . -}} -{{- $cloudProviderDockerRegistryUrl := include "jobnik-worker-boilerplate.cloudProviderDockerRegistryUrl" . -}} -{{- $cloudProviderImagePullSecretName := include "jobnik-worker-boilerplate.cloudProviderImagePullSecretName" . -}} -{{- $imageTag := include "jobnik-worker-boilerplate.tag" . -}} +{{- $chartName := include "cleaner.name" . -}} +{{- $cloudProviderFlavor := include "cleaner.cloudProviderFlavor" . -}} +{{- $cloudProviderDockerRegistryUrl := include "cleaner.cloudProviderDockerRegistryUrl" . -}} +{{- $cloudProviderImagePullSecretName := include "cleaner.cloudProviderImagePullSecretName" . -}} +{{- $imageTag := include "cleaner.tag" . -}} {{- if .Values.enabled -}} apiVersion: apps/v1 kind: Deployment metadata: - name: {{ printf "%s-deployment" (include "jobnik-worker-boilerplate.fullname" .) }} + name: {{ printf "%s-deployment" (include "cleaner.fullname" .) }} labels: app: {{ $chartName }} component: {{ $chartName }} release: {{ $releaseName }} - {{- include "jobnik-worker-boilerplate.labels" . | nindent 4 }} + {{- include "cleaner.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} @@ -22,14 +22,14 @@ spec: app: {{ $chartName }} release: {{ $releaseName }} run: {{ $releaseName }}-{{ $chartName }} - {{- include "jobnik-worker-boilerplate.selectorLabels" . | nindent 6 }} + {{- include "cleaner.selectorLabels" . | nindent 6 }} template: metadata: labels: app: {{ $chartName }} release: {{ $releaseName }} run: {{ $releaseName }}-{{ $chartName }} - {{- include "jobnik-worker-boilerplate.labels" . | nindent 8 }} + {{- include "cleaner.labels" . | nindent 8 }} annotations: {{ include "mclabels.annotations" . | nindent 8 }} {{- if .Values.resetOnConfigChange }} @@ -80,7 +80,7 @@ spec: {{- end }} envFrom: - configMapRef: - name: {{ printf "%s-configmap" (include "jobnik-worker-boilerplate.fullname" .) }} + name: {{ printf "%s-configmap" (include "cleaner.fullname" .) }} {{- if .Values.livenessProbe.enabled }} livenessProbe: initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} diff --git a/helm/values.yaml b/helm/values.yaml index cfaa261..8f3f207 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -2,11 +2,13 @@ global: cloudProvider: {} tracing: {} metrics: {} + jobDefinitions: {} + mclabels: component: backend - partOf: boilerplates - owner: common + partOf: core + owner: raster prometheus: enabled: true @@ -21,10 +23,21 @@ fullnameOverride: "" configManagement: offlineMode: false - name: 'jobnik-worker-boilerplate' + name: 'cleaner' version: 'latest' serverUrl: 'http://localhost:8080/api' +jobDefinitions: + jobs: + update: + type: "" + swapUpdate: + type: "" + tasks: + tiles-deletion: + type: "" + maxAttempts: 3 + livenessProbe: enabled: true initialDelaySeconds: 10 @@ -64,7 +77,7 @@ caPath: '/usr/local/share/ca-certificates' caKey: 'ca.crt' image: - repository: jobnik-worker-boilerplate + repository: cleaner # If commented, appVersion will be taken. See: _helpers.tpl # tag: 'latest' pullPolicy: IfNotPresent @@ -77,8 +90,8 @@ env: enabled: false url: http://localhost:55681/v1/trace queue: - jobManagerBaseUrl: "http://job-manager:8080" - heartbeatBaseUrl: "http://heartbeat:8080" + jobManagerBaseUrl: "http//localhost:8080" + heartbeatBaseUrl: "http//localhost:8081" heartbeatIntervalMs: 1000 dequeueIntervalMs: 3000 httpRetry: diff --git a/package-lock.json b/package-lock.json index f4e7e4a..702f1b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "jobnik-worker-boilerplate", + "name": "cleaner", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "jobnik-worker-boilerplate", + "name": "cleaner", "version": "1.0.0", "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index d0e8686..fe30fcb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "jobnik-worker-boilerplate", + "name": "cleaner", "version": "1.0.0", "description": "This is template for map colonies jobnik worker service", "main": "./src/index.ts", From 9776cc4c6d4a4b8cb53d0e16d3c1e1e2ba5717ac Mon Sep 17 00:00:00 2001 From: almog8k Date: Tue, 10 Feb 2026 14:45:37 +0200 Subject: [PATCH 15/25] feat(worker): add explicit capability pairs configuration Implement capability declaration pattern with explicit job/task pairs instead of cartesian product. Worker declares specific job/task combinations it handles, validated against global job definitions with ConfigurationError for misconfigurations. --- config/custom-environment-variables.json | 12 ++ config/default.json | 35 +++- helm/templates/_tplValues.tpl | 50 +++++ helm/templates/configmap.yaml | 7 + helm/values.yaml | 17 +- src/cleaner/errors/errors.ts | 11 ++ src/cleaner/errors/index.ts | 2 +- src/cleaner/types.ts | 46 ++++- src/cleaner/utils/index.ts | 1 + src/cleaner/utils/pairBuilder.ts | 48 +++++ tests/helpers/fakes.ts | 115 ----------- tests/pairBuilder.spec.ts | 231 +++++++++++++++++++++++ 12 files changed, 446 insertions(+), 129 deletions(-) create mode 100644 helm/templates/_tplValues.tpl create mode 100644 src/cleaner/utils/index.ts create mode 100644 src/cleaner/utils/pairBuilder.ts delete mode 100644 tests/helpers/fakes.ts create mode 100644 tests/pairBuilder.spec.ts diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 4328058..d89812e 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -20,6 +20,14 @@ "__format": "number" } }, + "worker": { + "capabilities": { + "pairs": { + "__name": "WORKER_CAPABILITY_PAIRS", + "__format": "json" + } + } + }, "queue": { "jobManagerBaseUrl": "QUEUE_JOB_MANAGER_BASE_URL", "heartbeatBaseUrl": "QUEUE_HEARTBEAT_BASE_URL", @@ -32,6 +40,10 @@ "__format": "number" } }, + "jobDefinitions": { + "__name": "JOB_DEFINITIONS", + "__format": "json" + }, "httpRetry": { "attempts": { "__name": "HTTP_RETRY_ATTEMPTS", diff --git a/config/default.json b/config/default.json index 582c0ec..1e3d435 100644 --- a/config/default.json +++ b/config/default.json @@ -13,18 +13,41 @@ "server": { "port": 8080 }, + "worker": { + "capabilities": { + "pairs": [ + { + "job": "Ingestion_Update", + "task": "tiles-deletion" + }, + { + "job": "Ingestion_Swap_Update", + "task": "tiles-deletion" + } + ] + } + }, "queue": { "jobManagerBaseUrl": "http//localhost:8080", "heartbeatBaseUrl": "http//localhost:8081", "heartbeatIntervalMs": 1000, - "dequeueIntervalMs": 3000, - "pairs": [ - { - "jobType": "Ingestion_Update", - "taskType": "tiles-deletion", + "dequeueIntervalMs": 3000 + }, + "jobDefinitions": { + "jobs": { + "update": { + "type": "Ingestion_Update" + }, + "swapUpdate": { + "type": "Ingestion_Swap_Update" + } + }, + "tasks": { + "tilesDeletion": { + "type": "tiles-deletion", "maxAttempts": 3 } - ] + } }, "httpRetry": { "attempts": 3, diff --git a/helm/templates/_tplValues.tpl b/helm/templates/_tplValues.tpl new file mode 100644 index 0000000..7da2b4d --- /dev/null +++ b/helm/templates/_tplValues.tpl @@ -0,0 +1,50 @@ +{{/* +Copyright VMware, Inc. +SPDX-License-Identifier: APACHE-2.0 +*/}} + +{{/* vim: set filetype=mustache: */}} +{{/* +Renders a value that contains template perhaps with scope if the scope is present. +Usage: +{{ include "common.tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $ ) }} +{{ include "common.tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $ "scope" $app ) }} +*/}} +{{- define "common.tplvalues.render" -}} +{{- $value := typeIs "string" .value | ternary .value (.value | toYaml) }} +{{- if contains "{{" (toJson .value) }} + {{- if .scope }} + {{- tpl (cat "{{- with $.RelativeScope -}}" $value "{{- end }}") (merge (dict "RelativeScope" .scope) .context) }} + {{- else }} + {{- tpl $value .context }} + {{- end }} +{{- else }} + {{- $value }} +{{- end }} +{{- end -}} + +{{/* +Merge a list of values that contains template after rendering them. +Merge precedence is consistent with http://masterminds.github.io/sprig/dicts.html#merge-mustmerge +Usage: +{{ include "common.tplvalues.merge" ( dict "values" (list .Values.path.to.the.Value1 .Values.path.to.the.Value2) "context" $ ) }} +*/}} +{{- define "common.tplvalues.merge" -}} +{{- $dst := dict -}} +{{- range .values -}} +{{- $dst = include "common.tplvalues.render" (dict "value" . "context" $.context "scope" $.scope) | fromYaml | merge $dst -}} +{{- end -}} +{{ $dst | toYaml }} +{{- end -}} + +{{/* +End of usage example +*/}} + +{{/* +Custom definitions +*/}} + +{{- define "common.jobDefinitions.merged" -}} +{{- include "common.tplvalues.merge" ( dict "values" ( list .Values.jobDefinitions .Values.global.jobDefinitions ) "context" . ) }} +{{- end -}} diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 5933157..e16c955 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -19,12 +19,19 @@ data: CONFIG_OFFLINE_MODE: {{ .offlineMode | quote }} CONFIG_SERVER_URL: {{ .serverUrl | quote }} {{- end }} + {{- with .Values.env.worker.capabilities }} + WORKER_CAPABILITY_PAIRS: {{ .pairs | toJson | quote }} + {{- end }} {{- with .Values.env.queue }} QUEUE_JOB_MANAGER_BASE_URL: {{ .jobManagerBaseUrl | quote }} QUEUE_HEARTBEAT_BASE_URL: {{ .heartbeatBaseUrl | quote }} QUEUE_HEARTBEAT_INTERVAL_MS: {{ .heartbeatIntervalMs | quote }} QUEUE_DEQUEUE_INTERVAL_MS: {{ .dequeueIntervalMs | quote }} {{- end }} + {{- $jobDefinitions := fromYaml (include "common.jobDefinitions.merged" .) }} + {{- if $jobDefinitions }} + JOB_DEFINITIONS: {{ $jobDefinitions | toJson | quote }} + {{- end }} {{- with .Values.env.httpRetry }} HTTP_RETRY_ATTEMPTS: {{ .attempts | quote }} HTTP_RETRY_DELAY: {{ .delay | quote }} diff --git a/helm/values.yaml b/helm/values.yaml index 8f3f207..347ae24 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -30,13 +30,13 @@ configManagement: jobDefinitions: jobs: update: - type: "" + type: "Ingestion_Update" swapUpdate: - type: "" + type: "Ingestion_Swap_Update" tasks: - tiles-deletion: - type: "" - maxAttempts: 3 + tilesDeletion: + type: "tiles-deletion" + maxAttempts: 3 livenessProbe: enabled: true @@ -94,6 +94,13 @@ env: heartbeatBaseUrl: "http//localhost:8081" heartbeatIntervalMs: 1000 dequeueIntervalMs: 3000 + worker: + capabilities: + pairs: + - job: "Ingestion_Update" + task: "tiles-deletion" + - job: "Ingestion_Swap_Update" + task: "tiles-deletion" httpRetry: attempts: 3 delay: "exponential" diff --git a/src/cleaner/errors/errors.ts b/src/cleaner/errors/errors.ts index 7723b83..7588bd7 100644 --- a/src/cleaner/errors/errors.ts +++ b/src/cleaner/errors/errors.ts @@ -24,6 +24,17 @@ export class UnrecoverableError extends Error { } } +/** + * Configuration error - invalid or missing configuration. + * This is always unrecoverable and indicates a deployment/configuration issue. + */ +export class ConfigurationError extends UnrecoverableError { + public constructor(message: string) { + super(message); + this.name = ConfigurationError.name; + } +} + /** * Validation error - task parameters failed schema validation. * This is always unrecoverable since invalid parameters won't become valid on retry. diff --git a/src/cleaner/errors/index.ts b/src/cleaner/errors/index.ts index c8d0da2..ef83cb3 100644 --- a/src/cleaner/errors/index.ts +++ b/src/cleaner/errors/index.ts @@ -1,2 +1,2 @@ -export { RecoverableError, UnrecoverableError, ValidationError, StrategyNotFoundError } from './errors'; +export { RecoverableError, UnrecoverableError, ConfigurationError, ValidationError, StrategyNotFoundError } from './errors'; export { ErrorHandler } from './errorHandler'; diff --git a/src/cleaner/types.ts b/src/cleaner/types.ts index e9d10c5..976b2b7 100644 --- a/src/cleaner/types.ts +++ b/src/cleaner/types.ts @@ -1,23 +1,65 @@ import type { ITaskResponse } from '@map-colonies/mc-priority-queue'; +/** + * Worker capability pair - a single job/task combination this worker handles. + */ +export interface WorkerCapabilityPair { + job: string; + task: string; +} + +/** + * Worker capabilities declaration. + * Defines explicit job/task pairs this worker can handle. + */ +export interface WorkerCapabilities { + pairs: WorkerCapabilityPair[]; +} + +/** + * Job definition from configuration. + */ +export interface JobDefinition { + type: string; +} + +/** + * Task definition from configuration. + */ +export interface TaskDefinition { + type: string; + maxAttempts?: number; + suspendJobOnFail?: boolean; +} + +/** + * Job definitions structure from configuration. + * Contains all jobs and tasks defined in the MapColonies ecosystem. + */ +export interface JobDefinitions { + jobs: Record; + tasks: Record; +} + /** * Job/Task pair configuration for polling. * Defines which job types and task types to poll from the queue. + * Built at runtime from WorkerCapabilities validated against JobDefinitions. */ export interface PollingPairConfig { jobType: string; taskType: string; + maxAttempts: number; } /** - * Queue configuration including polling pairs. + * Queue configuration. */ export interface QueueConfig { jobManagerBaseUrl: string; heartbeatBaseUrl: string; heartbeatIntervalMs: number; dequeueIntervalMs: number; - pairs: PollingPairConfig[]; } /** diff --git a/src/cleaner/utils/index.ts b/src/cleaner/utils/index.ts new file mode 100644 index 0000000..9f29449 --- /dev/null +++ b/src/cleaner/utils/index.ts @@ -0,0 +1 @@ +export { buildPollingPairs } from './pairBuilder'; diff --git a/src/cleaner/utils/pairBuilder.ts b/src/cleaner/utils/pairBuilder.ts new file mode 100644 index 0000000..a1e6bfd --- /dev/null +++ b/src/cleaner/utils/pairBuilder.ts @@ -0,0 +1,48 @@ +import { ConfigurationError } from '../errors'; +import type { JobDefinitions, PollingPairConfig, WorkerCapabilities } from '../types'; + +/** + * Default max attempts if not specified in task definition. + */ +const DEFAULT_MAX_ATTEMPTS = 3; + +/** + * Builds polling pairs from worker capabilities validated against job definitions. + * + * This function validates that all declared job/task pairs exist in the global + * job definitions and enriches them with maxAttempts from the task definition. + * + * @param jobDefinitions - The job definitions from global configuration + * @param capabilities - The worker's declared capability pairs + * @returns Array of polling pairs for task polling with maxAttempts included + * + * @throws {ConfigurationError} If a declared job or task type is not found in job definitions + */ +export function buildPollingPairs(jobDefinitions: JobDefinitions, capabilities: WorkerCapabilities): PollingPairConfig[] { + const pollingPairs: PollingPairConfig[] = []; + + for (const capabilityPair of capabilities.pairs) { + // Validate job type exists + const jobEntry = Object.entries(jobDefinitions.jobs).find(([, job]) => job.type === capabilityPair.job); + if (!jobEntry) { + throw new ConfigurationError(`Worker declares capability for job type "${capabilityPair.job}" but it's not defined in job definitions`); + } + + // Validate task type exists + const taskEntry = Object.entries(jobDefinitions.tasks).find(([, task]) => task.type === capabilityPair.task); + if (!taskEntry) { + throw new ConfigurationError(`Worker declares capability for task type "${capabilityPair.task}" but it's not defined in job definitions`); + } + + const [, taskDef] = taskEntry; + const maxAttempts = taskDef.maxAttempts ?? DEFAULT_MAX_ATTEMPTS; + + pollingPairs.push({ + jobType: capabilityPair.job, + taskType: capabilityPair.task, + maxAttempts, + }); + } + + return pollingPairs; +} diff --git a/tests/helpers/fakes.ts b/tests/helpers/fakes.ts deleted file mode 100644 index a6c1cd1..0000000 --- a/tests/helpers/fakes.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { simpleFaker } from '@faker-js/faker'; -import merge from 'deepmerge'; -import type { ApiClient, IProducer, Job, JobId, Stage, StageId, Task, TaskId } from '@map-colonies/jobnik-sdk'; -import type { PartialDeep } from 'type-fest'; -import jsLogger from '@map-colonies/js-logger'; -import { vitest } from 'vitest'; -import type { LogisticJobTypes, LogisticStageTypes } from '@src/logistics/types'; - -const LOCATION_LENGTH = 20; -const SIGNATURE_LENGTH = 15; - -type HazmatTransportJob = Job; -type DeliveryStage = Stage<'delivery', LogisticStageTypes['delivery']>; -type DeliveryTask = Task; - -export function createFakeHazmatTransportJob(override: PartialDeep = {}): HazmatTransportJob { - const job: HazmatTransportJob = { - id: simpleFaker.string.uuid() as JobId, - name: 'hazmatTransport', - status: 'IN_PROGRESS', - creationTime: simpleFaker.date.past().toISOString(), - data: { - unNumber: `UN${simpleFaker.number.int({ min: 1000, max: 9999 })}`, - hazardClass: simpleFaker.helpers.arrayElement(['1', '2.1', '2.2', '3', '4.1', '5.1', '6.1', '8', '9']), - }, - userMetadata: { - leaksCount: simpleFaker.number.int({ min: 0, max: 5 }), - casualtyCount: simpleFaker.number.int({ min: 0, max: 10 }), - }, - traceparent: simpleFaker.string.uuid(), - }; - - return merge(job, override) as HazmatTransportJob; -} - -export function createFakeDeliveryStage(override: PartialDeep = {}): DeliveryStage { - const stage: DeliveryStage = { - id: simpleFaker.string.uuid() as StageId, - type: 'delivery', - status: 'IN_PROGRESS', - jobId: simpleFaker.string.uuid() as JobId, - order: simpleFaker.number.int({ min: 1, max: 10 }), - summary: { - pending: 0, - inProgress: 1, - completed: 0, - failed: 0, - created: 0, - retried: 0, - total: 1, - }, - data: { - location: simpleFaker.string.alphanumeric(LOCATION_LENGTH), - timeWindowStart: simpleFaker.date.future().toISOString(), - timeWindowEnd: simpleFaker.date.future().toISOString(), - }, - userMetadata: { - signaturedBy: simpleFaker.string.alphanumeric(SIGNATURE_LENGTH), - }, - traceparent: simpleFaker.string.uuid(), - }; - return merge(stage, override) as DeliveryStage; -} - -export function createFakePickupTask(override: PartialDeep = {}): DeliveryTask { - const task: DeliveryTask = { - attempts: 0, - creationTime: simpleFaker.date.past().toISOString(), - id: simpleFaker.string.uuid() as TaskId, - stageId: simpleFaker.string.uuid() as StageId, - data: { - quantity: simpleFaker.number.int({ min: 1, max: 100 }), - itemId: simpleFaker.string.uuid(), - }, - status: 'IN_PROGRESS', - maxAttempts: 3, - traceparent: simpleFaker.string.uuid(), - }; - return merge(task, override) as DeliveryTask; -} - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function createFakeTaskHandlerContext(job: J, stage: S) { - return { - apiClient: {} as ApiClient, - logger: jsLogger({ enabled: false }), - job, - stage, - producer: {} as IProducer, - signal: new AbortController().signal, - updateStageUserMetadata: vitest.fn(), - updateJobUserMetadata: vitest.fn(), - updateTaskUserMetadata: vitest.fn(), - }; -} - -export function createFakeJobTaskChain( - jobOverride?: PartialDeep, - stageOverride?: PartialDeep, - taskOverride?: PartialDeep -): { - job: HazmatTransportJob; - stage: DeliveryStage; - task: DeliveryTask; -} { - const job = createFakeHazmatTransportJob(jobOverride); - const stage = createFakeDeliveryStage(stageOverride); - const task = createFakePickupTask(taskOverride); - - return { - job, - stage, - task, - }; -} diff --git a/tests/pairBuilder.spec.ts b/tests/pairBuilder.spec.ts new file mode 100644 index 0000000..54c3501 --- /dev/null +++ b/tests/pairBuilder.spec.ts @@ -0,0 +1,231 @@ +import { describe, it, expect } from 'vitest'; +import { buildPollingPairs } from '@src/cleaner/utils'; +import { ConfigurationError } from '@src/cleaner/errors'; +import type { JobDefinitions, WorkerCapabilities } from '@src/cleaner/types'; + +describe('buildPollingPairs', () => { + it('should build polling pairs from declared capability pairs', () => { + const capabilities: WorkerCapabilities = { + pairs: [ + { job: 'Ingestion_Update', task: 'tiles-deletion' }, + { job: 'Ingestion_Swap_Update', task: 'tiles-deletion' }, + ], + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + update: { type: 'Ingestion_Update' }, + swapUpdate: { type: 'Ingestion_Swap_Update' }, + export: { type: 'Export' }, // Not in capabilities + }, + tasks: { + tilesDeletion: { type: 'tiles-deletion', maxAttempts: 3 }, + init: { type: 'init' }, // Not in capabilities + }, + }; + + const pairs = buildPollingPairs(jobDefinitions, capabilities); + + expect(pairs).toHaveLength(2); + expect(pairs).toEqual([ + { jobType: 'Ingestion_Update', taskType: 'tiles-deletion', maxAttempts: 3 }, + { jobType: 'Ingestion_Swap_Update', taskType: 'tiles-deletion', maxAttempts: 3 }, + ]); + }); + + it('should use default maxAttempts when not specified in task definition', () => { + const capabilities: WorkerCapabilities = { + pairs: [{ job: 'Ingestion_Update', task: 'tiles-deletion' }], + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + update: { type: 'Ingestion_Update' }, + }, + tasks: { + tilesDeletion: { type: 'tiles-deletion' }, // No maxAttempts + }, + }; + + const pairs = buildPollingPairs(jobDefinitions, capabilities); + + expect(pairs).toHaveLength(1); + expect(pairs[0]).toEqual({ + jobType: 'Ingestion_Update', + taskType: 'tiles-deletion', + maxAttempts: 3, // Default + }); + }); + + it('should support different tasks for different jobs', () => { + const capabilities: WorkerCapabilities = { + pairs: [ + { job: 'Ingestion_Update', task: 'tiles-deletion' }, + { job: 'Ingestion_Update', task: 'tiles-validation' }, + { job: 'Export', task: 'tiles-validation' }, + ], + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + update: { type: 'Ingestion_Update' }, + export: { type: 'Export' }, + }, + tasks: { + tilesDeletion: { type: 'tiles-deletion', maxAttempts: 3 }, + tilesValidation: { type: 'tiles-validation', maxAttempts: 5 }, + }, + }; + + const pairs = buildPollingPairs(jobDefinitions, capabilities); + + expect(pairs).toHaveLength(3); + expect(pairs).toContainEqual({ + jobType: 'Ingestion_Update', + taskType: 'tiles-deletion', + maxAttempts: 3, + }); + expect(pairs).toContainEqual({ + jobType: 'Ingestion_Update', + taskType: 'tiles-validation', + maxAttempts: 5, + }); + expect(pairs).toContainEqual({ + jobType: 'Export', + taskType: 'tiles-validation', + maxAttempts: 5, + }); + }); + + it('should throw ConfigurationError when declared job type is not in job definitions', () => { + const capabilities: WorkerCapabilities = { + pairs: [{ job: 'Non_Existent_Job', task: 'tiles-deletion' }], + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + update: { type: 'Ingestion_Update' }, + }, + tasks: { + tilesDeletion: { type: 'tiles-deletion' }, + }, + }; + + expect(() => buildPollingPairs(jobDefinitions, capabilities)).toThrow(ConfigurationError); + expect(() => buildPollingPairs(jobDefinitions, capabilities)).toThrow( + 'Worker declares capability for job type "Non_Existent_Job" but it\'s not defined in job definitions' + ); + }); + + it('should throw ConfigurationError when declared task type is not in job definitions', () => { + const capabilities: WorkerCapabilities = { + pairs: [{ job: 'Ingestion_Update', task: 'non-existent-task' }], + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + update: { type: 'Ingestion_Update' }, + }, + tasks: { + tilesDeletion: { type: 'tiles-deletion' }, + }, + }; + + expect(() => buildPollingPairs(jobDefinitions, capabilities)).toThrow(ConfigurationError); + expect(() => buildPollingPairs(jobDefinitions, capabilities)).toThrow( + 'Worker declares capability for task type "non-existent-task" but it\'s not defined in job definitions' + ); + }); + + it('should return empty array when no capability pairs declared', () => { + const capabilities: WorkerCapabilities = { + pairs: [], // No pairs + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + update: { type: 'Ingestion_Update' }, + }, + tasks: { + tilesDeletion: { type: 'tiles-deletion', maxAttempts: 3 }, + }, + }; + + const pairs = buildPollingPairs(jobDefinitions, capabilities); + + expect(pairs).toHaveLength(0); + expect(pairs).toEqual([]); + }); + + it('should handle realistic production scenario', () => { + // Real-world scenario: cleaner worker handles tiles-deletion for specific update jobs + const capabilities: WorkerCapabilities = { + pairs: [ + { job: 'Ingestion_Update', task: 'tiles-deletion' }, + { job: 'Ingestion_Swap_Update', task: 'tiles-deletion' }, + ], + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + new: { type: 'Ingestion_New' }, + update: { type: 'Ingestion_Update' }, + swapUpdate: { type: 'Ingestion_Swap_Update' }, + seed: { type: 'TilesSeeding' }, + export: { type: 'Export' }, + }, + tasks: { + init: { type: 'init' }, + validation: { type: 'validation' }, + createTasks: { type: 'create-tasks' }, + merge: { type: 'tilesMerging' }, + polygonParts: { type: 'polygon-parts', suspendJobOnFail: true }, + finalize: { type: 'finalize' }, + seed: { type: 'TilesSeeding' }, + export: { type: 'tilesExporting' }, + tilesDeletion: { type: 'tiles-deletion', maxAttempts: 3 }, + }, + }; + + const pairs = buildPollingPairs(jobDefinitions, capabilities); + + // Should only create 2 pairs as explicitly declared + expect(pairs).toHaveLength(2); + expect(pairs).toEqual([ + { jobType: 'Ingestion_Update', taskType: 'tiles-deletion', maxAttempts: 3 }, + { jobType: 'Ingestion_Swap_Update', taskType: 'tiles-deletion', maxAttempts: 3 }, + ]); + + // Verify other job types are NOT included + expect(pairs).not.toContainEqual(expect.objectContaining({ jobType: 'Ingestion_New' })); + expect(pairs).not.toContainEqual(expect.objectContaining({ jobType: 'TilesSeeding' })); + expect(pairs).not.toContainEqual(expect.objectContaining({ jobType: 'Export' })); + }); + + it('should preserve maxAttempts from task definition for each pair', () => { + const capabilities: WorkerCapabilities = { + pairs: [ + { job: 'Ingestion_Update', task: 'tiles-deletion' }, + { job: 'Export', task: 'tiles-validation' }, + ], + }; + + const jobDefinitions: JobDefinitions = { + jobs: { + update: { type: 'Ingestion_Update' }, + export: { type: 'Export' }, + }, + tasks: { + tilesDeletion: { type: 'tiles-deletion', maxAttempts: 5 }, + tilesValidation: { type: 'tiles-validation', maxAttempts: 7 }, + }, + }; + + const pairs = buildPollingPairs(jobDefinitions, capabilities); + + expect(pairs).toEqual([ + { jobType: 'Ingestion_Update', taskType: 'tiles-deletion', maxAttempts: 5 }, + { jobType: 'Export', taskType: 'tiles-validation', maxAttempts: 7 }, + ]); + }); +}); From 6b71b86b35ff31254edf7c1b015f111f79971687 Mon Sep 17 00:00:00 2001 From: almog8k Date: Thu, 12 Feb 2026 10:02:51 +0200 Subject: [PATCH 16/25] refactor(validation): move validation to strategies with logger context Move task validation from centralized TaskValidator into individual strategies. Simplify StrategyFactory to single resolveWithContext() method for automatic logger enrichment with complete job context. - Add validate() method to ITaskStrategy interface - Create validateSchema() helper using DI container for logger - Implement validate() in TilesDeletionStrategy - Simplify StrategyFactory: remove resolve(), keep only resolveWithContext() - Add TaskContext with jobId, taskId, jobType, taskType for full tracing - Remove TaskValidator class and taskSchemas registry - Add comprehensive tests for validation and context enrichment - Fix mock logger to include child() method --- src/cleaner/strategies/index.ts | 2 +- src/cleaner/strategies/strategyFactory.ts | 35 ++++-- src/cleaner/strategies/taskStrategy.ts | 16 ++- .../strategies/tilesDeletionStrategy.ts | 14 ++- src/cleaner/utils/index.ts | 1 + src/cleaner/utils/validationHelper.ts | 39 +++++++ src/cleaner/validation/index.ts | 3 +- src/cleaner/validation/schemas.ts | 6 -- src/cleaner/validation/taskValidator.ts | 37 ------- tests/helpers/mocks.ts | 1 + tests/strategyFactory.spec.ts | 100 ++++++++++++++---- tests/tilesDeletionStrategy.spec.ts | 99 +++++++++++++++++ tests/validation.spec.ts | 39 ------- 13 files changed, 270 insertions(+), 122 deletions(-) create mode 100644 src/cleaner/utils/validationHelper.ts delete mode 100644 src/cleaner/validation/taskValidator.ts create mode 100644 tests/tilesDeletionStrategy.spec.ts delete mode 100644 tests/validation.spec.ts diff --git a/src/cleaner/strategies/index.ts b/src/cleaner/strategies/index.ts index 3f2da61..f3895d4 100644 --- a/src/cleaner/strategies/index.ts +++ b/src/cleaner/strategies/index.ts @@ -1,3 +1,3 @@ export { type ITaskStrategy } from './taskStrategy'; -export { StrategyFactory } from './strategyFactory'; +export { StrategyFactory, type TaskContext } from './strategyFactory'; export { TilesDeletionStrategy } from './tilesDeletionStrategy'; diff --git a/src/cleaner/strategies/strategyFactory.ts b/src/cleaner/strategies/strategyFactory.ts index 208d854..bc343f0 100644 --- a/src/cleaner/strategies/strategyFactory.ts +++ b/src/cleaner/strategies/strategyFactory.ts @@ -5,8 +5,14 @@ import { SERVICES } from '@common/constants'; import { StrategyNotFoundError } from '../errors'; import type { ITaskStrategy } from './taskStrategy'; +export interface TaskContext { + jobId: string; + taskId: string; + jobType: string; + taskType: string; +} + /** - * Factory for resolving task strategies by task type. * Strategies are registered in the DI container with their task type as the token. */ export class StrategyFactory { @@ -16,22 +22,29 @@ export class StrategyFactory { ) {} /** - * Resolves a strategy for the given task type. - * - * @param taskType - The task type to resolve (e.g., 'tiles-deletion') - * @returns The registered strategy instance + * Resolves a strategy with enriched logger context for a specific task. + * @param taskContext - Task context (jobId, taskId, jobType, taskType) + * @returns Strategy instance with enriched logger * @throws StrategyNotFoundError if no strategy is registered for the task type */ - public resolve(taskType: string): ITaskStrategy { - this.logger.debug({ msg: 'Resolving strategy', taskType }); + public resolveWithContext(taskContext: TaskContext): ITaskStrategy { + this.logger.debug({ msg: 'Resolving strategy with task context', ...taskContext }); - if (!this.container.isRegistered(taskType)) { - throw new StrategyNotFoundError(taskType); + if (!this.container.isRegistered(taskContext.taskType)) { + throw new StrategyNotFoundError(taskContext.taskType); } - const strategy = this.container.resolve(taskType); + const taskContainer = this.container.createChildContainer(); + + const taskLogger = this.logger.child({ + ...taskContext, + }); + + taskContainer.register(SERVICES.LOGGER, { useValue: taskLogger }); + + const strategy = taskContainer.resolve(taskContext.taskType); - this.logger.debug({ msg: 'Strategy resolved successfully', taskType }); + taskLogger.debug({ msg: 'Strategy resolved successfully with task context' }); return strategy; } diff --git a/src/cleaner/strategies/taskStrategy.ts b/src/cleaner/strategies/taskStrategy.ts index 9208cff..6a000b1 100644 --- a/src/cleaner/strategies/taskStrategy.ts +++ b/src/cleaner/strategies/taskStrategy.ts @@ -1,14 +1,24 @@ /** * Strategy interface for task execution. - * Concrete strategies implement task-specific business logic. + * Concrete strategies implement task-specific business logic and validation. * - * @template T - The type of validated task parameters (defaults to generic object) + * @template T - The type of validated task parameters */ export interface ITaskStrategy> { + /** + * Validates unknown task parameters against the strategy's schema. + * + * @param params - Unknown task parameters to validate + * @param taskType - Task type from the polled task (for logging/error messages) + * @returns Typed and validated parameters + * @throws ValidationError if parameters fail schema validation + */ + validate: (params: unknown, taskType: string) => T; + /** * Executes the task with validated parameters. * - * @param params - Validated task parameters (validated via Zod schema) + * @param params - Validated task parameters (output of validate()) * @throws RecoverableError for transient failures (will be retried) * @throws UnrecoverableError for permanent failures (will be rejected) */ diff --git a/src/cleaner/strategies/tilesDeletionStrategy.ts b/src/cleaner/strategies/tilesDeletionStrategy.ts index 2730255..cb4ac04 100644 --- a/src/cleaner/strategies/tilesDeletionStrategy.ts +++ b/src/cleaner/strategies/tilesDeletionStrategy.ts @@ -1,7 +1,8 @@ import { inject, injectable } from 'tsyringe'; import type { Logger } from '@map-colonies/js-logger'; import { SERVICES } from '@common/constants'; -import type { TilesDeletionParams } from '../validation/schemas'; +import { tilesDeletionParamsSchema, type TilesDeletionParams } from '../validation/schemas'; +import { validateSchema } from '../utils'; import type { ITaskStrategy } from './taskStrategy'; /** @@ -13,10 +14,17 @@ export class TilesDeletionStrategy implements ITaskStrategy public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} /** - * Executes tiles deletion task. + * Validates task parameters against tiles-deletion schema. * - * @param params - Validated tiles deletion parameters + * @param params - Unknown task parameters to validate + * @param taskType - Task type from polled task (for logging) + * @returns Typed and validated tiles deletion parameters + * @throws ValidationError if parameters fail schema validation */ + public validate(params: unknown, taskType: string): TilesDeletionParams { + return validateSchema(tilesDeletionParamsSchema, params, taskType); + } + public async execute(params: TilesDeletionParams): Promise { this.logger.info({ msg: 'Executing tiles deletion task', diff --git a/src/cleaner/utils/index.ts b/src/cleaner/utils/index.ts index 9f29449..fc203c9 100644 --- a/src/cleaner/utils/index.ts +++ b/src/cleaner/utils/index.ts @@ -1 +1,2 @@ export { buildPollingPairs } from './pairBuilder'; +export { validateSchema } from './validationHelper'; diff --git a/src/cleaner/utils/validationHelper.ts b/src/cleaner/utils/validationHelper.ts new file mode 100644 index 0000000..d949012 --- /dev/null +++ b/src/cleaner/utils/validationHelper.ts @@ -0,0 +1,39 @@ +import type { Logger } from '@map-colonies/js-logger'; +import type { ZodSchema } from 'zod'; +import { container } from 'tsyringe'; +import { SERVICES } from '@common/constants'; +import { ValidationError } from '../errors'; + +/** + * Generic schema validation helper for task parameters. + * + * Validates unknown task parameters against a Zod schema and returns + * the typed result or throws a ValidationError. + * + * Logger is resolved from the DI container, so no need to pass it. + * + * @param schema - The Zod schema to validate against + * @param params - Unknown task parameters to validate + * @param taskType - Task type name for error messages + * @returns Typed and validated parameters + * @throws {ValidationError} If validation fails + * + * @example + * const validatedParams = validateSchema( + * tilesDeletionParamsSchema, + * rawParams, + * 'tiles-deletion' + * ); + */ +export function validateSchema(schema: ZodSchema, params: unknown, taskType: string): T { + const logger = container.resolve(SERVICES.LOGGER); + const result = schema.safeParse(params); + + if (!result.success) { + logger.error({ taskType, errors: result.error.errors }, 'Task parameter validation failed'); + throw new ValidationError(`Invalid parameters for task type: ${taskType}`, result.error.errors); + } + + logger.debug({ taskType }, 'Task parameters validated successfully'); + return result.data; +} diff --git a/src/cleaner/validation/index.ts b/src/cleaner/validation/index.ts index f62dfde..9c0a49b 100644 --- a/src/cleaner/validation/index.ts +++ b/src/cleaner/validation/index.ts @@ -1,2 +1 @@ -export { TaskValidator } from './taskValidator'; -export { taskSchemas, type TaskType } from './schemas'; +export { tilesDeletionParamsSchema, type TilesDeletionParams } from './schemas'; diff --git a/src/cleaner/validation/schemas.ts b/src/cleaner/validation/schemas.ts index f3af4f1..799da41 100644 --- a/src/cleaner/validation/schemas.ts +++ b/src/cleaner/validation/schemas.ts @@ -5,9 +5,3 @@ export const tilesDeletionParamsSchema = z.object({ }); export type TilesDeletionParams = z.infer; - -export const taskSchemas = { - 'tiles-deletion': tilesDeletionParamsSchema, -} as const; - -export type TaskType = keyof typeof taskSchemas; diff --git a/src/cleaner/validation/taskValidator.ts b/src/cleaner/validation/taskValidator.ts deleted file mode 100644 index 32422cb..0000000 --- a/src/cleaner/validation/taskValidator.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { inject, injectable } from 'tsyringe'; -import { type Logger } from '@map-colonies/js-logger'; -import { type z } from 'zod'; -import { SERVICES } from '@common/constants'; -import { ValidationError } from '../errors'; -import { taskSchemas, type TaskType } from './schemas'; - -/** - * TaskValidator is responsible for validating task parameters against Zod schemas. - * Validation failures result in ValidationError (unrecoverable). - */ -@injectable() -export class TaskValidator { - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} - - /** - * Validates task parameters against the schema for the given task type. - * @param taskType - The task type (e.g., 'tiles-deletion') - * @param params - The task parameters to validate - * @returns The validated and typed parameters - * @throws ValidationError if validation fails or schema not found - */ - public validate(taskType: T, params: unknown): z.infer<(typeof taskSchemas)[T]> { - const schema = taskSchemas[taskType]; - - const result = schema.safeParse(params); - - if (!result.success) { - const message = `Task parameter validation failed for task type: ${taskType}`; - this.logger.error({ msg: message, errors: result.error.errors }); - throw new ValidationError(message, result.error.errors); - } - - this.logger.debug({ msg: 'Task parameters validated successfully', taskType }); - return result.data as z.infer<(typeof taskSchemas)[T]>; - } -} diff --git a/tests/helpers/mocks.ts b/tests/helpers/mocks.ts index 41e069b..09538d9 100644 --- a/tests/helpers/mocks.ts +++ b/tests/helpers/mocks.ts @@ -13,5 +13,6 @@ export function createMockLogger(): Logger { warn: vi.fn(), fatal: vi.fn(), trace: vi.fn(), + child: vi.fn(() => createMockLogger()), } as unknown as Logger; } diff --git a/tests/strategyFactory.spec.ts b/tests/strategyFactory.spec.ts index f2d4437..fdedd2c 100644 --- a/tests/strategyFactory.spec.ts +++ b/tests/strategyFactory.spec.ts @@ -1,14 +1,17 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { container, DependencyContainer } from 'tsyringe'; -import { faker } from '@faker-js/faker'; import type { Logger } from '@map-colonies/js-logger'; import { SERVICES } from '../src/common/constants'; -import { StrategyFactory, TilesDeletionStrategy, type ITaskStrategy } from '../src/cleaner/strategies'; +import { StrategyFactory, TilesDeletionStrategy, type ITaskStrategy, type TaskContext } from '../src/cleaner/strategies'; import { StrategyNotFoundError } from '../src/cleaner/errors'; import { createMockLogger } from './helpers/mocks'; // Mock strategy for testing class MockStrategy implements ITaskStrategy { + public validate(params: unknown): Record { + return params as Record; + } + public async execute(): Promise { // Mock implementation } @@ -32,20 +35,77 @@ describe('StrategyFactory', () => { childContainer.clearInstances(); }); - describe('resolve', () => { - it('should resolve registered strategy from container', () => { + describe('resolveWithContext', () => { + it('should resolve registered strategy with enriched logger context', () => { const taskType = 'tiles-deletion'; childContainer.register(taskType, { useClass: TilesDeletionStrategy }); - const strategy = strategyFactory.resolve(taskType); + const taskContext: TaskContext = { + jobId: 'job-abc123', + taskId: 'task-123', + jobType: 'Ingestion_Update', + taskType, + }; + + const strategy = strategyFactory.resolveWithContext(taskContext); expect(strategy).toBeInstanceOf(TilesDeletionStrategy); + expect(mockLogger.debug).toHaveBeenCalledWith( + expect.objectContaining({ + msg: 'Resolving strategy with task context', + jobId: 'job-abc123', + taskId: 'task-123', + jobType: 'Ingestion_Update', + taskType: 'tiles-deletion', + }) + ); + }); + + it('should create child logger with task context', () => { + const taskType = 'tiles-deletion'; + childContainer.register(taskType, { useClass: TilesDeletionStrategy }); + + const taskContext: TaskContext = { + jobId: 'job-def456', + taskId: 'task-456', + jobType: 'Ingestion_Swap_Update', + taskType, + }; + + strategyFactory.resolveWithContext(taskContext); + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockLogger.child).toHaveBeenCalledWith({ + jobId: 'job-def456', + taskId: 'task-456', + jobType: 'Ingestion_Swap_Update', + taskType: 'tiles-deletion', + }); }); it('should throw StrategyNotFoundError for unregistered task type', () => { - const taskType = faker.string.alpha(10); + const taskContext: TaskContext = { + jobId: 'job-xyz789', + taskId: 'task-789', + jobType: 'Export', + taskType: 'non-existent-task', + }; + + expect(() => strategyFactory.resolveWithContext(taskContext)).toThrow(StrategyNotFoundError); + }); + + it('should create separate instances for different tasks (child container isolation)', () => { + const taskType = 'tiles-deletion'; + childContainer.register(taskType, { useClass: TilesDeletionStrategy }); + + const context1: TaskContext = { jobId: 'job-1', taskId: 'task-1', jobType: 'Ingestion_Update', taskType }; + const context2: TaskContext = { jobId: 'job-1', taskId: 'task-2', jobType: 'Ingestion_Update', taskType }; + + const strategy1 = strategyFactory.resolveWithContext(context1); + const strategy2 = strategyFactory.resolveWithContext(context2); - expect(() => strategyFactory.resolve(taskType)).toThrow(StrategyNotFoundError); + // Different instances because of child containers + expect(strategy1).not.toBe(strategy2); }); it('should resolve different strategies for different task types', () => { @@ -55,29 +115,29 @@ describe('StrategyFactory', () => { childContainer.register(taskType1, { useClass: TilesDeletionStrategy }); childContainer.register(taskType2, { useClass: MockStrategy }); - const strategy1 = strategyFactory.resolve(taskType1); - const strategy2 = strategyFactory.resolve(taskType2); + const context1: TaskContext = { jobId: 'job-multi', taskId: 'task-1', jobType: 'Ingestion_Update', taskType: taskType1 }; + const context2: TaskContext = { jobId: 'job-multi', taskId: 'task-2', jobType: 'Ingestion_Update', taskType: taskType2 }; + + const strategy1 = strategyFactory.resolveWithContext(context1); + const strategy2 = strategyFactory.resolveWithContext(context2); expect(strategy1).toBeInstanceOf(TilesDeletionStrategy); expect(strategy2).toBeInstanceOf(MockStrategy); expect(strategy1).not.toBe(strategy2); }); - it('should resolve same strategy instance for multiple calls (singleton behavior)', () => { - const taskType = 'tiles-deletion'; - childContainer.registerSingleton(taskType, TilesDeletionStrategy); - - const strategy1 = strategyFactory.resolve(taskType); - const strategy2 = strategyFactory.resolve(taskType); - - expect(strategy1).toBe(strategy2); - }); - it('should handle special characters in task type', () => { const taskType = 'task-with-special_chars.v2'; childContainer.register(taskType, { useClass: MockStrategy }); - const strategy = strategyFactory.resolve(taskType); + const taskContext: TaskContext = { + jobId: 'job-special', + taskId: 'task-special', + jobType: 'CustomJob', + taskType, + }; + + const strategy = strategyFactory.resolveWithContext(taskContext); expect(strategy).toBeInstanceOf(MockStrategy); }); diff --git a/tests/tilesDeletionStrategy.spec.ts b/tests/tilesDeletionStrategy.spec.ts new file mode 100644 index 0000000..595f9d1 --- /dev/null +++ b/tests/tilesDeletionStrategy.spec.ts @@ -0,0 +1,99 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { container } from 'tsyringe'; +import { SERVICES } from '@common/constants'; +import { TilesDeletionStrategy } from '@src/cleaner/strategies/tilesDeletionStrategy'; +import { ValidationError } from '@src/cleaner/errors'; +import { createMockLogger } from './helpers/mocks'; + +describe('TilesDeletionStrategy', () => { + let strategy: TilesDeletionStrategy; + const mockLogger = createMockLogger(); + + beforeEach(() => { + container.clearInstances(); + container.register(SERVICES.LOGGER, { useValue: mockLogger }); + strategy = container.resolve(TilesDeletionStrategy); + }); + + describe('validate', () => { + it('should validate and return typed parameters when schema passes', () => { + const params = {}; // Empty object is valid for current TODO schema + const taskType = 'tiles-deletion'; + + const result = strategy.validate(params, taskType); + + expect(result).toEqual({}); + expect(mockLogger.debug).toHaveBeenCalledWith({ taskType }, 'Task parameters validated successfully'); + }); + + it('should throw ValidationError when schema validation fails', () => { + const invalidParams = null; // null is not a valid object + const taskType = 'tiles-deletion'; + + expect(() => strategy.validate(invalidParams, taskType)).toThrow(ValidationError); + expect(() => strategy.validate(invalidParams, taskType)).toThrow(`Invalid parameters for task type: ${taskType}`); + expect(mockLogger.error).toHaveBeenCalledWith( + expect.objectContaining({ + taskType, + errors: expect.any(Array) as unknown[], + }), + 'Task parameter validation failed' + ); + }); + + it('should throw ValidationError with Zod error details', () => { + const invalidParams = 'not an object'; + const taskType = 'tiles-deletion'; + + try { + strategy.validate(invalidParams, taskType); + expect.fail('Should have thrown ValidationError'); + } catch (error) { + expect(error).toBeInstanceOf(ValidationError); + const validationError = error as ValidationError; + expect(validationError.validationDetails).toBeDefined(); + expect(Array.isArray(validationError.validationDetails)).toBe(true); + } + }); + + it('should use task type from polled task for logging', () => { + const params = {}; + const taskType = 'custom-task-type'; // Different task type + + strategy.validate(params, taskType); + + expect(mockLogger.debug).toHaveBeenCalledWith({ taskType: 'custom-task-type' }, 'Task parameters validated successfully'); + }); + }); + + describe('execute', () => { + it('should execute with valid typed parameters', async () => { + const validParams = {}; // Matches TilesDeletionParams type + + await expect(strategy.execute(validParams)).resolves.toBeUndefined(); + + expect(mockLogger.info).toHaveBeenCalledWith({ + msg: 'Executing tiles deletion task', + params: validParams, + }); + expect(mockLogger.info).toHaveBeenCalledWith({ + msg: 'Tiles deletion task completed', + }); + }); + }); + + describe('validate + execute flow', () => { + it('should validate then execute successfully', async () => { + const rawParams = {}; + const taskType = 'tiles-deletion'; + + const validatedParams = strategy.validate(rawParams, taskType); + await strategy.execute(validatedParams); + + expect(mockLogger.debug).toHaveBeenCalledWith({ taskType }, 'Task parameters validated successfully'); + expect(mockLogger.info).toHaveBeenCalledWith({ + msg: 'Tiles deletion task completed', + }); + }); + }); +}); diff --git a/tests/validation.spec.ts b/tests/validation.spec.ts deleted file mode 100644 index a416a7e..0000000 --- a/tests/validation.spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import type { Logger } from '@map-colonies/js-logger'; -import { TaskValidator } from '../src/cleaner/validation'; -import { createMockLogger } from './helpers/mocks'; - -describe('TaskValidator', () => { - let taskValidator: TaskValidator; - let mockLogger: Logger; - - beforeEach(() => { - mockLogger = createMockLogger(); - taskValidator = new TaskValidator(mockLogger); - }); - - describe('validate', () => { - it('should validate valid tiles-deletion parameters', () => { - //TODO: implement valid parameters for tiles-deletion task once the schema is defined - const validParams = {}; - - const result = taskValidator.validate('tiles-deletion', validParams); - - expect(result).toEqual(validParams); - expect(mockLogger.debug).toHaveBeenCalledWith({ - msg: 'Task parameters validated successfully', - taskType: 'tiles-deletion', - }); - }); - - it('should return validated data on success', () => { - const params = { someField: 'value' }; - const result = taskValidator.validate('tiles-deletion', params); - - expect(result).toBeDefined(); - expect(mockLogger.debug).toHaveBeenCalled(); - }); - - //TODO: implement test for invalid parameters once the schema is defined - }); -}); From e9facc23cb76221d97f32fd2e33aab76c407e615 Mon Sep 17 00:00:00 2001 From: almog8k Date: Thu, 12 Feb 2026 10:18:28 +0200 Subject: [PATCH 17/25] refactor(validation): simplify validation method and improve error logging --- src/cleaner/errors/errors.ts | 2 +- .../strategies/tilesDeletionStrategy.ts | 5 +-- src/cleaner/utils/validationHelper.ts | 17 ++------ tests/tilesDeletionStrategy.spec.ts | 42 ++----------------- 4 files changed, 10 insertions(+), 56 deletions(-) diff --git a/src/cleaner/errors/errors.ts b/src/cleaner/errors/errors.ts index 7588bd7..fc7de22 100644 --- a/src/cleaner/errors/errors.ts +++ b/src/cleaner/errors/errors.ts @@ -56,6 +56,6 @@ export class ValidationError extends UnrecoverableError { export class StrategyNotFoundError extends UnrecoverableError { public constructor(taskType: string) { super(`No strategy registered for task type: ${taskType}`); - this.name = 'StrategyNotFoundError'; + this.name = StrategyNotFoundError.name; } } diff --git a/src/cleaner/strategies/tilesDeletionStrategy.ts b/src/cleaner/strategies/tilesDeletionStrategy.ts index cb4ac04..a209227 100644 --- a/src/cleaner/strategies/tilesDeletionStrategy.ts +++ b/src/cleaner/strategies/tilesDeletionStrategy.ts @@ -17,12 +17,11 @@ export class TilesDeletionStrategy implements ITaskStrategy * Validates task parameters against tiles-deletion schema. * * @param params - Unknown task parameters to validate - * @param taskType - Task type from polled task (for logging) * @returns Typed and validated tiles deletion parameters * @throws ValidationError if parameters fail schema validation */ - public validate(params: unknown, taskType: string): TilesDeletionParams { - return validateSchema(tilesDeletionParamsSchema, params, taskType); + public validate(params: unknown): TilesDeletionParams { + return validateSchema(tilesDeletionParamsSchema, params, this.logger); } public async execute(params: TilesDeletionParams): Promise { diff --git a/src/cleaner/utils/validationHelper.ts b/src/cleaner/utils/validationHelper.ts index d949012..875eca2 100644 --- a/src/cleaner/utils/validationHelper.ts +++ b/src/cleaner/utils/validationHelper.ts @@ -1,7 +1,6 @@ import type { Logger } from '@map-colonies/js-logger'; import type { ZodSchema } from 'zod'; import { container } from 'tsyringe'; -import { SERVICES } from '@common/constants'; import { ValidationError } from '../errors'; /** @@ -17,23 +16,15 @@ import { ValidationError } from '../errors'; * @param taskType - Task type name for error messages * @returns Typed and validated parameters * @throws {ValidationError} If validation fails - * - * @example - * const validatedParams = validateSchema( - * tilesDeletionParamsSchema, - * rawParams, - * 'tiles-deletion' - * ); */ -export function validateSchema(schema: ZodSchema, params: unknown, taskType: string): T { - const logger = container.resolve(SERVICES.LOGGER); +export function validateSchema(schema: ZodSchema, params: unknown, logger: Logger): T { const result = schema.safeParse(params); if (!result.success) { - logger.error({ taskType, errors: result.error.errors }, 'Task parameter validation failed'); - throw new ValidationError(`Invalid parameters for task type: ${taskType}`, result.error.errors); + logger.error({ errors: result.error.errors }, 'Task parameter validation failed'); + throw new ValidationError(`Invalid parameters for task`, result.error.errors); } - logger.debug({ taskType }, 'Task parameters validated successfully'); + logger.debug('Task parameters validated successfully'); return result.data; } diff --git a/tests/tilesDeletionStrategy.spec.ts b/tests/tilesDeletionStrategy.spec.ts index 595f9d1..63a66fa 100644 --- a/tests/tilesDeletionStrategy.spec.ts +++ b/tests/tilesDeletionStrategy.spec.ts @@ -18,35 +18,23 @@ describe('TilesDeletionStrategy', () => { describe('validate', () => { it('should validate and return typed parameters when schema passes', () => { const params = {}; // Empty object is valid for current TODO schema - const taskType = 'tiles-deletion'; - const result = strategy.validate(params, taskType); + const result = strategy.validate(params); expect(result).toEqual({}); - expect(mockLogger.debug).toHaveBeenCalledWith({ taskType }, 'Task parameters validated successfully'); }); it('should throw ValidationError when schema validation fails', () => { const invalidParams = null; // null is not a valid object - const taskType = 'tiles-deletion'; - expect(() => strategy.validate(invalidParams, taskType)).toThrow(ValidationError); - expect(() => strategy.validate(invalidParams, taskType)).toThrow(`Invalid parameters for task type: ${taskType}`); - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ - taskType, - errors: expect.any(Array) as unknown[], - }), - 'Task parameter validation failed' - ); + expect(() => strategy.validate(invalidParams)).toThrow(ValidationError); }); it('should throw ValidationError with Zod error details', () => { const invalidParams = 'not an object'; - const taskType = 'tiles-deletion'; try { - strategy.validate(invalidParams, taskType); + strategy.validate(invalidParams); expect.fail('Should have thrown ValidationError'); } catch (error) { expect(error).toBeInstanceOf(ValidationError); @@ -55,15 +43,6 @@ describe('TilesDeletionStrategy', () => { expect(Array.isArray(validationError.validationDetails)).toBe(true); } }); - - it('should use task type from polled task for logging', () => { - const params = {}; - const taskType = 'custom-task-type'; // Different task type - - strategy.validate(params, taskType); - - expect(mockLogger.debug).toHaveBeenCalledWith({ taskType: 'custom-task-type' }, 'Task parameters validated successfully'); - }); }); describe('execute', () => { @@ -81,19 +60,4 @@ describe('TilesDeletionStrategy', () => { }); }); }); - - describe('validate + execute flow', () => { - it('should validate then execute successfully', async () => { - const rawParams = {}; - const taskType = 'tiles-deletion'; - - const validatedParams = strategy.validate(rawParams, taskType); - await strategy.execute(validatedParams); - - expect(mockLogger.debug).toHaveBeenCalledWith({ taskType }, 'Task parameters validated successfully'); - expect(mockLogger.info).toHaveBeenCalledWith({ - msg: 'Tiles deletion task completed', - }); - }); - }); }); From 708a4459ceaba4d8866169792ec1660acd14675a Mon Sep 17 00:00:00 2001 From: almog8k Date: Thu, 12 Feb 2026 10:22:48 +0200 Subject: [PATCH 18/25] fix: ensure stack trace is captured for custom errors --- src/cleaner/errors/errors.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cleaner/errors/errors.ts b/src/cleaner/errors/errors.ts index fc7de22..a54f13a 100644 --- a/src/cleaner/errors/errors.ts +++ b/src/cleaner/errors/errors.ts @@ -32,6 +32,7 @@ export class ConfigurationError extends UnrecoverableError { public constructor(message: string) { super(message); this.name = ConfigurationError.name; + Error.captureStackTrace(this, this.constructor); } } @@ -46,6 +47,7 @@ export class ValidationError extends UnrecoverableError { ) { super(message); this.name = ValidationError.name; + Error.captureStackTrace(this, this.constructor); } } @@ -57,5 +59,6 @@ export class StrategyNotFoundError extends UnrecoverableError { public constructor(taskType: string) { super(`No strategy registered for task type: ${taskType}`); this.name = StrategyNotFoundError.name; + Error.captureStackTrace(this, this.constructor); } } From fafe8ddca789470a2323a1058fc3147243e99b6e Mon Sep 17 00:00:00 2001 From: almog8k Date: Tue, 17 Feb 2026 10:36:04 +0200 Subject: [PATCH 19/25] fix(tests): use faker for dynamic job and task IDs in strategy tests --- ai-docs/testing.md | 3 ++- tests/strategyFactory.spec.ts | 41 ++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/ai-docs/testing.md b/ai-docs/testing.md index b1dc2bd..f61de43 100644 --- a/ai-docs/testing.md +++ b/ai-docs/testing.md @@ -56,6 +56,7 @@ tests/ ### Basic Structure ```typescript +import { faker } from '@faker-js/faker'; import { describe, it, expect, beforeEach } from 'vitest'; import { ErrorHandler } from '../src/cleaner/errors'; import { createMockLogger } from './helpers/mocks'; @@ -71,7 +72,7 @@ describe('ErrorHandler', () => { it('should retry recoverable errors when attempts remain', () => { const error = new RecoverableError('Network timeout'); - const context = { jobId: '123', taskId: '456', attemptNumber: 1, maxAttempts: 3, error }; + const context = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), attemptNumber: 1, maxAttempts: 3, error }; const decision = errorHandler.handleError(context); diff --git a/tests/strategyFactory.spec.ts b/tests/strategyFactory.spec.ts index fdedd2c..e7f1485 100644 --- a/tests/strategyFactory.spec.ts +++ b/tests/strategyFactory.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { container, DependencyContainer } from 'tsyringe'; +import { faker } from '@faker-js/faker'; import type { Logger } from '@map-colonies/js-logger'; import { SERVICES } from '../src/common/constants'; import { StrategyFactory, TilesDeletionStrategy, type ITaskStrategy, type TaskContext } from '../src/cleaner/strategies'; @@ -41,8 +42,8 @@ describe('StrategyFactory', () => { childContainer.register(taskType, { useClass: TilesDeletionStrategy }); const taskContext: TaskContext = { - jobId: 'job-abc123', - taskId: 'task-123', + jobId: faker.string.uuid(), + taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType, }; @@ -53,10 +54,10 @@ describe('StrategyFactory', () => { expect(mockLogger.debug).toHaveBeenCalledWith( expect.objectContaining({ msg: 'Resolving strategy with task context', - jobId: 'job-abc123', - taskId: 'task-123', - jobType: 'Ingestion_Update', - taskType: 'tiles-deletion', + jobId: taskContext.jobId, + taskId: taskContext.taskId, + jobType: taskContext.jobType, + taskType: taskContext.taskType, }) ); }); @@ -66,8 +67,8 @@ describe('StrategyFactory', () => { childContainer.register(taskType, { useClass: TilesDeletionStrategy }); const taskContext: TaskContext = { - jobId: 'job-def456', - taskId: 'task-456', + jobId: faker.string.uuid(), + taskId: faker.string.uuid(), jobType: 'Ingestion_Swap_Update', taskType, }; @@ -76,17 +77,17 @@ describe('StrategyFactory', () => { // eslint-disable-next-line @typescript-eslint/unbound-method expect(mockLogger.child).toHaveBeenCalledWith({ - jobId: 'job-def456', - taskId: 'task-456', - jobType: 'Ingestion_Swap_Update', - taskType: 'tiles-deletion', + jobId: taskContext.jobId, + taskId: taskContext.taskId, + jobType: taskContext.jobType, + taskType: taskContext.taskType, }); }); it('should throw StrategyNotFoundError for unregistered task type', () => { const taskContext: TaskContext = { - jobId: 'job-xyz789', - taskId: 'task-789', + jobId: faker.string.uuid(), + taskId: faker.string.uuid(), jobType: 'Export', taskType: 'non-existent-task', }; @@ -98,8 +99,8 @@ describe('StrategyFactory', () => { const taskType = 'tiles-deletion'; childContainer.register(taskType, { useClass: TilesDeletionStrategy }); - const context1: TaskContext = { jobId: 'job-1', taskId: 'task-1', jobType: 'Ingestion_Update', taskType }; - const context2: TaskContext = { jobId: 'job-1', taskId: 'task-2', jobType: 'Ingestion_Update', taskType }; + const context1: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType }; + const context2: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType }; const strategy1 = strategyFactory.resolveWithContext(context1); const strategy2 = strategyFactory.resolveWithContext(context2); @@ -115,8 +116,8 @@ describe('StrategyFactory', () => { childContainer.register(taskType1, { useClass: TilesDeletionStrategy }); childContainer.register(taskType2, { useClass: MockStrategy }); - const context1: TaskContext = { jobId: 'job-multi', taskId: 'task-1', jobType: 'Ingestion_Update', taskType: taskType1 }; - const context2: TaskContext = { jobId: 'job-multi', taskId: 'task-2', jobType: 'Ingestion_Update', taskType: taskType2 }; + const context1: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType: taskType1 }; + const context2: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType: taskType2 }; const strategy1 = strategyFactory.resolveWithContext(context1); const strategy2 = strategyFactory.resolveWithContext(context2); @@ -131,8 +132,8 @@ describe('StrategyFactory', () => { childContainer.register(taskType, { useClass: MockStrategy }); const taskContext: TaskContext = { - jobId: 'job-special', - taskId: 'task-special', + jobId: faker.string.uuid(), + taskId: faker.string.uuid(), jobType: 'CustomJob', taskType, }; From 21a0bc98ba15be4c25fa89c60978d4609dffc0d8 Mon Sep 17 00:00:00 2001 From: almog8k Date: Tue, 17 Feb 2026 11:14:00 +0200 Subject: [PATCH 20/25] refactor(strategy): update resolveWithContext to return ITaskStrategyHandle with async disposal --- src/cleaner/strategies/strategyFactory.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/cleaner/strategies/strategyFactory.ts b/src/cleaner/strategies/strategyFactory.ts index bc343f0..eba6110 100644 --- a/src/cleaner/strategies/strategyFactory.ts +++ b/src/cleaner/strategies/strategyFactory.ts @@ -12,6 +12,10 @@ export interface TaskContext { taskType: string; } +export interface ITaskStrategyHandle extends AsyncDisposable { + strategy: ITaskStrategy; +} + /** * Strategies are registered in the DI container with their task type as the token. */ @@ -27,7 +31,7 @@ export class StrategyFactory { * @returns Strategy instance with enriched logger * @throws StrategyNotFoundError if no strategy is registered for the task type */ - public resolveWithContext(taskContext: TaskContext): ITaskStrategy { + public resolveWithContext(taskContext: TaskContext): ITaskStrategyHandle { this.logger.debug({ msg: 'Resolving strategy with task context', ...taskContext }); if (!this.container.isRegistered(taskContext.taskType)) { @@ -46,6 +50,12 @@ export class StrategyFactory { taskLogger.debug({ msg: 'Strategy resolved successfully with task context' }); - return strategy; + return { + strategy, + [Symbol.asyncDispose]: async (): Promise => { + taskLogger.debug({ msg: 'Disposing task container' }); + await taskContainer.dispose(); + }, + }; } } From 5ffcda2cfd5ba0568de2036f30a6f666edcc52e3 Mon Sep 17 00:00:00 2001 From: almog8k Date: Tue, 17 Feb 2026 13:30:42 +0200 Subject: [PATCH 21/25] refactor(tests): update strategyFactory tests to use handle and check asyncDispose method --- src/cleaner/utils/validationHelper.ts | 1 - tests/strategyFactory.spec.ts | 46 +++++++++++++++++++-------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/cleaner/utils/validationHelper.ts b/src/cleaner/utils/validationHelper.ts index 875eca2..f30119e 100644 --- a/src/cleaner/utils/validationHelper.ts +++ b/src/cleaner/utils/validationHelper.ts @@ -1,6 +1,5 @@ import type { Logger } from '@map-colonies/js-logger'; import type { ZodSchema } from 'zod'; -import { container } from 'tsyringe'; import { ValidationError } from '../errors'; /** diff --git a/tests/strategyFactory.spec.ts b/tests/strategyFactory.spec.ts index e7f1485..bd208d2 100644 --- a/tests/strategyFactory.spec.ts +++ b/tests/strategyFactory.spec.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { container, DependencyContainer } from 'tsyringe'; import { faker } from '@faker-js/faker'; import type { Logger } from '@map-colonies/js-logger'; @@ -48,9 +48,9 @@ describe('StrategyFactory', () => { taskType, }; - const strategy = strategyFactory.resolveWithContext(taskContext); + const handle = strategyFactory.resolveWithContext(taskContext); - expect(strategy).toBeInstanceOf(TilesDeletionStrategy); + expect(handle.strategy).toBeInstanceOf(TilesDeletionStrategy); expect(mockLogger.debug).toHaveBeenCalledWith( expect.objectContaining({ msg: 'Resolving strategy with task context', @@ -102,11 +102,11 @@ describe('StrategyFactory', () => { const context1: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType }; const context2: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType }; - const strategy1 = strategyFactory.resolveWithContext(context1); - const strategy2 = strategyFactory.resolveWithContext(context2); + const handle1 = strategyFactory.resolveWithContext(context1); + const handle2 = strategyFactory.resolveWithContext(context2); // Different instances because of child containers - expect(strategy1).not.toBe(strategy2); + expect(handle1.strategy).not.toBe(handle2.strategy); }); it('should resolve different strategies for different task types', () => { @@ -119,12 +119,12 @@ describe('StrategyFactory', () => { const context1: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType: taskType1 }; const context2: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType: taskType2 }; - const strategy1 = strategyFactory.resolveWithContext(context1); - const strategy2 = strategyFactory.resolveWithContext(context2); + const handle1 = strategyFactory.resolveWithContext(context1); + const handle2 = strategyFactory.resolveWithContext(context2); - expect(strategy1).toBeInstanceOf(TilesDeletionStrategy); - expect(strategy2).toBeInstanceOf(MockStrategy); - expect(strategy1).not.toBe(strategy2); + expect(handle1.strategy).toBeInstanceOf(TilesDeletionStrategy); + expect(handle2.strategy).toBeInstanceOf(MockStrategy); + expect(handle1.strategy).not.toBe(handle2.strategy); }); it('should handle special characters in task type', () => { @@ -138,9 +138,29 @@ describe('StrategyFactory', () => { taskType, }; - const strategy = strategyFactory.resolveWithContext(taskContext); + const handle = strategyFactory.resolveWithContext(taskContext); - expect(strategy).toBeInstanceOf(MockStrategy); + expect(handle.strategy).toBeInstanceOf(MockStrategy); + }); + + it('should have asyncDispose method for cleanup', async () => { + const taskType = 'tiles-deletion'; + childContainer.register(taskType, { useClass: TilesDeletionStrategy }); + + const taskContext: TaskContext = { + jobId: faker.string.uuid(), + taskId: faker.string.uuid(), + jobType: 'Ingestion_Update', + taskType, + }; + + const handle = strategyFactory.resolveWithContext(taskContext); + + expect(handle[Symbol.asyncDispose]).toBeDefined(); + expect(typeof handle[Symbol.asyncDispose]).toBe('function'); + + // Cleanup should not throw + await expect(handle[Symbol.asyncDispose]()).resolves.not.toThrow(); }); }); }); From 92e2a9d4002ae87556b22a8a4f83d96baa6fed05 Mon Sep 17 00:00:00 2001 From: almog8k Date: Wed, 18 Feb 2026 10:39:28 +0200 Subject: [PATCH 22/25] refactor(strategy): simplify StrategyFactory and update ITaskStrategy interface --- src/cleaner/strategies/strategyFactory.ts | 28 +++------ src/cleaner/strategies/taskStrategy.ts | 2 +- src/common/constants.ts | 3 +- src/containerConfig.ts | 8 ++- tests/strategyFactory.spec.ts | 74 ++++++++--------------- 5 files changed, 42 insertions(+), 73 deletions(-) diff --git a/src/cleaner/strategies/strategyFactory.ts b/src/cleaner/strategies/strategyFactory.ts index eba6110..0ce5ff2 100644 --- a/src/cleaner/strategies/strategyFactory.ts +++ b/src/cleaner/strategies/strategyFactory.ts @@ -1,5 +1,4 @@ -import { inject } from 'tsyringe'; -import type { DependencyContainer } from 'tsyringe'; +import { container, inject, injectable } from 'tsyringe'; import type { Logger } from '@map-colonies/js-logger'; import { SERVICES } from '@common/constants'; import { StrategyNotFoundError } from '../errors'; @@ -12,18 +11,12 @@ export interface TaskContext { taskType: string; } -export interface ITaskStrategyHandle extends AsyncDisposable { - strategy: ITaskStrategy; -} - /** * Strategies are registered in the DI container with their task type as the token. */ +@injectable() export class StrategyFactory { - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - private readonly container: DependencyContainer - ) {} + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} /** * Resolves a strategy with enriched logger context for a specific task. @@ -31,15 +24,14 @@ export class StrategyFactory { * @returns Strategy instance with enriched logger * @throws StrategyNotFoundError if no strategy is registered for the task type */ - public resolveWithContext(taskContext: TaskContext): ITaskStrategyHandle { + public resolveWithContext(taskContext: TaskContext): ITaskStrategy { this.logger.debug({ msg: 'Resolving strategy with task context', ...taskContext }); - if (!this.container.isRegistered(taskContext.taskType)) { + if (!container.isRegistered(taskContext.taskType)) { throw new StrategyNotFoundError(taskContext.taskType); } - const taskContainer = this.container.createChildContainer(); - + const taskContainer = container.createChildContainer(); const taskLogger = this.logger.child({ ...taskContext, }); @@ -50,12 +42,6 @@ export class StrategyFactory { taskLogger.debug({ msg: 'Strategy resolved successfully with task context' }); - return { - strategy, - [Symbol.asyncDispose]: async (): Promise => { - taskLogger.debug({ msg: 'Disposing task container' }); - await taskContainer.dispose(); - }, - }; + return strategy; } } diff --git a/src/cleaner/strategies/taskStrategy.ts b/src/cleaner/strategies/taskStrategy.ts index 6a000b1..b8ed92b 100644 --- a/src/cleaner/strategies/taskStrategy.ts +++ b/src/cleaner/strategies/taskStrategy.ts @@ -13,7 +13,7 @@ export interface ITaskStrategy> { * @returns Typed and validated parameters * @throws ValidationError if parameters fail schema validation */ - validate: (params: unknown, taskType: string) => T; + validate: (params: unknown) => T; /** * Executes the task with validated parameters. diff --git a/src/common/constants.ts b/src/common/constants.ts index 98bb821..3ef599e 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -14,9 +14,9 @@ export const SERVICES = { METRICS: Symbol('METRICS'), QUEUE_CLIENT: Symbol('QueueClient'), TASK_POLLER: Symbol('TaskPoller'), + ERROR_HANDLER: Symbol('ErrorHandler'), STRATEGY_FACTORY: Symbol('StrategyFactory'), TASK_VALIDATOR: Symbol('TaskValidator'), - ERROR_HANDLER: Symbol('ErrorHandler'), // ============================================================================= // TODO: When we move to the new job-manager, we will use @map-colonies/jobnik-sdk // The tokens below are kept for future migration. @@ -24,4 +24,3 @@ export const SERVICES = { JOBNIK_SDK: Symbol('JobnikSDK'), WORKER: Symbol('Worker'), } satisfies Record; -/* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/containerConfig.ts b/src/containerConfig.ts index 243dac7..a61c1ed 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -10,6 +10,7 @@ import { SERVICES, SERVICE_NAME } from '@common/constants'; import { getTracing } from '@common/tracing'; import { ConfigType, getConfig } from './common/config'; import { workerBuilder } from './worker'; +import { StrategyFactory } from './cleaner/strategies'; export interface RegisterOptions { override?: InjectionObject[]; @@ -54,7 +55,12 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise useFactory: instancePerContainerCachingFactory(workerBuilder), }, }, - + { + token: SERVICES.STRATEGY_FACTORY, + provider: { + useClass: StrategyFactory, + }, + }, { token: 'onSignal', provider: { diff --git a/tests/strategyFactory.spec.ts b/tests/strategyFactory.spec.ts index bd208d2..dcf0986 100644 --- a/tests/strategyFactory.spec.ts +++ b/tests/strategyFactory.spec.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { container, DependencyContainer } from 'tsyringe'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { container } from 'tsyringe'; import { faker } from '@faker-js/faker'; import type { Logger } from '@map-colonies/js-logger'; import { SERVICES } from '../src/common/constants'; @@ -7,7 +7,6 @@ import { StrategyFactory, TilesDeletionStrategy, type ITaskStrategy, type TaskCo import { StrategyNotFoundError } from '../src/cleaner/errors'; import { createMockLogger } from './helpers/mocks'; -// Mock strategy for testing class MockStrategy implements ITaskStrategy { public validate(params: unknown): Record { return params as Record; @@ -21,25 +20,25 @@ class MockStrategy implements ITaskStrategy { describe('StrategyFactory', () => { let strategyFactory: StrategyFactory; let mockLogger: Logger; - let childContainer: DependencyContainer; beforeEach(() => { mockLogger = createMockLogger(); - childContainer = container.createChildContainer(); - childContainer.register(SERVICES.LOGGER, { useValue: mockLogger }); - // Create factory with child container - strategyFactory = new StrategyFactory(mockLogger, childContainer); + // Register in global container (which StrategyFactory uses) + container.register(SERVICES.LOGGER, { useValue: mockLogger }); + + strategyFactory = new StrategyFactory(mockLogger); }); afterEach(() => { - childContainer.clearInstances(); + // Clear registrations from global container + container.clearInstances(); }); describe('resolveWithContext', () => { it('should resolve registered strategy with enriched logger context', () => { const taskType = 'tiles-deletion'; - childContainer.register(taskType, { useClass: TilesDeletionStrategy }); + container.register(taskType, { useClass: TilesDeletionStrategy }); const taskContext: TaskContext = { jobId: faker.string.uuid(), @@ -48,9 +47,9 @@ describe('StrategyFactory', () => { taskType, }; - const handle = strategyFactory.resolveWithContext(taskContext); + const strategy = strategyFactory.resolveWithContext(taskContext); - expect(handle.strategy).toBeInstanceOf(TilesDeletionStrategy); + expect(strategy).toBeInstanceOf(TilesDeletionStrategy); expect(mockLogger.debug).toHaveBeenCalledWith( expect.objectContaining({ msg: 'Resolving strategy with task context', @@ -64,7 +63,7 @@ describe('StrategyFactory', () => { it('should create child logger with task context', () => { const taskType = 'tiles-deletion'; - childContainer.register(taskType, { useClass: TilesDeletionStrategy }); + container.register(taskType, { useClass: TilesDeletionStrategy }); const taskContext: TaskContext = { jobId: faker.string.uuid(), @@ -97,39 +96,38 @@ describe('StrategyFactory', () => { it('should create separate instances for different tasks (child container isolation)', () => { const taskType = 'tiles-deletion'; - childContainer.register(taskType, { useClass: TilesDeletionStrategy }); + container.register(taskType, { useClass: TilesDeletionStrategy }); const context1: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType }; const context2: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType }; - const handle1 = strategyFactory.resolveWithContext(context1); - const handle2 = strategyFactory.resolveWithContext(context2); + const strategy1 = strategyFactory.resolveWithContext(context1); + const strategy2 = strategyFactory.resolveWithContext(context2); - // Different instances because of child containers - expect(handle1.strategy).not.toBe(handle2.strategy); + expect(strategy1).not.toBe(strategy2); }); it('should resolve different strategies for different task types', () => { const taskType1 = 'tiles-deletion'; const taskType2 = 'files-deletion'; - childContainer.register(taskType1, { useClass: TilesDeletionStrategy }); - childContainer.register(taskType2, { useClass: MockStrategy }); + container.register(taskType1, { useClass: TilesDeletionStrategy }); + container.register(taskType2, { useClass: MockStrategy }); const context1: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType: taskType1 }; const context2: TaskContext = { jobId: faker.string.uuid(), taskId: faker.string.uuid(), jobType: 'Ingestion_Update', taskType: taskType2 }; - const handle1 = strategyFactory.resolveWithContext(context1); - const handle2 = strategyFactory.resolveWithContext(context2); + const strategy1 = strategyFactory.resolveWithContext(context1); + const strategy2 = strategyFactory.resolveWithContext(context2); - expect(handle1.strategy).toBeInstanceOf(TilesDeletionStrategy); - expect(handle2.strategy).toBeInstanceOf(MockStrategy); - expect(handle1.strategy).not.toBe(handle2.strategy); + expect(strategy1).toBeInstanceOf(TilesDeletionStrategy); + expect(strategy2).toBeInstanceOf(MockStrategy); + expect(strategy1).not.toBe(strategy2); }); it('should handle special characters in task type', () => { const taskType = 'task-with-special_chars.v2'; - childContainer.register(taskType, { useClass: MockStrategy }); + container.register(taskType, { useClass: MockStrategy }); const taskContext: TaskContext = { jobId: faker.string.uuid(), @@ -138,29 +136,9 @@ describe('StrategyFactory', () => { taskType, }; - const handle = strategyFactory.resolveWithContext(taskContext); - - expect(handle.strategy).toBeInstanceOf(MockStrategy); - }); - - it('should have asyncDispose method for cleanup', async () => { - const taskType = 'tiles-deletion'; - childContainer.register(taskType, { useClass: TilesDeletionStrategy }); - - const taskContext: TaskContext = { - jobId: faker.string.uuid(), - taskId: faker.string.uuid(), - jobType: 'Ingestion_Update', - taskType, - }; - - const handle = strategyFactory.resolveWithContext(taskContext); - - expect(handle[Symbol.asyncDispose]).toBeDefined(); - expect(typeof handle[Symbol.asyncDispose]).toBe('function'); + const strategy = strategyFactory.resolveWithContext(taskContext); - // Cleanup should not throw - await expect(handle[Symbol.asyncDispose]()).resolves.not.toThrow(); + expect(strategy).toBeInstanceOf(MockStrategy); }); }); }); From 9d687f803acdb62c720ed6838e7b50a6f1a200a8 Mon Sep 17 00:00:00 2001 From: almog8k Date: Wed, 18 Feb 2026 14:02:27 +0200 Subject: [PATCH 23/25] refactor(values): clean up jobDefinitions structure in values.yaml refactor(validation): update logging messages in validateSchema function refactor(tests): simplify ErrorHandler tests by removing mock logger assertions --- helm/values.yaml | 11 +---------- src/cleaner/utils/validationHelper.ts | 4 ++-- tests/errorHandler.spec.ts | 25 ++----------------------- 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/helm/values.yaml b/helm/values.yaml index 347ae24..f2a55e9 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -27,16 +27,7 @@ configManagement: version: 'latest' serverUrl: 'http://localhost:8080/api' -jobDefinitions: - jobs: - update: - type: "Ingestion_Update" - swapUpdate: - type: "Ingestion_Swap_Update" - tasks: - tilesDeletion: - type: "tiles-deletion" - maxAttempts: 3 +jobDefinitions: {} livenessProbe: enabled: true diff --git a/src/cleaner/utils/validationHelper.ts b/src/cleaner/utils/validationHelper.ts index f30119e..4217105 100644 --- a/src/cleaner/utils/validationHelper.ts +++ b/src/cleaner/utils/validationHelper.ts @@ -20,10 +20,10 @@ export function validateSchema(schema: ZodSchema, params: unknown, logger: const result = schema.safeParse(params); if (!result.success) { - logger.error({ errors: result.error.errors }, 'Task parameter validation failed'); + logger.error({ msg: 'Task parameter validation failed', errors: result.error.errors }); throw new ValidationError(`Invalid parameters for task`, result.error.errors); } - logger.debug('Task parameters validated successfully'); + logger.debug({ msg: 'Task parameters validated successfully' }); return result.data; } diff --git a/tests/errorHandler.spec.ts b/tests/errorHandler.spec.ts index b3a776f..eb00770 100644 --- a/tests/errorHandler.spec.ts +++ b/tests/errorHandler.spec.ts @@ -1,17 +1,14 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { faker } from '@faker-js/faker'; -import type { Logger } from '@map-colonies/js-logger'; +import jsLogger from '@map-colonies/js-logger'; import { ErrorHandler, RecoverableError, UnrecoverableError, ValidationError } from '../src/cleaner/errors'; import type { ErrorContext } from '../src/cleaner/types'; -import { createMockLogger } from './helpers/mocks'; describe('ErrorHandler', () => { let errorHandler: ErrorHandler; - let mockLogger: Logger; beforeEach(() => { - mockLogger = createMockLogger(); - errorHandler = new ErrorHandler(mockLogger); + errorHandler = new ErrorHandler(jsLogger({ enabled: false })); }); describe('handleError', () => { @@ -30,9 +27,6 @@ describe('ErrorHandler', () => { const decision = errorHandler.handleError(context); expect(decision.shouldRetry).toBe(false); - expect(decision.reason).toContain('Unrecoverable error'); - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockLogger.warn).toHaveBeenCalled(); }); it('should not retry even on first attempt', () => { @@ -53,10 +47,6 @@ describe('ErrorHandler', () => { const decision = errorHandler.handleError(context); expect(decision.shouldRetry).toBe(true); - expect(decision.reason).toContain('RecoverableError'); - expect(decision.reason).toContain('Network timeout'); - expect(decision.reason).toContain('1/3'); - expect(mockLogger.info).toHaveBeenCalled(); }); it('should not retry when max attempts reached', () => { @@ -66,10 +56,6 @@ describe('ErrorHandler', () => { const decision = errorHandler.handleError(context); expect(decision.shouldRetry).toBe(false); - expect(decision.reason).toContain('Max attempts'); - expect(decision.reason).toContain('3'); - expect(decision.reason).toContain('Network timeout'); - expect(mockLogger.warn).toHaveBeenCalled(); }); it('should retry on second attempt if max is 3', () => { @@ -91,10 +77,6 @@ describe('ErrorHandler', () => { const decision = errorHandler.handleError(context); expect(decision.shouldRetry).toBe(true); - expect(decision.reason).toContain('Unknown error'); - expect(decision.reason).toContain('Unknown error type'); - expect(decision.reason).toContain('1/3'); - expect(mockLogger.warn).toHaveBeenCalled(); }); it('should not retry unknown errors when max attempts reached', () => { @@ -104,9 +86,6 @@ describe('ErrorHandler', () => { const decision = errorHandler.handleError(context); expect(decision.shouldRetry).toBe(false); - expect(decision.reason).toContain('Max attempts'); - expect(decision.reason).toContain('Unknown error'); - expect(mockLogger.error).toHaveBeenCalled(); }); }); }); From 45a68f584cdb91cee2f8b830bd97cf79b24e5ee7 Mon Sep 17 00:00:00 2001 From: almog8k <60139576+almog8k@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:26:35 +0000 Subject: [PATCH 24/25] refactor(tests): remove redundant logging assertions in tests --- tests/strategyFactory.spec.ts | 1 - tests/tilesDeletionStrategy.spec.ts | 8 -------- 2 files changed, 9 deletions(-) diff --git a/tests/strategyFactory.spec.ts b/tests/strategyFactory.spec.ts index dcf0986..0f613e8 100644 --- a/tests/strategyFactory.spec.ts +++ b/tests/strategyFactory.spec.ts @@ -52,7 +52,6 @@ describe('StrategyFactory', () => { expect(strategy).toBeInstanceOf(TilesDeletionStrategy); expect(mockLogger.debug).toHaveBeenCalledWith( expect.objectContaining({ - msg: 'Resolving strategy with task context', jobId: taskContext.jobId, taskId: taskContext.taskId, jobType: taskContext.jobType, diff --git a/tests/tilesDeletionStrategy.spec.ts b/tests/tilesDeletionStrategy.spec.ts index 63a66fa..6a18fae 100644 --- a/tests/tilesDeletionStrategy.spec.ts +++ b/tests/tilesDeletionStrategy.spec.ts @@ -50,14 +50,6 @@ describe('TilesDeletionStrategy', () => { const validParams = {}; // Matches TilesDeletionParams type await expect(strategy.execute(validParams)).resolves.toBeUndefined(); - - expect(mockLogger.info).toHaveBeenCalledWith({ - msg: 'Executing tiles deletion task', - params: validParams, - }); - expect(mockLogger.info).toHaveBeenCalledWith({ - msg: 'Tiles deletion task completed', - }); }); }); }); From 1ed8e3f9f7c9af0c41816102397357fb69223980 Mon Sep 17 00:00:00 2001 From: almog8k <60139576+almog8k@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:31:42 +0000 Subject: [PATCH 25/25] refactor(helm): remove local.yaml --- helm/local.yaml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 helm/local.yaml diff --git a/helm/local.yaml b/helm/local.yaml deleted file mode 100644 index cb93657..0000000 --- a/helm/local.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# Local development overrides -# This file is gitignored and serves as an example for local Helm values - -jobDefinitions: - jobs: - update: - type: "" - swapUpdate: - type: "" - tasks: - tiles-deletion: - type: "tiles-deletion" - maxAttempts: 3 - -env: - queue: - jobManagerBaseUrl: "http://localhost:8080" - heartbeatBaseUrl: "http://localhost:8081" - heartbeatIntervalMs: 1000 - dequeueIntervalMs: 3000 - httpRetry: - attempts: 3 - delay: "exponential" - shouldResetTimeout: true