@warlock.js/scheduler 5.24.0 → 5.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.mjs","names":[],"sources":["../../../../../../scheduler/src/scheduler.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { Job } from \"./job\";\nimport type { JobAroundHook, JobCallback } from \"./job\";\nimport type { JobResult, SchedulerEvents } from \"./types\";\n\n/**\n * Type-safe event emitter interface for Scheduler events\n */\ninterface TypedEventEmitter<TEvents extends Record<string, unknown[]>> {\n on<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;\n once<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;\n off<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;\n emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): boolean;\n}\n\n/**\n * Scheduler class manages and executes scheduled jobs.\n *\n * Features:\n * - Event-based observability\n * - Parallel or sequential job execution\n * - Drift compensation for accurate timing\n * - Graceful shutdown with job draining\n *\n * @example\n * ```typescript\n * const scheduler = new Scheduler();\n *\n * scheduler.on('job:error', (jobName, error) => {\n * logger.error(`Job ${jobName} failed:`, error);\n * });\n *\n * scheduler\n * .addJob(cleanupJob)\n * .addJob(reportJob)\n * .runInParallel(true)\n * .start();\n *\n * // Graceful shutdown\n * process.on('SIGTERM', () => scheduler.shutdown());\n * ```\n */\nexport class Scheduler\n extends (EventEmitter as new () => TypedEventEmitter<SchedulerEvents>)\n implements TypedEventEmitter<SchedulerEvents>\n{\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Properties\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * List of registered jobs\n */\n private _jobs: Job[] = [];\n\n /**\n * Hooks wrapping every callback execution, first registered = outermost\n */\n private _aroundHooks: readonly JobAroundHook[] = [];\n\n /**\n * Reference to the current timeout for stopping\n */\n private _timeoutId: NodeJS.Timeout | null = null;\n\n /**\n * Tick interval in milliseconds (how often to check for due jobs)\n */\n private _tickInterval = 1000;\n\n /**\n * Whether to run due jobs in parallel\n */\n private _runInParallel = false;\n\n /**\n * Maximum concurrent jobs when running in parallel\n */\n private _maxConcurrency = 10;\n\n /**\n * Flag indicating scheduler is shutting down\n */\n private _isShuttingDown = false;\n\n /**\n * Whether `start()` has ever been called on this scheduler.\n *\n * Distinct from `isRunning`: it stays `true` after `stop()`, so the\n * registered-but-never-started warning never fires once the scheduler has\n * been started at least once.\n */\n private _hasStarted = false;\n\n /**\n * One-shot deferred timer that warns when jobs are registered but\n * `start()` is never called. Armed on the first `addJob`/`addJobs` and\n * cleared by `start()` (or once it has fired).\n */\n private _startWarningTimer: NodeJS.Timeout | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Public Getters\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Returns true if the scheduler is currently running\n */\n public get isRunning(): boolean {\n return this._timeoutId !== null;\n }\n\n /**\n * Returns the number of registered jobs\n */\n public get jobCount(): number {\n return this._jobs.length;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Configuration Methods (Fluent API)\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Add a job to the scheduler\n *\n * @param job - Job instance to schedule\n * @returns this for chaining\n */\n public addJob(job: Job): this {\n job.bindAroundHooks(() => this._aroundHooks);\n this._jobs.push(job);\n\n if (this.isRunning) {\n job.prepare();\n }\n\n this._armStartWarning();\n\n return this;\n }\n\n /**\n * Wrap every job callback execution (each retry attempt included) in a hook.\n *\n * The first registered hook is the outermost. A hook that never calls `run`\n * skips the execution (`job:skip` is emitted); a hook that throws is treated\n * like a callback error.\n *\n * @returns Unsubscribe function\n *\n * @example\n * ```typescript\n * scheduler.around((job, run) => storage.run({ job: job.name }, run));\n * ```\n */\n public around(hook: JobAroundHook): () => void {\n this._aroundHooks = [...this._aroundHooks, hook];\n\n return () => {\n this._aroundHooks = this._aroundHooks.filter(item => item !== hook);\n };\n }\n\n /**\n * Alias to create a new job directly and store it\n */\n public newJob(name: string, jobCallback: JobCallback) {\n const job = new Job(name, jobCallback);\n this.addJob(job);\n return job;\n }\n\n /**\n * Add multiple jobs to the scheduler.\n *\n * If the scheduler is already running, every newly-added job is prepared\n * (its initial `nextRun` is computed) so it begins firing on the next tick.\n *\n * @param jobs - Array of Job instances\n * @returns this for chaining\n */\n public addJobs(jobs: Job[]): this {\n for (const job of jobs) {\n job.bindAroundHooks(() => this._aroundHooks);\n }\n\n this._jobs.push(...jobs);\n\n if (this.isRunning) {\n for (const job of jobs) {\n job.prepare();\n }\n }\n\n this._armStartWarning();\n\n return this;\n }\n\n /**\n * Remove a job from the scheduler by name\n *\n * @param jobName - Name of the job to remove\n * @returns true if job was found and removed\n */\n public removeJob(jobName: string): boolean {\n const index = this._jobs.findIndex(j => j.name === jobName);\n\n if (index !== -1) {\n this._jobs.splice(index, 1);\n return true;\n }\n\n return false;\n }\n\n /**\n * Get a job by name\n *\n * @param jobName - Name of the job to find\n * @returns Job instance or undefined\n */\n public getJob(jobName: string): Job | undefined {\n return this._jobs.find(j => j.name === jobName);\n }\n\n /**\n * Get all registered jobs\n *\n * @returns Array of registered jobs (readonly)\n */\n public list(): readonly Job[] {\n return this._jobs;\n }\n\n /**\n * Set the tick interval (how often to check for due jobs)\n *\n * @param ms - Interval in milliseconds (minimum 100ms)\n * @returns this for chaining\n */\n public runEvery(ms: number): this {\n if (ms < 100) {\n throw new Error(\"Tick interval must be at least 100ms\");\n }\n\n this._tickInterval = ms;\n return this;\n }\n\n /**\n * Configure whether jobs should run in parallel\n *\n * @param parallel - Enable parallel execution\n * @param maxConcurrency - Maximum concurrent jobs (default: 10)\n * @returns this for chaining\n */\n public runInParallel(parallel: boolean, maxConcurrency = 10): this {\n this._runInParallel = parallel;\n this._maxConcurrency = maxConcurrency;\n return this;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Lifecycle Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Start the scheduler\n *\n * @throws Error if scheduler is already running\n */\n public start(): void {\n if (this.isRunning) {\n throw new Error(\"Scheduler is already running.\");\n }\n\n if (this._jobs.length === 0) {\n throw new Error(\"Cannot start scheduler with no jobs.\");\n }\n\n this._hasStarted = true;\n this._clearStartWarning();\n\n for (const job of this._jobs) {\n job.prepare();\n }\n\n this._isShuttingDown = false;\n this._scheduleTick(this._tickInterval);\n\n this.emit(\"scheduler:started\");\n }\n\n /**\n * Stop the scheduler immediately.\n *\n * No-op if the scheduler isn't running. Does not wait for in-flight jobs —\n * use `shutdown()` for graceful termination.\n */\n public stop(): void {\n if (!this._timeoutId) return;\n\n clearTimeout(this._timeoutId);\n this._timeoutId = null;\n\n this.emit(\"scheduler:stopped\");\n }\n\n /**\n * Gracefully shutdown the scheduler\n *\n * Stops scheduling new jobs and waits for currently running jobs to complete.\n *\n * @param timeout - Maximum time to wait for jobs (default: 30000ms)\n * @returns Promise that resolves when shutdown is complete\n */\n public async shutdown(timeout = 30000): Promise<void> {\n this._isShuttingDown = true;\n this.stop();\n\n const runningJobs = this._jobs.filter(j => j.isRunning);\n\n if (runningJobs.length > 0) {\n await Promise.race([\n Promise.all(runningJobs.map(j => j.waitForCompletion())),\n new Promise<void>(resolve => setTimeout(resolve, timeout)),\n ]);\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Arm the one-shot \"registered but never started\" development warning.\n *\n * Called whenever a job is registered. Does nothing in production\n * (`NODE_ENV === \"production\"`), once `start()` has been called, or when a\n * check is already armed. The deferred check is unref'd so it can never keep\n * the event loop (and the process) alive on its own.\n */\n private _armStartWarning(): void {\n if (process.env.NODE_ENV === \"production\") {\n return;\n }\n\n if (this._hasStarted || this._startWarningTimer !== null) {\n return;\n }\n\n const timer = setTimeout(() => {\n this._startWarningTimer = null;\n\n if (this._hasStarted || this._jobs.length === 0) {\n return;\n }\n\n // eslint-disable-next-line no-console\n console.warn(\n `[@warlock.js/scheduler] ${this._jobs.length} job(s) registered but ` +\n `scheduler.start() was never called — no jobs will run. ` +\n `Call scheduler.start() to begin executing them.`,\n );\n }, 0);\n\n // Never let the deferred check hold the process open.\n timer.unref?.();\n\n this._startWarningTimer = timer;\n }\n\n /**\n * Clear the armed \"registered but never started\" warning, if any.\n */\n private _clearStartWarning(): void {\n if (this._startWarningTimer !== null) {\n clearTimeout(this._startWarningTimer);\n this._startWarningTimer = null;\n }\n }\n\n /**\n * Schedule the next tick.\n *\n * Drift-compensated: after each tick, the next delay is `tickInterval -\n * elapsed-tick-time` (clamped to 0). A tick that takes 600 ms on a 1 000 ms\n * interval will be followed by a 400 ms delay so the *period* between tick\n * starts averages `tickInterval` instead of `tickInterval + work-time`.\n */\n private _scheduleTick(delay: number): void {\n if (this._isShuttingDown) return;\n\n this._timeoutId = setTimeout(async () => {\n const tickStart = Date.now();\n\n await this._tick();\n\n const elapsed = Date.now() - tickStart;\n const nextDelay = Math.max(this._tickInterval - elapsed, 0);\n\n this._scheduleTick(nextDelay);\n }, delay);\n }\n\n /**\n * Execute a scheduler tick - check and run due jobs\n */\n private async _tick(): Promise<void> {\n this.emit(\"scheduler:tick\", new Date());\n\n const dueJobs = this._jobs.filter(job => {\n // Time-based readiness only; running state is checked below so a\n // `job:skip` event fires whenever overlap blocks a due run.\n if (!job.isDue()) return false;\n\n if (job.isRunning) {\n this.emit(\"job:skip\", job.name, \"Job is already running\");\n return false;\n }\n\n return true;\n });\n\n if (dueJobs.length === 0) return;\n\n if (this._runInParallel) {\n await this._runJobsInParallel(dueJobs);\n } else {\n await this._runJobsSequentially(dueJobs);\n }\n }\n\n /**\n * Run jobs sequentially\n */\n private async _runJobsSequentially(jobs: Job[]): Promise<void> {\n for (const job of jobs) {\n if (this._isShuttingDown) break;\n await this._runJob(job);\n }\n }\n\n /**\n * Run jobs in parallel with concurrency limit\n */\n private async _runJobsInParallel(jobs: Job[]): Promise<void> {\n const batches: Job[][] = [];\n\n for (let i = 0; i < jobs.length; i += this._maxConcurrency) {\n batches.push(jobs.slice(i, i + this._maxConcurrency));\n }\n\n for (const batch of batches) {\n if (this._isShuttingDown) break;\n await Promise.allSettled(batch.map(job => this._runJob(job)));\n }\n }\n\n /**\n * Run a single job and emit events\n */\n private async _runJob(job: Job): Promise<JobResult> {\n this.emit(\"job:start\", job.name);\n\n const result = await job.run();\n\n if (result.skipped) {\n this.emit(\"job:skip\", job.name, result.skipReason ?? \"Another server holds the lock\");\n } else if (result.success) {\n this.emit(\"job:complete\", job.name, result);\n } else {\n this.emit(\"job:error\", job.name, result.error);\n }\n\n return result;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Export\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default scheduler instance for simple use cases\n *\n * @example\n * ```typescript\n * import { scheduler, job } from \"@warlock.js/scheduler\";\n *\n * scheduler.addJob(job(\"cleanup\", cleanupFn).daily());\n * scheduler.start();\n * ```\n */\nexport const scheduler = new Scheduler();\n"],"mappings":"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"}
1
+ {"version":3,"file":"scheduler.mjs","names":[],"sources":["../../../../../../scheduler/src/scheduler.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { Job } from \"./job\";\nimport type { JobAroundHook, JobCallback } from \"./job\";\nimport { hasWorkerRole, rolesLabel } from \"./process-roles\";\nimport type { JobResult, SchedulerEvents } from \"./types\";\n\n/**\n * Type-safe event emitter interface for Scheduler events\n */\ninterface TypedEventEmitter<TEvents extends Record<string, unknown[]>> {\n on<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;\n once<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;\n off<K extends keyof TEvents>(event: K, listener: (...args: TEvents[K]) => void): this;\n emit<K extends keyof TEvents>(event: K, ...args: TEvents[K]): boolean;\n}\n\n/**\n * Scheduler class manages and executes scheduled jobs.\n *\n * Features:\n * - Event-based observability\n * - Parallel or sequential job execution\n * - Drift compensation for accurate timing\n * - Graceful shutdown with job draining\n *\n * @example\n * ```typescript\n * const scheduler = new Scheduler();\n *\n * scheduler.on('job:error', (jobName, error) => {\n * logger.error(`Job ${jobName} failed:`, error);\n * });\n *\n * scheduler\n * .addJob(cleanupJob)\n * .addJob(reportJob)\n * .runInParallel(true)\n * .start();\n *\n * // Graceful shutdown\n * process.on('SIGTERM', () => scheduler.shutdown());\n * ```\n */\nexport class Scheduler\n extends (EventEmitter as new () => TypedEventEmitter<SchedulerEvents>)\n implements TypedEventEmitter<SchedulerEvents>\n{\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Properties\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * List of registered jobs\n */\n private _jobs: Job[] = [];\n\n /**\n * Hooks wrapping every callback execution, first registered = outermost\n */\n private _aroundHooks: readonly JobAroundHook[] = [];\n\n /**\n * Reference to the current timeout for stopping\n */\n private _timeoutId: NodeJS.Timeout | null = null;\n\n /**\n * Tick interval in milliseconds (how often to check for due jobs)\n */\n private _tickInterval = 1000;\n\n /**\n * Whether to run due jobs in parallel\n */\n private _runInParallel = false;\n\n /**\n * Maximum concurrent jobs when running in parallel\n */\n private _maxConcurrency = 10;\n\n /**\n * Flag indicating scheduler is shutting down\n */\n private _isShuttingDown = false;\n\n /**\n * Whether `start()` has ever been called on this scheduler.\n *\n * Distinct from `isRunning`: it stays `true` after `stop()`, so the\n * registered-but-never-started warning never fires once the scheduler has\n * been started at least once.\n */\n private _hasStarted = false;\n\n /**\n * One-shot deferred timer that warns when jobs are registered but\n * `start()` is never called. Armed on the first `addJob`/`addJobs` and\n * cleared by `start()` (or once it has fired).\n */\n private _startWarningTimer: NodeJS.Timeout | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Public Getters\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Returns true if the scheduler is currently running\n */\n public get isRunning(): boolean {\n return this._timeoutId !== null;\n }\n\n /**\n * Returns the number of registered jobs\n */\n public get jobCount(): number {\n return this._jobs.length;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Configuration Methods (Fluent API)\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Add a job to the scheduler\n *\n * @param job - Job instance to schedule\n * @returns this for chaining\n */\n public addJob(job: Job): this {\n job.bindAroundHooks(() => this._aroundHooks);\n this._jobs.push(job);\n\n if (this.isRunning) {\n job.prepare();\n }\n\n this._armStartWarning();\n\n return this;\n }\n\n /**\n * Wrap every job callback execution (each retry attempt included) in a hook.\n *\n * The first registered hook is the outermost. A hook that never calls `run`\n * skips the execution (`job:skip` is emitted); a hook that throws is treated\n * like a callback error.\n *\n * @returns Unsubscribe function\n *\n * @example\n * ```typescript\n * scheduler.around((job, run) => storage.run({ job: job.name }, run));\n * ```\n */\n public around(hook: JobAroundHook): () => void {\n this._aroundHooks = [...this._aroundHooks, hook];\n\n return () => {\n this._aroundHooks = this._aroundHooks.filter(item => item !== hook);\n };\n }\n\n /**\n * Alias to create a new job directly and store it\n */\n public newJob(name: string, jobCallback: JobCallback) {\n const job = new Job(name, jobCallback);\n this.addJob(job);\n return job;\n }\n\n /**\n * Add multiple jobs to the scheduler.\n *\n * If the scheduler is already running, every newly-added job is prepared\n * (its initial `nextRun` is computed) so it begins firing on the next tick.\n *\n * @param jobs - Array of Job instances\n * @returns this for chaining\n */\n public addJobs(jobs: Job[]): this {\n for (const job of jobs) {\n job.bindAroundHooks(() => this._aroundHooks);\n }\n\n this._jobs.push(...jobs);\n\n if (this.isRunning) {\n for (const job of jobs) {\n job.prepare();\n }\n }\n\n this._armStartWarning();\n\n return this;\n }\n\n /**\n * Remove a job from the scheduler by name\n *\n * @param jobName - Name of the job to remove\n * @returns true if job was found and removed\n */\n public removeJob(jobName: string): boolean {\n const index = this._jobs.findIndex(j => j.name === jobName);\n\n if (index !== -1) {\n this._jobs.splice(index, 1);\n return true;\n }\n\n return false;\n }\n\n /**\n * Get a job by name\n *\n * @param jobName - Name of the job to find\n * @returns Job instance or undefined\n */\n public getJob(jobName: string): Job | undefined {\n return this._jobs.find(j => j.name === jobName);\n }\n\n /**\n * Get all registered jobs\n *\n * @returns Array of registered jobs (readonly)\n */\n public list(): readonly Job[] {\n return this._jobs;\n }\n\n /**\n * Set the tick interval (how often to check for due jobs)\n *\n * @param ms - Interval in milliseconds (minimum 100ms)\n * @returns this for chaining\n */\n public runEvery(ms: number): this {\n if (ms < 100) {\n throw new Error(\"Tick interval must be at least 100ms\");\n }\n\n this._tickInterval = ms;\n return this;\n }\n\n /**\n * Configure whether jobs should run in parallel\n *\n * @param parallel - Enable parallel execution\n * @param maxConcurrency - Maximum concurrent jobs (default: 10)\n * @returns this for chaining\n */\n public runInParallel(parallel: boolean, maxConcurrency = 10): this {\n this._runInParallel = parallel;\n this._maxConcurrency = maxConcurrency;\n return this;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Lifecycle Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Start the scheduler\n *\n * A no-op, logging one info line, on a process that does not serve the\n * `worker` role (`WARLOCK_ROLES`) — so an app that calls this in `main.ts`\n * doesn't run its jobs on every `web`/`api` node. With no `--role` passed,\n * every role is on and this behaves exactly as before.\n *\n * @throws Error if scheduler is already running\n */\n public start(): void {\n if (!hasWorkerRole()) {\n // eslint-disable-next-line no-console\n console.info(`scheduler: not started (role: ${rolesLabel()})`);\n return;\n }\n\n if (this.isRunning) {\n throw new Error(\"Scheduler is already running.\");\n }\n\n if (this._jobs.length === 0) {\n throw new Error(\"Cannot start scheduler with no jobs.\");\n }\n\n this._hasStarted = true;\n this._clearStartWarning();\n\n for (const job of this._jobs) {\n job.prepare();\n }\n\n this._isShuttingDown = false;\n this._scheduleTick(this._tickInterval);\n\n this.emit(\"scheduler:started\");\n }\n\n /**\n * Stop the scheduler immediately.\n *\n * No-op if the scheduler isn't running. Does not wait for in-flight jobs —\n * use `shutdown()` for graceful termination.\n */\n public stop(): void {\n if (!this._timeoutId) return;\n\n clearTimeout(this._timeoutId);\n this._timeoutId = null;\n\n this.emit(\"scheduler:stopped\");\n }\n\n /**\n * Gracefully shutdown the scheduler\n *\n * Stops scheduling new jobs and waits for currently running jobs to complete.\n *\n * @param timeout - Maximum time to wait for jobs (default: 30000ms)\n * @returns Promise that resolves when shutdown is complete\n */\n public async shutdown(timeout = 30000): Promise<void> {\n this._isShuttingDown = true;\n this.stop();\n\n const runningJobs = this._jobs.filter(j => j.isRunning);\n\n if (runningJobs.length > 0) {\n await Promise.race([\n Promise.all(runningJobs.map(j => j.waitForCompletion())),\n new Promise<void>(resolve => setTimeout(resolve, timeout)),\n ]);\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Arm the one-shot \"registered but never started\" development warning.\n *\n * Called whenever a job is registered. Does nothing in production\n * (`NODE_ENV === \"production\"`), once `start()` has been called, when a\n * check is already armed, or when this process doesn't serve the `worker`\n * role — `start()` is never expected to be called there, so the warning\n * would only be noise. The deferred check is unref'd so it can never keep\n * the event loop (and the process) alive on its own.\n */\n private _armStartWarning(): void {\n if (process.env.NODE_ENV === \"production\" || !hasWorkerRole()) {\n return;\n }\n\n if (this._hasStarted || this._startWarningTimer !== null) {\n return;\n }\n\n const timer = setTimeout(() => {\n this._startWarningTimer = null;\n\n if (this._hasStarted || this._jobs.length === 0) {\n return;\n }\n\n // eslint-disable-next-line no-console\n console.warn(\n `[@warlock.js/scheduler] ${this._jobs.length} job(s) registered but ` +\n `scheduler.start() was never called — no jobs will run. ` +\n `Call scheduler.start() to begin executing them.`,\n );\n }, 0);\n\n // Never let the deferred check hold the process open.\n timer.unref?.();\n\n this._startWarningTimer = timer;\n }\n\n /**\n * Clear the armed \"registered but never started\" warning, if any.\n */\n private _clearStartWarning(): void {\n if (this._startWarningTimer !== null) {\n clearTimeout(this._startWarningTimer);\n this._startWarningTimer = null;\n }\n }\n\n /**\n * Schedule the next tick.\n *\n * Drift-compensated: after each tick, the next delay is `tickInterval -\n * elapsed-tick-time` (clamped to 0). A tick that takes 600 ms on a 1 000 ms\n * interval will be followed by a 400 ms delay so the *period* between tick\n * starts averages `tickInterval` instead of `tickInterval + work-time`.\n */\n private _scheduleTick(delay: number): void {\n if (this._isShuttingDown) return;\n\n this._timeoutId = setTimeout(async () => {\n const tickStart = Date.now();\n\n await this._tick();\n\n const elapsed = Date.now() - tickStart;\n const nextDelay = Math.max(this._tickInterval - elapsed, 0);\n\n this._scheduleTick(nextDelay);\n }, delay);\n }\n\n /**\n * Execute a scheduler tick - check and run due jobs\n */\n private async _tick(): Promise<void> {\n this.emit(\"scheduler:tick\", new Date());\n\n const dueJobs = this._jobs.filter(job => {\n // Time-based readiness only; running state is checked below so a\n // `job:skip` event fires whenever overlap blocks a due run.\n if (!job.isDue()) return false;\n\n if (job.isRunning) {\n this.emit(\"job:skip\", job.name, \"Job is already running\");\n return false;\n }\n\n return true;\n });\n\n if (dueJobs.length === 0) return;\n\n if (this._runInParallel) {\n await this._runJobsInParallel(dueJobs);\n } else {\n await this._runJobsSequentially(dueJobs);\n }\n }\n\n /**\n * Run jobs sequentially\n */\n private async _runJobsSequentially(jobs: Job[]): Promise<void> {\n for (const job of jobs) {\n if (this._isShuttingDown) break;\n await this._runJob(job);\n }\n }\n\n /**\n * Run jobs in parallel with concurrency limit\n */\n private async _runJobsInParallel(jobs: Job[]): Promise<void> {\n const batches: Job[][] = [];\n\n for (let i = 0; i < jobs.length; i += this._maxConcurrency) {\n batches.push(jobs.slice(i, i + this._maxConcurrency));\n }\n\n for (const batch of batches) {\n if (this._isShuttingDown) break;\n await Promise.allSettled(batch.map(job => this._runJob(job)));\n }\n }\n\n /**\n * Run a single job and emit events\n */\n private async _runJob(job: Job): Promise<JobResult> {\n this.emit(\"job:start\", job.name);\n\n const result = await job.run();\n\n if (result.skipped) {\n this.emit(\"job:skip\", job.name, result.skipReason ?? \"Another server holds the lock\");\n } else if (result.success) {\n this.emit(\"job:complete\", job.name, result);\n } else {\n this.emit(\"job:error\", job.name, result.error);\n }\n\n return result;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Default Export\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Default scheduler instance for simple use cases\n *\n * @example\n * ```typescript\n * import { scheduler, job } from \"@warlock.js/scheduler\";\n *\n * scheduler.addJob(job(\"cleanup\", cleanupFn).daily());\n * scheduler.start();\n * ```\n */\nexport const scheduler = new Scheduler();\n"],"mappings":"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"}