{"version":3,"sources":["../../src/cleanup/index.ts","../../src/utils/time.ts","../../src/cleanup/CleanupScheduler.ts"],"sourcesContent":["export { CleanupScheduler, sleepForCleanup } from './CleanupScheduler.js'\nexport type { MeshCleanupTaskContext, MeshCleanupTaskOptions, MeshCleanupTaskSnapshot, MeshCleanupTaskStatus } from './types.js'\n","export function nowIso(): string {\n  return new Date().toISOString()\n}\n\nexport function formatDurationSince(iso: string): string {\n  const started = new Date(iso).getTime()\n  const diff = Math.max(0, Date.now() - started)\n  const seconds = Math.floor(diff / 1000)\n  if (seconds < 60) return `${seconds}s`\n  const minutes = Math.floor(seconds / 60)\n  if (minutes < 60) return `${minutes}m`\n  const hours = Math.floor(minutes / 60)\n  if (hours < 24) return `${hours}h`\n  const days = Math.floor(hours / 24)\n  return `${days}d`\n}\n\nexport async function sleep(ms: number): Promise<void> {\n  if (ms <= 0) return\n  await new Promise<void>(resolve => setTimeout(resolve, ms))\n}\n","import { sleep } from '../utils/time.js'\nimport type { LeaderElection } from '../leader/LeaderElection.js'\nimport type { LockManager } from '../locks/LockManager.js'\nimport type { MeshCleanupTaskOptions, MeshCleanupTaskSnapshot, MeshCleanupTaskStatus } from './types.js'\n\ninterface RegisteredTask {\n  readonly name: string\n  readonly options: MeshCleanupTaskOptions\n  status: MeshCleanupTaskStatus\n  runs: number\n  failures: number\n  lastRunAt?: string\n  nextRunAt?: string\n  lastDurationMs?: number\n  lastError?: string\n  controller?: AbortController\n}\n\nexport class CleanupScheduler {\n  private readonly tasks = new Map<string, RegisteredTask>()\n  private stopped = false\n\n  public constructor(private readonly deps: { readonly locks?: LockManager; readonly leader?: LeaderElection } = {}) {}\n\n  public task(name: string, options: MeshCleanupTaskOptions): this {\n    if (!name.trim()) throw new Error('Cleanup task name is required.')\n    if (!Number.isFinite(options.intervalMs) || options.intervalMs < 1) throw new Error(`Cleanup task \"${name}\" intervalMs must be positive.`)\n    this.tasks.set(name, { name, options, status: 'idle', runs: 0, failures: 0 })\n    return this\n  }\n\n  public start(): void {\n    for (const task of this.tasks.values()) this.schedule(task, task.options.initialDelayMs ?? 0)\n  }\n\n  public async stop(): Promise<void> {\n    this.stopped = true\n    for (const task of this.tasks.values()) {\n      task.status = 'stopped'\n      task.controller?.abort()\n    }\n  }\n\n  public snapshots(): readonly MeshCleanupTaskSnapshot[] {\n    return Array.from(this.tasks.values()).map(task => ({\n      name: task.name,\n      status: task.status,\n      intervalMs: task.options.intervalMs,\n      ...(task.lastRunAt ? { lastRunAt: task.lastRunAt } : {}),\n      ...(task.nextRunAt ? { nextRunAt: task.nextRunAt } : {}),\n      ...(task.lastDurationMs !== undefined ? { lastDurationMs: task.lastDurationMs } : {}),\n      ...(task.lastError ? { lastError: task.lastError } : {}),\n      runs: task.runs,\n      failures: task.failures\n    })).sort((a, b) => a.name.localeCompare(b.name))\n  }\n\n  private schedule(task: RegisteredTask, delayMs: number): void {\n    if (this.stopped) return\n    const jitter = task.options.jitterMs ? Math.floor(Math.random() * task.options.jitterMs) : 0\n    const wait = Math.max(0, delayMs + jitter)\n    task.nextRunAt = new Date(Date.now() + wait).toISOString()\n    const timer = setTimeout(() => { void this.runTask(task) }, wait)\n    timer.unref?.()\n  }\n\n  private async runTask(task: RegisteredTask): Promise<void> {\n    if (this.stopped) return\n    const started = Date.now()\n    const controller = new AbortController()\n    task.controller = controller\n    task.status = 'running'\n    task.lastRunAt = new Date(started).toISOString()\n    let timeout: NodeJS.Timeout | null = null\n    if (task.options.maxRuntimeMs) {\n      timeout = setTimeout(() => controller.abort(), task.options.maxRuntimeMs)\n      timeout.unref?.()\n    }\n\n    try {\n      await this.runProtected(task, controller)\n      task.status = 'succeeded'\n      task.runs += 1\n      delete task.lastError\n    } catch (error) {\n      task.status = 'failed'\n      task.failures += 1\n      task.lastError = error instanceof Error ? error.message : String(error)\n    } finally {\n      if (timeout) clearTimeout(timeout)\n      delete task.controller\n      task.lastDurationMs = Date.now() - started\n      if (!this.stopped) this.schedule(task, task.options.intervalMs)\n    }\n  }\n\n  private async runProtected(task: RegisteredTask, controller: AbortController): Promise<void> {\n    const leaderGroup = typeof task.options.leader === 'string' ? task.options.leader : task.options.leader ? `cleanup:${task.name}` : undefined\n    const lockKey = task.options.lockKey ?? (task.options.leader ? undefined : `cleanup:${task.name}`)\n    const execute = async (): Promise<void> => {\n      await task.options.run({ task: task.name, signal: controller.signal, startedAt: new Date() })\n    }\n    if (leaderGroup) {\n      if (!this.deps.leader) throw new Error(`Cleanup task \"${task.name}\" requires a leader election dependency.`)\n      const result = await this.deps.leader.runWhenLeader(leaderGroup, async signal => {\n        signal.addEventListener('abort', () => controller.abort(), { once: true })\n        await execute()\n      }, { ttlMs: Math.max(task.options.intervalMs * 2, 30_000), metadata: { task: task.name } })\n      if (result === null) task.status = 'skipped'\n      return\n    }\n    if (lockKey) {\n      if (!this.deps.locks) throw new Error(`Cleanup task \"${task.name}\" requires a lock dependency.`)\n      await this.deps.locks.runExclusive(lockKey, async () => execute(), { ttlMs: Math.max(task.options.intervalMs * 2, 30_000), waitMs: 0, metadata: { task: task.name, kind: 'cleanup' } })\n      return\n    }\n    await execute()\n  }\n}\n\nexport async function sleepForCleanup(ms: number, signal?: AbortSignal): Promise<void> {\n  if (!signal) return sleep(ms)\n  await new Promise<void>((resolve, reject) => {\n    const timer = setTimeout(resolve, ms)\n    const abort = (): void => {\n      clearTimeout(timer)\n      reject(new Error('Cleanup sleep aborted.'))\n    }\n    signal.addEventListener('abort', abort, { once: true })\n  })\n}\n"],"mappings":"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","names":[]}