@ecosy/schedule 0.1.0 → 0.1.1

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.
Files changed (63) hide show
  1. package/dist/cron.d.ts +44 -0
  2. package/dist/cron.d.ts.map +1 -0
  3. package/dist/cron.js +2 -0
  4. package/dist/cron.js.map +1 -0
  5. package/dist/cron.mjs +2 -0
  6. package/dist/cron.mjs.map +1 -0
  7. package/dist/hook/index.d.ts +32 -0
  8. package/dist/hook/index.d.ts.map +1 -0
  9. package/dist/hook/index.js +2 -0
  10. package/dist/hook/index.js.map +1 -0
  11. package/dist/hook/index.mjs +2 -0
  12. package/dist/hook/index.mjs.map +1 -0
  13. package/{src/index.ts → dist/index.d.ts} +1 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/index.mjs +2 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/registry.d.ts +39 -0
  20. package/dist/registry.d.ts.map +1 -0
  21. package/dist/registry.js +2 -0
  22. package/dist/registry.js.map +1 -0
  23. package/dist/registry.mjs +2 -0
  24. package/dist/registry.mjs.map +1 -0
  25. package/dist/runner.d.ts +12 -0
  26. package/dist/runner.d.ts.map +1 -0
  27. package/dist/runner.js +2 -0
  28. package/dist/runner.js.map +1 -0
  29. package/dist/runner.mjs +2 -0
  30. package/dist/runner.mjs.map +1 -0
  31. package/dist/schedule.d.ts +121 -0
  32. package/dist/schedule.d.ts.map +1 -0
  33. package/dist/schedule.js +2 -0
  34. package/dist/schedule.js.map +1 -0
  35. package/dist/schedule.mjs +2 -0
  36. package/dist/schedule.mjs.map +1 -0
  37. package/dist/source/index.d.ts +37 -0
  38. package/dist/source/index.d.ts.map +1 -0
  39. package/dist/source/index.js +3 -0
  40. package/dist/source/index.js.map +1 -0
  41. package/dist/source/index.mjs +3 -0
  42. package/dist/source/index.mjs.map +1 -0
  43. package/dist/task.d.ts +60 -0
  44. package/dist/task.d.ts.map +1 -0
  45. package/dist/task.js +2 -0
  46. package/dist/task.js.map +1 -0
  47. package/dist/task.mjs +2 -0
  48. package/dist/task.mjs.map +1 -0
  49. package/{src/types.ts → dist/types.d.ts} +55 -67
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +2 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/types.mjs +2 -0
  54. package/dist/types.mjs.map +1 -0
  55. package/package.json +25 -9
  56. package/src/cron.ts +0 -368
  57. package/src/hook/index.ts +0 -101
  58. package/src/registry.ts +0 -100
  59. package/src/runner.ts +0 -150
  60. package/src/schedule.ts +0 -387
  61. package/src/source/index.ts +0 -126
  62. package/src/task.ts +0 -124
  63. package/tsconfig.json +0 -23
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","sources":["../src/schedule.ts"],"sourcesContent":["import { Registry, type IRegistry } from \"./registry\";\nimport { DEFAULT_TIMEOUT, runTarget } from \"./runner\";\nimport { Task, type TaskStatus } from \"./task\";\nimport type {\n ClassType, Coordinator, ErrorHandler, Hook, InjectMap, Injected, NotFoundHandler,\n Source, TaskDefinition, TaskEvent, TaskParser,\n} from \"./types\";\n\n/** How often due tasks are checked. One second, because expressions have a seconds field. */\nconst DEFAULT_TICK = 1_000;\nconst DEFAULT_SYNC = 60_000;\nconst DEFAULT_RETRY = 0;\n\n/** What happens to a task that disappears from the source. */\nexport type CascadePolicy =\n /** Stop scheduling, let the run in flight finish, then drop it. */\n | \"drain\"\n /** Stop scheduling now. A file target is killed; the other two are let go — see `runner`. */\n | \"stop\"\n /** Leave it running. For when the source is not the only authority. */\n | \"keep\";\n\ninterface Descriptor<Entry, Context> {\n injects: InjectMap;\n source?: Source<Entry, Context>;\n parser?: ClassType<TaskParser<Entry>>;\n registry: IRegistry<Context>;\n coordinator?: ClassType<Coordinator>;\n hook?: ClassType<Hook>;\n retry: number;\n timeout: number;\n syncMs: number | false;\n tickMs: number;\n cascade: CascadePolicy;\n onError?: ErrorHandler;\n notFound?: NotFoundHandler;\n}\n\nexport interface ScheduleStatus {\n running: boolean;\n lastSyncAt: Date | null;\n lastSyncOk: boolean | null;\n tasks: TaskStatus[];\n}\n\n/**\n * A configured scheduler.\n *\n * A plain instance with no global state: constructing two gives two, and\n * whether that should happen is the caller's business. Whatever runs this once\n * — a bootstrap, a framework's init — already owns that question, and\n * answering it here as well would only take the choice away.\n */\nexport class ScheduleRunner<Entry = string, Context = unknown> {\n private readonly d: Descriptor<Entry, Context>;\n private readonly tasks = new Map<string, Task>();\n\n private context!: Injected<Context, InjectMap>;\n private parser!: TaskParser<Entry>;\n private coordinator?: Coordinator;\n private hook?: Hook;\n\n private tickTimer: ReturnType<typeof setInterval> | null = null;\n private syncTimer: ReturnType<typeof setInterval> | null = null;\n private inFlight = new Set<Promise<void>>();\n\n private started = false;\n private lastSyncAt: Date | null = null;\n private lastSyncOk: boolean | null = null;\n\n constructor(descriptor: Descriptor<Entry, Context>) {\n this.d = descriptor;\n }\n\n async start(): Promise<void> {\n // Idempotent on a single instance: starting twice must not produce two\n // sets of timers. This says nothing about two separate instances.\n if (this.started) return;\n if (!this.d.source) throw new Error(\"Schedule: a source is required — call .source(...)\");\n if (!this.d.parser) throw new Error(\"Schedule: a task parser is required — call .task(...)\");\n\n this.started = true;\n\n const context = {} as Record<string, unknown>;\n for (const [name, Token] of Object.entries(this.d.injects)) context[name] = new Token();\n this.context = context as Injected<Context, InjectMap>;\n\n this.parser = new this.d.parser();\n this.coordinator = this.d.coordinator && new this.d.coordinator();\n this.hook = this.d.hook && new this.d.hook();\n\n await this.sync();\n\n this.tickTimer = setInterval(() => this.tick(), this.d.tickMs);\n\n /* `false` means the source is read once, at start, and never reconciled\n again — right for a task list baked into the deployment, wrong for one an\n operator edits. The reconcile still happened above, so the tasks are\n loaded either way. */\n if (this.d.syncMs !== false) {\n this.syncTimer = setInterval(() => {\n this.sync().catch((error) => {\n console.warn(\"[schedule] sync failed:\", error);\n });\n }, this.d.syncMs);\n }\n }\n\n /** Clears both timers and waits for runs already in flight. */\n async stop(): Promise<void> {\n if (this.tickTimer) clearInterval(this.tickTimer);\n if (this.syncTimer) clearInterval(this.syncTimer);\n this.tickTimer = this.syncTimer = null;\n this.started = false;\n\n await Promise.allSettled([...this.inFlight]);\n }\n\n status(): ScheduleStatus {\n return {\n running: this.started,\n lastSyncAt: this.lastSyncAt,\n lastSyncOk: this.lastSyncOk,\n tasks: [...this.tasks.values()].map((task) => task.status()),\n };\n }\n\n /* ------------------------------------------------------------ syncing */\n\n private async read(): Promise<Entry[]> {\n return new this.d.source!().read(this.context as Context);\n }\n\n /**\n * Re-reads the source and reconciles.\n *\n * Two failure modes are kept apart on purpose. A source that throws leaves\n * the schedule exactly as it was — a database blip is not an instruction to\n * cancel everything. A source that returns nothing while tasks exist is\n * treated the same way: it is far more likely to be a partial read than a\n * deliberate deletion of every job at once, and getting that wrong wipes a\n * schedule during an incident, which is the worst possible moment.\n */\n async sync(): Promise<void> {\n let entries: Entry[];\n\n try {\n entries = await this.read();\n\n /* A source that answers with something other than a list is a broken\n source, not an empty schedule. Checked here so it goes down the same\n path as a failed read — report it, keep the tasks already running —\n rather than throwing past this try and out of a timer callback. */\n if (!Array.isArray(entries)) {\n throw new Error(`source returned ${typeof entries}, expected an array`);\n }\n\n this.lastSyncOk = true;\n } catch (error) {\n this.lastSyncOk = false;\n this.lastSyncAt = new Date();\n await this.report(`source read failed: ${error instanceof Error ? error.message : String(error)}`);\n return;\n }\n\n this.lastSyncAt = new Date();\n\n const seen = new Set<string>();\n const now = new Date();\n\n for (const entry of entries) {\n let definition: TaskDefinition;\n\n try {\n definition = this.parser.parse(entry);\n } catch (error) {\n await this.report(`unparseable entry: ${error instanceof Error ? error.message : String(error)}`);\n continue;\n }\n\n seen.add(definition.key);\n const existing = this.tasks.get(definition.key);\n\n if (existing) existing.update(definition, now);\n else this.tasks.set(definition.key, new Task(definition, now));\n }\n\n if (entries.length === 0 && this.tasks.size > 0) {\n await this.report(`source returned nothing while ${this.tasks.size} tasks are live — keeping them`);\n return;\n }\n\n for (const [key, task] of this.tasks) {\n if (seen.has(key)) continue;\n if (this.d.cascade === \"keep\") continue;\n\n task.nextAt = null;\n\n // `drain` leaves the entry until the run in flight settles; the tick\n // loop drops it once `running` clears.\n if (this.d.cascade === \"stop\" || !task.running) this.tasks.delete(key);\n }\n }\n\n /* ------------------------------------------------------------ running */\n\n private tick(): void {\n const now = new Date();\n\n for (const [key, task] of this.tasks) {\n // A drained task keeps its slot only until its run settles.\n if (task.nextAt === null && !task.running) {\n this.tasks.delete(key);\n continue;\n }\n\n if (!task.due(now)) continue;\n\n const run = this.run(task).finally(() => this.inFlight.delete(run));\n this.inFlight.add(run);\n }\n }\n\n private async run(task: Task): Promise<void> {\n // Advanced before the run, so a slow task does not drag its own schedule\n // along behind it. Overlap is held off by `running`, not by the clock.\n const scheduledFor = task.advance();\n task.running = true;\n\n try {\n if (this.coordinator && !(await this.coordinator.claim(task.key, scheduledFor))) {\n return; // another instance owns this fire\n }\n\n const definition = task.current;\n const attempts = (definition.retry ?? this.d.retry) + 1;\n const timeout = definition.timeout ?? this.d.timeout;\n\n let event!: TaskEvent;\n\n for (let attempt = 1; attempt <= attempts; attempt++) {\n const startedAt = new Date();\n const outcome = await runTarget(definition, this.d.registry, this.context, timeout, this.d.notFound);\n\n event = {\n key: task.key,\n scheduledFor,\n startedAt,\n durationMs: Date.now() - startedAt.getTime(),\n attempt,\n ok: outcome.ok,\n reason: outcome.reason,\n detail: outcome.detail,\n raw: outcome.raw,\n };\n\n if (outcome.ok) break;\n\n await this.d.onError?.(event, definition);\n\n // Backoff between attempts: retrying a failing endpoint three times in\n // the same millisecond is three failures, not three chances.\n if (attempt < attempts) {\n await new Promise((r) => setTimeout(r, Math.min(2 ** attempt * 1000, 30_000)));\n }\n }\n\n task.record(event.ok, event.startedAt);\n\n // A hook that throws or hangs must not take the run down with it — the\n // work already happened, and reporting is a separate concern.\n await this.safely(() => this.hook?.notify(event));\n await this.safely(() => this.coordinator?.release(task.key, scheduledFor, event));\n } finally {\n task.running = false;\n }\n }\n\n private async safely(work: () => unknown): Promise<void> {\n try {\n await work();\n } catch (error) {\n console.warn(\"[schedule] reporting failed:\", error);\n }\n }\n\n private async report(detail: string): Promise<void> {\n console.warn(`[schedule] ${detail}`);\n\n await this.safely(() =>\n this.d.onError?.(\n { key: \"@schedule\", scheduledFor: new Date(), startedAt: new Date(), durationMs: 0, attempt: 1, ok: false, reason: \"unknown\", detail },\n { key: \"@schedule\", expression: \"\", target: { type: \"handler\", key: \"@schedule\" } },\n ),\n );\n }\n}\n\n/* -------------------------------------------------------------- the chain */\n\nexport interface IScheduleBuilder<Entry = string, Context = unknown>\n extends ClassType<ScheduleRunner<Entry, Context>> {\n source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;\n task(parser: ClassType<TaskParser<Entry>>): IScheduleBuilder<Entry, Context>;\n registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;\n coordinator(coordinator: ClassType<Coordinator>): IScheduleBuilder<Entry, Context>;\n hook(hook: ClassType<Hook>): IScheduleBuilder<Entry, Context>;\n retry(times: number): IScheduleBuilder<Entry, Context>;\n timeout(ms: number): IScheduleBuilder<Entry, Context>;\n /**\n * Whether, and how often, the source is re-read and reconciled against the\n * tasks already running: a source entry that is new becomes a task, one that\n * changed is updated, one that disappeared is handled by `cascade`.\n *\n * `false` turns the repeat off — read once at start and never again.\n * `true` uses the default interval. A number sets it.\n */\n sync(every: number | boolean): IScheduleBuilder<Entry, Context>;\n tick(ms: number): IScheduleBuilder<Entry, Context>;\n cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;\n onError(handler: ErrorHandler): IScheduleBuilder<Entry, Context>;\n notFound(handler: NotFoundHandler): IScheduleBuilder<Entry, Context>;\n}\n\nfunction chain<Entry, Context>(d: Descriptor<Entry, Context>): IScheduleBuilder<Entry, Context> {\n // Each step returns a new class rather than mutating one, so a\n // half-configured chain can be shared as a base and branched.\n const step = <E>(patch: Partial<Descriptor<E, Context>>) =>\n chain({ ...(d as unknown as Descriptor<E, Context>), ...patch });\n\n return class extends ScheduleRunner<Entry, Context> {\n constructor() {\n super(d);\n }\n\n static source<E>(source: Source<E, Context>) { return step<E>({ source }); }\n static task(parser: ClassType<TaskParser<Entry>>) { return step<Entry>({ parser }); }\n static registry(registry: IRegistry<Context>) { return step<Entry>({ registry }); }\n static coordinator(coordinator: ClassType<Coordinator>) { return step<Entry>({ coordinator }); }\n static hook(hook: ClassType<Hook>) { return step<Entry>({ hook }); }\n static retry(times: number) { return step<Entry>({ retry: times }); }\n static timeout(ms: number) { return step<Entry>({ timeout: ms }); }\n static sync(every: number | boolean) {\n const syncMs = every === true ? DEFAULT_SYNC : every;\n return step<Entry>({ syncMs });\n }\n static tick(ms: number) { return step<Entry>({ tickMs: ms }); }\n static cascade(policy: CascadePolicy) { return step<Entry>({ cascade: policy }); }\n static onError(handler: ErrorHandler) { return step<Entry>({ onError: handler }); }\n static notFound(handler: NotFoundHandler) { return step<Entry>({ notFound: handler }); }\n } as unknown as IScheduleBuilder<Entry, Context>;\n}\n\n/**\n * Chain-style configuration for a scheduler.\n *\n * ```ts\n * export const AppSchedule = Schedule({ db: DataSource })\n * .source((ctx) => ctx.db.query(\"select * from crons\"))\n * .task(RowParser)\n * .registry(Registry.add(Registry(\"session.cleanup\", (ctx) => ...)))\n * .coordinator(PgCoordinator)\n * .hook(Hook.combine(LoggerHook, TelegramHook))\n * .retry(2)\n * .sync(30_000)\n * .cascade(\"drain\");\n *\n * const schedule = new AppSchedule();\n * await schedule.start();\n * ```\n *\n * The chain is the class — there is no terminal to call, and whoever holds the\n * instance decides when it starts and stops.\n */\nexport function Schedule<Injects extends InjectMap = Record<string, never>>(\n injects: Injects = {} as Injects,\n): IScheduleBuilder<string, Injected<unknown, Injects>> {\n return chain({\n injects,\n registry: Registry.empty(),\n retry: DEFAULT_RETRY,\n timeout: DEFAULT_TIMEOUT,\n syncMs: DEFAULT_SYNC,\n tickMs: DEFAULT_TICK,\n cascade: \"drain\",\n });\n}\n"],"names":["DEFAULT_TICK","DEFAULT_SYNC","DEFAULT_RETRY","ScheduleRunner","descriptor","__publicField","context","name","Token","error","task","entries","seen","now","entry","definition","existing","Task","key","run","scheduledFor","attempts","timeout","event","attempt","startedAt","outcome","runTarget","r","work","detail","chain","d","step","patch","source","parser","registry","coordinator","hook","times","ms","every","policy","handler","Schedule","injects","Registry","DEFAULT_TIMEOUT"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{Registry as w}from"./registry.mjs";import{runTarget as f,DEFAULT_TIMEOUT as m}from"./runner.mjs";import{Task as p}from"./task.mjs";var g=Object.defineProperty,S=(n,t,e)=>t in n?g(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,i=(n,t,e)=>S(n,typeof t!="symbol"?t+"":t,e);const T=1e3,l=6e4,A=0;class d{constructor(t){i(this,"d"),i(this,"tasks",new Map),i(this,"context"),i(this,"parser"),i(this,"coordinator"),i(this,"hook"),i(this,"tickTimer",null),i(this,"syncTimer",null),i(this,"inFlight",new Set),i(this,"started",!1),i(this,"lastSyncAt",null),i(this,"lastSyncOk",null),this.d=t}async start(){if(this.started)return;if(!this.d.source)throw new Error("Schedule: a source is required \u2014 call .source(...)");if(!this.d.parser)throw new Error("Schedule: a task parser is required \u2014 call .task(...)");this.started=!0;const t={};for(const[e,s]of Object.entries(this.d.injects))t[e]=new s;this.context=t,this.parser=new this.d.parser,this.coordinator=this.d.coordinator&&new this.d.coordinator,this.hook=this.d.hook&&new this.d.hook,await this.sync(),this.tickTimer=setInterval(()=>this.tick(),this.d.tickMs),this.d.syncMs!==!1&&(this.syncTimer=setInterval(()=>{this.sync().catch(e=>{console.warn("[schedule] sync failed:",e)})},this.d.syncMs))}async stop(){this.tickTimer&&clearInterval(this.tickTimer),this.syncTimer&&clearInterval(this.syncTimer),this.tickTimer=this.syncTimer=null,this.started=!1,await Promise.allSettled([...this.inFlight])}status(){return{running:this.started,lastSyncAt:this.lastSyncAt,lastSyncOk:this.lastSyncOk,tasks:[...this.tasks.values()].map(t=>t.status())}}async read(){return new this.d.source().read(this.context)}async sync(){let t;try{if(t=await this.read(),!Array.isArray(t))throw new Error(`source returned ${typeof t}, expected an array`);this.lastSyncOk=!0}catch(r){this.lastSyncOk=!1,this.lastSyncAt=new Date,await this.report(`source read failed: ${r instanceof Error?r.message:String(r)}`);return}this.lastSyncAt=new Date;const e=new Set,s=new Date;for(const r of t){let a;try{a=this.parser.parse(r)}catch(c){await this.report(`unparseable entry: ${c instanceof Error?c.message:String(c)}`);continue}e.add(a.key);const o=this.tasks.get(a.key);o?o.update(a,s):this.tasks.set(a.key,new p(a,s))}if(t.length===0&&this.tasks.size>0){await this.report(`source returned nothing while ${this.tasks.size} tasks are live \u2014 keeping them`);return}for(const[r,a]of this.tasks)e.has(r)||this.d.cascade!=="keep"&&(a.nextAt=null,(this.d.cascade==="stop"||!a.running)&&this.tasks.delete(r))}tick(){const t=new Date;for(const[e,s]of this.tasks){if(s.nextAt===null&&!s.running){this.tasks.delete(e);continue}if(!s.due(t))continue;const r=this.run(s).finally(()=>this.inFlight.delete(r));this.inFlight.add(r)}}async run(t){const e=t.advance();t.running=!0;try{if(this.coordinator&&!await this.coordinator.claim(t.key,e))return;const s=t.current,r=(s.retry??this.d.retry)+1,a=s.timeout??this.d.timeout;let o;for(let c=1;c<=r;c++){const u=new Date,h=await f(s,this.d.registry,this.context,a,this.d.notFound);if(o={key:t.key,scheduledFor:e,startedAt:u,durationMs:Date.now()-u.getTime(),attempt:c,ok:h.ok,reason:h.reason,detail:h.detail,raw:h.raw},h.ok)break;await this.d.onError?.(o,s),c<r&&await new Promise(k=>setTimeout(k,Math.min(2**c*1e3,3e4)))}t.record(o.ok,o.startedAt),await this.safely(()=>this.hook?.notify(o)),await this.safely(()=>this.coordinator?.release(t.key,e,o))}finally{t.running=!1}}async safely(t){try{await t()}catch(e){console.warn("[schedule] reporting failed:",e)}}async report(t){console.warn(`[schedule] ${t}`),await this.safely(()=>this.d.onError?.({key:"@schedule",scheduledFor:new Date,startedAt:new Date,durationMs:0,attempt:1,ok:!1,reason:"unknown",detail:t},{key:"@schedule",expression:"",target:{type:"handler",key:"@schedule"}}))}}function y(n){const t=e=>y({...n,...e});return class extends d{constructor(){super(n)}static source(e){return t({source:e})}static task(e){return t({parser:e})}static registry(e){return t({registry:e})}static coordinator(e){return t({coordinator:e})}static hook(e){return t({hook:e})}static retry(e){return t({retry:e})}static timeout(e){return t({timeout:e})}static sync(e){return t({syncMs:e===!0?l:e})}static tick(e){return t({tickMs:e})}static cascade(e){return t({cascade:e})}static onError(e){return t({onError:e})}static notFound(e){return t({notFound:e})}}}function M(n={}){return y({injects:n,registry:w.empty(),retry:A,timeout:m,syncMs:l,tickMs:T,cascade:"drain"})}export{M as Schedule,d as ScheduleRunner};
2
+ //# sourceMappingURL=schedule.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.mjs","sources":["../src/schedule.ts"],"sourcesContent":["import { Registry, type IRegistry } from \"./registry\";\nimport { DEFAULT_TIMEOUT, runTarget } from \"./runner\";\nimport { Task, type TaskStatus } from \"./task\";\nimport type {\n ClassType, Coordinator, ErrorHandler, Hook, InjectMap, Injected, NotFoundHandler,\n Source, TaskDefinition, TaskEvent, TaskParser,\n} from \"./types\";\n\n/** How often due tasks are checked. One second, because expressions have a seconds field. */\nconst DEFAULT_TICK = 1_000;\nconst DEFAULT_SYNC = 60_000;\nconst DEFAULT_RETRY = 0;\n\n/** What happens to a task that disappears from the source. */\nexport type CascadePolicy =\n /** Stop scheduling, let the run in flight finish, then drop it. */\n | \"drain\"\n /** Stop scheduling now. A file target is killed; the other two are let go — see `runner`. */\n | \"stop\"\n /** Leave it running. For when the source is not the only authority. */\n | \"keep\";\n\ninterface Descriptor<Entry, Context> {\n injects: InjectMap;\n source?: Source<Entry, Context>;\n parser?: ClassType<TaskParser<Entry>>;\n registry: IRegistry<Context>;\n coordinator?: ClassType<Coordinator>;\n hook?: ClassType<Hook>;\n retry: number;\n timeout: number;\n syncMs: number | false;\n tickMs: number;\n cascade: CascadePolicy;\n onError?: ErrorHandler;\n notFound?: NotFoundHandler;\n}\n\nexport interface ScheduleStatus {\n running: boolean;\n lastSyncAt: Date | null;\n lastSyncOk: boolean | null;\n tasks: TaskStatus[];\n}\n\n/**\n * A configured scheduler.\n *\n * A plain instance with no global state: constructing two gives two, and\n * whether that should happen is the caller's business. Whatever runs this once\n * — a bootstrap, a framework's init — already owns that question, and\n * answering it here as well would only take the choice away.\n */\nexport class ScheduleRunner<Entry = string, Context = unknown> {\n private readonly d: Descriptor<Entry, Context>;\n private readonly tasks = new Map<string, Task>();\n\n private context!: Injected<Context, InjectMap>;\n private parser!: TaskParser<Entry>;\n private coordinator?: Coordinator;\n private hook?: Hook;\n\n private tickTimer: ReturnType<typeof setInterval> | null = null;\n private syncTimer: ReturnType<typeof setInterval> | null = null;\n private inFlight = new Set<Promise<void>>();\n\n private started = false;\n private lastSyncAt: Date | null = null;\n private lastSyncOk: boolean | null = null;\n\n constructor(descriptor: Descriptor<Entry, Context>) {\n this.d = descriptor;\n }\n\n async start(): Promise<void> {\n // Idempotent on a single instance: starting twice must not produce two\n // sets of timers. This says nothing about two separate instances.\n if (this.started) return;\n if (!this.d.source) throw new Error(\"Schedule: a source is required — call .source(...)\");\n if (!this.d.parser) throw new Error(\"Schedule: a task parser is required — call .task(...)\");\n\n this.started = true;\n\n const context = {} as Record<string, unknown>;\n for (const [name, Token] of Object.entries(this.d.injects)) context[name] = new Token();\n this.context = context as Injected<Context, InjectMap>;\n\n this.parser = new this.d.parser();\n this.coordinator = this.d.coordinator && new this.d.coordinator();\n this.hook = this.d.hook && new this.d.hook();\n\n await this.sync();\n\n this.tickTimer = setInterval(() => this.tick(), this.d.tickMs);\n\n /* `false` means the source is read once, at start, and never reconciled\n again — right for a task list baked into the deployment, wrong for one an\n operator edits. The reconcile still happened above, so the tasks are\n loaded either way. */\n if (this.d.syncMs !== false) {\n this.syncTimer = setInterval(() => {\n this.sync().catch((error) => {\n console.warn(\"[schedule] sync failed:\", error);\n });\n }, this.d.syncMs);\n }\n }\n\n /** Clears both timers and waits for runs already in flight. */\n async stop(): Promise<void> {\n if (this.tickTimer) clearInterval(this.tickTimer);\n if (this.syncTimer) clearInterval(this.syncTimer);\n this.tickTimer = this.syncTimer = null;\n this.started = false;\n\n await Promise.allSettled([...this.inFlight]);\n }\n\n status(): ScheduleStatus {\n return {\n running: this.started,\n lastSyncAt: this.lastSyncAt,\n lastSyncOk: this.lastSyncOk,\n tasks: [...this.tasks.values()].map((task) => task.status()),\n };\n }\n\n /* ------------------------------------------------------------ syncing */\n\n private async read(): Promise<Entry[]> {\n return new this.d.source!().read(this.context as Context);\n }\n\n /**\n * Re-reads the source and reconciles.\n *\n * Two failure modes are kept apart on purpose. A source that throws leaves\n * the schedule exactly as it was — a database blip is not an instruction to\n * cancel everything. A source that returns nothing while tasks exist is\n * treated the same way: it is far more likely to be a partial read than a\n * deliberate deletion of every job at once, and getting that wrong wipes a\n * schedule during an incident, which is the worst possible moment.\n */\n async sync(): Promise<void> {\n let entries: Entry[];\n\n try {\n entries = await this.read();\n\n /* A source that answers with something other than a list is a broken\n source, not an empty schedule. Checked here so it goes down the same\n path as a failed read — report it, keep the tasks already running —\n rather than throwing past this try and out of a timer callback. */\n if (!Array.isArray(entries)) {\n throw new Error(`source returned ${typeof entries}, expected an array`);\n }\n\n this.lastSyncOk = true;\n } catch (error) {\n this.lastSyncOk = false;\n this.lastSyncAt = new Date();\n await this.report(`source read failed: ${error instanceof Error ? error.message : String(error)}`);\n return;\n }\n\n this.lastSyncAt = new Date();\n\n const seen = new Set<string>();\n const now = new Date();\n\n for (const entry of entries) {\n let definition: TaskDefinition;\n\n try {\n definition = this.parser.parse(entry);\n } catch (error) {\n await this.report(`unparseable entry: ${error instanceof Error ? error.message : String(error)}`);\n continue;\n }\n\n seen.add(definition.key);\n const existing = this.tasks.get(definition.key);\n\n if (existing) existing.update(definition, now);\n else this.tasks.set(definition.key, new Task(definition, now));\n }\n\n if (entries.length === 0 && this.tasks.size > 0) {\n await this.report(`source returned nothing while ${this.tasks.size} tasks are live — keeping them`);\n return;\n }\n\n for (const [key, task] of this.tasks) {\n if (seen.has(key)) continue;\n if (this.d.cascade === \"keep\") continue;\n\n task.nextAt = null;\n\n // `drain` leaves the entry until the run in flight settles; the tick\n // loop drops it once `running` clears.\n if (this.d.cascade === \"stop\" || !task.running) this.tasks.delete(key);\n }\n }\n\n /* ------------------------------------------------------------ running */\n\n private tick(): void {\n const now = new Date();\n\n for (const [key, task] of this.tasks) {\n // A drained task keeps its slot only until its run settles.\n if (task.nextAt === null && !task.running) {\n this.tasks.delete(key);\n continue;\n }\n\n if (!task.due(now)) continue;\n\n const run = this.run(task).finally(() => this.inFlight.delete(run));\n this.inFlight.add(run);\n }\n }\n\n private async run(task: Task): Promise<void> {\n // Advanced before the run, so a slow task does not drag its own schedule\n // along behind it. Overlap is held off by `running`, not by the clock.\n const scheduledFor = task.advance();\n task.running = true;\n\n try {\n if (this.coordinator && !(await this.coordinator.claim(task.key, scheduledFor))) {\n return; // another instance owns this fire\n }\n\n const definition = task.current;\n const attempts = (definition.retry ?? this.d.retry) + 1;\n const timeout = definition.timeout ?? this.d.timeout;\n\n let event!: TaskEvent;\n\n for (let attempt = 1; attempt <= attempts; attempt++) {\n const startedAt = new Date();\n const outcome = await runTarget(definition, this.d.registry, this.context, timeout, this.d.notFound);\n\n event = {\n key: task.key,\n scheduledFor,\n startedAt,\n durationMs: Date.now() - startedAt.getTime(),\n attempt,\n ok: outcome.ok,\n reason: outcome.reason,\n detail: outcome.detail,\n raw: outcome.raw,\n };\n\n if (outcome.ok) break;\n\n await this.d.onError?.(event, definition);\n\n // Backoff between attempts: retrying a failing endpoint three times in\n // the same millisecond is three failures, not three chances.\n if (attempt < attempts) {\n await new Promise((r) => setTimeout(r, Math.min(2 ** attempt * 1000, 30_000)));\n }\n }\n\n task.record(event.ok, event.startedAt);\n\n // A hook that throws or hangs must not take the run down with it — the\n // work already happened, and reporting is a separate concern.\n await this.safely(() => this.hook?.notify(event));\n await this.safely(() => this.coordinator?.release(task.key, scheduledFor, event));\n } finally {\n task.running = false;\n }\n }\n\n private async safely(work: () => unknown): Promise<void> {\n try {\n await work();\n } catch (error) {\n console.warn(\"[schedule] reporting failed:\", error);\n }\n }\n\n private async report(detail: string): Promise<void> {\n console.warn(`[schedule] ${detail}`);\n\n await this.safely(() =>\n this.d.onError?.(\n { key: \"@schedule\", scheduledFor: new Date(), startedAt: new Date(), durationMs: 0, attempt: 1, ok: false, reason: \"unknown\", detail },\n { key: \"@schedule\", expression: \"\", target: { type: \"handler\", key: \"@schedule\" } },\n ),\n );\n }\n}\n\n/* -------------------------------------------------------------- the chain */\n\nexport interface IScheduleBuilder<Entry = string, Context = unknown>\n extends ClassType<ScheduleRunner<Entry, Context>> {\n source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;\n task(parser: ClassType<TaskParser<Entry>>): IScheduleBuilder<Entry, Context>;\n registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;\n coordinator(coordinator: ClassType<Coordinator>): IScheduleBuilder<Entry, Context>;\n hook(hook: ClassType<Hook>): IScheduleBuilder<Entry, Context>;\n retry(times: number): IScheduleBuilder<Entry, Context>;\n timeout(ms: number): IScheduleBuilder<Entry, Context>;\n /**\n * Whether, and how often, the source is re-read and reconciled against the\n * tasks already running: a source entry that is new becomes a task, one that\n * changed is updated, one that disappeared is handled by `cascade`.\n *\n * `false` turns the repeat off — read once at start and never again.\n * `true` uses the default interval. A number sets it.\n */\n sync(every: number | boolean): IScheduleBuilder<Entry, Context>;\n tick(ms: number): IScheduleBuilder<Entry, Context>;\n cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;\n onError(handler: ErrorHandler): IScheduleBuilder<Entry, Context>;\n notFound(handler: NotFoundHandler): IScheduleBuilder<Entry, Context>;\n}\n\nfunction chain<Entry, Context>(d: Descriptor<Entry, Context>): IScheduleBuilder<Entry, Context> {\n // Each step returns a new class rather than mutating one, so a\n // half-configured chain can be shared as a base and branched.\n const step = <E>(patch: Partial<Descriptor<E, Context>>) =>\n chain({ ...(d as unknown as Descriptor<E, Context>), ...patch });\n\n return class extends ScheduleRunner<Entry, Context> {\n constructor() {\n super(d);\n }\n\n static source<E>(source: Source<E, Context>) { return step<E>({ source }); }\n static task(parser: ClassType<TaskParser<Entry>>) { return step<Entry>({ parser }); }\n static registry(registry: IRegistry<Context>) { return step<Entry>({ registry }); }\n static coordinator(coordinator: ClassType<Coordinator>) { return step<Entry>({ coordinator }); }\n static hook(hook: ClassType<Hook>) { return step<Entry>({ hook }); }\n static retry(times: number) { return step<Entry>({ retry: times }); }\n static timeout(ms: number) { return step<Entry>({ timeout: ms }); }\n static sync(every: number | boolean) {\n const syncMs = every === true ? DEFAULT_SYNC : every;\n return step<Entry>({ syncMs });\n }\n static tick(ms: number) { return step<Entry>({ tickMs: ms }); }\n static cascade(policy: CascadePolicy) { return step<Entry>({ cascade: policy }); }\n static onError(handler: ErrorHandler) { return step<Entry>({ onError: handler }); }\n static notFound(handler: NotFoundHandler) { return step<Entry>({ notFound: handler }); }\n } as unknown as IScheduleBuilder<Entry, Context>;\n}\n\n/**\n * Chain-style configuration for a scheduler.\n *\n * ```ts\n * export const AppSchedule = Schedule({ db: DataSource })\n * .source((ctx) => ctx.db.query(\"select * from crons\"))\n * .task(RowParser)\n * .registry(Registry.add(Registry(\"session.cleanup\", (ctx) => ...)))\n * .coordinator(PgCoordinator)\n * .hook(Hook.combine(LoggerHook, TelegramHook))\n * .retry(2)\n * .sync(30_000)\n * .cascade(\"drain\");\n *\n * const schedule = new AppSchedule();\n * await schedule.start();\n * ```\n *\n * The chain is the class — there is no terminal to call, and whoever holds the\n * instance decides when it starts and stops.\n */\nexport function Schedule<Injects extends InjectMap = Record<string, never>>(\n injects: Injects = {} as Injects,\n): IScheduleBuilder<string, Injected<unknown, Injects>> {\n return chain({\n injects,\n registry: Registry.empty(),\n retry: DEFAULT_RETRY,\n timeout: DEFAULT_TIMEOUT,\n syncMs: DEFAULT_SYNC,\n tickMs: DEFAULT_TICK,\n cascade: \"drain\",\n });\n}\n"],"names":["DEFAULT_TICK","DEFAULT_SYNC","DEFAULT_RETRY","ScheduleRunner","descriptor","__publicField","context","name","Token","error","task","entries","seen","now","entry","definition","existing","Task","key","run","scheduledFor","attempts","timeout","event","attempt","startedAt","outcome","runTarget","r","work","detail","chain","d","step","patch","source","parser","registry","coordinator","hook","times","ms","every","policy","handler","Schedule","injects","Registry","DEFAULT_TIMEOUT"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { ClassType, Promisable, SourceAdapter, TaskDefinition, TaskParser } from "../types";
2
+ /**
3
+ * Built-in sources and the line format they produce.
4
+ *
5
+ * Only local files and HTTP: sharing a file between instances behind a load
6
+ * balancer is a mount, not a library concern.
7
+ */
8
+ /**
9
+ * Wraps a reader function into a source class.
10
+ *
11
+ * `.source()` takes a class, not a callback, so that nothing in the scheduler
12
+ * has to work out at runtime what it was handed. This keeps the one-line case
13
+ * one line without reintroducing that ambiguity.
14
+ */
15
+ export declare function Source<Entry = string, Context = unknown>(read: (context: Context) => Promisable<Entry[]>): ClassType<SourceAdapter<Entry, Context>>;
16
+ export declare function FileSource(path: string): ClassType<SourceAdapter<string, unknown>>;
17
+ export declare function HttpSource(url: string, init?: RequestInit): ClassType<SourceAdapter<string, unknown>>;
18
+ /**
19
+ * Crontab-shaped, one task per line:
20
+ *
21
+ * ```
22
+ * # every five minutes
23
+ * 0 *\/5 * * * * handler:session.cleanup
24
+ * 0 0 3 * * * api:https://app/internal/report tz=Asia/Ho_Chi_Minh retry=2
25
+ * 0 0 4 * * * file:./scripts/rollup.js name=nightly-rollup
26
+ * ```
27
+ *
28
+ * The target doubles as the key, since one target on one schedule is the usual
29
+ * case. `name=` overrides it, which is what you need to run the same handler on
30
+ * two different expressions.
31
+ */
32
+ export declare class LineParser implements TaskParser<string> {
33
+ parse(line: string): TaskDefinition;
34
+ private target;
35
+ private options;
36
+ }
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/source/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,UAAU,EAAc,MAAM,UAAU,CAAC;AAE7G;;;;;GAKG;AAEH;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,KAAK,GAAG,MAAM,EAAE,OAAO,GAAG,OAAO,EACtD,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,UAAU,CAAC,KAAK,EAAE,CAAC,GAC9C,SAAS,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAM1C;AAUD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAOlF;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAYrG;AAID;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAW,YAAW,UAAU,CAAC,MAAM,CAAC;IACnD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc;IA0BnC,OAAO,CAAC,MAAM;IAed,OAAO,CAAC,OAAO;CAUhB"}
@@ -0,0 +1,3 @@
1
+ "use strict";var l=Object.create;var o=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var p=Object.getPrototypeOf,h=Object.prototype.hasOwnProperty;var w=(t,e,r,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of d(e))!h.call(t,i)&&i!==r&&o(t,i,{get:()=>e[i],enumerable:!(n=f(e,i))||n.enumerable});return t};var m=(t,e,r)=>(r=t!=null?l(p(t)):{},w(e||!t||!t.__esModule?o(r,"default",{value:t,enumerable:!0}):r,t));function y(t){return class{read(e){return t(e)}}}function c(t){return t.split(`
2
+ `).map(e=>e.trim()).filter(e=>e.length>0&&!e.startsWith("#"))}function S(t){return class{async read(){const{readFile:e}=await import("node:fs/promises");return c(await e(t,"utf-8"))}}}function $(t,e){return class{async read(){const r=await fetch(t,e);if(!r.ok)throw new Error(`HttpSource: ${t} answered ${r.status}`);return c(await r.text())}}}class g{parse(e){const r=e.split(/\s+/),n=r.findIndex(u=>/^(handler|api|file):/.test(u));if(n<5||n>6)throw new Error(`LineParser: expected a handler:/api:/file: target after 5 or 6 cron fields \u2014 "${e}"`);const i=r.slice(0,n).join(" "),a=this.target(r[n],e),s=this.options(r.slice(n+1));return{key:s.name??r[n],expression:i,target:a,timezone:s.tz,enabled:s.enabled!=="false",retry:s.retry===void 0?void 0:Number(s.retry),timeout:s.timeout===void 0?void 0:Number(s.timeout)}}target(e,r){const n=e.indexOf(":"),i=e.slice(0,n),a=e.slice(n+1);if(!a)throw new Error(`LineParser: "${i}:" has no value \u2014 "${r}"`);switch(i){case"handler":return{type:"handler",key:a};case"api":return{type:"api",url:a};case"file":return{type:"file",path:a};default:throw new Error(`LineParser: unknown target "${i}" \u2014 "${r}"`)}}options(e){const r={};for(const n of e){const i=n.indexOf("=");i>0&&(r[n.slice(0,i)]=n.slice(i+1))}return r}}exports.FileSource=S,exports.HttpSource=$,exports.LineParser=g,exports.Source=y;
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/source/index.ts"],"sourcesContent":["import type { ClassType, Promisable, SourceAdapter, TaskDefinition, TaskParser, TaskTarget } from \"../types\";\n\n/**\n * Built-in sources and the line format they produce.\n *\n * Only local files and HTTP: sharing a file between instances behind a load\n * balancer is a mount, not a library concern.\n */\n\n/**\n * Wraps a reader function into a source class.\n *\n * `.source()` takes a class, not a callback, so that nothing in the scheduler\n * has to work out at runtime what it was handed. This keeps the one-line case\n * one line without reintroducing that ambiguity.\n */\nexport function Source<Entry = string, Context = unknown>(\n read: (context: Context) => Promisable<Entry[]>,\n): ClassType<SourceAdapter<Entry, Context>> {\n return class implements SourceAdapter<Entry, Context> {\n read(context: Context) {\n return read(context);\n }\n };\n}\n\n/** Drops comments and blank lines. Everything else is a task. */\nfunction lines(text: string): string[] {\n return text\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line.length > 0 && !line.startsWith(\"#\"));\n}\n\nexport function FileSource(path: string): ClassType<SourceAdapter<string, unknown>> {\n return class implements SourceAdapter<string, unknown> {\n async read(): Promise<string[]> {\n const { readFile } = await import(\"node:fs/promises\");\n return lines(await readFile(path, \"utf-8\"));\n }\n };\n}\n\nexport function HttpSource(url: string, init?: RequestInit): ClassType<SourceAdapter<string, unknown>> {\n return class implements SourceAdapter<string, unknown> {\n async read(): Promise<string[]> {\n const response = await fetch(url, init);\n\n // Thrown, not swallowed: the scheduler treats a failed read as \"keep\n // what we have\", and it can only do that if it hears about the failure.\n if (!response.ok) throw new Error(`HttpSource: ${url} answered ${response.status}`);\n\n return lines(await response.text());\n }\n };\n}\n\n/* ------------------------------------------------------------ line format */\n\n/**\n * Crontab-shaped, one task per line:\n *\n * ```\n * # every five minutes\n * 0 *\\/5 * * * * handler:session.cleanup\n * 0 0 3 * * * api:https://app/internal/report tz=Asia/Ho_Chi_Minh retry=2\n * 0 0 4 * * * file:./scripts/rollup.js name=nightly-rollup\n * ```\n *\n * The target doubles as the key, since one target on one schedule is the usual\n * case. `name=` overrides it, which is what you need to run the same handler on\n * two different expressions.\n */\nexport class LineParser implements TaskParser<string> {\n parse(line: string): TaskDefinition {\n const tokens = line.split(/\\s+/);\n\n // Five cron fields or six, then the target: count from the target\n // backwards would be ambiguous, so find it by its `kind:` prefix.\n const targetAt = tokens.findIndex((token) => /^(handler|api|file):/.test(token));\n\n if (targetAt < 5 || targetAt > 6) {\n throw new Error(`LineParser: expected a handler:/api:/file: target after 5 or 6 cron fields — \"${line}\"`);\n }\n\n const expression = tokens.slice(0, targetAt).join(\" \");\n const target = this.target(tokens[targetAt], line);\n const options = this.options(tokens.slice(targetAt + 1));\n\n return {\n key: options.name ?? tokens[targetAt],\n expression,\n target,\n timezone: options.tz,\n enabled: options.enabled !== \"false\",\n retry: options.retry === undefined ? undefined : Number(options.retry),\n timeout: options.timeout === undefined ? undefined : Number(options.timeout),\n };\n }\n\n private target(token: string, line: string): TaskTarget {\n const at = token.indexOf(\":\");\n const kind = token.slice(0, at);\n const rest = token.slice(at + 1);\n\n if (!rest) throw new Error(`LineParser: \"${kind}:\" has no value — \"${line}\"`);\n\n switch (kind) {\n case \"handler\": return { type: \"handler\", key: rest };\n case \"api\": return { type: \"api\", url: rest };\n case \"file\": return { type: \"file\", path: rest };\n default: throw new Error(`LineParser: unknown target \"${kind}\" — \"${line}\"`);\n }\n }\n\n private options(tokens: string[]): Record<string, string | undefined> {\n const options: Record<string, string> = {};\n\n for (const token of tokens) {\n const at = token.indexOf(\"=\");\n if (at > 0) options[token.slice(0, at)] = token.slice(at + 1);\n }\n\n return options;\n }\n}\n"],"names":["Source","read","context","lines","text","line","FileSource","path","readFile","HttpSource","url","init","response","LineParser","tokens","targetAt","token","expression","target","options","at","kind","rest"],"mappings":"wdAgBO,SAASA,EACdC,EAC0C,CAC1C,OAAO,KAA+C,CACpD,KAAKC,EAAkB,CACrB,OAAOD,EAAKC,CAAO,CACrB,CACF,CACF,CAGA,SAASC,EAAMC,EAAwB,CACrC,OAAOA,EACJ,MAAM;AAAA,CAAI,EACV,IAAKC,GAASA,EAAK,MAAM,EACzB,OAAQA,GAASA,EAAK,OAAS,GAAK,CAACA,EAAK,WAAW,GAAG,CAAC,CAC9D,CAEO,SAASC,EAAWC,EAAyD,CAClF,OAAO,KAAgD,CACrD,MAAM,MAA0B,CAC9B,KAAM,CAAE,SAAAC,CAAS,EAAI,KAAM,QAAO,kBAAkB,EACpD,OAAOL,EAAM,MAAMK,EAASD,EAAM,OAAO,CAAC,CAC5C,CACF,CACF,CAEO,SAASE,EAAWC,EAAaC,EAA+D,CACrG,OAAO,KAAgD,CACrD,MAAM,MAA0B,CAC9B,MAAMC,EAAW,MAAM,MAAMF,EAAKC,CAAI,EAItC,GAAI,CAACC,EAAS,GAAI,MAAM,IAAI,MAAM,eAAeF,CAAG,aAAaE,EAAS,MAAM,EAAE,EAElF,OAAOT,EAAM,MAAMS,EAAS,MAAM,CACpC,CACF,CACF,CAkBO,MAAMC,CAAyC,CACpD,MAAMR,EAA8B,CAClC,MAAMS,EAAST,EAAK,MAAM,KAAK,EAIzBU,EAAWD,EAAO,UAAWE,GAAU,uBAAuB,KAAKA,CAAK,CAAC,EAE/E,GAAID,EAAW,GAAKA,EAAW,EAC7B,MAAM,IAAI,MAAM,sFAAiFV,CAAI,GAAG,EAG1G,MAAMY,EAAaH,EAAO,MAAM,EAAGC,CAAQ,EAAE,KAAK,GAAG,EAC/CG,EAAS,KAAK,OAAOJ,EAAOC,CAAQ,EAAGV,CAAI,EAC3Cc,EAAU,KAAK,QAAQL,EAAO,MAAMC,EAAW,CAAC,CAAC,EAEvD,MAAO,CACL,IAAKI,EAAQ,MAAQL,EAAOC,CAAQ,EACpC,WAAAE,EACA,OAAAC,EACA,SAAUC,EAAQ,GAClB,QAASA,EAAQ,UAAY,QAC7B,MAAOA,EAAQ,QAAU,OAAY,OAAY,OAAOA,EAAQ,KAAK,EACrE,QAASA,EAAQ,UAAY,OAAY,OAAY,OAAOA,EAAQ,OAAO,CAC7E,CACF,CAEQ,OAAOH,EAAeX,EAA0B,CACtD,MAAMe,EAAKJ,EAAM,QAAQ,GAAG,EACtBK,EAAOL,EAAM,MAAM,EAAGI,CAAE,EACxBE,EAAON,EAAM,MAAMI,EAAK,CAAC,EAE/B,GAAI,CAACE,EAAM,MAAM,IAAI,MAAM,gBAAgBD,CAAI,2BAAsBhB,CAAI,GAAG,EAE5E,OAAQgB,EAAAA,CACN,IAAK,UAAW,MAAO,CAAE,KAAM,UAAW,IAAKC,CAAK,EACpD,IAAK,MAAO,MAAO,CAAE,KAAM,MAAO,IAAKA,CAAK,EAC5C,IAAK,OAAQ,MAAO,CAAE,KAAM,OAAQ,KAAMA,CAAK,EAC/C,QAAS,MAAM,IAAI,MAAM,+BAA+BD,CAAI,aAAQhB,CAAI,GAAG,CAC7E,CACF,CAEQ,QAAQS,EAAsD,CACpE,MAAMK,EAAkC,CAAA,EAExC,UAAWH,KAASF,EAAQ,CAC1B,MAAMM,EAAKJ,EAAM,QAAQ,GAAG,EACxBI,EAAK,IAAGD,EAAQH,EAAM,MAAM,EAAGI,CAAE,CAAC,EAAIJ,EAAM,MAAMI,EAAK,CAAC,EAC9D,CAEA,OAAOD,CACT,CACF"}
@@ -0,0 +1,3 @@
1
+ function u(n){return class{read(e){return n(e)}}}function o(n){return n.split(`
2
+ `).map(e=>e.trim()).filter(e=>e.length>0&&!e.startsWith("#"))}function l(n){return class{async read(){const{readFile:e}=await import("node:fs/promises");return o(await e(n,"utf-8"))}}}function f(n,e){return class{async read(){const r=await fetch(n,e);if(!r.ok)throw new Error(`HttpSource: ${n} answered ${r.status}`);return o(await r.text())}}}class d{parse(e){const r=e.split(/\s+/),t=r.findIndex(c=>/^(handler|api|file):/.test(c));if(t<5||t>6)throw new Error(`LineParser: expected a handler:/api:/file: target after 5 or 6 cron fields \u2014 "${e}"`);const i=r.slice(0,t).join(" "),s=this.target(r[t],e),a=this.options(r.slice(t+1));return{key:a.name??r[t],expression:i,target:s,timezone:a.tz,enabled:a.enabled!=="false",retry:a.retry===void 0?void 0:Number(a.retry),timeout:a.timeout===void 0?void 0:Number(a.timeout)}}target(e,r){const t=e.indexOf(":"),i=e.slice(0,t),s=e.slice(t+1);if(!s)throw new Error(`LineParser: "${i}:" has no value \u2014 "${r}"`);switch(i){case"handler":return{type:"handler",key:s};case"api":return{type:"api",url:s};case"file":return{type:"file",path:s};default:throw new Error(`LineParser: unknown target "${i}" \u2014 "${r}"`)}}options(e){const r={};for(const t of e){const i=t.indexOf("=");i>0&&(r[t.slice(0,i)]=t.slice(i+1))}return r}}export{l as FileSource,f as HttpSource,d as LineParser,u as Source};
3
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../../src/source/index.ts"],"sourcesContent":["import type { ClassType, Promisable, SourceAdapter, TaskDefinition, TaskParser, TaskTarget } from \"../types\";\n\n/**\n * Built-in sources and the line format they produce.\n *\n * Only local files and HTTP: sharing a file between instances behind a load\n * balancer is a mount, not a library concern.\n */\n\n/**\n * Wraps a reader function into a source class.\n *\n * `.source()` takes a class, not a callback, so that nothing in the scheduler\n * has to work out at runtime what it was handed. This keeps the one-line case\n * one line without reintroducing that ambiguity.\n */\nexport function Source<Entry = string, Context = unknown>(\n read: (context: Context) => Promisable<Entry[]>,\n): ClassType<SourceAdapter<Entry, Context>> {\n return class implements SourceAdapter<Entry, Context> {\n read(context: Context) {\n return read(context);\n }\n };\n}\n\n/** Drops comments and blank lines. Everything else is a task. */\nfunction lines(text: string): string[] {\n return text\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line.length > 0 && !line.startsWith(\"#\"));\n}\n\nexport function FileSource(path: string): ClassType<SourceAdapter<string, unknown>> {\n return class implements SourceAdapter<string, unknown> {\n async read(): Promise<string[]> {\n const { readFile } = await import(\"node:fs/promises\");\n return lines(await readFile(path, \"utf-8\"));\n }\n };\n}\n\nexport function HttpSource(url: string, init?: RequestInit): ClassType<SourceAdapter<string, unknown>> {\n return class implements SourceAdapter<string, unknown> {\n async read(): Promise<string[]> {\n const response = await fetch(url, init);\n\n // Thrown, not swallowed: the scheduler treats a failed read as \"keep\n // what we have\", and it can only do that if it hears about the failure.\n if (!response.ok) throw new Error(`HttpSource: ${url} answered ${response.status}`);\n\n return lines(await response.text());\n }\n };\n}\n\n/* ------------------------------------------------------------ line format */\n\n/**\n * Crontab-shaped, one task per line:\n *\n * ```\n * # every five minutes\n * 0 *\\/5 * * * * handler:session.cleanup\n * 0 0 3 * * * api:https://app/internal/report tz=Asia/Ho_Chi_Minh retry=2\n * 0 0 4 * * * file:./scripts/rollup.js name=nightly-rollup\n * ```\n *\n * The target doubles as the key, since one target on one schedule is the usual\n * case. `name=` overrides it, which is what you need to run the same handler on\n * two different expressions.\n */\nexport class LineParser implements TaskParser<string> {\n parse(line: string): TaskDefinition {\n const tokens = line.split(/\\s+/);\n\n // Five cron fields or six, then the target: count from the target\n // backwards would be ambiguous, so find it by its `kind:` prefix.\n const targetAt = tokens.findIndex((token) => /^(handler|api|file):/.test(token));\n\n if (targetAt < 5 || targetAt > 6) {\n throw new Error(`LineParser: expected a handler:/api:/file: target after 5 or 6 cron fields — \"${line}\"`);\n }\n\n const expression = tokens.slice(0, targetAt).join(\" \");\n const target = this.target(tokens[targetAt], line);\n const options = this.options(tokens.slice(targetAt + 1));\n\n return {\n key: options.name ?? tokens[targetAt],\n expression,\n target,\n timezone: options.tz,\n enabled: options.enabled !== \"false\",\n retry: options.retry === undefined ? undefined : Number(options.retry),\n timeout: options.timeout === undefined ? undefined : Number(options.timeout),\n };\n }\n\n private target(token: string, line: string): TaskTarget {\n const at = token.indexOf(\":\");\n const kind = token.slice(0, at);\n const rest = token.slice(at + 1);\n\n if (!rest) throw new Error(`LineParser: \"${kind}:\" has no value — \"${line}\"`);\n\n switch (kind) {\n case \"handler\": return { type: \"handler\", key: rest };\n case \"api\": return { type: \"api\", url: rest };\n case \"file\": return { type: \"file\", path: rest };\n default: throw new Error(`LineParser: unknown target \"${kind}\" — \"${line}\"`);\n }\n }\n\n private options(tokens: string[]): Record<string, string | undefined> {\n const options: Record<string, string> = {};\n\n for (const token of tokens) {\n const at = token.indexOf(\"=\");\n if (at > 0) options[token.slice(0, at)] = token.slice(at + 1);\n }\n\n return options;\n }\n}\n"],"names":["Source","read","context","lines","text","line","FileSource","path","readFile","HttpSource","url","init","response","LineParser","tokens","targetAt","token","expression","target","options","at","kind","rest"],"mappings":"AAgBO,SAASA,EACdC,EAC0C,CAC1C,OAAO,KAA+C,CACpD,KAAKC,EAAkB,CACrB,OAAOD,EAAKC,CAAO,CACrB,CACF,CACF,CAGA,SAASC,EAAMC,EAAwB,CACrC,OAAOA,EACJ,MAAM;AAAA,CAAI,EACV,IAAKC,GAASA,EAAK,MAAM,EACzB,OAAQA,GAASA,EAAK,OAAS,GAAK,CAACA,EAAK,WAAW,GAAG,CAAC,CAC9D,CAEO,SAASC,EAAWC,EAAyD,CAClF,OAAO,KAAgD,CACrD,MAAM,MAA0B,CAC9B,KAAM,CAAE,SAAAC,CAAS,EAAI,KAAM,QAAO,kBAAkB,EACpD,OAAOL,EAAM,MAAMK,EAASD,EAAM,OAAO,CAAC,CAC5C,CACF,CACF,CAEO,SAASE,EAAWC,EAAaC,EAA+D,CACrG,OAAO,KAAgD,CACrD,MAAM,MAA0B,CAC9B,MAAMC,EAAW,MAAM,MAAMF,EAAKC,CAAI,EAItC,GAAI,CAACC,EAAS,GAAI,MAAM,IAAI,MAAM,eAAeF,CAAG,aAAaE,EAAS,MAAM,EAAE,EAElF,OAAOT,EAAM,MAAMS,EAAS,MAAM,CACpC,CACF,CACF,CAkBO,MAAMC,CAAyC,CACpD,MAAMR,EAA8B,CAClC,MAAMS,EAAST,EAAK,MAAM,KAAK,EAIzBU,EAAWD,EAAO,UAAWE,GAAU,uBAAuB,KAAKA,CAAK,CAAC,EAE/E,GAAID,EAAW,GAAKA,EAAW,EAC7B,MAAM,IAAI,MAAM,sFAAiFV,CAAI,GAAG,EAG1G,MAAMY,EAAaH,EAAO,MAAM,EAAGC,CAAQ,EAAE,KAAK,GAAG,EAC/CG,EAAS,KAAK,OAAOJ,EAAOC,CAAQ,EAAGV,CAAI,EAC3Cc,EAAU,KAAK,QAAQL,EAAO,MAAMC,EAAW,CAAC,CAAC,EAEvD,MAAO,CACL,IAAKI,EAAQ,MAAQL,EAAOC,CAAQ,EACpC,WAAAE,EACA,OAAAC,EACA,SAAUC,EAAQ,GAClB,QAASA,EAAQ,UAAY,QAC7B,MAAOA,EAAQ,QAAU,OAAY,OAAY,OAAOA,EAAQ,KAAK,EACrE,QAASA,EAAQ,UAAY,OAAY,OAAY,OAAOA,EAAQ,OAAO,CAC7E,CACF,CAEQ,OAAOH,EAAeX,EAA0B,CACtD,MAAMe,EAAKJ,EAAM,QAAQ,GAAG,EACtBK,EAAOL,EAAM,MAAM,EAAGI,CAAE,EACxBE,EAAON,EAAM,MAAMI,EAAK,CAAC,EAE/B,GAAI,CAACE,EAAM,MAAM,IAAI,MAAM,gBAAgBD,CAAI,2BAAsBhB,CAAI,GAAG,EAE5E,OAAQgB,EAAAA,CACN,IAAK,UAAW,MAAO,CAAE,KAAM,UAAW,IAAKC,CAAK,EACpD,IAAK,MAAO,MAAO,CAAE,KAAM,MAAO,IAAKA,CAAK,EAC5C,IAAK,OAAQ,MAAO,CAAE,KAAM,OAAQ,KAAMA,CAAK,EAC/C,QAAS,MAAM,IAAI,MAAM,+BAA+BD,CAAI,aAAQhB,CAAI,GAAG,CAC7E,CACF,CAEQ,QAAQS,EAAsD,CACpE,MAAMK,EAAkC,CAAA,EAExC,UAAWH,KAASF,EAAQ,CAC1B,MAAMM,EAAKJ,EAAM,QAAQ,GAAG,EACxBI,EAAK,IAAGD,EAAQH,EAAM,MAAM,EAAGI,CAAE,CAAC,EAAIJ,EAAM,MAAMI,EAAK,CAAC,EAC9D,CAEA,OAAOD,CACT,CACF"}
package/dist/task.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type { TaskDefinition } from "./types";
2
+ export interface TaskStatus {
3
+ key: string;
4
+ enabled: boolean;
5
+ expression: string;
6
+ timezone: string;
7
+ nextAt: Date | null;
8
+ running: boolean;
9
+ lastRunAt: Date | null;
10
+ lastOk: boolean | null;
11
+ runs: number;
12
+ failures: number;
13
+ }
14
+ /**
15
+ * One scheduled task: its definition, when it fires next, and how it has been
16
+ * doing.
17
+ *
18
+ * The split between the two matters at sync time. A definition that changed
19
+ * gets a freshly computed schedule — the new expression decides the next fire,
20
+ * not an adjustment of the old one — while the history carries over, because
21
+ * "when did this last succeed" is the question people ask after an edit, not
22
+ * before it.
23
+ */
24
+ export declare class Task {
25
+ readonly key: string;
26
+ private definition;
27
+ private cron;
28
+ nextAt: Date | null;
29
+ running: boolean;
30
+ lastRunAt: Date | null;
31
+ lastOk: boolean | null;
32
+ runs: number;
33
+ failures: number;
34
+ constructor(definition: TaskDefinition, from?: Date);
35
+ get enabled(): boolean;
36
+ get timezone(): string;
37
+ get current(): TaskDefinition;
38
+ /** Recomputes the next fire from scratch. History is untouched. */
39
+ private schedule;
40
+ /**
41
+ * Applies a changed definition.
42
+ *
43
+ * Only reparses when the expression or zone actually moved: reparsing on
44
+ * every sync would reset `nextAt` each time and, for anything firing less
45
+ * often than the sync interval, push the fire permanently into the future.
46
+ */
47
+ update(definition: TaskDefinition, from?: Date): void;
48
+ due(now: Date): boolean;
49
+ /**
50
+ * Moves to the fire after this one.
51
+ *
52
+ * Called the moment a run is picked up, before it finishes, so a task that
53
+ * takes longer than its interval does not push its own schedule along behind
54
+ * it. Overlap is prevented by `running`, not by delaying the clock.
55
+ */
56
+ advance(): Date;
57
+ record(ok: boolean, at: Date): void;
58
+ status(): TaskStatus;
59
+ }
60
+ //# sourceMappingURL=task.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../src/task.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;GASG;AACH,qBAAa,IAAI;IACf,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,IAAI,CAAiB;IAE7B,MAAM,EAAE,IAAI,GAAG,IAAI,CAAQ;IAC3B,OAAO,UAAS;IAEhB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAQ;IAC9B,MAAM,EAAE,OAAO,GAAG,IAAI,CAAQ;IAC9B,IAAI,SAAK;IACT,QAAQ,SAAK;gBAED,UAAU,EAAE,cAAc,EAAE,IAAI,GAAE,IAAiB;IAO/D,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,IAAI,OAAO,IAAI,cAAc,CAE5B;IAED,mEAAmE;IACnE,OAAO,CAAC,QAAQ;IAIhB;;;;;;OAMG;IACH,MAAM,CAAC,UAAU,EAAE,cAAc,EAAE,IAAI,GAAE,IAAiB,GAAG,IAAI;IAcjE,GAAG,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO;IAIvB;;;;;;OAMG;IACH,OAAO,IAAI,IAAI;IAMf,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,GAAG,IAAI;IAOnC,MAAM,IAAI,UAAU;CAcrB"}
package/dist/task.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var s=require("./cron.js"),h=Object.defineProperty,a=(n,t,e)=>t in n?h(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,i=(n,t,e)=>a(n,typeof t!="symbol"?t+"":t,e);class l{constructor(t,e=new Date){i(this,"key"),i(this,"definition"),i(this,"cron"),i(this,"nextAt",null),i(this,"running",!1),i(this,"lastRunAt",null),i(this,"lastOk",null),i(this,"runs",0),i(this,"failures",0),this.key=t.key,this.definition=t,this.cron=s.parseCron(t.expression),this.schedule(e)}get enabled(){return this.definition.enabled!==!1}get timezone(){return this.definition.timezone??"UTC"}get current(){return this.definition}schedule(t){this.nextAt=this.enabled?s.nextFire(this.cron,t,this.timezone):null}update(t,e=new Date){const r=t.expression!==this.definition.expression||t.timezone!==this.definition.timezone||t.enabled!==!1!==this.enabled;this.definition=t,r&&(this.cron=s.parseCron(t.expression),this.schedule(e))}due(t){return this.enabled&&!this.running&&this.nextAt!==null&&this.nextAt<=t}advance(){const t=this.nextAt??new Date;return this.nextAt=s.nextFire(this.cron,t,this.timezone),t}record(t,e){this.runs++,t||this.failures++,this.lastRunAt=e,this.lastOk=t}status(){return{key:this.key,enabled:this.enabled,expression:this.definition.expression,timezone:this.timezone,nextAt:this.nextAt,running:this.running,lastRunAt:this.lastRunAt,lastOk:this.lastOk,runs:this.runs,failures:this.failures}}}exports.Task=l;
2
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.js","sources":["../src/task.ts"],"sourcesContent":["import { nextFire, parseCron, type CronExpression } from \"./cron\";\nimport type { TaskDefinition } from \"./types\";\n\nexport interface TaskStatus {\n key: string;\n enabled: boolean;\n expression: string;\n timezone: string;\n nextAt: Date | null;\n running: boolean;\n lastRunAt: Date | null;\n lastOk: boolean | null;\n runs: number;\n failures: number;\n}\n\n/**\n * One scheduled task: its definition, when it fires next, and how it has been\n * doing.\n *\n * The split between the two matters at sync time. A definition that changed\n * gets a freshly computed schedule — the new expression decides the next fire,\n * not an adjustment of the old one — while the history carries over, because\n * \"when did this last succeed\" is the question people ask after an edit, not\n * before it.\n */\nexport class Task {\n readonly key: string;\n\n private definition: TaskDefinition;\n private cron: CronExpression;\n\n nextAt: Date | null = null;\n running = false;\n\n lastRunAt: Date | null = null;\n lastOk: boolean | null = null;\n runs = 0;\n failures = 0;\n\n constructor(definition: TaskDefinition, from: Date = new Date()) {\n this.key = definition.key;\n this.definition = definition;\n this.cron = parseCron(definition.expression);\n this.schedule(from);\n }\n\n get enabled(): boolean {\n return this.definition.enabled !== false;\n }\n\n get timezone(): string {\n return this.definition.timezone ?? \"UTC\";\n }\n\n get current(): TaskDefinition {\n return this.definition;\n }\n\n /** Recomputes the next fire from scratch. History is untouched. */\n private schedule(from: Date): void {\n this.nextAt = this.enabled ? nextFire(this.cron, from, this.timezone) : null;\n }\n\n /**\n * Applies a changed definition.\n *\n * Only reparses when the expression or zone actually moved: reparsing on\n * every sync would reset `nextAt` each time and, for anything firing less\n * often than the sync interval, push the fire permanently into the future.\n */\n update(definition: TaskDefinition, from: Date = new Date()): void {\n const rescheduled =\n definition.expression !== this.definition.expression ||\n definition.timezone !== this.definition.timezone ||\n (definition.enabled !== false) !== this.enabled;\n\n this.definition = definition;\n\n if (rescheduled) {\n this.cron = parseCron(definition.expression);\n this.schedule(from);\n }\n }\n\n due(now: Date): boolean {\n return this.enabled && !this.running && this.nextAt !== null && this.nextAt <= now;\n }\n\n /**\n * Moves to the fire after this one.\n *\n * Called the moment a run is picked up, before it finishes, so a task that\n * takes longer than its interval does not push its own schedule along behind\n * it. Overlap is prevented by `running`, not by delaying the clock.\n */\n advance(): Date {\n const fired = this.nextAt ?? new Date();\n this.nextAt = nextFire(this.cron, fired, this.timezone);\n return fired;\n }\n\n record(ok: boolean, at: Date): void {\n this.runs++;\n if (!ok) this.failures++;\n this.lastRunAt = at;\n this.lastOk = ok;\n }\n\n status(): TaskStatus {\n return {\n key: this.key,\n enabled: this.enabled,\n expression: this.definition.expression,\n timezone: this.timezone,\n nextAt: this.nextAt,\n running: this.running,\n lastRunAt: this.lastRunAt,\n lastOk: this.lastOk,\n runs: this.runs,\n failures: this.failures,\n };\n }\n}\n"],"names":["Task","definition","from","__publicField","parseCron","nextFire","rescheduled","now","fired","ok","at"],"mappings":"sMA0BaA,CAAK,CAchB,YAAYC,EAA4BC,EAAa,IAAI,KAAQ,CAbjEC,EAAA,KAAS,KAAA,EAETA,EAAA,KAAQ,cACRA,EAAA,KAAQ,MAAA,EAERA,EAAA,KAAA,SAAsB,IAAA,EACtBA,EAAA,KAAA,UAAU,EAAA,EAEVA,EAAA,KAAA,YAAyB,IAAA,EACzBA,EAAA,cAAyB,IAAA,EACzBA,EAAA,KAAA,OAAO,CAAA,EACPA,EAAA,KAAA,WAAW,GAGT,KAAK,IAAMF,EAAW,IACtB,KAAK,WAAaA,EAClB,KAAK,KAAOG,EAAAA,UAAUH,EAAW,UAAU,EAC3C,KAAK,SAASC,CAAI,CACpB,CAEA,IAAI,SAAmB,CACrB,OAAO,KAAK,WAAW,UAAY,EACrC,CAEA,IAAI,UAAmB,CACrB,OAAO,KAAK,WAAW,UAAY,KACrC,CAEA,IAAI,SAA0B,CAC5B,OAAO,KAAK,UACd,CAGQ,SAASA,EAAkB,CACjC,KAAK,OAAS,KAAK,QAAUG,EAAAA,SAAS,KAAK,KAAMH,EAAM,KAAK,QAAQ,EAAI,IAC1E,CASA,OAAOD,EAA4BC,EAAa,IAAI,KAAc,CAChE,MAAMI,EACJL,EAAW,aAAe,KAAK,WAAW,YAC1CA,EAAW,WAAa,KAAK,WAAW,UACvCA,EAAW,UAAY,KAAW,KAAK,QAE1C,KAAK,WAAaA,EAEdK,IACF,KAAK,KAAOF,EAAAA,UAAUH,EAAW,UAAU,EAC3C,KAAK,SAASC,CAAI,EAEtB,CAEA,IAAIK,EAAoB,CACtB,OAAO,KAAK,SAAW,CAAC,KAAK,SAAW,KAAK,SAAW,MAAQ,KAAK,QAAUA,CACjF,CASA,SAAgB,CACd,MAAMC,EAAQ,KAAK,QAAU,IAAI,KACjC,OAAA,KAAK,OAASH,EAAAA,SAAS,KAAK,KAAMG,EAAO,KAAK,QAAQ,EAC/CA,CACT,CAEA,OAAOC,EAAaC,EAAgB,CAClC,KAAK,OACAD,GAAI,KAAK,WACd,KAAK,UAAYC,EACjB,KAAK,OAASD,CAChB,CAEA,QAAqB,CACnB,MAAO,CACL,IAAK,KAAK,IACV,QAAS,KAAK,QACd,WAAY,KAAK,WAAW,WAC5B,SAAU,KAAK,SACf,OAAQ,KAAK,OACb,QAAS,KAAK,QACd,UAAW,KAAK,UAChB,OAAQ,KAAK,OACb,KAAM,KAAK,KACX,SAAU,KAAK,QACjB,CACF,CACF"}
package/dist/task.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{parseCron as s,nextFire as r}from"./cron.mjs";var l=Object.defineProperty,o=(n,t,e)=>t in n?l(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,i=(n,t,e)=>o(n,typeof t!="symbol"?t+"":t,e);class a{constructor(t,e=new Date){i(this,"key"),i(this,"definition"),i(this,"cron"),i(this,"nextAt",null),i(this,"running",!1),i(this,"lastRunAt",null),i(this,"lastOk",null),i(this,"runs",0),i(this,"failures",0),this.key=t.key,this.definition=t,this.cron=s(t.expression),this.schedule(e)}get enabled(){return this.definition.enabled!==!1}get timezone(){return this.definition.timezone??"UTC"}get current(){return this.definition}schedule(t){this.nextAt=this.enabled?r(this.cron,t,this.timezone):null}update(t,e=new Date){const h=t.expression!==this.definition.expression||t.timezone!==this.definition.timezone||t.enabled!==!1!==this.enabled;this.definition=t,h&&(this.cron=s(t.expression),this.schedule(e))}due(t){return this.enabled&&!this.running&&this.nextAt!==null&&this.nextAt<=t}advance(){const t=this.nextAt??new Date;return this.nextAt=r(this.cron,t,this.timezone),t}record(t,e){this.runs++,t||this.failures++,this.lastRunAt=e,this.lastOk=t}status(){return{key:this.key,enabled:this.enabled,expression:this.definition.expression,timezone:this.timezone,nextAt:this.nextAt,running:this.running,lastRunAt:this.lastRunAt,lastOk:this.lastOk,runs:this.runs,failures:this.failures}}}export{a as Task};
2
+ //# sourceMappingURL=task.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.mjs","sources":["../src/task.ts"],"sourcesContent":["import { nextFire, parseCron, type CronExpression } from \"./cron\";\nimport type { TaskDefinition } from \"./types\";\n\nexport interface TaskStatus {\n key: string;\n enabled: boolean;\n expression: string;\n timezone: string;\n nextAt: Date | null;\n running: boolean;\n lastRunAt: Date | null;\n lastOk: boolean | null;\n runs: number;\n failures: number;\n}\n\n/**\n * One scheduled task: its definition, when it fires next, and how it has been\n * doing.\n *\n * The split between the two matters at sync time. A definition that changed\n * gets a freshly computed schedule — the new expression decides the next fire,\n * not an adjustment of the old one — while the history carries over, because\n * \"when did this last succeed\" is the question people ask after an edit, not\n * before it.\n */\nexport class Task {\n readonly key: string;\n\n private definition: TaskDefinition;\n private cron: CronExpression;\n\n nextAt: Date | null = null;\n running = false;\n\n lastRunAt: Date | null = null;\n lastOk: boolean | null = null;\n runs = 0;\n failures = 0;\n\n constructor(definition: TaskDefinition, from: Date = new Date()) {\n this.key = definition.key;\n this.definition = definition;\n this.cron = parseCron(definition.expression);\n this.schedule(from);\n }\n\n get enabled(): boolean {\n return this.definition.enabled !== false;\n }\n\n get timezone(): string {\n return this.definition.timezone ?? \"UTC\";\n }\n\n get current(): TaskDefinition {\n return this.definition;\n }\n\n /** Recomputes the next fire from scratch. History is untouched. */\n private schedule(from: Date): void {\n this.nextAt = this.enabled ? nextFire(this.cron, from, this.timezone) : null;\n }\n\n /**\n * Applies a changed definition.\n *\n * Only reparses when the expression or zone actually moved: reparsing on\n * every sync would reset `nextAt` each time and, for anything firing less\n * often than the sync interval, push the fire permanently into the future.\n */\n update(definition: TaskDefinition, from: Date = new Date()): void {\n const rescheduled =\n definition.expression !== this.definition.expression ||\n definition.timezone !== this.definition.timezone ||\n (definition.enabled !== false) !== this.enabled;\n\n this.definition = definition;\n\n if (rescheduled) {\n this.cron = parseCron(definition.expression);\n this.schedule(from);\n }\n }\n\n due(now: Date): boolean {\n return this.enabled && !this.running && this.nextAt !== null && this.nextAt <= now;\n }\n\n /**\n * Moves to the fire after this one.\n *\n * Called the moment a run is picked up, before it finishes, so a task that\n * takes longer than its interval does not push its own schedule along behind\n * it. Overlap is prevented by `running`, not by delaying the clock.\n */\n advance(): Date {\n const fired = this.nextAt ?? new Date();\n this.nextAt = nextFire(this.cron, fired, this.timezone);\n return fired;\n }\n\n record(ok: boolean, at: Date): void {\n this.runs++;\n if (!ok) this.failures++;\n this.lastRunAt = at;\n this.lastOk = ok;\n }\n\n status(): TaskStatus {\n return {\n key: this.key,\n enabled: this.enabled,\n expression: this.definition.expression,\n timezone: this.timezone,\n nextAt: this.nextAt,\n running: this.running,\n lastRunAt: this.lastRunAt,\n lastOk: this.lastOk,\n runs: this.runs,\n failures: this.failures,\n };\n }\n}\n"],"names":["Task","definition","from","__publicField","parseCron","nextFire","rescheduled","now","fired","ok","at"],"mappings":"uNA0BaA,CAAK,CAchB,YAAYC,EAA4BC,EAAa,IAAI,KAAQ,CAbjEC,EAAA,KAAS,KAAA,EAETA,EAAA,KAAQ,cACRA,EAAA,KAAQ,MAAA,EAERA,EAAA,KAAA,SAAsB,IAAA,EACtBA,EAAA,KAAA,UAAU,EAAA,EAEVA,EAAA,KAAA,YAAyB,IAAA,EACzBA,EAAA,cAAyB,IAAA,EACzBA,EAAA,KAAA,OAAO,CAAA,EACPA,EAAA,KAAA,WAAW,GAGT,KAAK,IAAMF,EAAW,IACtB,KAAK,WAAaA,EAClB,KAAK,KAAOG,EAAUH,EAAW,UAAU,EAC3C,KAAK,SAASC,CAAI,CACpB,CAEA,IAAI,SAAmB,CACrB,OAAO,KAAK,WAAW,UAAY,EACrC,CAEA,IAAI,UAAmB,CACrB,OAAO,KAAK,WAAW,UAAY,KACrC,CAEA,IAAI,SAA0B,CAC5B,OAAO,KAAK,UACd,CAGQ,SAASA,EAAkB,CACjC,KAAK,OAAS,KAAK,QAAUG,EAAS,KAAK,KAAMH,EAAM,KAAK,QAAQ,EAAI,IAC1E,CASA,OAAOD,EAA4BC,EAAa,IAAI,KAAc,CAChE,MAAMI,EACJL,EAAW,aAAe,KAAK,WAAW,YAC1CA,EAAW,WAAa,KAAK,WAAW,UACvCA,EAAW,UAAY,KAAW,KAAK,QAE1C,KAAK,WAAaA,EAEdK,IACF,KAAK,KAAOF,EAAUH,EAAW,UAAU,EAC3C,KAAK,SAASC,CAAI,EAEtB,CAEA,IAAIK,EAAoB,CACtB,OAAO,KAAK,SAAW,CAAC,KAAK,SAAW,KAAK,SAAW,MAAQ,KAAK,QAAUA,CACjF,CASA,SAAgB,CACd,MAAMC,EAAQ,KAAK,QAAU,IAAI,KACjC,OAAA,KAAK,OAASH,EAAS,KAAK,KAAMG,EAAO,KAAK,QAAQ,EAC/CA,CACT,CAEA,OAAOC,EAAaC,EAAgB,CAClC,KAAK,OACAD,GAAI,KAAK,WACd,KAAK,UAAYC,EACjB,KAAK,OAASD,CAChB,CAEA,QAAqB,CACnB,MAAO,CACL,IAAK,KAAK,IACV,QAAS,KAAK,QACd,WAAY,KAAK,WAAW,WAC5B,SAAU,KAAK,SACf,OAAQ,KAAK,OACb,QAAS,KAAK,QACd,UAAW,KAAK,UAChB,OAAQ,KAAK,OACb,KAAM,KAAK,KACX,SAAU,KAAK,QACjB,CACF,CACF"}
@@ -6,7 +6,6 @@
6
6
  * when absent, so a scheduler with just a source and a registry is a complete,
7
7
  * working single-instance scheduler.
8
8
  */
9
-
10
9
  /**
11
10
  * A class constructible with no arguments.
12
11
  *
@@ -19,84 +18,77 @@
19
18
  * here: nothing has to work out at runtime what it was given.
20
19
  */
21
20
  export type ClassType<Instance = unknown> = new () => Instance;
22
-
23
21
  export type InjectMap = Record<string, ClassType>;
24
-
25
22
  export type Injected<Context, Injects extends InjectMap> = Context & {
26
- [K in keyof Injects]: Injects[K] extends ClassType<infer Instance> ? Instance : never;
23
+ [K in keyof Injects]: Injects[K] extends ClassType<infer Instance> ? Instance : never;
27
24
  };
28
-
29
25
  export type Promisable<T> = T | Promise<T>;
30
-
31
- /* ------------------------------------------------------------ task shape */
32
-
33
26
  /**
34
27
  * What a task does when it fires.
35
28
  *
36
29
  * Three kinds, and they fail in genuinely different ways — see `TaskFailure`.
37
30
  */
38
- export type TaskTarget =
39
- /** A function registered in code. Fast and typed; cannot be interrupted. */
40
- | { type: "handler"; key: string }
41
- /** An HTTP call. Works across instances; the request can be abandoned but the server keeps going. */
42
- | { type: "api"; url: string; method?: string; headers?: Record<string, string>; body?: string }
43
- /** A child process. The only kind that can genuinely be killed. */
44
- | { type: "file"; path: string; args?: string[] };
45
-
31
+ export type TaskTarget =
32
+ /** A function registered in code. Fast and typed; cannot be interrupted. */
33
+ {
34
+ type: "handler";
35
+ key: string;
36
+ }
37
+ /** An HTTP call. Works across instances; the request can be abandoned but the server keeps going. */
38
+ | {
39
+ type: "api";
40
+ url: string;
41
+ method?: string;
42
+ headers?: Record<string, string>;
43
+ body?: string;
44
+ }
45
+ /** A child process. The only kind that can genuinely be killed. */
46
+ | {
47
+ type: "file";
48
+ path: string;
49
+ args?: string[];
50
+ };
46
51
  /** One task, after a parser has read it out of whatever the source returned. */
47
52
  export interface TaskDefinition {
48
- /** Unique. Used for the registry lookup, for coordination, and as the sync identity. */
49
- key: string;
50
- /** Six-field cron, or five for standard crontab. */
51
- expression: string;
52
- target: TaskTarget;
53
- /** IANA zone. Defaults to UTC — never the host zone, which differs between machines. */
54
- timezone?: string;
55
- /** False keeps the task known but unscheduled, so its history survives a pause. */
56
- enabled?: boolean;
57
- /** Overrides the schedule-level default. */
58
- retry?: number;
59
- /** Milliseconds. Overrides the schedule-level default. */
60
- timeout?: number;
53
+ /** Unique. Used for the registry lookup, for coordination, and as the sync identity. */
54
+ key: string;
55
+ /** Six-field cron, or five for standard crontab. */
56
+ expression: string;
57
+ target: TaskTarget;
58
+ /** IANA zone. Defaults to UTC — never the host zone, which differs between machines. */
59
+ timezone?: string;
60
+ /** False keeps the task known but unscheduled, so its history survives a pause. */
61
+ enabled?: boolean;
62
+ /** Overrides the schedule-level default. */
63
+ retry?: number;
64
+ /** Milliseconds. Overrides the schedule-level default. */
65
+ timeout?: number;
61
66
  }
62
-
63
67
  /** Turns one entry from a source into a task. Implement this to accept your own format. */
64
68
  export interface TaskParser<Entry = string> {
65
- parse(entry: Entry): TaskDefinition;
69
+ parse(entry: Entry): TaskDefinition;
66
70
  }
67
-
68
- /* -------------------------------------------------------------- outcomes */
69
-
70
71
  /**
71
72
  * Why a run failed, normalised across the three target kinds.
72
73
  *
73
74
  * Without this every consumer of a hook would branch on target type to find
74
75
  * out what went wrong. `raw` keeps the original for anyone who needs it.
75
76
  */
76
- export type TaskFailure =
77
- | "throw" // handler threw
78
- | "status" // api answered outside 2xx
79
- | "timeout" // deadline passed
80
- | "exit" // file exited non-zero, or was killed
81
- | "unknown";
82
-
77
+ export type TaskFailure = "throw" | "status" | "timeout" | "exit" | "unknown";
83
78
  export interface TaskEvent {
84
- key: string;
85
- /** The fire time this run belongs to, computed from the expression — not `now`. */
86
- scheduledFor: Date;
87
- startedAt: Date;
88
- durationMs: number;
89
- /** 1 for the first try. */
90
- attempt: number;
91
- ok: boolean;
92
- reason?: TaskFailure;
93
- detail?: string;
94
- /** Whatever the target produced: return value, response body, stdout. */
95
- raw?: unknown;
79
+ key: string;
80
+ /** The fire time this run belongs to, computed from the expression — not `now`. */
81
+ scheduledFor: Date;
82
+ startedAt: Date;
83
+ durationMs: number;
84
+ /** 1 for the first try. */
85
+ attempt: number;
86
+ ok: boolean;
87
+ reason?: TaskFailure;
88
+ detail?: string;
89
+ /** Whatever the target produced: return value, response body, stdout. */
90
+ raw?: unknown;
96
91
  }
97
-
98
- /* ----------------------------------------------------------------- ports */
99
-
100
92
  /**
101
93
  * Where task definitions come from. Required.
102
94
  *
@@ -105,11 +97,9 @@ export interface TaskEvent {
105
97
  * `Source(fn)` wraps a one-line reader into one, so brevity costs nothing.
106
98
  */
107
99
  export interface SourceAdapter<Entry = string, Context = unknown> {
108
- read(context: Context): Promisable<Entry[]>;
100
+ read(context: Context): Promisable<Entry[]>;
109
101
  }
110
-
111
102
  export type Source<Entry = string, Context = unknown> = ClassType<SourceAdapter<Entry, Context>>;
112
-
113
103
  /**
114
104
  * Decides which instance runs a given fire. Optional.
115
105
  *
@@ -123,12 +113,11 @@ export type Source<Entry = string, Context = unknown> = ClassType<SourceAdapter<
123
113
  * it leaves a run history behind for free.
124
114
  */
125
115
  export interface Coordinator {
126
- /** True when this instance won the right to run. False when another already has it. */
127
- claim(key: string, scheduledFor: Date): Promisable<boolean>;
128
- /** Called once the run ends, so an abandoned claim can be told from a live one. */
129
- release(key: string, scheduledFor: Date, event: TaskEvent): Promisable<void>;
116
+ /** True when this instance won the right to run. False when another already has it. */
117
+ claim(key: string, scheduledFor: Date): Promisable<boolean>;
118
+ /** Called once the run ends, so an abandoned claim can be told from a live one. */
119
+ release(key: string, scheduledFor: Date, event: TaskEvent): Promisable<void>;
130
120
  }
131
-
132
121
  /**
133
122
  * Where run outcomes go. Optional — without one, a finished task reports
134
123
  * nowhere.
@@ -137,11 +126,10 @@ export interface Coordinator {
137
126
  * downstream ever branches on how many there are.
138
127
  */
139
128
  export interface Hook {
140
- notify(event: TaskEvent): Promisable<void>;
129
+ notify(event: TaskEvent): Promisable<void>;
141
130
  }
142
-
143
131
  /** Called when a source entry names a handler that was never registered. */
144
132
  export type NotFoundHandler = (key: string, task: TaskDefinition) => Promisable<void>;
145
-
146
133
  /** Called when a run fails, before any retry decision. */
147
134
  export type ErrorHandler = (event: TaskEvent, task: TaskDefinition) => Promisable<void>;
135
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/dist/types.mjs ADDED
@@ -0,0 +1,2 @@
1
+
2
+ //# sourceMappingURL=types.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,30 +1,34 @@
1
1
  {
2
2
  "name": "@ecosy/schedule",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Cron scheduling with pluggable sources, coordination and reporting — no dependencies, runs anywhere Node does",
5
- "main": "src/index.ts",
6
- "types": "dist/index.d.ts",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": {
9
9
  "source": "./src/index.ts",
10
10
  "types": "./dist/index.d.ts",
11
- "default": "./src/index.ts"
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
12
13
  },
13
14
  "./source": {
14
15
  "source": "./src/source/index.ts",
15
16
  "types": "./dist/source/index.d.ts",
16
- "default": "./src/source/index.ts"
17
+ "import": "./dist/source/index.mjs",
18
+ "require": "./dist/source/index.js"
17
19
  },
18
20
  "./hook": {
19
21
  "source": "./src/hook/index.ts",
20
22
  "types": "./dist/hook/index.d.ts",
21
- "default": "./src/hook/index.ts"
23
+ "import": "./dist/hook/index.mjs",
24
+ "require": "./dist/hook/index.js"
22
25
  }
23
26
  },
24
27
  "license": "MIT",
25
28
  "scripts": {
26
- "build": "tsc",
27
- "prepublishOnly": "yarn build"
29
+ "build": "tsc && rollup -c",
30
+ "prepublishOnly": "yarn clean && yarn build",
31
+ "clean": "rimraf dist"
28
32
  },
29
33
  "publishConfig": {
30
34
  "access": "public"
@@ -35,7 +39,19 @@
35
39
  },
36
40
  "author": "material-atomic",
37
41
  "devDependencies": {
42
+ "@rollup/plugin-commonjs": "^29.0.3",
43
+ "@rollup/plugin-node-resolve": "^16.0.3",
38
44
  "@types/node": "^26.2.0",
45
+ "esbuild": "^0.25.0",
46
+ "glob": "^13.0.6",
47
+ "rimraf": "^6.1.3",
48
+ "rollup": "^4.62.4",
49
+ "rollup-plugin-esbuild": "^6.2.1",
39
50
  "typescript": "^5.9.3"
40
- }
51
+ },
52
+ "module": "./dist/index.mjs",
53
+ "files": [
54
+ "dist",
55
+ "README.md"
56
+ ]
41
57
  }