@warlock.js/scheduler 5.28.0 → 5.30.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.
- package/CHANGELOG.md +12 -0
- package/cjs/index.cjs +4 -4
- package/cjs/index.cjs.map +1 -1
- package/esm/cron-parser.d.mts +1 -1
- package/esm/cron-parser.d.mts.map +1 -1
- package/esm/cron-parser.mjs +1 -1
- package/esm/cron-parser.mjs.map +1 -1
- package/esm/job.d.mts +1 -1
- package/esm/job.d.mts.map +1 -1
- package/esm/job.mjs +1 -1
- package/esm/job.mjs.map +1 -1
- package/esm/process-roles.mjs +1 -1
- package/esm/process-roles.mjs.map +1 -1
- package/esm/scheduler.d.mts +1 -1
- package/esm/scheduler.d.mts.map +1 -1
- package/esm/scheduler.mjs +1 -1
- package/esm/scheduler.mjs.map +1 -1
- package/esm/types.d.mts +1 -1
- package/esm/types.d.mts.map +1 -1
- package/llms-full.txt +35 -35
- package/llms.txt +5 -5
- package/package.json +4 -3
- package/skills/configure-retry-and-overlap/SKILL.md +3 -3
- package/skills/observe-scheduler/SKILL.md +3 -3
- package/skills/overview/SKILL.md +8 -8
- package/skills/pin-schedule-timezone/SKILL.md +4 -4
- package/skills/schedule-fluently/SKILL.md +5 -5
- package/skills/schedule-with-cron/SKILL.md +4 -4
- package/skills/scheduler-basics/SKILL.md +8 -8
package/esm/job.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"job.mjs","names":[],"sources":["../../../../../../scheduler/src/job.ts"],"sourcesContent":["import dayjs, { type Dayjs } from \"dayjs\";\nimport isSameOrAfter from \"dayjs/plugin/isSameOrAfter.js\";\nimport timezone from \"dayjs/plugin/timezone.js\";\nimport utc from \"dayjs/plugin/utc.js\";\nimport { CronParser } from \"./cron-parser\";\nimport type { Day, JobIntervals, JobResult, RetryConfig, TimeType } from \"./types\";\n\n/**\n * `@warlock.js/cache` is an optional peer, loaded only when `onOneServer()` is\n * used. Memoized so every job shares one module instance.\n */\nlet cacheModule: Promise<typeof import(\"@warlock.js/cache\")> | undefined;\n\nfunction loadCache(): Promise<typeof import(\"@warlock.js/cache\")> {\n cacheModule ??= import(\"@warlock.js/cache\");\n\n return cacheModule;\n}\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\ndayjs.extend(isSameOrAfter);\n\ntype CacheTtl = number | string;\n\n/** Milliseconds per interval unit (month/year approximate; only used to size the lock TTL) */\nconst UNIT_MS: Record<TimeType, number> = {\n second: 1000,\n minute: 60_000,\n hour: 3_600_000,\n day: 86_400_000,\n week: 604_800_000,\n month: 2_592_000_000,\n year: 31_536_000_000,\n};\n\nexport type JobCallback = (job: Job) => Promise<any>;\n\n/**\n * Hook wrapping every callback execution (each retry attempt included).\n * Not calling `run` skips the execution; throwing counts as a callback error.\n */\nconst HOOK_SKIP_REASON = \"An around hook did not run the job\";\n\nexport type JobAroundHook = (job: Job, run: () => Promise<unknown>) => Promise<unknown>;\n/**\n * Days of week mapping (lowercase for consistency with Day type)\n */\nconst DAYS_OF_WEEK: Day[] = [\n \"sunday\",\n \"monday\",\n \"tuesday\",\n \"wednesday\",\n \"thursday\",\n \"friday\",\n \"saturday\",\n];\n\n/**\n * Validate a `HH:mm` or `HH:mm:ss` string and return its parts.\n * Throws when the format is malformed or any part is out of range.\n */\nfunction parseTimeString(time: string): {\n hour: number;\n minute: number;\n second: number;\n} {\n if (!/^\\d{1,2}:\\d{2}(:\\d{2})?$/.test(time)) {\n throw new Error(\"Invalid time format. Use HH:mm or HH:mm:ss.\");\n }\n\n const [hour = NaN, minute = NaN, second = 0] = time.split(\":\").map(Number);\n\n if (hour < 0 || hour > 23) {\n throw new Error(`Invalid hour in time \"${time}\". Must be between 0 and 23.`);\n }\n\n if (minute < 0 || minute > 59) {\n throw new Error(`Invalid minute in time \"${time}\". Must be between 0 and 59.`);\n }\n\n if (second < 0 || second > 59) {\n throw new Error(`Invalid second in time \"${time}\". Must be between 0 and 59.`);\n }\n\n return { hour, minute, second };\n}\n\n/**\n * Job class represents a scheduled task with configurable timing and execution options.\n *\n * @example\n * ```typescript\n * const job = new Job(\"cleanup\", async () => {\n * await cleanupOldFiles();\n * })\n * .everyDay()\n * .at(\"03:00\")\n * .inTimezone(\"America/New_York\")\n * .preventOverlap()\n * .retry(3, 1000);\n * ```\n */\nexport class Job {\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Properties\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Interval configuration for scheduling\n */\n private _intervals: JobIntervals = {};\n\n /**\n * Last execution timestamp.\n *\n * Updated after every run attempt — successful or failed — so the next-run\n * calculation always advances even when a job throws. Use the `job:complete`\n * / `job:error` events if you need to distinguish success from failure.\n */\n private _lastRun: Dayjs | null = null;\n\n /**\n * Whether the job is currently executing\n */\n private _isRunning = false;\n\n /**\n * Supplies the owning scheduler's around hooks (set via `bindAroundHooks`).\n */\n private _aroundHooks: () => readonly JobAroundHook[] = () => [];\n\n /**\n * Skip execution if job is already running\n */\n private _skipIfRunning = false;\n\n /**\n * Retry configuration\n */\n private _retryConfig: RetryConfig | null = null;\n\n /**\n * Timezone for scheduling (defaults to UTC)\n */\n private _timezone = \"UTC\";\n\n /**\n * Cron expression parser (mutually exclusive with interval config)\n */\n private _cronParser: CronParser | null = null;\n\n /**\n * All pending `waitForCompletion()` resolvers — drained on every run end.\n */\n private _completionResolvers: (() => void)[] = [];\n\n /**\n * Cross-server lock configuration (set by `onOneServer()`)\n */\n private _oneServer: { lockTtl?: CacheTtl; key: string } | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Public Properties\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Next scheduled execution time\n */\n public nextRun: Dayjs | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Constructor\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Creates a new Job instance\n *\n * @param name - Unique identifier for the job\n * @param callback - Function to execute when the job runs\n */\n public constructor(\n public readonly name: string,\n private readonly _callback: JobCallback,\n ) {}\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Public Getters\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Returns true if the job is currently executing\n */\n public get isRunning(): boolean {\n return this._isRunning;\n }\n\n /**\n * Returns the last execution timestamp (success OR failure).\n */\n public get lastRun(): Dayjs | null {\n return this._lastRun;\n }\n\n /**\n * Returns the current interval configuration (readonly)\n */\n public get intervals(): Readonly<JobIntervals> {\n return this._intervals;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Interval Configuration Methods (Fluent API)\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Set a custom interval for job execution\n *\n * @param value - Number of time units (must be > 0)\n * @param timeType - Type of time unit\n * @returns this for chaining\n * @throws Error if `value` is not a positive finite number\n *\n * @example\n * ```typescript\n * job.every(5, \"minute\"); // Run every 5 minutes\n * job.every(2, \"hour\"); // Run every 2 hours\n * ```\n */\n public every(value: number, timeType: TimeType): this {\n if (!Number.isFinite(value) || value <= 0) {\n throw new Error(\n `Invalid interval value: ${value}. Must be a positive finite number.`,\n );\n }\n\n this._intervals.every = { type: timeType, value };\n this._determineNextRun();\n\n return this;\n }\n\n /**\n * Run job every second (use with caution - high frequency)\n */\n public everySecond(): this {\n return this.every(1, \"second\");\n }\n\n /**\n * Run job every specified number of seconds\n */\n public everySeconds(seconds: number): this {\n return this.every(seconds, \"second\");\n }\n\n /**\n * Run job every minute\n */\n public everyMinute(): this {\n return this.every(1, \"minute\");\n }\n\n /**\n * Run job every specified number of minutes\n */\n public everyMinutes(minutes: number): this {\n return this.every(minutes, \"minute\");\n }\n\n /**\n * Run job every hour\n */\n public everyHour(): this {\n return this.every(1, \"hour\");\n }\n\n /**\n * Run job every specified number of hours\n */\n public everyHours(hours: number): this {\n return this.every(hours, \"hour\");\n }\n\n /**\n * Run job every day at midnight\n */\n public everyDay(): this {\n return this.every(1, \"day\");\n }\n\n /**\n * Alias for everyDay()\n */\n public daily(): this {\n return this.everyDay();\n }\n\n /**\n * Run job twice a day (every 12 hours)\n */\n public twiceDaily(): this {\n return this.every(12, \"hour\");\n }\n\n /**\n * Run job every week\n */\n public everyWeek(): this {\n return this.every(1, \"week\");\n }\n\n /**\n * Alias for everyWeek()\n */\n public weekly(): this {\n return this.everyWeek();\n }\n\n /**\n * Run job every month\n */\n public everyMonth(): this {\n return this.every(1, \"month\");\n }\n\n /**\n * Alias for everyMonth()\n */\n public monthly(): this {\n return this.everyMonth();\n }\n\n /**\n * Run job every year\n */\n public everyYear(): this {\n return this.every(1, \"year\");\n }\n\n /**\n * Alias for everyYear()\n */\n public yearly(): this {\n return this.everyYear();\n }\n\n /**\n * Alias for everyMinute() - job runs continuously every minute\n */\n public always(): this {\n return this.everyMinute();\n }\n\n /**\n * Schedule job using a cron expression\n *\n * Supports standard 5-field cron syntax:\n * ```\n * ┌───────────── minute (0-59)\n * │ ┌───────────── hour (0-23)\n * │ │ ┌───────────── day of month (1-31)\n * │ │ │ ┌───────────── month (1-12)\n * │ │ │ │ ┌───────────── day of week (0-6, Sunday = 0)\n * │ │ │ │ │\n * * * * * *\n * ```\n *\n * Supports:\n * - '*' - any value\n * - '5' - specific value\n * - '1,3,5' - list of values\n * - '1-5' - range of values\n * - '*/5' - step values (every 5)\n * - '1-10/2' - range with step\n *\n * @param expression - Standard 5-field cron expression\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.cron(\"0 9 * * 1-5\"); // 9 AM weekdays\n * job.cron(\"*/5 * * * *\"); // Every 5 minutes\n * job.cron(\"0 0 1 * *\"); // First day of month at midnight\n * job.cron(\"0 */2 * * *\"); // Every 2 hours\n * ```\n */\n public cron(expression: string): this {\n this._cronParser = new CronParser(expression);\n // Clear interval config since cron takes precedence\n this._intervals = {};\n this._determineNextRun();\n return this;\n }\n\n /**\n * Get the cron expression if one is set\n */\n public get cronExpression(): string | null {\n return this._cronParser?.expression ?? null;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Day & Time Configuration Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Schedule job on a specific day\n *\n * @param day - Day of week (string) or day of month (number 1-31)\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.on(\"monday\"); // Run on Mondays\n * job.on(15); // Run on the 15th of each month\n * ```\n */\n public on(day: Day | number): this {\n if (typeof day === \"number\" && (day < 1 || day > 31)) {\n throw new Error(\"Invalid day of the month. Must be between 1 and 31.\");\n }\n\n this._intervals.day = day;\n\n if (typeof day === \"number\") {\n this._intervals.dayOfMonthMode = \"specific\";\n }\n\n this._determineNextRun();\n\n return this;\n }\n\n /**\n * Schedule job at a specific time\n *\n * @param time - Time in HH:mm or HH:mm:ss format\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.daily().at(\"09:00\"); // Run daily at 9 AM\n * job.weekly().at(\"14:30\"); // Run weekly at 2:30 PM\n * ```\n */\n public at(time: string): this {\n parseTimeString(time);\n this._intervals.time = time;\n this._determineNextRun();\n\n return this;\n }\n\n /**\n * Run task at the beginning of the specified time period.\n *\n * - `\"day\"` → 00:00 every day\n * - `\"month\"` → 1st of every month at 00:00\n * - `\"year\"` → January 1st at 00:00 every year\n *\n * @param type - Time type (day, month, year)\n */\n public beginOf(type: TimeType): this {\n switch (type) {\n case \"day\":\n this._intervals.every = { type: \"day\", value: 1 };\n break;\n\n case \"month\":\n this._intervals.day = 1;\n this._intervals.dayOfMonthMode = \"specific\";\n this._intervals.every = { type: \"month\", value: 1 };\n break;\n\n case \"year\":\n this._intervals.month = 1;\n this._intervals.day = 1;\n this._intervals.dayOfMonthMode = \"specific\";\n this._intervals.every = { type: \"year\", value: 1 };\n break;\n\n default:\n throw new Error(`Unsupported type for beginOf: ${type}`);\n }\n\n return this.at(\"00:00\");\n }\n\n /**\n * Run task at the end of the specified time period.\n *\n * - `\"day\"` → 23:59 every day\n * - `\"month\"` → last day of every month at 23:59 (recomputed each cycle —\n * correct in February vs. March)\n * - `\"year\"` → December 31st at 23:59 every year\n *\n * @param type - Time type (day, month, year)\n */\n public endOf(type: TimeType): this {\n switch (type) {\n case \"day\":\n this._intervals.every = { type: \"day\", value: 1 };\n break;\n\n case \"month\":\n this._intervals.dayOfMonthMode = \"last\";\n this._intervals.every = { type: \"month\", value: 1 };\n break;\n\n case \"year\":\n this._intervals.month = 12;\n this._intervals.day = 31;\n this._intervals.dayOfMonthMode = \"specific\";\n this._intervals.every = { type: \"year\", value: 1 };\n break;\n\n default:\n throw new Error(`Unsupported type for endOf: ${type}`);\n }\n\n return this.at(\"23:59\");\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Timezone Configuration\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Set the timezone for this job's scheduling\n *\n * @param tz - IANA timezone string (e.g., \"America/New_York\", \"Europe/London\")\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.daily().at(\"09:00\").inTimezone(\"America/New_York\");\n * ```\n */\n public inTimezone(tz: string): this {\n this._timezone = tz;\n this._determineNextRun();\n return this;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Execution Options\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Prevent overlapping executions of this job.\n *\n * When enabled, if the job is already running when it's scheduled to run again,\n * the new execution will be skipped.\n *\n * @param skip - Whether to skip if already running (default: true)\n * @returns this for chaining\n */\n public preventOverlap(skip = true): this {\n this._skipIfRunning = skip;\n return this;\n }\n\n /**\n * Run each tick on only one server.\n *\n * Every tick, servers race for a create-only cache claim keyed\n * `scheduler.<key ?? name>.<scheduledTickEpochMs>` (the scheduled tick time\n * truncated to the second, so all servers compute the same key). The winner\n * runs the job; the others skip that tick silently. Works with `preventOverlap()`.\n *\n * Requires a shared cache driver (redis / pg) and the optional\n * `@warlock.js/cache` peer. On a memory driver it only dedupes within one process.\n *\n * @param options.lockTtl - Lock TTL. Default: the job's interval capped at 1h, minimum 60s\n * @param options.key - Stable key overriding the job name\n * @throws Error if the job has no name/key\n */\n public onOneServer(options: { lockTtl?: CacheTtl; key?: string } = {}): this {\n const key = options.key ?? this.name;\n\n if (!key) {\n throw new Error(\n \"onOneServer() requires a job name or `key` — the lock key must be stable across servers.\",\n );\n }\n\n this._oneServer = { lockTtl: options.lockTtl, key };\n\n return this;\n }\n\n /**\n * Configure automatic retry on failure\n *\n * @param maxRetries - Maximum number of retry attempts (must be ≥ 0)\n * @param delay - Delay between retries in milliseconds (must be ≥ 0)\n * @param backoffMultiplier - Optional multiplier for exponential backoff (must be > 0)\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.retry(3, 1000); // Retry 3 times with 1s delay\n * job.retry(5, 1000, 2); // Exponential backoff: 1s, 2s, 4s, 8s, 16s\n * ```\n */\n public retry(maxRetries: number, delay = 1000, backoffMultiplier?: number): this {\n if (!Number.isFinite(maxRetries) || maxRetries < 0) {\n throw new Error(`Invalid maxRetries: ${maxRetries}. Must be ≥ 0.`);\n }\n\n if (!Number.isFinite(delay) || delay < 0) {\n throw new Error(`Invalid retry delay: ${delay}. Must be ≥ 0.`);\n }\n\n if (\n backoffMultiplier !== undefined &&\n (!Number.isFinite(backoffMultiplier) || backoffMultiplier <= 0)\n ) {\n throw new Error(\n `Invalid backoffMultiplier: ${backoffMultiplier}. Must be > 0.`,\n );\n }\n\n this._retryConfig = {\n maxRetries,\n delay,\n backoffMultiplier,\n };\n\n return this;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Execution Control\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Terminate the job and clear all scheduling data\n */\n public terminate(): this {\n this._intervals = {};\n this._cronParser = null;\n this.nextRun = null;\n this._lastRun = null;\n this._isRunning = false;\n return this;\n }\n\n /**\n * Prepare the job by calculating the next run time\n * Called by the scheduler when starting\n */\n public prepare(): void {\n this._determineNextRun();\n }\n\n /**\n * Returns true if the job's `nextRun` has arrived, regardless of whether\n * it is currently running. Used by the scheduler to decide whether a\n * tick is \"due\" before checking overlap state.\n */\n public isDue(): boolean {\n return this.nextRun !== null && this._now().isSameOrAfter(this.nextRun);\n }\n\n /**\n * Determine if the job should run now.\n *\n * Combines `isDue()` with the overlap-prevention rule: when\n * `preventOverlap()` is on and the job is already running, this returns\n * false even if the next-run time has arrived.\n *\n * @returns true if the job should execute\n */\n public shouldRun(): boolean {\n if (this._skipIfRunning && this._isRunning) {\n return false;\n }\n\n return this.isDue();\n }\n\n /**\n * Execute the job once.\n *\n * Always advances `lastRun` and recalculates `nextRun` (success OR failure)\n * so a permanently failing job does not re-fire on every scheduler tick.\n *\n * @returns Promise resolving to the job result\n */\n public async run(): Promise<JobResult> {\n const startTime = Date.now();\n const scheduledAt = this.nextRun;\n\n this._isRunning = true;\n\n let result: JobResult;\n let executedRetries = 0;\n\n try {\n let skipped = false;\n let skipReason: string | undefined;\n\n if (this._oneServer) {\n const lockKey = this._lockKey(scheduledAt);\n const { cache } = await loadCache();\n\n // A CLAIM, not a lock: it is never released, only expires. A released\n // lock would let a server whose timer fires late (after the winner has\n // already finished) win the same tick again and run it twice.\n const claim = (await cache.set(lockKey, `pid.${process.pid}`, {\n onConflict: \"create\",\n ttl: this._lockTtl(),\n })) as { wasSet?: boolean } | undefined;\n\n skipped = claim?.wasSet !== true;\n\n if (!skipped) {\n const inner = await this._executeWithRetry();\n executedRetries = inner.retries;\n if (inner.skipped) {\n skipped = true;\n skipReason = HOOK_SKIP_REASON;\n }\n }\n } else {\n const inner = await this._executeWithRetry();\n executedRetries = inner.retries;\n\n if (inner.skipped) {\n skipped = true;\n skipReason = HOOK_SKIP_REASON;\n }\n }\n\n result = {\n success: true,\n duration: Date.now() - startTime,\n retries: executedRetries,\n ...(skipped ? { skipped: true } : {}),\n ...(skipReason ? { skipReason } : {}),\n };\n } catch (error) {\n result = {\n success: false,\n duration: Date.now() - startTime,\n error,\n retries: this._retryConfig?.maxRetries ?? 0,\n };\n } finally {\n this._lastRun = this._now();\n this._determineNextRun();\n this._isRunning = false;\n\n // Drain ALL pending waiters (waitForCompletion may be called more than once).\n const resolvers = this._completionResolvers.splice(0);\n\n for (const resolve of resolvers) {\n resolve();\n }\n }\n\n return result;\n }\n\n /**\n * Bind a provider of around hooks (called by the Scheduler on registration).\n * A provider, not a snapshot, so later `around()` / unsubscribe calls apply.\n *\n * @internal\n */\n public bindAroundHooks(provider: () => readonly JobAroundHook[]): this {\n this._aroundHooks = provider;\n\n return this;\n }\n\n /**\n * Wait for the currently executing run to complete.\n *\n * Multiple concurrent waiters are all resolved when the run finishes.\n * Useful for graceful shutdown.\n *\n * @returns Promise that resolves when the job completes\n */\n public waitForCompletion(): Promise<void> {\n if (!this._isRunning) {\n return Promise.resolve();\n }\n\n return new Promise(resolve => {\n this._completionResolvers.push(resolve);\n });\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Cross-server lock key for a scheduled tick (truncated to the second).\n */\n private _lockKey(scheduledAt: Dayjs | null): string {\n const tick = Math.floor((scheduledAt ?? this._now()).valueOf() / 1000) * 1000;\n\n return `scheduler.${this._oneServer!.key}.${tick}`;\n }\n\n /**\n * Lock TTL: explicit option, else min(interval, 1h) with a 60s floor.\n */\n private _lockTtl(): CacheTtl {\n if (this._oneServer!.lockTtl !== undefined) return this._oneServer!.lockTtl;\n\n const every = this._intervals.every;\n const intervalMs =\n every?.value !== undefined && every.type !== undefined\n ? every.value * UNIT_MS[every.type]\n : 3_600_000;\n\n return Math.max(60, Math.floor(Math.min(intervalMs, 3_600_000) / 1000));\n }\n\n /**\n * Get current time, respecting the configured timezone.\n */\n private _now(): Dayjs {\n return dayjs().tz(this._timezone);\n }\n\n /**\n * Execute the callback with retry logic\n */\n private async _executeWithRetry(): Promise<{ retries: number; skipped?: boolean }> {\n let lastError: unknown;\n let attempts = 0;\n const maxAttempts = (this._retryConfig?.maxRetries ?? 0) + 1;\n\n while (attempts < maxAttempts) {\n try {\n if (!(await this._invokeCallback())) {\n return { retries: attempts, skipped: true };\n }\n\n return { retries: attempts };\n } catch (error) {\n lastError = error;\n attempts++;\n\n if (attempts < maxAttempts && this._retryConfig) {\n const delay = this._calculateRetryDelay(attempts);\n await this._sleep(delay);\n }\n }\n }\n\n throw lastError;\n }\n\n /**\n * Run the callback inside the around hooks (first registered = outermost).\n *\n * @returns false when a hook never called `run` (the callback did not execute)\n */\n private async _invokeCallback(): Promise<boolean> {\n let reached = false;\n\n let chain: () => Promise<unknown> = async () => {\n reached = true;\n await this._callback(this);\n };\n\n const hooks = [...this._aroundHooks()];\n\n for (let index = hooks.length - 1; index >= 0; index--) {\n const hook = hooks[index]!;\n const next = chain;\n\n chain = () => hook(this, next);\n }\n\n await chain();\n\n return reached;\n }\n\n /**\n * Calculate retry delay with optional exponential backoff\n */\n private _calculateRetryDelay(attempt: number): number {\n if (!this._retryConfig) return 0;\n\n const { delay, backoffMultiplier } = this._retryConfig;\n\n if (backoffMultiplier) {\n return delay * Math.pow(backoffMultiplier, attempt - 1);\n }\n\n return delay;\n }\n\n /**\n * Sleep for specified milliseconds\n */\n private _sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n /**\n * Apply month / day-of-month / day-of-week / time constraints to a Dayjs.\n *\n * Re-applied after every interval advance inside `_determineNextRun` so\n * dynamic constraints (`dayOfMonthMode: \"last\"` recomputed per month, month\n * lock for `beginOf/endOf(\"year\")`) stay correct as the candidate moves\n * forward.\n */\n private _applyConstraints(date: Dayjs): Dayjs {\n let result = date;\n\n if (this._intervals.month !== undefined) {\n result = result.month(this._intervals.month - 1);\n }\n\n if (this._intervals.dayOfMonthMode === \"last\") {\n result = result.date(result.daysInMonth());\n } else if (this._intervals.day !== undefined) {\n if (typeof this._intervals.day === \"number\") {\n result = result.date(this._intervals.day);\n } else {\n const targetDay = DAYS_OF_WEEK.indexOf(this._intervals.day);\n\n if (targetDay !== -1) {\n result = result.day(targetDay);\n }\n }\n }\n\n if (this._intervals.time) {\n const { hour, minute, second } = parseTimeString(this._intervals.time);\n result = result.hour(hour).minute(minute).second(second).millisecond(0);\n }\n\n return result;\n }\n\n /**\n * Calculate the next run time based on interval or cron configuration.\n *\n * Strategy: apply all constraints (month, day, time) ONCE before the\n * advance loop, then advance by `every` until the candidate is in the\n * future. Static constraints (numeric day, fixed month, fixed time)\n * survive `dayjs.add()` automatically. The only dynamic constraint —\n * `dayOfMonthMode: \"last\"` — is re-applied inside the loop so the\n * candidate always lands on the *new* month's last day after each\n * advance.\n *\n * Re-applying time/day inside the loop would deadlock: with\n * `twiceDaily().at(\"06:00\")` we'd advance 06:00 → 18:00 → snap back to\n * 06:00 → 18:00 → ... forever.\n */\n private _determineNextRun(): void {\n if (this._cronParser) {\n const now = this._now();\n this.nextRun = this._cronParser.nextRun(now);\n return;\n }\n\n const intervalValue = this._intervals.every?.value;\n const intervalType = this._intervals.every?.type;\n const hasInterval = !!(intervalValue && intervalType);\n\n // After a previous run, jump ahead by EXACTLY one interval. Previously\n // we used `lastRun + 1s` and relied on the advance loop to catch up,\n // but that loop only triggers when the candidate is in the past — so\n // for fast-completing callbacks the candidate stayed at `lastRun + 1s`,\n // making `everySeconds(2)` fire every second. Honor the interval up\n // front instead.\n let date: Dayjs;\n\n if (this._lastRun && hasInterval) {\n date = this._lastRun.add(intervalValue!, intervalType);\n } else if (this._lastRun) {\n date = this._lastRun.add(1, \"second\");\n } else {\n date = this._now();\n }\n\n date = this._applyConstraints(date);\n\n while (date.isBefore(this._now())) {\n if (hasInterval) {\n date = date.add(intervalValue!, intervalType);\n } else {\n date = date.add(1, \"day\");\n }\n\n if (this._intervals.dayOfMonthMode === \"last\") {\n date = date.date(date.daysInMonth());\n }\n }\n\n this.nextRun = date;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Factory Function\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Factory function to create a new Job instance\n *\n * @param name - Unique identifier for the job\n * @param callback - Function to execute when the job runs\n * @returns New Job instance\n *\n * @example\n * ```typescript\n * const cleanupJob = job(\"cleanup\", async () => {\n * await db.deleteExpiredTokens();\n * }).daily().at(\"03:00\");\n * ```\n */\nexport function job(name: string, callback: JobCallback): Job {\n return new Job(name, callback);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"job.mjs","names":[],"sources":["../../../../../../../packages/scheduler/src/job.ts"],"sourcesContent":["import dayjs, { type Dayjs } from \"dayjs\";\nimport isSameOrAfter from \"dayjs/plugin/isSameOrAfter.js\";\nimport timezone from \"dayjs/plugin/timezone.js\";\nimport utc from \"dayjs/plugin/utc.js\";\nimport { CronParser } from \"./cron-parser\";\nimport type { Day, JobIntervals, JobResult, RetryConfig, TimeType } from \"./types\";\n\n/**\n * `@warlock.js/cache` is an optional peer, loaded only when `onOneServer()` is\n * used. Memoized so every job shares one module instance.\n */\nlet cacheModule: Promise<typeof import(\"@warlock.js/cache\")> | undefined;\n\nfunction loadCache(): Promise<typeof import(\"@warlock.js/cache\")> {\n cacheModule ??= import(\"@warlock.js/cache\");\n\n return cacheModule;\n}\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\ndayjs.extend(isSameOrAfter);\n\ntype CacheTtl = number | string;\n\n/** Milliseconds per interval unit (month/year approximate; only used to size the lock TTL) */\nconst UNIT_MS: Record<TimeType, number> = {\n second: 1000,\n minute: 60_000,\n hour: 3_600_000,\n day: 86_400_000,\n week: 604_800_000,\n month: 2_592_000_000,\n year: 31_536_000_000,\n};\n\nexport type JobCallback = (job: Job) => Promise<any>;\n\n/**\n * Hook wrapping every callback execution (each retry attempt included).\n * Not calling `run` skips the execution; throwing counts as a callback error.\n */\nconst HOOK_SKIP_REASON = \"An around hook did not run the job\";\n\nexport type JobAroundHook = (job: Job, run: () => Promise<unknown>) => Promise<unknown>;\n/**\n * Days of week mapping (lowercase for consistency with Day type)\n */\nconst DAYS_OF_WEEK: Day[] = [\n \"sunday\",\n \"monday\",\n \"tuesday\",\n \"wednesday\",\n \"thursday\",\n \"friday\",\n \"saturday\",\n];\n\n/**\n * Validate a `HH:mm` or `HH:mm:ss` string and return its parts.\n * Throws when the format is malformed or any part is out of range.\n */\nfunction parseTimeString(time: string): {\n hour: number;\n minute: number;\n second: number;\n} {\n if (!/^\\d{1,2}:\\d{2}(:\\d{2})?$/.test(time)) {\n throw new Error(\"Invalid time format. Use HH:mm or HH:mm:ss.\");\n }\n\n const [hour = NaN, minute = NaN, second = 0] = time.split(\":\").map(Number);\n\n if (hour < 0 || hour > 23) {\n throw new Error(`Invalid hour in time \"${time}\". Must be between 0 and 23.`);\n }\n\n if (minute < 0 || minute > 59) {\n throw new Error(`Invalid minute in time \"${time}\". Must be between 0 and 59.`);\n }\n\n if (second < 0 || second > 59) {\n throw new Error(`Invalid second in time \"${time}\". Must be between 0 and 59.`);\n }\n\n return { hour, minute, second };\n}\n\n/**\n * Job class represents a scheduled task with configurable timing and execution options.\n *\n * @example\n * ```typescript\n * const job = new Job(\"cleanup\", async () => {\n * await cleanupOldFiles();\n * })\n * .everyDay()\n * .at(\"03:00\")\n * .inTimezone(\"America/New_York\")\n * .preventOverlap()\n * .retry(3, 1000);\n * ```\n */\nexport class Job {\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Properties\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Interval configuration for scheduling\n */\n private _intervals: JobIntervals = {};\n\n /**\n * Last execution timestamp.\n *\n * Updated after every run attempt — successful or failed — so the next-run\n * calculation always advances even when a job throws. Use the `job:complete`\n * / `job:error` events if you need to distinguish success from failure.\n */\n private _lastRun: Dayjs | null = null;\n\n /**\n * Whether the job is currently executing\n */\n private _isRunning = false;\n\n /**\n * Supplies the owning scheduler's around hooks (set via `bindAroundHooks`).\n */\n private _aroundHooks: () => readonly JobAroundHook[] = () => [];\n\n /**\n * Skip execution if job is already running\n */\n private _skipIfRunning = false;\n\n /**\n * Retry configuration\n */\n private _retryConfig: RetryConfig | null = null;\n\n /**\n * Timezone for scheduling (defaults to UTC)\n */\n private _timezone = \"UTC\";\n\n /**\n * Cron expression parser (mutually exclusive with interval config)\n */\n private _cronParser: CronParser | null = null;\n\n /**\n * All pending `waitForCompletion()` resolvers — drained on every run end.\n */\n private _completionResolvers: (() => void)[] = [];\n\n /**\n * Cross-server lock configuration (set by `onOneServer()`)\n */\n private _oneServer: { lockTtl?: CacheTtl; key: string } | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Public Properties\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Next scheduled execution time\n */\n public nextRun: Dayjs | null = null;\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Constructor\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Creates a new Job instance\n *\n * @param name - Unique identifier for the job\n * @param callback - Function to execute when the job runs\n */\n public constructor(\n public readonly name: string,\n private readonly _callback: JobCallback,\n ) {}\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Public Getters\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Returns true if the job is currently executing\n */\n public get isRunning(): boolean {\n return this._isRunning;\n }\n\n /**\n * Returns the last execution timestamp (success OR failure).\n */\n public get lastRun(): Dayjs | null {\n return this._lastRun;\n }\n\n /**\n * Returns the current interval configuration (readonly)\n */\n public get intervals(): Readonly<JobIntervals> {\n return this._intervals;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Interval Configuration Methods (Fluent API)\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Set a custom interval for job execution\n *\n * @param value - Number of time units (must be > 0)\n * @param timeType - Type of time unit\n * @returns this for chaining\n * @throws Error if `value` is not a positive finite number\n *\n * @example\n * ```typescript\n * job.every(5, \"minute\"); // Run every 5 minutes\n * job.every(2, \"hour\"); // Run every 2 hours\n * ```\n */\n public every(value: number, timeType: TimeType): this {\n if (!Number.isFinite(value) || value <= 0) {\n throw new Error(\n `Invalid interval value: ${value}. Must be a positive finite number.`,\n );\n }\n\n this._intervals.every = { type: timeType, value };\n this._determineNextRun();\n\n return this;\n }\n\n /**\n * Run job every second (use with caution - high frequency)\n */\n public everySecond(): this {\n return this.every(1, \"second\");\n }\n\n /**\n * Run job every specified number of seconds\n */\n public everySeconds(seconds: number): this {\n return this.every(seconds, \"second\");\n }\n\n /**\n * Run job every minute\n */\n public everyMinute(): this {\n return this.every(1, \"minute\");\n }\n\n /**\n * Run job every specified number of minutes\n */\n public everyMinutes(minutes: number): this {\n return this.every(minutes, \"minute\");\n }\n\n /**\n * Run job every hour\n */\n public everyHour(): this {\n return this.every(1, \"hour\");\n }\n\n /**\n * Run job every specified number of hours\n */\n public everyHours(hours: number): this {\n return this.every(hours, \"hour\");\n }\n\n /**\n * Run job every day at midnight\n */\n public everyDay(): this {\n return this.every(1, \"day\");\n }\n\n /**\n * Alias for everyDay()\n */\n public daily(): this {\n return this.everyDay();\n }\n\n /**\n * Run job twice a day (every 12 hours)\n */\n public twiceDaily(): this {\n return this.every(12, \"hour\");\n }\n\n /**\n * Run job every week\n */\n public everyWeek(): this {\n return this.every(1, \"week\");\n }\n\n /**\n * Alias for everyWeek()\n */\n public weekly(): this {\n return this.everyWeek();\n }\n\n /**\n * Run job every month\n */\n public everyMonth(): this {\n return this.every(1, \"month\");\n }\n\n /**\n * Alias for everyMonth()\n */\n public monthly(): this {\n return this.everyMonth();\n }\n\n /**\n * Run job every year\n */\n public everyYear(): this {\n return this.every(1, \"year\");\n }\n\n /**\n * Alias for everyYear()\n */\n public yearly(): this {\n return this.everyYear();\n }\n\n /**\n * Alias for everyMinute() - job runs continuously every minute\n */\n public always(): this {\n return this.everyMinute();\n }\n\n /**\n * Schedule job using a cron expression\n *\n * Supports standard 5-field cron syntax:\n * ```\n * ┌───────────── minute (0-59)\n * │ ┌───────────── hour (0-23)\n * │ │ ┌───────────── day of month (1-31)\n * │ │ │ ┌───────────── month (1-12)\n * │ │ │ │ ┌───────────── day of week (0-6, Sunday = 0)\n * │ │ │ │ │\n * * * * * *\n * ```\n *\n * Supports:\n * - '*' - any value\n * - '5' - specific value\n * - '1,3,5' - list of values\n * - '1-5' - range of values\n * - '*/5' - step values (every 5)\n * - '1-10/2' - range with step\n *\n * @param expression - Standard 5-field cron expression\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.cron(\"0 9 * * 1-5\"); // 9 AM weekdays\n * job.cron(\"*/5 * * * *\"); // Every 5 minutes\n * job.cron(\"0 0 1 * *\"); // First day of month at midnight\n * job.cron(\"0 */2 * * *\"); // Every 2 hours\n * ```\n */\n public cron(expression: string): this {\n this._cronParser = new CronParser(expression);\n // Clear interval config since cron takes precedence\n this._intervals = {};\n this._determineNextRun();\n return this;\n }\n\n /**\n * Get the cron expression if one is set\n */\n public get cronExpression(): string | null {\n return this._cronParser?.expression ?? null;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Day & Time Configuration Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Schedule job on a specific day\n *\n * @param day - Day of week (string) or day of month (number 1-31)\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.on(\"monday\"); // Run on Mondays\n * job.on(15); // Run on the 15th of each month\n * ```\n */\n public on(day: Day | number): this {\n if (typeof day === \"number\" && (day < 1 || day > 31)) {\n throw new Error(\"Invalid day of the month. Must be between 1 and 31.\");\n }\n\n this._intervals.day = day;\n\n if (typeof day === \"number\") {\n this._intervals.dayOfMonthMode = \"specific\";\n }\n\n this._determineNextRun();\n\n return this;\n }\n\n /**\n * Schedule job at a specific time\n *\n * @param time - Time in HH:mm or HH:mm:ss format\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.daily().at(\"09:00\"); // Run daily at 9 AM\n * job.weekly().at(\"14:30\"); // Run weekly at 2:30 PM\n * ```\n */\n public at(time: string): this {\n parseTimeString(time);\n this._intervals.time = time;\n this._determineNextRun();\n\n return this;\n }\n\n /**\n * Run task at the beginning of the specified time period.\n *\n * - `\"day\"` → 00:00 every day\n * - `\"month\"` → 1st of every month at 00:00\n * - `\"year\"` → January 1st at 00:00 every year\n *\n * @param type - Time type (day, month, year)\n */\n public beginOf(type: TimeType): this {\n switch (type) {\n case \"day\":\n this._intervals.every = { type: \"day\", value: 1 };\n break;\n\n case \"month\":\n this._intervals.day = 1;\n this._intervals.dayOfMonthMode = \"specific\";\n this._intervals.every = { type: \"month\", value: 1 };\n break;\n\n case \"year\":\n this._intervals.month = 1;\n this._intervals.day = 1;\n this._intervals.dayOfMonthMode = \"specific\";\n this._intervals.every = { type: \"year\", value: 1 };\n break;\n\n default:\n throw new Error(`Unsupported type for beginOf: ${type}`);\n }\n\n return this.at(\"00:00\");\n }\n\n /**\n * Run task at the end of the specified time period.\n *\n * - `\"day\"` → 23:59 every day\n * - `\"month\"` → last day of every month at 23:59 (recomputed each cycle —\n * correct in February vs. March)\n * - `\"year\"` → December 31st at 23:59 every year\n *\n * @param type - Time type (day, month, year)\n */\n public endOf(type: TimeType): this {\n switch (type) {\n case \"day\":\n this._intervals.every = { type: \"day\", value: 1 };\n break;\n\n case \"month\":\n this._intervals.dayOfMonthMode = \"last\";\n this._intervals.every = { type: \"month\", value: 1 };\n break;\n\n case \"year\":\n this._intervals.month = 12;\n this._intervals.day = 31;\n this._intervals.dayOfMonthMode = \"specific\";\n this._intervals.every = { type: \"year\", value: 1 };\n break;\n\n default:\n throw new Error(`Unsupported type for endOf: ${type}`);\n }\n\n return this.at(\"23:59\");\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Timezone Configuration\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Set the timezone for this job's scheduling\n *\n * @param tz - IANA timezone string (e.g., \"America/New_York\", \"Europe/London\")\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.daily().at(\"09:00\").inTimezone(\"America/New_York\");\n * ```\n */\n public inTimezone(tz: string): this {\n this._timezone = tz;\n this._determineNextRun();\n return this;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Execution Options\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Prevent overlapping executions of this job.\n *\n * When enabled, if the job is already running when it's scheduled to run again,\n * the new execution will be skipped.\n *\n * @param skip - Whether to skip if already running (default: true)\n * @returns this for chaining\n */\n public preventOverlap(skip = true): this {\n this._skipIfRunning = skip;\n return this;\n }\n\n /**\n * Run each tick on only one server.\n *\n * Every tick, servers race for a create-only cache claim keyed\n * `scheduler.<key ?? name>.<scheduledTickEpochMs>` (the scheduled tick time\n * truncated to the second, so all servers compute the same key). The winner\n * runs the job; the others skip that tick silently. Works with `preventOverlap()`.\n *\n * Requires a shared cache driver (redis / pg) and the optional\n * `@warlock.js/cache` peer. On a memory driver it only dedupes within one process.\n *\n * @param options.lockTtl - Lock TTL. Default: the job's interval capped at 1h, minimum 60s\n * @param options.key - Stable key overriding the job name\n * @throws Error if the job has no name/key\n */\n public onOneServer(options: { lockTtl?: CacheTtl; key?: string } = {}): this {\n const key = options.key ?? this.name;\n\n if (!key) {\n throw new Error(\n \"onOneServer() requires a job name or `key` — the lock key must be stable across servers.\",\n );\n }\n\n this._oneServer = { lockTtl: options.lockTtl, key };\n\n return this;\n }\n\n /**\n * Configure automatic retry on failure\n *\n * @param maxRetries - Maximum number of retry attempts (must be ≥ 0)\n * @param delay - Delay between retries in milliseconds (must be ≥ 0)\n * @param backoffMultiplier - Optional multiplier for exponential backoff (must be > 0)\n * @returns this for chaining\n *\n * @example\n * ```typescript\n * job.retry(3, 1000); // Retry 3 times with 1s delay\n * job.retry(5, 1000, 2); // Exponential backoff: 1s, 2s, 4s, 8s, 16s\n * ```\n */\n public retry(maxRetries: number, delay = 1000, backoffMultiplier?: number): this {\n if (!Number.isFinite(maxRetries) || maxRetries < 0) {\n throw new Error(`Invalid maxRetries: ${maxRetries}. Must be ≥ 0.`);\n }\n\n if (!Number.isFinite(delay) || delay < 0) {\n throw new Error(`Invalid retry delay: ${delay}. Must be ≥ 0.`);\n }\n\n if (\n backoffMultiplier !== undefined &&\n (!Number.isFinite(backoffMultiplier) || backoffMultiplier <= 0)\n ) {\n throw new Error(\n `Invalid backoffMultiplier: ${backoffMultiplier}. Must be > 0.`,\n );\n }\n\n this._retryConfig = {\n maxRetries,\n delay,\n backoffMultiplier,\n };\n\n return this;\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Execution Control\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Terminate the job and clear all scheduling data\n */\n public terminate(): this {\n this._intervals = {};\n this._cronParser = null;\n this.nextRun = null;\n this._lastRun = null;\n this._isRunning = false;\n return this;\n }\n\n /**\n * Prepare the job by calculating the next run time\n * Called by the scheduler when starting\n */\n public prepare(): void {\n this._determineNextRun();\n }\n\n /**\n * Returns true if the job's `nextRun` has arrived, regardless of whether\n * it is currently running. Used by the scheduler to decide whether a\n * tick is \"due\" before checking overlap state.\n */\n public isDue(): boolean {\n return this.nextRun !== null && this._now().isSameOrAfter(this.nextRun);\n }\n\n /**\n * Determine if the job should run now.\n *\n * Combines `isDue()` with the overlap-prevention rule: when\n * `preventOverlap()` is on and the job is already running, this returns\n * false even if the next-run time has arrived.\n *\n * @returns true if the job should execute\n */\n public shouldRun(): boolean {\n if (this._skipIfRunning && this._isRunning) {\n return false;\n }\n\n return this.isDue();\n }\n\n /**\n * Execute the job once.\n *\n * Always advances `lastRun` and recalculates `nextRun` (success OR failure)\n * so a permanently failing job does not re-fire on every scheduler tick.\n *\n * @returns Promise resolving to the job result\n */\n public async run(): Promise<JobResult> {\n const startTime = Date.now();\n const scheduledAt = this.nextRun;\n\n this._isRunning = true;\n\n let result: JobResult;\n let executedRetries = 0;\n\n try {\n let skipped = false;\n let skipReason: string | undefined;\n\n if (this._oneServer) {\n const lockKey = this._lockKey(scheduledAt);\n const { cache } = await loadCache();\n\n // A CLAIM, not a lock: it is never released, only expires. A released\n // lock would let a server whose timer fires late (after the winner has\n // already finished) win the same tick again and run it twice.\n const claim = (await cache.set(lockKey, `pid.${process.pid}`, {\n onConflict: \"create\",\n ttl: this._lockTtl(),\n })) as { wasSet?: boolean } | undefined;\n\n skipped = claim?.wasSet !== true;\n\n if (!skipped) {\n const inner = await this._executeWithRetry();\n executedRetries = inner.retries;\n if (inner.skipped) {\n skipped = true;\n skipReason = HOOK_SKIP_REASON;\n }\n }\n } else {\n const inner = await this._executeWithRetry();\n executedRetries = inner.retries;\n\n if (inner.skipped) {\n skipped = true;\n skipReason = HOOK_SKIP_REASON;\n }\n }\n\n result = {\n success: true,\n duration: Date.now() - startTime,\n retries: executedRetries,\n ...(skipped ? { skipped: true } : {}),\n ...(skipReason ? { skipReason } : {}),\n };\n } catch (error) {\n result = {\n success: false,\n duration: Date.now() - startTime,\n error,\n retries: this._retryConfig?.maxRetries ?? 0,\n };\n } finally {\n this._lastRun = this._now();\n this._determineNextRun();\n this._isRunning = false;\n\n // Drain ALL pending waiters (waitForCompletion may be called more than once).\n const resolvers = this._completionResolvers.splice(0);\n\n for (const resolve of resolvers) {\n resolve();\n }\n }\n\n return result;\n }\n\n /**\n * Bind a provider of around hooks (called by the Scheduler on registration).\n * A provider, not a snapshot, so later `around()` / unsubscribe calls apply.\n *\n * @internal\n */\n public bindAroundHooks(provider: () => readonly JobAroundHook[]): this {\n this._aroundHooks = provider;\n\n return this;\n }\n\n /**\n * Wait for the currently executing run to complete.\n *\n * Multiple concurrent waiters are all resolved when the run finishes.\n * Useful for graceful shutdown.\n *\n * @returns Promise that resolves when the job completes\n */\n public waitForCompletion(): Promise<void> {\n if (!this._isRunning) {\n return Promise.resolve();\n }\n\n return new Promise(resolve => {\n this._completionResolvers.push(resolve);\n });\n }\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Private Methods\n // ─────────────────────────────────────────────────────────────────────────────\n\n /**\n * Cross-server lock key for a scheduled tick (truncated to the second).\n */\n private _lockKey(scheduledAt: Dayjs | null): string {\n const tick = Math.floor((scheduledAt ?? this._now()).valueOf() / 1000) * 1000;\n\n return `scheduler.${this._oneServer!.key}.${tick}`;\n }\n\n /**\n * Lock TTL: explicit option, else min(interval, 1h) with a 60s floor.\n */\n private _lockTtl(): CacheTtl {\n if (this._oneServer!.lockTtl !== undefined) return this._oneServer!.lockTtl;\n\n const every = this._intervals.every;\n const intervalMs =\n every?.value !== undefined && every.type !== undefined\n ? every.value * UNIT_MS[every.type]\n : 3_600_000;\n\n return Math.max(60, Math.floor(Math.min(intervalMs, 3_600_000) / 1000));\n }\n\n /**\n * Get current time, respecting the configured timezone.\n */\n private _now(): Dayjs {\n return dayjs().tz(this._timezone);\n }\n\n /**\n * Execute the callback with retry logic\n */\n private async _executeWithRetry(): Promise<{ retries: number; skipped?: boolean }> {\n let lastError: unknown;\n let attempts = 0;\n const maxAttempts = (this._retryConfig?.maxRetries ?? 0) + 1;\n\n while (attempts < maxAttempts) {\n try {\n if (!(await this._invokeCallback())) {\n return { retries: attempts, skipped: true };\n }\n\n return { retries: attempts };\n } catch (error) {\n lastError = error;\n attempts++;\n\n if (attempts < maxAttempts && this._retryConfig) {\n const delay = this._calculateRetryDelay(attempts);\n await this._sleep(delay);\n }\n }\n }\n\n throw lastError;\n }\n\n /**\n * Run the callback inside the around hooks (first registered = outermost).\n *\n * @returns false when a hook never called `run` (the callback did not execute)\n */\n private async _invokeCallback(): Promise<boolean> {\n let reached = false;\n\n let chain: () => Promise<unknown> = async () => {\n reached = true;\n await this._callback(this);\n };\n\n const hooks = [...this._aroundHooks()];\n\n for (let index = hooks.length - 1; index >= 0; index--) {\n const hook = hooks[index]!;\n const next = chain;\n\n chain = () => hook(this, next);\n }\n\n await chain();\n\n return reached;\n }\n\n /**\n * Calculate retry delay with optional exponential backoff\n */\n private _calculateRetryDelay(attempt: number): number {\n if (!this._retryConfig) return 0;\n\n const { delay, backoffMultiplier } = this._retryConfig;\n\n if (backoffMultiplier) {\n return delay * Math.pow(backoffMultiplier, attempt - 1);\n }\n\n return delay;\n }\n\n /**\n * Sleep for specified milliseconds\n */\n private _sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n /**\n * Apply month / day-of-month / day-of-week / time constraints to a Dayjs.\n *\n * Re-applied after every interval advance inside `_determineNextRun` so\n * dynamic constraints (`dayOfMonthMode: \"last\"` recomputed per month, month\n * lock for `beginOf/endOf(\"year\")`) stay correct as the candidate moves\n * forward.\n */\n private _applyConstraints(date: Dayjs): Dayjs {\n let result = date;\n\n if (this._intervals.month !== undefined) {\n result = result.month(this._intervals.month - 1);\n }\n\n if (this._intervals.dayOfMonthMode === \"last\") {\n result = result.date(result.daysInMonth());\n } else if (this._intervals.day !== undefined) {\n if (typeof this._intervals.day === \"number\") {\n result = result.date(this._intervals.day);\n } else {\n const targetDay = DAYS_OF_WEEK.indexOf(this._intervals.day);\n\n if (targetDay !== -1) {\n result = result.day(targetDay);\n }\n }\n }\n\n if (this._intervals.time) {\n const { hour, minute, second } = parseTimeString(this._intervals.time);\n result = result.hour(hour).minute(minute).second(second).millisecond(0);\n }\n\n return result;\n }\n\n /**\n * Calculate the next run time based on interval or cron configuration.\n *\n * Strategy: apply all constraints (month, day, time) ONCE before the\n * advance loop, then advance by `every` until the candidate is in the\n * future. Static constraints (numeric day, fixed month, fixed time)\n * survive `dayjs.add()` automatically. The only dynamic constraint —\n * `dayOfMonthMode: \"last\"` — is re-applied inside the loop so the\n * candidate always lands on the *new* month's last day after each\n * advance.\n *\n * Re-applying time/day inside the loop would deadlock: with\n * `twiceDaily().at(\"06:00\")` we'd advance 06:00 → 18:00 → snap back to\n * 06:00 → 18:00 → ... forever.\n */\n private _determineNextRun(): void {\n if (this._cronParser) {\n const now = this._now();\n this.nextRun = this._cronParser.nextRun(now);\n return;\n }\n\n const intervalValue = this._intervals.every?.value;\n const intervalType = this._intervals.every?.type;\n const hasInterval = !!(intervalValue && intervalType);\n\n // After a previous run, jump ahead by EXACTLY one interval. Previously\n // we used `lastRun + 1s` and relied on the advance loop to catch up,\n // but that loop only triggers when the candidate is in the past — so\n // for fast-completing callbacks the candidate stayed at `lastRun + 1s`,\n // making `everySeconds(2)` fire every second. Honor the interval up\n // front instead.\n let date: Dayjs;\n\n if (this._lastRun && hasInterval) {\n date = this._lastRun.add(intervalValue!, intervalType);\n } else if (this._lastRun) {\n date = this._lastRun.add(1, \"second\");\n } else {\n date = this._now();\n }\n\n date = this._applyConstraints(date);\n\n while (date.isBefore(this._now())) {\n if (hasInterval) {\n date = date.add(intervalValue!, intervalType);\n } else {\n date = date.add(1, \"day\");\n }\n\n if (this._intervals.dayOfMonthMode === \"last\") {\n date = date.date(date.daysInMonth());\n }\n }\n\n this.nextRun = date;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Factory Function\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Factory function to create a new Job instance\n *\n * @param name - Unique identifier for the job\n * @param callback - Function to execute when the job runs\n * @returns New Job instance\n *\n * @example\n * ```typescript\n * const cleanupJob = job(\"cleanup\", async () => {\n * await db.deleteExpiredTokens();\n * }).daily().at(\"03:00\");\n * ```\n */\nexport function job(name: string, callback: JobCallback): Job {\n return new Job(name, callback);\n}\n"],"mappings":"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"}
|
package/esm/process-roles.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-roles.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"process-roles.mjs","names":[],"sources":["../../../../../../../packages/scheduler/src/process-roles.ts"],"sourcesContent":["/**\n * `WARLOCK_ROLES`, parsed for the scheduler's own use.\n *\n * The scheduler has no dependency on `@warlock.js/core` (see package.json),\n * so it cannot call `Application.hasRole`. Core's `parseRoles` already\n * validates the raw value at process boot — an unknown role or an explicit\n * empty list throws there — so this only needs to decide membership, not\n * re-validate.\n */\nfunction parsedRoles(): string[] {\n const value = process.env.WARLOCK_ROLES;\n\n if (!value) return [];\n\n return value\n .split(\",\")\n .map((role) => role.trim().toLowerCase())\n .filter((role) => role.length > 0);\n}\n\n/**\n * Whether this process serves the `worker` role.\n *\n * `true` when `WARLOCK_ROLES` is unset, matching core's default of \"every\n * role is on\" for plain `start`, `dev`, and tests.\n */\nexport function hasWorkerRole(): boolean {\n const roles = parsedRoles();\n\n return roles.length === 0 || roles.includes(\"worker\");\n}\n\n/**\n * The roles this process serves, formatted for a log line — `\"all\"` when\n * `WARLOCK_ROLES` is unset.\n */\nexport function rolesLabel(): string {\n const roles = parsedRoles();\n\n return roles.length === 0 ? \"all\" : roles.join(\",\");\n}\n"],"mappings":";;;;;;;;;;AASA,SAAS,cAAwB;CAC/B,MAAM,QAAQ,QAAQ,IAAI;CAE1B,IAAI,CAAC,OAAO,OAAO,CAAC;CAEpB,OAAO,MACJ,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CACxC,QAAQ,SAAS,KAAK,SAAS,CAAC;AACrC;;;;;;;AAQA,SAAgB,gBAAyB;CACvC,MAAM,QAAQ,YAAY;CAE1B,OAAO,MAAM,WAAW,KAAK,MAAM,SAAS,QAAQ;AACtD;;;;;AAMA,SAAgB,aAAqB;CACnC,MAAM,QAAQ,YAAY;CAE1B,OAAO,MAAM,WAAW,IAAI,QAAQ,MAAM,KAAK,GAAG;AACpD"}
|
package/esm/scheduler.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SchedulerEvents } from "./types.mjs";
|
|
2
2
|
import { Job, JobAroundHook, JobCallback } from "./job.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/scheduler/src/scheduler.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Type-safe event emitter interface for Scheduler events
|
|
7
7
|
*/
|
package/esm/scheduler.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"scheduler.d.mts","names":[],"sources":["../../../../../../../packages/scheduler/src/scheduler.ts"],"mappings":";;;;;AAI0D;;UAKhD,iBAAA,iBAAkC,MAAA;EAC1C,EAAA,iBAAmB,OAAA,EAAS,KAAA,EAAO,CAAA,EAAG,QAAA,MAAc,IAAA,EAAM,OAAA,CAAQ,CAAA;EAClE,IAAA,iBAAqB,OAAA,EAAS,KAAA,EAAO,CAAA,EAAG,QAAA,MAAc,IAAA,EAAM,OAAA,CAAQ,CAAA;EACpE,GAAA,iBAAoB,OAAA,EAAS,KAAA,EAAO,CAAA,EAAG,QAAA,MAAc,IAAA,EAAM,OAAA,CAAQ,CAAA;EACnE,IAAA,iBAAqB,OAAA,EAAS,KAAA,EAAO,CAAA,KAAM,IAAA,EAAM,OAAA,CAAQ,CAAA;AAAA;AAAA,cAAC,cAAA,YA+BvB,iBAAiB,CAAC,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAD1C,SAAA,SACH,cAAA,YACG,iBAAA,CAAkB,eAAA;EAlCC;;;EAAA,QA2CtB,KAAA;EA3CgC;;;EAAA,QAgDhC,YAAA;EA/C4B;;;EAAA,QAoD5B,UAAA;EApD2D;;;EAAA,QAyD3D,aAAA;EAxDa;;;EAAA,QA6Db,cAAA;EA7DyC;;;EAAA,QAkEzC,eAAA;EAlEkD;;;EAAA,QAuElD,eAAA;EAxC4D;AADtE;;;;;;EACsE,QAiD5D,WAAA;EA2E4C;;;;;EAAA,QApE5C,kBAAA;EAvDG;;;EAAA,IAgEA,SAAA;EAhEA;;;EAAA,IAuEA,QAAA;EApDH;;;;;;EAkED,MAAA,CAAO,GAAA,EAAK,GAAA;EArBR;;;;;;;;;;;;;;EAgDJ,MAAA,CAAO,IAAA,EAAM,aAAA;EA0BL;;;EAfR,MAAA,CAAO,IAAA,UAAc,WAAA,EAAa,WAAA,GAAW,GAAA;EAwDtC;;;;;;;;;EAzCP,OAAA,CAAQ,IAAA,EAAM,GAAA;EAkId;;;;;;EA1GA,SAAA,CAAU,OAAA;EAyNH;;;;;AAsDO;EA9Pd,MAAA,CAAO,OAAA,WAAkB,GAAA;EA8RM;;;AAAA;;EArR/B,IAAA,aAAiB,GAAA;;;;;;;EAUjB,QAAA,CAAS,EAAA;;;;;;;;EAgBT,aAAA,CAAc,QAAA,WAAmB,cAAA;;;;;;;;;;;EAoBjC,KAAA;;;;;;;EAkCA,IAAA;;;;;;;;;EAiBM,QAAA,CAAS,OAAA,YAAkB,OAAA;;;;;;;;;;;UA4BhC,gBAAA;;;;UAiCA,kBAAA;;;;;;;;;UAeA,aAAA;;;;UAkBM,KAAA;;;;UA4BA,oBAAA;;;;UAUA,kBAAA;;;;UAgBA,OAAA;AAAA;;;;;;;;;;;;cAgCH,SAAA,EAAS,SAAkB"}
|
package/esm/scheduler.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { Job } from "./job.mjs";
|
|
|
2
2
|
import { hasWorkerRole, rolesLabel } from "./process-roles.mjs";
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../../packages/scheduler/src/scheduler.ts
|
|
6
6
|
/**
|
|
7
7
|
* Scheduler class manages and executes scheduled jobs.
|
|
8
8
|
*
|
package/esm/scheduler.mjs.map
CHANGED
|
@@ -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 { 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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"}
|
|
1
|
+
{"version":3,"file":"scheduler.mjs","names":[],"sources":["../../../../../../../packages/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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"}
|
package/esm/types.d.mts
CHANGED
package/esm/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/scheduler/src/types.ts"],"mappings":";;AAGA;;KAAY,QAAA;;AAAQ;AAKpB;KAAY,GAAA;;;AAAG;AAgBf;;;;KAAY,cAAA;AAKZ;;;AAAA,KAAY,YAAA;EAOO,kDALjB,GAAA,GAAM,GAAA;EAYW;;;;EAPjB,cAAA,GAAiB,cAAA,EAAA;EAEjB,KAAA,WAEA;EAAA,IAAA,WAGE;EADF,KAAA;IACE,IAAA,GAAO,QAAA;IACP,KAAA;EAAA;AAAA;;;;KAOQ,SAAA;EAIV,6CAFA,OAAA,WAMA;EAJA,QAAA,UAQA;EANA,KAAA,YAMU;EAJV,OAAA,WAUmB;EARnB,OAAA,YAQmB;EANnB,UAAA;AAAA;;;;KAMU,SAAA;;;;KAKA,WAAA;EAYA,uCAVV,UAAA;EAEA,KAAA,UASA;EAPA,iBAAA;AAAA;;;;KAMU,eAAA;EACV,WAAA,GAAc,OAAA;EACd,cAAA,GAAiB,OAAA,UAAiB,MAAA,EAAQ,SAAA;EAC1C,WAAA,GAAc,OAAA,UAAiB,KAAA;EAC/B,UAAA,GAAa,OAAA,UAAiB,MAAA;EAC9B,mBAAA;EACA,mBAAA;EACA,gBAAA,GAAmB,SAAA,EAAW,IAAI;AAAA"}
|
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: configure-retry-and-overlap
|
|
11
|
-
description: 'Configure `.retry(maxRetries, delay?, backoffMultiplier?)` for fixed / exponential backoff and `.preventOverlap()` for external-invocation safety. Triggers: `.retry`, `.preventOverlap`, `maxRetries`, `backoffMultiplier`, `JobResult.retries`, `job:error`, `job:skip`; "how do I retry a failed job", "exponential backoff for scheduled job", "prevent concurrent job runs", "stop firing after N consecutive failures"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: events / shutdown —
|
|
11
|
+
description: 'Configure `.retry(maxRetries, delay?, backoffMultiplier?)` for fixed / exponential backoff and `.preventOverlap()` for external-invocation safety. Triggers: `.retry`, `.preventOverlap`, `maxRetries`, `backoffMultiplier`, `JobResult.retries`, `job:error`, `job:skip`; "how do I retry a failed job", "exponential backoff for scheduled job", "prevent concurrent job runs", "stop firing after N consecutive failures"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: events / shutdown — the `observe-scheduler` topic; building the schedule itself — the `schedule-fluently` topic; competing libs `p-retry`, `async-retry`, `bullmq`.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Retry, backoff, and overlap
|
|
@@ -141,8 +141,8 @@ If retries push the total work past the next interval, `preventOverlap()` ensure
|
|
|
141
141
|
|
|
142
142
|
## See also
|
|
143
143
|
|
|
144
|
-
-
|
|
145
|
-
-
|
|
144
|
+
- The `observe-scheduler` topic — full event reference, `JobResult` shape, lifecycle
|
|
145
|
+
- The `schedule-fluently` topic — the scheduling methods these compose with
|
|
146
146
|
|
|
147
147
|
## Running on one server only: `onOneServer()`
|
|
148
148
|
|
|
@@ -164,7 +164,7 @@ job("sync", sync).everyMinutes(5).onOneServer({ lockTtl: "10m", key: "sync-v2" }
|
|
|
164
164
|
|
|
165
165
|
---
|
|
166
166
|
name: observe-scheduler
|
|
167
|
-
description: 'Subscribe to scheduler lifecycle and job events for logging / metrics / alerting / graceful shutdown — seven typed `SchedulerEvents`, `JobResult` payload, `start()` / `stop()` / `shutdown()`. Triggers: `scheduler.on`, `scheduler.shutdown`, `JobResult`, `job:complete`, `job:error`, `scheduler:started`, `scheduler.getJob`; "graceful SIGTERM shutdown", "did this job run", "scheduler metrics and alerts", "subscribe to job events"; typical import `import { scheduler, type JobResult } from "@warlock.js/scheduler"`. Skip: retry / overlap —
|
|
167
|
+
description: 'Subscribe to scheduler lifecycle and job events for logging / metrics / alerting / graceful shutdown — seven typed `SchedulerEvents`, `JobResult` payload, `start()` / `stop()` / `shutdown()`. Triggers: `scheduler.on`, `scheduler.shutdown`, `JobResult`, `job:complete`, `job:error`, `scheduler:started`, `scheduler.getJob`; "graceful SIGTERM shutdown", "did this job run", "scheduler metrics and alerts", "subscribe to job events"; typical import `import { scheduler, type JobResult } from "@warlock.js/scheduler"`. Skip: retry / overlap — the `configure-retry-and-overlap` topic; building schedules — the `schedule-fluently` topic; native `EventEmitter`, `process.on`.'
|
|
168
168
|
---
|
|
169
169
|
|
|
170
170
|
# Observability and lifecycle
|
|
@@ -313,8 +313,8 @@ job.cronExpression; // string | null
|
|
|
313
313
|
|
|
314
314
|
## See also
|
|
315
315
|
|
|
316
|
-
-
|
|
317
|
-
-
|
|
316
|
+
- The `configure-retry-and-overlap` topic — what triggers each event in detail
|
|
317
|
+
- The `schedule-fluently` topic — the methods that mutate the state these getters expose
|
|
318
318
|
|
|
319
319
|
|
|
320
320
|
## overview `@warlock.js/scheduler/overview/SKILL.md`
|
|
@@ -350,28 +350,28 @@ Six task skills cover the full surface. Most callers only need `scheduler-basics
|
|
|
350
350
|
|
|
351
351
|
### Foundations
|
|
352
352
|
|
|
353
|
-
####
|
|
353
|
+
#### `scheduler-basics`
|
|
354
354
|
Start here. The `job(name, fn)` factory, the `scheduler` singleton, the 2-primitive surface, UTC default, why the API is factory-first.
|
|
355
355
|
|
|
356
356
|
### Scheduling
|
|
357
357
|
|
|
358
|
-
####
|
|
358
|
+
#### `schedule-fluently`
|
|
359
359
|
Build schedules without writing cron: `.everyMinutes(N)`, `.daily().at("03:00")`, `.weekly().on("monday")`, `.monthly()`, `.beginOf("month")`, `.endOf("year")`, and friends. Reach for this 80% of the time.
|
|
360
360
|
|
|
361
|
-
####
|
|
361
|
+
#### `schedule-with-cron`
|
|
362
362
|
Drop down to `.cron("0 9 * * 1-5")` when the fluent API can't express it (Vixie OR semantics for DOM/DOW, complex multi-value lists). Includes `parseCron()` for previewing next-run times before deploy.
|
|
363
363
|
|
|
364
364
|
### Production concerns
|
|
365
365
|
|
|
366
|
-
####
|
|
366
|
+
#### `configure-retry-and-overlap`
|
|
367
367
|
`.retry(maxRetries, delay?, backoffMultiplier?)` for fixed or exponential backoff; `.preventOverlap()` so a slow run never collides with the next tick. Reach for both anytime a job calls external services.
|
|
368
368
|
|
|
369
|
-
####
|
|
369
|
+
#### `pin-schedule-timezone`
|
|
370
370
|
`.inTimezone("America/New_York")` pins wall-clock fire times across DST and across servers. The default is UTC — if your job description includes "9 AM," you almost certainly want this skill.
|
|
371
371
|
|
|
372
372
|
### Observability + shutdown
|
|
373
373
|
|
|
374
|
-
####
|
|
374
|
+
#### `observe-scheduler`
|
|
375
375
|
The seven typed `SchedulerEvents` (`job:start` / `complete` / `error` / `skip`, `scheduler:started` / `stopped` / `tick`), the `JobResult` payload, `start()` / `stop()` / `shutdown()`. Wire this for logging, metrics, alerting, and graceful SIGTERM handling.
|
|
376
376
|
|
|
377
377
|
## Quick taste
|
|
@@ -413,15 +413,15 @@ process.on("SIGTERM", () => scheduler.shutdown());
|
|
|
413
413
|
|
|
414
414
|
## See also
|
|
415
415
|
|
|
416
|
-
-
|
|
417
|
-
- `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview
|
|
416
|
+
- The `overview` topic of the `warlock-js-core` skill — the parent framework that scheduled jobs typically run alongside.
|
|
417
|
+
- `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview` topic becomes the front-door skill in `.claude/skills/warlock-js-scheduler-overview/`. Every cross-link above names the topic (and the owning skill, across packages) instead of a file path so it survives that flattening.
|
|
418
418
|
|
|
419
419
|
|
|
420
420
|
## pin-schedule-timezone `@warlock.js/scheduler/pin-schedule-timezone/SKILL.md`
|
|
421
421
|
|
|
422
422
|
---
|
|
423
423
|
name: pin-schedule-timezone
|
|
424
|
-
description: 'Pin a job to a specific IANA timezone via `.inTimezone(zone)` so its wall-clock fire time stays correct regardless of server location / DST. Triggers: `.inTimezone`, `America/New_York`, `Europe/Berlin`, `Asia/Tokyo`, `NODE_ICU_DATA`; "schedule job in a timezone", "DST drift on non-UTC server", "multi-region fan-out scheduling", "fire at 9am ET regardless of server"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: fluent interval methods —
|
|
424
|
+
description: 'Pin a job to a specific IANA timezone via `.inTimezone(zone)` so its wall-clock fire time stays correct regardless of server location / DST. Triggers: `.inTimezone`, `America/New_York`, `Europe/Berlin`, `Asia/Tokyo`, `NODE_ICU_DATA`; "schedule job in a timezone", "DST drift on non-UTC server", "multi-region fan-out scheduling", "fire at 9am ET regardless of server"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: fluent interval methods — the `schedule-fluently` topic; cron syntax — the `schedule-with-cron` topic; competing libs `dayjs-timezone`, `luxon`, `date-fns-tz`.'
|
|
425
425
|
---
|
|
426
426
|
|
|
427
427
|
# Per-job timezones
|
|
@@ -530,16 +530,16 @@ Node ships full ICU on most platforms, so all IANA zones work out of the box. If
|
|
|
530
530
|
|
|
531
531
|
## See also
|
|
532
532
|
|
|
533
|
-
-
|
|
534
|
-
-
|
|
535
|
-
-
|
|
533
|
+
- The `schedule-fluently` topic — `at()`, `on()`, `daily()`, etc.
|
|
534
|
+
- The `schedule-with-cron` topic — cron schedules are timezone-aware too
|
|
535
|
+
- The `observe-scheduler` topic — `job.nextRun.toISOString()` always renders in UTC; format with `.tz(zone)` to see local time
|
|
536
536
|
|
|
537
537
|
|
|
538
538
|
## schedule-fluently `@warlock.js/scheduler/schedule-fluently/SKILL.md`
|
|
539
539
|
|
|
540
540
|
---
|
|
541
541
|
name: schedule-fluently
|
|
542
|
-
description: 'Build a job schedule via `every*` / `daily` / `weekly` / `monthly` / `at` / `on` / `beginOf` / `endOf`. Triggers: `.everyMinutes`, `.daily`, `.weekly`, `.monthly`, `.at`, `.on`, `.every`, `.beginOf`, `.endOf`, `.twiceDaily`, `scheduler.newJob`; "run every 5 minutes", "daily at 3am", "monday standup", "first of every month", "last day of month"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: cron —
|
|
542
|
+
description: 'Build a job schedule via `every*` / `daily` / `weekly` / `monthly` / `at` / `on` / `beginOf` / `endOf`. Triggers: `.everyMinutes`, `.daily`, `.weekly`, `.monthly`, `.at`, `.on`, `.every`, `.beginOf`, `.endOf`, `.twiceDaily`, `scheduler.newJob`; "run every 5 minutes", "daily at 3am", "monday standup", "first of every month", "last day of month"; typical import `import { job, scheduler } from "@warlock.js/scheduler"`. Skip: cron — the `schedule-with-cron` topic; timezone — the `pin-schedule-timezone` topic; retry — the `configure-retry-and-overlap` topic; competing `node-cron`, `node-schedule`, `agenda`; native `setInterval`.'
|
|
543
543
|
---
|
|
544
544
|
|
|
545
545
|
# Fluent scheduling API
|
|
@@ -623,7 +623,7 @@ Both accept `"day" | "month" | "year"`.
|
|
|
623
623
|
|
|
624
624
|
## Cron is an alternative, not an addition
|
|
625
625
|
|
|
626
|
-
`.cron("…")` clears any prior interval/`at`/`on`/`beginOf`/`endOf` config — and vice versa. They're mutually exclusive. See
|
|
626
|
+
`.cron("…")` clears any prior interval/`at`/`on`/`beginOf`/`endOf` config — and vice versa. They're mutually exclusive. See the `schedule-with-cron` topic.
|
|
627
627
|
|
|
628
628
|
## Chaining order doesn't matter (for the most part)
|
|
629
629
|
|
|
@@ -675,16 +675,16 @@ j?.cronExpression; // null if using fluent API
|
|
|
675
675
|
|
|
676
676
|
## See also
|
|
677
677
|
|
|
678
|
-
-
|
|
679
|
-
-
|
|
680
|
-
-
|
|
678
|
+
- The `schedule-with-cron` topic — when the fluent API isn't enough
|
|
679
|
+
- The `configure-retry-and-overlap` topic — `.retry()` and `.preventOverlap()` on the same job
|
|
680
|
+
- The `pin-schedule-timezone` topic — `.inTimezone()`
|
|
681
681
|
|
|
682
682
|
|
|
683
683
|
## schedule-with-cron `@warlock.js/scheduler/schedule-with-cron/SKILL.md`
|
|
684
684
|
|
|
685
685
|
---
|
|
686
686
|
name: schedule-with-cron
|
|
687
|
-
description: 'Write `.cron(''…'')` expressions — 5-field syntax, operators (`*` `,` `-` `/`), Vixie OR semantics for DOM / DOW, `parseCron()` preview utility. Triggers: `.cron`, `parseCron`, `CronParser`, `*/5 * * * *`, `0 9 * * 1-5`; "write a cron expression", "every weekday at 9am cron", "preview next cron run", "migrate crontab(5) entry"; typical import `import { parseCron } from "@warlock.js/scheduler"`. Skip: human-readable schedules —
|
|
687
|
+
description: 'Write `.cron(''…'')` expressions — 5-field syntax, operators (`*` `,` `-` `/`), Vixie OR semantics for DOM / DOW, `parseCron()` preview utility. Triggers: `.cron`, `parseCron`, `CronParser`, `*/5 * * * *`, `0 9 * * 1-5`; "write a cron expression", "every weekday at 9am cron", "preview next cron run", "migrate crontab(5) entry"; typical import `import { parseCron } from "@warlock.js/scheduler"`. Skip: human-readable schedules — the `schedule-fluently` topic; timezone — the `pin-schedule-timezone` topic; competing libs `node-cron`, `cron`, `croner`, `cronstrue`.'
|
|
688
688
|
---
|
|
689
689
|
|
|
690
690
|
# Cron expressions
|
|
@@ -799,12 +799,12 @@ Impossible expressions are rejected eagerly at construction (see Validation), so
|
|
|
799
799
|
|
|
800
800
|
## Timezone interaction
|
|
801
801
|
|
|
802
|
-
If the job has `.inTimezone(tz)`, the cron parser receives a timezone-aware `dayjs` object — so `0 9 * * *` with `.inTimezone("Asia/Tokyo")` fires at 09:00 JST. See
|
|
802
|
+
If the job has `.inTimezone(tz)`, the cron parser receives a timezone-aware `dayjs` object — so `0 9 * * *` with `.inTimezone("Asia/Tokyo")` fires at 09:00 JST. See the `pin-schedule-timezone` topic.
|
|
803
803
|
|
|
804
804
|
## See also
|
|
805
805
|
|
|
806
|
-
-
|
|
807
|
-
-
|
|
806
|
+
- The `schedule-fluently` topic — the higher-level alternative for human-readable schedules
|
|
807
|
+
- The `pin-schedule-timezone` topic — pinning a cron schedule to a wall-clock timezone
|
|
808
808
|
|
|
809
809
|
|
|
810
810
|
## scheduler-basics `@warlock.js/scheduler/scheduler-basics/SKILL.md`
|
|
@@ -840,9 +840,9 @@ npm install @warlock.js/scheduler
|
|
|
840
840
|
The 9 things that are true in every scheduler use:
|
|
841
841
|
|
|
842
842
|
1. **Factory-first.** `import { job, scheduler } from "@warlock.js/scheduler"`. Users do not call `new Job()` directly (it works, but `job()` is the documented surface).
|
|
843
|
-
2. **Default timezone is UTC.** `daily().at("09:00")` fires at 09:00 UTC, regardless of the server's clock. Pin a job to wall-clock with `.inTimezone("America/New_York")`. See
|
|
844
|
-
3. **The scheduler awaits each tick.** Concurrent same-job runs from the scheduler's own loop are structurally impossible. `preventOverlap()` is for jobs that ALSO get invoked outside the loop. See
|
|
845
|
-
4. **Cron uses 5-field Vixie semantics.** When both `dayOfMonth` and `dayOfWeek` are restricted (neither is `*`), a date matches if EITHER constraint matches. Standard-cron compatible. See
|
|
843
|
+
2. **Default timezone is UTC.** `daily().at("09:00")` fires at 09:00 UTC, regardless of the server's clock. Pin a job to wall-clock with `.inTimezone("America/New_York")`. See the `pin-schedule-timezone` topic.
|
|
844
|
+
3. **The scheduler awaits each tick.** Concurrent same-job runs from the scheduler's own loop are structurally impossible. `preventOverlap()` is for jobs that ALSO get invoked outside the loop. See the `configure-retry-and-overlap` topic.
|
|
845
|
+
4. **Cron uses 5-field Vixie semantics.** When both `dayOfMonth` and `dayOfWeek` are restricted (neither is `*`), a date matches if EITHER constraint matches. Standard-cron compatible. See the `schedule-with-cron` topic.
|
|
846
846
|
5. **`nextRun` always advances after a run — success OR failure.** A permanently failing job re-fires on its next scheduled slot, not on every tick.
|
|
847
847
|
6. **Validation throws at definition time.** `every(0)`, `at("24:00")`, `on(32)`, `retry(-1)`, malformed cron — all throw immediately when you wire the job, not at runtime.
|
|
848
848
|
7. **`Job.run()` returns a `JobResult`, never throws.** Errors funnel into `result.error`. The scheduler emits `job:error` after all retries are exhausted.
|
|
@@ -892,11 +892,11 @@ scheduler.around((job, run) => storage.run({ job: job.name }, run));
|
|
|
892
892
|
|
|
893
893
|
| If the task is about… | Load |
|
|
894
894
|
| --- | --- |
|
|
895
|
-
| Building schedules via `every*`/`daily`/`weekly`/`monthly`/`at`/`on`/`beginOf`/`endOf` |
|
|
896
|
-
| Writing `.cron("…")` expressions, debugging DOM/DOW behavior, using `parseCron()` for preview |
|
|
897
|
-
| Configuring `.retry()` / exponential backoff, `.preventOverlap()`, understanding failure rescheduling |
|
|
898
|
-
| Subscribing to scheduler events, reading `JobResult`, lifecycle, graceful shutdown |
|
|
899
|
-
| Per-job `.inTimezone()`, multi-region patterns, DST handling |
|
|
895
|
+
| Building schedules via `every*`/`daily`/`weekly`/`monthly`/`at`/`on`/`beginOf`/`endOf` | the `schedule-fluently` topic |
|
|
896
|
+
| Writing `.cron("…")` expressions, debugging DOM/DOW behavior, using `parseCron()` for preview | the `schedule-with-cron` topic |
|
|
897
|
+
| Configuring `.retry()` / exponential backoff, `.preventOverlap()`, understanding failure rescheduling | the `configure-retry-and-overlap` topic |
|
|
898
|
+
| Subscribing to scheduler events, reading `JobResult`, lifecycle, graceful shutdown | the `observe-scheduler` topic |
|
|
899
|
+
| Per-job `.inTimezone()`, multi-region patterns, DST handling | the `pin-schedule-timezone` topic |
|
|
900
900
|
|
|
901
901
|
## When NOT to use this skill
|
|
902
902
|
|