@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
package/dist/cron.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Cron expression parsing and next-fire calculation.
3
+ *
4
+ * Six fields, seconds first:
5
+ *
6
+ * ```
7
+ * ┌─────── second 0-59
8
+ * │ ┌───── minute 0-59
9
+ * │ │ ┌─── hour 0-23
10
+ * │ │ │ ┌─ day of month 1-31
11
+ * │ │ │ │ ┌ month 1-12 or JAN-DEC
12
+ * │ │ │ │ │ ┌ day of week 0-6 or SUN-SAT (7 also means Sunday)
13
+ * * * * * * *
14
+ * ```
15
+ *
16
+ * Five fields are accepted too and read as standard crontab — seconds become
17
+ * 0. That is not leniency for its own sake: an expression copied from a
18
+ * crontab into a six-field parser shifts every value one column left and runs
19
+ * at the wrong time without erroring. Counting the fields removes the trap.
20
+ */
21
+ export interface CronField {
22
+ /** Sorted, de-duplicated values this field matches. */
23
+ values: number[];
24
+ /** `*` — true when the field constrains nothing. Needed for the dom/dow rule. */
25
+ wildcard: boolean;
26
+ }
27
+ export interface CronExpression {
28
+ second: CronField;
29
+ minute: CronField;
30
+ hour: CronField;
31
+ dayOfMonth: CronField;
32
+ month: CronField;
33
+ dayOfWeek: CronField;
34
+ }
35
+ export declare function parseCron(expression: string): CronExpression;
36
+ /**
37
+ * The first instant strictly after `from` that matches `expression` in `timeZone`.
38
+ *
39
+ * Returns null when the expression can never match — `0 0 0 30 2 *` asks for
40
+ * February 30th. Returning null rather than looping forever means a bad row in
41
+ * a cron table is reported, not a hung process.
42
+ */
43
+ export declare function nextFire(expr: CronExpression, from: Date, timeZone?: string): Date | null;
44
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../src/cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,SAAS;IACxB,uDAAuD;IACvD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,iFAAiF;IACjF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,EAAE,SAAS,CAAC;IACtB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;CACtB;AA8FD,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAgB5D;AAqHD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,IAAI,EACV,QAAQ,SAAQ,GACf,IAAI,GAAG,IAAI,CA8Fb"}
package/dist/cron.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";const p=["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"],v=["sun","mon","tue","wed","thu","fri","sat"],C=[{name:"second",min:0,max:59},{name:"minute",min:0,max:59},{name:"hour",min:0,max:23},{name:"day of month",min:1,max:31},{name:"month",min:1,max:12,names:p,nameBase:1},{name:"day of week",min:0,max:6,names:v,nameBase:0}];function f(e,n){throw new Error(`Cron: ${n} in "${e}"`)}function w(e,n,t){const a=n.names?.indexOf(e.toLowerCase());if(a!==void 0&&a>=0)return a+(n.nameBase??0);/^\d+$/.test(e)||f(t,`"${e}" is not a valid ${n.name}`);const o=Number(e);return n.name==="day of week"&&o===7?0:((o<n.min||o>n.max)&&f(t,`${n.name} ${o} is outside ${n.min}-${n.max}`),o)}function T(e,n,t){const a=e==="?"?"*":e,o=a==="*",r=new Set;for(const u of a.split(",")){const[s,i]=u.split("/");i!==void 0&&!/^\d+$/.test(i)&&f(t,`step "${i}" is not a number`);const m=i===void 0?1:Number(i);m<1&&f(t,"step must be at least 1");let c,d;if(s==="*")c=n.min,d=n.max;else if(s.includes("-")){const[l,h]=s.split("-");c=w(l,n,t),d=w(h,n,t)}else c=w(s,n,t),d=i===void 0?c:n.max;c>d&&f(t,`range ${c}-${d} runs backwards`);for(let l=c;l<=d;l+=m)r.add(l)}return r.size===0&&f(t,`field "${e}" matches nothing`),{values:[...r].sort((u,s)=>u-s),wildcard:o}}function O(e){const n=e.trim().split(/\s+/),t=n.length===5?["0",...n]:n;t.length!==6&&f(e,`expected 5 or 6 fields, got ${n.length}`);const[a,o,r,u,s,i]=t.map((m,c)=>T(m,C[c],e));return{second:a,minute:o,hour:r,dayOfMonth:u,month:s,dayOfWeek:i}}function g(e,n){const t=new Intl.DateTimeFormat("en-US",{timeZone:n,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}).formatToParts(e),a=o=>Number(t.find(r=>r.type===o).value);return{year:a("year"),month:a("month"),day:a("day"),hour:a("hour"),minute:a("minute"),second:a("second")}}function b(e,n){const t=Date.UTC(e.year,e.month-1,e.day,e.hour,e.minute,e.second);let a=t;for(let r=0;r<3;r++){const u=g(new Date(a),n),s=Date.UTC(u.year,u.month-1,u.day,u.hour,u.minute,u.second),i=t-(s-a);if(i===a)break;a=i}const o=g(new Date(a),n);return o.year===e.year&&o.month===e.month&&o.day===e.day&&o.hour===e.hour&&o.minute===e.minute&&o.second===e.second?new Date(a):null}function k(e,n,t){return new Date(Date.UTC(e,n-1,t)).getUTCDay()}function D(e,n){return new Date(Date.UTC(e,n,0)).getUTCDate()}function y(e,n){for(const t of e.values)if(t>=n)return t;return null}function U(e,n,t,a){const o=e.dayOfMonth.values.includes(a),r=e.dayOfWeek.values.includes(k(n,t,a));return e.dayOfMonth.wildcard&&e.dayOfWeek.wildcard?!0:e.dayOfMonth.wildcard?r:e.dayOfWeek.wildcard?o:o||r}const M=366*5;function F(e,n,t="UTC"){const a=g(new Date(n.getTime()+1e3),t);let{year:o,month:r,day:u,hour:s,minute:i,second:m}=a,c=0;for(;c<=M;){const d=y(e.month,r);if(d===null){o++,r=e.month.values[0],u=1,s=i=m=0;continue}if(d!==r&&(r=d,u=1,s=i=m=0),u>D(o,r)||!U(e,o,r,u)){u++,s=i=m=0,c++,u>D(o,r)&&(u=1,r++,r>12&&(r=1,o++));continue}const l=y(e.hour,s);if(l===null){u++,s=i=m=0,c++;continue}l!==s&&(s=l,i=m=0);const h=y(e.minute,i);if(h===null){s++,i=m=0;continue}h!==i&&(i=h,m=0);const $=y(e.second,m);if($===null){i++,m=0;continue}m=$;const x=b({year:o,month:r,day:u,hour:s,minute:i,second:m},t);if(x===null){m++,m>59&&(m=0,i++,i>59&&(i=0,s++));continue}return x}return null}exports.nextFire=F,exports.parseCron=O;
2
+ //# sourceMappingURL=cron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.js","sources":["../src/cron.ts"],"sourcesContent":["/**\n * Cron expression parsing and next-fire calculation.\n *\n * Six fields, seconds first:\n *\n * ```\n * ┌─────── second 0-59\n * │ ┌───── minute 0-59\n * │ │ ┌─── hour 0-23\n * │ │ │ ┌─ day of month 1-31\n * │ │ │ │ ┌ month 1-12 or JAN-DEC\n * │ │ │ │ │ ┌ day of week 0-6 or SUN-SAT (7 also means Sunday)\n * * * * * * *\n * ```\n *\n * Five fields are accepted too and read as standard crontab — seconds become\n * 0. That is not leniency for its own sake: an expression copied from a\n * crontab into a six-field parser shifts every value one column left and runs\n * at the wrong time without erroring. Counting the fields removes the trap.\n */\n\nexport interface CronField {\n /** Sorted, de-duplicated values this field matches. */\n values: number[];\n /** `*` — true when the field constrains nothing. Needed for the dom/dow rule. */\n wildcard: boolean;\n}\n\nexport interface CronExpression {\n second: CronField;\n minute: CronField;\n hour: CronField;\n dayOfMonth: CronField;\n month: CronField;\n dayOfWeek: CronField;\n}\n\nconst MONTHS = [\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"];\nconst DAYS = [\"sun\", \"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\"];\n\ninterface FieldSpec {\n name: string;\n min: number;\n max: number;\n names?: string[];\n /** Offset to add to a name index, e.g. months are 1-based. */\n nameBase?: number;\n}\n\nconst SPECS: FieldSpec[] = [\n { name: \"second\", min: 0, max: 59 },\n { name: \"minute\", min: 0, max: 59 },\n { name: \"hour\", min: 0, max: 23 },\n { name: \"day of month\", min: 1, max: 31 },\n { name: \"month\", min: 1, max: 12, names: MONTHS, nameBase: 1 },\n { name: \"day of week\", min: 0, max: 6, names: DAYS, nameBase: 0 },\n];\n\nfunction fail(expression: string, message: string): never {\n throw new Error(`Cron: ${message} in \"${expression}\"`);\n}\n\nfunction toNumber(token: string, spec: FieldSpec, expression: string): number {\n const named = spec.names?.indexOf(token.toLowerCase());\n\n if (named !== undefined && named >= 0) {\n return named + (spec.nameBase ?? 0);\n }\n\n if (!/^\\d+$/.test(token)) {\n fail(expression, `\"${token}\" is not a valid ${spec.name}`);\n }\n\n const value = Number(token);\n\n // 7 is Sunday as well as 0 — both spellings are in the wild, and rejecting\n // one of them breaks expressions people copy from working systems.\n if (spec.name === \"day of week\" && value === 7) return 0;\n\n if (value < spec.min || value > spec.max) {\n fail(expression, `${spec.name} ${value} is outside ${spec.min}-${spec.max}`);\n }\n\n return value;\n}\n\nfunction parseField(raw: string, spec: FieldSpec, expression: string): CronField {\n // `?` means \"no specific value\" in Quartz. Treated as `*`, which is what it\n // amounts to once the dom/dow rule below is applied.\n const field = raw === \"?\" ? \"*\" : raw;\n const wildcard = field === \"*\";\n const values = new Set<number>();\n\n for (const part of field.split(\",\")) {\n const [range, stepRaw] = part.split(\"/\");\n\n if (stepRaw !== undefined && !/^\\d+$/.test(stepRaw)) {\n fail(expression, `step \"${stepRaw}\" is not a number`);\n }\n\n const step = stepRaw === undefined ? 1 : Number(stepRaw);\n if (step < 1) fail(expression, `step must be at least 1`);\n\n let from: number;\n let to: number;\n\n if (range === \"*\") {\n from = spec.min;\n to = spec.max;\n } else if (range.includes(\"-\")) {\n const [a, b] = range.split(\"-\");\n from = toNumber(a, spec, expression);\n to = toNumber(b, spec, expression);\n } else {\n from = toNumber(range, spec, expression);\n // A bare value with a step means \"from here to the end\", e.g. `5/15`.\n to = stepRaw === undefined ? from : spec.max;\n }\n\n if (from > to) fail(expression, `range ${from}-${to} runs backwards`);\n\n for (let value = from; value <= to; value += step) values.add(value);\n }\n\n if (values.size === 0) fail(expression, `field \"${raw}\" matches nothing`);\n\n return { values: [...values].sort((a, b) => a - b), wildcard };\n}\n\nexport function parseCron(expression: string): CronExpression {\n const fields = expression.trim().split(/\\s+/);\n\n // Five fields is standard crontab: prepend a zero second rather than\n // silently reading minute-as-second.\n const parts = fields.length === 5 ? [\"0\", ...fields] : fields;\n\n if (parts.length !== 6) {\n fail(expression, `expected 5 or 6 fields, got ${fields.length}`);\n }\n\n const [second, minute, hour, dayOfMonth, month, dayOfWeek] = parts.map((part, i) =>\n parseField(part, SPECS[i], expression),\n );\n\n return { second, minute, hour, dayOfMonth, month, dayOfWeek };\n}\n\n/* ------------------------------------------------------ next fire */\n\ninterface Wall {\n year: number;\n month: number;\n day: number;\n hour: number;\n minute: number;\n second: number;\n}\n\n/**\n * Wall-clock fields of an instant in a zone.\n *\n * `hourCycle: \"h23\"` matters: with plain `hour12: false` some locales render\n * midnight as 24, which then reads as an invalid hour.\n */\nfunction partsInZone(instant: Date, timeZone: string): Wall {\n const parts = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hourCycle: \"h23\",\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n }).formatToParts(instant);\n\n const read = (type: string) => Number(parts.find((p) => p.type === type)!.value);\n\n return {\n year: read(\"year\"),\n month: read(\"month\"),\n day: read(\"day\"),\n hour: read(\"hour\"),\n minute: read(\"minute\"),\n second: read(\"second\"),\n };\n}\n\n/**\n * The instant at which a zone's wall clock reads `wall`.\n *\n * Returns null when no such instant exists — the hour skipped by a\n * spring-forward transition. The caller treats that as \"this occurrence does\n * not happen\" and looks for the next one, which is what every cron does with a\n * time that the calendar simply never reaches.\n *\n * Ambiguous times, the hour repeated by a fall-back transition, resolve to the\n * first occurrence. Since the search always moves forward from the previous\n * fire, the second occurrence is never selected and the job runs once.\n */\nfunction instantFromWall(wall: Wall, timeZone: string): Date | null {\n const asIfUtc = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second);\n\n // Offsets can shift between the guess and the answer, so iterate: each pass\n // measures the zone's offset at the current guess and corrects. Two passes\n // suffice everywhere; a third is cheap insurance against odd historical\n // zones.\n let guess = asIfUtc;\n\n for (let i = 0; i < 3; i++) {\n const seen = partsInZone(new Date(guess), timeZone);\n const seenAsUtc = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second);\n const corrected = asIfUtc - (seenAsUtc - guess);\n\n if (corrected === guess) break;\n guess = corrected;\n }\n\n const check = partsInZone(new Date(guess), timeZone);\n const exists =\n check.year === wall.year && check.month === wall.month && check.day === wall.day &&\n check.hour === wall.hour && check.minute === wall.minute && check.second === wall.second;\n\n return exists ? new Date(guess) : null;\n}\n\n/** Weekday of a calendar date. Independent of zone: a date has one weekday. */\nfunction weekdayOf(year: number, month: number, day: number): number {\n return new Date(Date.UTC(year, month - 1, day)).getUTCDay();\n}\n\nfunction daysInMonth(year: number, month: number): number {\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\nfunction nextValue(field: CronField, from: number): number | null {\n for (const value of field.values) if (value >= from) return value;\n return null;\n}\n\n/**\n * Whether a date matches the day fields.\n *\n * The rule is inherited from Vixie cron and surprises nearly everyone: when\n * *both* day-of-month and day-of-week are restricted, a date matches if\n * *either* does — not both. `0 0 0 1 * 1` fires on the 1st of every month and\n * on every Monday. When one of them is `*`, only the other constrains.\n */\nfunction matchesDay(expr: CronExpression, year: number, month: number, day: number): boolean {\n const domMatch = expr.dayOfMonth.values.includes(day);\n const dowMatch = expr.dayOfWeek.values.includes(weekdayOf(year, month, day));\n\n if (expr.dayOfMonth.wildcard && expr.dayOfWeek.wildcard) return true;\n if (expr.dayOfMonth.wildcard) return dowMatch;\n if (expr.dayOfWeek.wildcard) return domMatch;\n\n return domMatch || dowMatch;\n}\n\n/** Bound on the search, in candidate days. Beyond five years an expression is unsatisfiable. */\nconst MAX_DAYS = 366 * 5;\n\n/**\n * The first instant strictly after `from` that matches `expression` in `timeZone`.\n *\n * Returns null when the expression can never match — `0 0 0 30 2 *` asks for\n * February 30th. Returning null rather than looping forever means a bad row in\n * a cron table is reported, not a hung process.\n */\nexport function nextFire(\n expr: CronExpression,\n from: Date,\n timeZone = \"UTC\",\n): Date | null {\n const start = partsInZone(new Date(from.getTime() + 1000), timeZone);\n\n let { year, month, day, hour, minute, second } = start;\n let daysScanned = 0;\n\n while (daysScanned <= MAX_DAYS) {\n // ── month\n const nextMonth = nextValue(expr.month, month);\n if (nextMonth === null) {\n year++;\n month = expr.month.values[0];\n day = 1;\n hour = minute = second = 0;\n continue;\n }\n if (nextMonth !== month) {\n month = nextMonth;\n day = 1;\n hour = minute = second = 0;\n }\n\n // ── day\n if (day > daysInMonth(year, month) || !matchesDay(expr, year, month, day)) {\n day++;\n hour = minute = second = 0;\n daysScanned++;\n\n if (day > daysInMonth(year, month)) {\n day = 1;\n month++;\n if (month > 12) {\n month = 1;\n year++;\n }\n }\n continue;\n }\n\n // ── hour\n const nextHour = nextValue(expr.hour, hour);\n if (nextHour === null) {\n day++;\n hour = minute = second = 0;\n daysScanned++;\n continue;\n }\n if (nextHour !== hour) {\n hour = nextHour;\n minute = second = 0;\n }\n\n // ── minute\n const nextMinute = nextValue(expr.minute, minute);\n if (nextMinute === null) {\n hour++;\n minute = second = 0;\n continue;\n }\n if (nextMinute !== minute) {\n minute = nextMinute;\n second = 0;\n }\n\n // ── second\n const nextSecond = nextValue(expr.second, second);\n if (nextSecond === null) {\n minute++;\n second = 0;\n continue;\n }\n second = nextSecond;\n\n const instant = instantFromWall({ year, month, day, hour, minute, second }, timeZone);\n\n // A wall time inside a spring-forward gap never happens. Step past it and\n // keep looking rather than reporting a time the clock will not reach.\n if (instant === null) {\n second++;\n if (second > 59) {\n second = 0;\n minute++;\n if (minute > 59) {\n minute = 0;\n hour++;\n }\n }\n continue;\n }\n\n return instant;\n }\n\n return null;\n}\n"],"names":["MONTHS","DAYS","SPECS","fail","expression","message","toNumber","token","spec","named","value","parseField","raw","field","wildcard","values","part","range","stepRaw","step","from","to","a","b","parseCron","fields","parts","second","minute","hour","dayOfMonth","month","dayOfWeek","i","partsInZone","instant","timeZone","read","type","p","instantFromWall","wall","asIfUtc","guess","seen","seenAsUtc","corrected","check","weekdayOf","year","day","daysInMonth","nextValue","matchesDay","expr","domMatch","dowMatch","MAX_DAYS","nextFire","start","daysScanned","nextMonth","nextHour","nextMinute","nextSecond"],"mappings":"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"}
package/dist/cron.mjs ADDED
@@ -0,0 +1,2 @@
1
+ const p=["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"],v=["sun","mon","tue","wed","thu","fri","sat"],C=[{name:"second",min:0,max:59},{name:"minute",min:0,max:59},{name:"hour",min:0,max:23},{name:"day of month",min:1,max:31},{name:"month",min:1,max:12,names:p,nameBase:1},{name:"day of week",min:0,max:6,names:v,nameBase:0}];function f(e,n){throw new Error(`Cron: ${n} in "${e}"`)}function w(e,n,t){const a=n.names?.indexOf(e.toLowerCase());if(a!==void 0&&a>=0)return a+(n.nameBase??0);/^\d+$/.test(e)||f(t,`"${e}" is not a valid ${n.name}`);const o=Number(e);return n.name==="day of week"&&o===7?0:((o<n.min||o>n.max)&&f(t,`${n.name} ${o} is outside ${n.min}-${n.max}`),o)}function T(e,n,t){const a=e==="?"?"*":e,o=a==="*",r=new Set;for(const u of a.split(",")){const[s,i]=u.split("/");i!==void 0&&!/^\d+$/.test(i)&&f(t,`step "${i}" is not a number`);const m=i===void 0?1:Number(i);m<1&&f(t,"step must be at least 1");let d,c;if(s==="*")d=n.min,c=n.max;else if(s.includes("-")){const[l,h]=s.split("-");d=w(l,n,t),c=w(h,n,t)}else d=w(s,n,t),c=i===void 0?d:n.max;d>c&&f(t,`range ${d}-${c} runs backwards`);for(let l=d;l<=c;l+=m)r.add(l)}return r.size===0&&f(t,`field "${e}" matches nothing`),{values:[...r].sort((u,s)=>u-s),wildcard:o}}function O(e){const n=e.trim().split(/\s+/),t=n.length===5?["0",...n]:n;t.length!==6&&f(e,`expected 5 or 6 fields, got ${n.length}`);const[a,o,r,u,s,i]=t.map((m,d)=>T(m,C[d],e));return{second:a,minute:o,hour:r,dayOfMonth:u,month:s,dayOfWeek:i}}function g(e,n){const t=new Intl.DateTimeFormat("en-US",{timeZone:n,hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit"}).formatToParts(e),a=o=>Number(t.find(r=>r.type===o).value);return{year:a("year"),month:a("month"),day:a("day"),hour:a("hour"),minute:a("minute"),second:a("second")}}function b(e,n){const t=Date.UTC(e.year,e.month-1,e.day,e.hour,e.minute,e.second);let a=t;for(let r=0;r<3;r++){const u=g(new Date(a),n),s=Date.UTC(u.year,u.month-1,u.day,u.hour,u.minute,u.second),i=t-(s-a);if(i===a)break;a=i}const o=g(new Date(a),n);return o.year===e.year&&o.month===e.month&&o.day===e.day&&o.hour===e.hour&&o.minute===e.minute&&o.second===e.second?new Date(a):null}function k(e,n,t){return new Date(Date.UTC(e,n-1,t)).getUTCDay()}function D(e,n){return new Date(Date.UTC(e,n,0)).getUTCDate()}function y(e,n){for(const t of e.values)if(t>=n)return t;return null}function U(e,n,t,a){const o=e.dayOfMonth.values.includes(a),r=e.dayOfWeek.values.includes(k(n,t,a));return e.dayOfMonth.wildcard&&e.dayOfWeek.wildcard?!0:e.dayOfMonth.wildcard?r:e.dayOfWeek.wildcard?o:o||r}const M=366*5;function W(e,n,t="UTC"){const a=g(new Date(n.getTime()+1e3),t);let{year:o,month:r,day:u,hour:s,minute:i,second:m}=a,d=0;for(;d<=M;){const c=y(e.month,r);if(c===null){o++,r=e.month.values[0],u=1,s=i=m=0;continue}if(c!==r&&(r=c,u=1,s=i=m=0),u>D(o,r)||!U(e,o,r,u)){u++,s=i=m=0,d++,u>D(o,r)&&(u=1,r++,r>12&&(r=1,o++));continue}const l=y(e.hour,s);if(l===null){u++,s=i=m=0,d++;continue}l!==s&&(s=l,i=m=0);const h=y(e.minute,i);if(h===null){s++,i=m=0;continue}h!==i&&(i=h,m=0);const $=y(e.second,m);if($===null){i++,m=0;continue}m=$;const x=b({year:o,month:r,day:u,hour:s,minute:i,second:m},t);if(x===null){m++,m>59&&(m=0,i++,i>59&&(i=0,s++));continue}return x}return null}export{W as nextFire,O as parseCron};
2
+ //# sourceMappingURL=cron.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.mjs","sources":["../src/cron.ts"],"sourcesContent":["/**\n * Cron expression parsing and next-fire calculation.\n *\n * Six fields, seconds first:\n *\n * ```\n * ┌─────── second 0-59\n * │ ┌───── minute 0-59\n * │ │ ┌─── hour 0-23\n * │ │ │ ┌─ day of month 1-31\n * │ │ │ │ ┌ month 1-12 or JAN-DEC\n * │ │ │ │ │ ┌ day of week 0-6 or SUN-SAT (7 also means Sunday)\n * * * * * * *\n * ```\n *\n * Five fields are accepted too and read as standard crontab — seconds become\n * 0. That is not leniency for its own sake: an expression copied from a\n * crontab into a six-field parser shifts every value one column left and runs\n * at the wrong time without erroring. Counting the fields removes the trap.\n */\n\nexport interface CronField {\n /** Sorted, de-duplicated values this field matches. */\n values: number[];\n /** `*` — true when the field constrains nothing. Needed for the dom/dow rule. */\n wildcard: boolean;\n}\n\nexport interface CronExpression {\n second: CronField;\n minute: CronField;\n hour: CronField;\n dayOfMonth: CronField;\n month: CronField;\n dayOfWeek: CronField;\n}\n\nconst MONTHS = [\"jan\", \"feb\", \"mar\", \"apr\", \"may\", \"jun\", \"jul\", \"aug\", \"sep\", \"oct\", \"nov\", \"dec\"];\nconst DAYS = [\"sun\", \"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\"];\n\ninterface FieldSpec {\n name: string;\n min: number;\n max: number;\n names?: string[];\n /** Offset to add to a name index, e.g. months are 1-based. */\n nameBase?: number;\n}\n\nconst SPECS: FieldSpec[] = [\n { name: \"second\", min: 0, max: 59 },\n { name: \"minute\", min: 0, max: 59 },\n { name: \"hour\", min: 0, max: 23 },\n { name: \"day of month\", min: 1, max: 31 },\n { name: \"month\", min: 1, max: 12, names: MONTHS, nameBase: 1 },\n { name: \"day of week\", min: 0, max: 6, names: DAYS, nameBase: 0 },\n];\n\nfunction fail(expression: string, message: string): never {\n throw new Error(`Cron: ${message} in \"${expression}\"`);\n}\n\nfunction toNumber(token: string, spec: FieldSpec, expression: string): number {\n const named = spec.names?.indexOf(token.toLowerCase());\n\n if (named !== undefined && named >= 0) {\n return named + (spec.nameBase ?? 0);\n }\n\n if (!/^\\d+$/.test(token)) {\n fail(expression, `\"${token}\" is not a valid ${spec.name}`);\n }\n\n const value = Number(token);\n\n // 7 is Sunday as well as 0 — both spellings are in the wild, and rejecting\n // one of them breaks expressions people copy from working systems.\n if (spec.name === \"day of week\" && value === 7) return 0;\n\n if (value < spec.min || value > spec.max) {\n fail(expression, `${spec.name} ${value} is outside ${spec.min}-${spec.max}`);\n }\n\n return value;\n}\n\nfunction parseField(raw: string, spec: FieldSpec, expression: string): CronField {\n // `?` means \"no specific value\" in Quartz. Treated as `*`, which is what it\n // amounts to once the dom/dow rule below is applied.\n const field = raw === \"?\" ? \"*\" : raw;\n const wildcard = field === \"*\";\n const values = new Set<number>();\n\n for (const part of field.split(\",\")) {\n const [range, stepRaw] = part.split(\"/\");\n\n if (stepRaw !== undefined && !/^\\d+$/.test(stepRaw)) {\n fail(expression, `step \"${stepRaw}\" is not a number`);\n }\n\n const step = stepRaw === undefined ? 1 : Number(stepRaw);\n if (step < 1) fail(expression, `step must be at least 1`);\n\n let from: number;\n let to: number;\n\n if (range === \"*\") {\n from = spec.min;\n to = spec.max;\n } else if (range.includes(\"-\")) {\n const [a, b] = range.split(\"-\");\n from = toNumber(a, spec, expression);\n to = toNumber(b, spec, expression);\n } else {\n from = toNumber(range, spec, expression);\n // A bare value with a step means \"from here to the end\", e.g. `5/15`.\n to = stepRaw === undefined ? from : spec.max;\n }\n\n if (from > to) fail(expression, `range ${from}-${to} runs backwards`);\n\n for (let value = from; value <= to; value += step) values.add(value);\n }\n\n if (values.size === 0) fail(expression, `field \"${raw}\" matches nothing`);\n\n return { values: [...values].sort((a, b) => a - b), wildcard };\n}\n\nexport function parseCron(expression: string): CronExpression {\n const fields = expression.trim().split(/\\s+/);\n\n // Five fields is standard crontab: prepend a zero second rather than\n // silently reading minute-as-second.\n const parts = fields.length === 5 ? [\"0\", ...fields] : fields;\n\n if (parts.length !== 6) {\n fail(expression, `expected 5 or 6 fields, got ${fields.length}`);\n }\n\n const [second, minute, hour, dayOfMonth, month, dayOfWeek] = parts.map((part, i) =>\n parseField(part, SPECS[i], expression),\n );\n\n return { second, minute, hour, dayOfMonth, month, dayOfWeek };\n}\n\n/* ------------------------------------------------------ next fire */\n\ninterface Wall {\n year: number;\n month: number;\n day: number;\n hour: number;\n minute: number;\n second: number;\n}\n\n/**\n * Wall-clock fields of an instant in a zone.\n *\n * `hourCycle: \"h23\"` matters: with plain `hour12: false` some locales render\n * midnight as 24, which then reads as an invalid hour.\n */\nfunction partsInZone(instant: Date, timeZone: string): Wall {\n const parts = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hourCycle: \"h23\",\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n }).formatToParts(instant);\n\n const read = (type: string) => Number(parts.find((p) => p.type === type)!.value);\n\n return {\n year: read(\"year\"),\n month: read(\"month\"),\n day: read(\"day\"),\n hour: read(\"hour\"),\n minute: read(\"minute\"),\n second: read(\"second\"),\n };\n}\n\n/**\n * The instant at which a zone's wall clock reads `wall`.\n *\n * Returns null when no such instant exists — the hour skipped by a\n * spring-forward transition. The caller treats that as \"this occurrence does\n * not happen\" and looks for the next one, which is what every cron does with a\n * time that the calendar simply never reaches.\n *\n * Ambiguous times, the hour repeated by a fall-back transition, resolve to the\n * first occurrence. Since the search always moves forward from the previous\n * fire, the second occurrence is never selected and the job runs once.\n */\nfunction instantFromWall(wall: Wall, timeZone: string): Date | null {\n const asIfUtc = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second);\n\n // Offsets can shift between the guess and the answer, so iterate: each pass\n // measures the zone's offset at the current guess and corrects. Two passes\n // suffice everywhere; a third is cheap insurance against odd historical\n // zones.\n let guess = asIfUtc;\n\n for (let i = 0; i < 3; i++) {\n const seen = partsInZone(new Date(guess), timeZone);\n const seenAsUtc = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second);\n const corrected = asIfUtc - (seenAsUtc - guess);\n\n if (corrected === guess) break;\n guess = corrected;\n }\n\n const check = partsInZone(new Date(guess), timeZone);\n const exists =\n check.year === wall.year && check.month === wall.month && check.day === wall.day &&\n check.hour === wall.hour && check.minute === wall.minute && check.second === wall.second;\n\n return exists ? new Date(guess) : null;\n}\n\n/** Weekday of a calendar date. Independent of zone: a date has one weekday. */\nfunction weekdayOf(year: number, month: number, day: number): number {\n return new Date(Date.UTC(year, month - 1, day)).getUTCDay();\n}\n\nfunction daysInMonth(year: number, month: number): number {\n return new Date(Date.UTC(year, month, 0)).getUTCDate();\n}\n\nfunction nextValue(field: CronField, from: number): number | null {\n for (const value of field.values) if (value >= from) return value;\n return null;\n}\n\n/**\n * Whether a date matches the day fields.\n *\n * The rule is inherited from Vixie cron and surprises nearly everyone: when\n * *both* day-of-month and day-of-week are restricted, a date matches if\n * *either* does — not both. `0 0 0 1 * 1` fires on the 1st of every month and\n * on every Monday. When one of them is `*`, only the other constrains.\n */\nfunction matchesDay(expr: CronExpression, year: number, month: number, day: number): boolean {\n const domMatch = expr.dayOfMonth.values.includes(day);\n const dowMatch = expr.dayOfWeek.values.includes(weekdayOf(year, month, day));\n\n if (expr.dayOfMonth.wildcard && expr.dayOfWeek.wildcard) return true;\n if (expr.dayOfMonth.wildcard) return dowMatch;\n if (expr.dayOfWeek.wildcard) return domMatch;\n\n return domMatch || dowMatch;\n}\n\n/** Bound on the search, in candidate days. Beyond five years an expression is unsatisfiable. */\nconst MAX_DAYS = 366 * 5;\n\n/**\n * The first instant strictly after `from` that matches `expression` in `timeZone`.\n *\n * Returns null when the expression can never match — `0 0 0 30 2 *` asks for\n * February 30th. Returning null rather than looping forever means a bad row in\n * a cron table is reported, not a hung process.\n */\nexport function nextFire(\n expr: CronExpression,\n from: Date,\n timeZone = \"UTC\",\n): Date | null {\n const start = partsInZone(new Date(from.getTime() + 1000), timeZone);\n\n let { year, month, day, hour, minute, second } = start;\n let daysScanned = 0;\n\n while (daysScanned <= MAX_DAYS) {\n // ── month\n const nextMonth = nextValue(expr.month, month);\n if (nextMonth === null) {\n year++;\n month = expr.month.values[0];\n day = 1;\n hour = minute = second = 0;\n continue;\n }\n if (nextMonth !== month) {\n month = nextMonth;\n day = 1;\n hour = minute = second = 0;\n }\n\n // ── day\n if (day > daysInMonth(year, month) || !matchesDay(expr, year, month, day)) {\n day++;\n hour = minute = second = 0;\n daysScanned++;\n\n if (day > daysInMonth(year, month)) {\n day = 1;\n month++;\n if (month > 12) {\n month = 1;\n year++;\n }\n }\n continue;\n }\n\n // ── hour\n const nextHour = nextValue(expr.hour, hour);\n if (nextHour === null) {\n day++;\n hour = minute = second = 0;\n daysScanned++;\n continue;\n }\n if (nextHour !== hour) {\n hour = nextHour;\n minute = second = 0;\n }\n\n // ── minute\n const nextMinute = nextValue(expr.minute, minute);\n if (nextMinute === null) {\n hour++;\n minute = second = 0;\n continue;\n }\n if (nextMinute !== minute) {\n minute = nextMinute;\n second = 0;\n }\n\n // ── second\n const nextSecond = nextValue(expr.second, second);\n if (nextSecond === null) {\n minute++;\n second = 0;\n continue;\n }\n second = nextSecond;\n\n const instant = instantFromWall({ year, month, day, hour, minute, second }, timeZone);\n\n // A wall time inside a spring-forward gap never happens. Step past it and\n // keep looking rather than reporting a time the clock will not reach.\n if (instant === null) {\n second++;\n if (second > 59) {\n second = 0;\n minute++;\n if (minute > 59) {\n minute = 0;\n hour++;\n }\n }\n continue;\n }\n\n return instant;\n }\n\n return null;\n}\n"],"names":["MONTHS","DAYS","SPECS","fail","expression","message","toNumber","token","spec","named","value","parseField","raw","field","wildcard","values","part","range","stepRaw","step","from","to","a","b","parseCron","fields","parts","second","minute","hour","dayOfMonth","month","dayOfWeek","i","partsInZone","instant","timeZone","read","type","p","instantFromWall","wall","asIfUtc","guess","seen","seenAsUtc","corrected","check","weekdayOf","year","day","daysInMonth","nextValue","matchesDay","expr","domMatch","dowMatch","MAX_DAYS","nextFire","start","daysScanned","nextMonth","nextHour","nextMinute","nextSecond"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { ClassType, Hook as HookPort } from "../types";
2
+ /**
3
+ * Builds a hook class that writes each outcome as a log line.
4
+ *
5
+ * A factory returning a class, not an object: the scheduler constructs whatever
6
+ * it is handed, so everything given to `.hook()` arrives in the same shape and
7
+ * nothing has to be told apart at runtime.
8
+ *
9
+ * Takes anything console-shaped, which `console` itself already is, so it costs
10
+ * no dependency and works before real logging is wired up.
11
+ */
12
+ export declare function LoggerHook(logger?: {
13
+ info(...a: unknown[]): void;
14
+ error(...a: unknown[]): void;
15
+ }): ClassType<HookPort>;
16
+ interface HookFactory {
17
+ /**
18
+ * Fans one event out to several hooks, as a single hook class.
19
+ *
20
+ * Concurrently, and each isolated: a Telegram call that fails must not stop
21
+ * the log line from being written, and a slow one must not hold the
22
+ * scheduler — the task has already finished, reporting is separate work.
23
+ *
24
+ * Flattens, so `combine(a, combine(b, c))` and `combine(a, b, c)` behave the
25
+ * same. With no arguments it is a valid no-op, which makes it a usable
26
+ * default rather than something callers must guard against.
27
+ */
28
+ combine(...hooks: ClassType<HookPort>[]): ClassType<HookPort>;
29
+ }
30
+ export declare const Hook: HookFactory;
31
+ export {};
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hook/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,IAAI,QAAQ,EAAyB,MAAM,UAAU,CAAC;AAKnF;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,MAAM,GAAE;IAAE,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAY,GAC9E,SAAS,CAAC,QAAQ,CAAC,CAgBrB;AAkCD,UAAU,WAAW;IACnB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,GAAG,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;CAC/D;AAED,eAAO,MAAM,IAAI,EAAE,WAkBlB,CAAC"}
@@ -0,0 +1,2 @@
1
+ "use strict";var l=Object.defineProperty,u=(o,e,t)=>e in o?l(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t,a=(o,e,t)=>u(o,typeof e!="symbol"?e+"":e,t);function m(o=console){return class{notify(e){const t=`${e.durationMs}ms`;if(e.ok){o.info(`[schedule] ${e.key} ok in ${t}`);return}o.error(`[schedule] ${e.key} failed (${e.reason}) after ${t}${e.attempt>1?` on attempt ${e.attempt}`:""}: ${e.detail??""}`)}}}function f(o,e,t){return new Promise(n=>{const r=setTimeout(()=>{console.warn(`[schedule] hook ${t} exceeded ${e}ms \u2014 abandoned`),n()},e);Promise.resolve().then(o).then(()=>{clearTimeout(r),n()},s=>{clearTimeout(r),console.warn(`[schedule] hook ${t} threw:`,s),n()})})}const c=Symbol.for("@ecosy/schedule:hook-parts"),d={combine(...o){var e,t;const n=o.flatMap(r=>r[c]??[r]);return e=c,t=class{constructor(){a(this,"parts",n.map(r=>new r))}async notify(r){await Promise.all(this.parts.map((s,i)=>f(()=>s.notify(r),1e4,`#${i}`)))}},a(t,e,n),t}};exports.Hook=d,exports.LoggerHook=m;
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../src/hook/index.ts"],"sourcesContent":["import type { ClassType, Hook as HookPort, Promisable, TaskEvent } from \"../types\";\n\n/** Milliseconds a hook gets before it is abandoned. Separate from the task's own timeout. */\nconst HOOK_TIMEOUT = 10_000;\n\n/**\n * Builds a hook class that writes each outcome as a log line.\n *\n * A factory returning a class, not an object: the scheduler constructs whatever\n * it is handed, so everything given to `.hook()` arrives in the same shape and\n * nothing has to be told apart at runtime.\n *\n * Takes anything console-shaped, which `console` itself already is, so it costs\n * no dependency and works before real logging is wired up.\n */\nexport function LoggerHook(\n logger: { info(...a: unknown[]): void; error(...a: unknown[]): void } = console,\n): ClassType<HookPort> {\n return class implements HookPort {\n notify(event: TaskEvent) {\n const took = `${event.durationMs}ms`;\n\n if (event.ok) {\n logger.info(`[schedule] ${event.key} ok in ${took}`);\n return;\n }\n\n logger.error(\n `[schedule] ${event.key} failed (${event.reason}) after ${took}` +\n `${event.attempt > 1 ? ` on attempt ${event.attempt}` : \"\"}: ${event.detail ?? \"\"}`,\n );\n }\n };\n}\n\n/**\n * Runs one hook under a deadline, swallowing whatever it does.\n *\n * Takes a function rather than a promise on purpose. Calling `notify()` at the\n * call site and passing the result would leave a *synchronous* throw outside\n * this handler entirely — it escapes before there is anything to catch it, and\n * one badly written hook takes down every other hook in the same batch.\n * Invoking inside `.then` turns that throw into a rejection like any other.\n */\nfunction deadline(work: () => Promisable<void>, ms: number, label: string): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n console.warn(`[schedule] hook ${label} exceeded ${ms}ms — abandoned`);\n resolve();\n }, ms);\n\n Promise.resolve()\n .then(work)\n .then(\n () => { clearTimeout(timer); resolve(); },\n (error) => {\n clearTimeout(timer);\n console.warn(`[schedule] hook ${label} threw:`, error);\n resolve();\n },\n );\n });\n}\n\n/** Marks a composite so nesting one inside another flattens instead of nesting. */\nconst PARTS = Symbol.for(\"@ecosy/schedule:hook-parts\");\n\ninterface HookFactory {\n /**\n * Fans one event out to several hooks, as a single hook class.\n *\n * Concurrently, and each isolated: a Telegram call that fails must not stop\n * the log line from being written, and a slow one must not hold the\n * scheduler — the task has already finished, reporting is separate work.\n *\n * Flattens, so `combine(a, combine(b, c))` and `combine(a, b, c)` behave the\n * same. With no arguments it is a valid no-op, which makes it a usable\n * default rather than something callers must guard against.\n */\n combine(...hooks: ClassType<HookPort>[]): ClassType<HookPort>;\n}\n\nexport const Hook: HookFactory = {\n combine(...hooks: ClassType<HookPort>[]): ClassType<HookPort> {\n const flat = hooks.flatMap(\n (hook) => (hook as { [PARTS]?: ClassType<HookPort>[] })[PARTS] ?? [hook],\n );\n\n return class Combined implements HookPort {\n static readonly [PARTS] = flat;\n\n private readonly parts = flat.map((Hook) => new Hook());\n\n async notify(event: TaskEvent) {\n await Promise.all(\n this.parts.map((hook, i) => deadline(() => hook.notify(event), HOOK_TIMEOUT, `#${i}`)),\n );\n }\n };\n },\n};\n"],"names":["LoggerHook","logger","event","took","deadline","work","ms","label","resolve","timer","error","PARTS","Hook","hooks","_a","_b","flat","hook","__publicField","i"],"mappings":"yKAeO,SAASA,EACdC,EAAwE,QACnD,CACrB,OAAO,KAA0B,CAC/B,OAAOC,EAAkB,CACvB,MAAMC,EAAO,GAAGD,EAAM,UAAU,KAEhC,GAAIA,EAAM,GAAI,CACZD,EAAO,KAAK,cAAcC,EAAM,GAAG,UAAUC,CAAI,EAAE,EACnD,MACF,CAEAF,EAAO,MACL,cAAcC,EAAM,GAAG,YAAYA,EAAM,MAAM,WAAWC,CAAI,GACzDD,EAAM,QAAU,EAAI,eAAeA,EAAM,OAAO,GAAK,EAAE,KAAKA,EAAM,QAAU,EAAE,EACrF,CACF,CACF,CACF,CAWA,SAASE,EAASC,EAA8BC,EAAYC,EAA8B,CACxF,OAAO,IAAI,QAASC,GAAY,CAC9B,MAAMC,EAAQ,WAAW,IAAM,CAC7B,QAAQ,KAAK,mBAAmBF,CAAK,aAAaD,CAAE,qBAAgB,EACpEE,EAAAA,CACF,EAAGF,CAAE,EAEL,QAAQ,UACL,KAAKD,CAAI,EACT,KACC,IAAM,CAAE,aAAaI,CAAK,EAAGD,GAAW,EACvCE,GAAU,CACT,aAAaD,CAAK,EAClB,QAAQ,KAAK,mBAAmBF,CAAK,UAAWG,CAAK,EACrDF,GACF,CACF,CACJ,CAAC,CACH,CAGA,MAAMG,EAAQ,OAAO,IAAI,4BAA4B,EAiBxCC,EAAoB,CAC/B,WAAWC,EAAmD,CAnFhE,IAAAC,EAAAC,EAoFI,MAAMC,EAAOH,EAAM,QAChBI,GAAUA,EAA6CN,CAAK,GAAK,CAACM,CAAI,CACzE,EAEA,OACmBH,EAAAH,EADZI,EAAA,KAAmC,CAAnC,aAAA,CAGLG,EAAA,KAAiB,QAAQF,EAAK,IAAKJ,GAAS,IAAIA,CAAM,GAEtD,MAAM,OAAOV,EAAkB,CAC7B,MAAM,QAAQ,IACZ,KAAK,MAAM,IAAI,CAACe,EAAME,IAAMf,EAAS,IAAMa,EAAK,OAAOf,CAAK,EAAG,IAAc,IAAIiB,CAAC,EAAE,CAAC,CACvF,CACF,CACF,EATED,EADKH,EACYD,EAASE,CAAAA,EADrBD,CAWT,CACF"}
@@ -0,0 +1,2 @@
1
+ var l=Object.defineProperty,u=(r,e,t)=>e in r?l(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,s=(r,e,t)=>u(r,typeof e!="symbol"?e+"":e,t);function m(r=console){return class{notify(e){const t=`${e.durationMs}ms`;if(e.ok){r.info(`[schedule] ${e.key} ok in ${t}`);return}r.error(`[schedule] ${e.key} failed (${e.reason}) after ${t}${e.attempt>1?` on attempt ${e.attempt}`:""}: ${e.detail??""}`)}}}function f(r,e,t){return new Promise(n=>{const o=setTimeout(()=>{console.warn(`[schedule] hook ${t} exceeded ${e}ms \u2014 abandoned`),n()},e);Promise.resolve().then(r).then(()=>{clearTimeout(o),n()},a=>{clearTimeout(o),console.warn(`[schedule] hook ${t} threw:`,a),n()})})}const c=Symbol.for("@ecosy/schedule:hook-parts"),d={combine(...r){var e,t;const n=r.flatMap(o=>o[c]??[o]);return e=c,t=class{constructor(){s(this,"parts",n.map(o=>new o))}async notify(o){await Promise.all(this.parts.map((a,i)=>f(()=>a.notify(o),1e4,`#${i}`)))}},s(t,e,n),t}};export{d as Hook,m as LoggerHook};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../../src/hook/index.ts"],"sourcesContent":["import type { ClassType, Hook as HookPort, Promisable, TaskEvent } from \"../types\";\n\n/** Milliseconds a hook gets before it is abandoned. Separate from the task's own timeout. */\nconst HOOK_TIMEOUT = 10_000;\n\n/**\n * Builds a hook class that writes each outcome as a log line.\n *\n * A factory returning a class, not an object: the scheduler constructs whatever\n * it is handed, so everything given to `.hook()` arrives in the same shape and\n * nothing has to be told apart at runtime.\n *\n * Takes anything console-shaped, which `console` itself already is, so it costs\n * no dependency and works before real logging is wired up.\n */\nexport function LoggerHook(\n logger: { info(...a: unknown[]): void; error(...a: unknown[]): void } = console,\n): ClassType<HookPort> {\n return class implements HookPort {\n notify(event: TaskEvent) {\n const took = `${event.durationMs}ms`;\n\n if (event.ok) {\n logger.info(`[schedule] ${event.key} ok in ${took}`);\n return;\n }\n\n logger.error(\n `[schedule] ${event.key} failed (${event.reason}) after ${took}` +\n `${event.attempt > 1 ? ` on attempt ${event.attempt}` : \"\"}: ${event.detail ?? \"\"}`,\n );\n }\n };\n}\n\n/**\n * Runs one hook under a deadline, swallowing whatever it does.\n *\n * Takes a function rather than a promise on purpose. Calling `notify()` at the\n * call site and passing the result would leave a *synchronous* throw outside\n * this handler entirely — it escapes before there is anything to catch it, and\n * one badly written hook takes down every other hook in the same batch.\n * Invoking inside `.then` turns that throw into a rejection like any other.\n */\nfunction deadline(work: () => Promisable<void>, ms: number, label: string): Promise<void> {\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n console.warn(`[schedule] hook ${label} exceeded ${ms}ms — abandoned`);\n resolve();\n }, ms);\n\n Promise.resolve()\n .then(work)\n .then(\n () => { clearTimeout(timer); resolve(); },\n (error) => {\n clearTimeout(timer);\n console.warn(`[schedule] hook ${label} threw:`, error);\n resolve();\n },\n );\n });\n}\n\n/** Marks a composite so nesting one inside another flattens instead of nesting. */\nconst PARTS = Symbol.for(\"@ecosy/schedule:hook-parts\");\n\ninterface HookFactory {\n /**\n * Fans one event out to several hooks, as a single hook class.\n *\n * Concurrently, and each isolated: a Telegram call that fails must not stop\n * the log line from being written, and a slow one must not hold the\n * scheduler — the task has already finished, reporting is separate work.\n *\n * Flattens, so `combine(a, combine(b, c))` and `combine(a, b, c)` behave the\n * same. With no arguments it is a valid no-op, which makes it a usable\n * default rather than something callers must guard against.\n */\n combine(...hooks: ClassType<HookPort>[]): ClassType<HookPort>;\n}\n\nexport const Hook: HookFactory = {\n combine(...hooks: ClassType<HookPort>[]): ClassType<HookPort> {\n const flat = hooks.flatMap(\n (hook) => (hook as { [PARTS]?: ClassType<HookPort>[] })[PARTS] ?? [hook],\n );\n\n return class Combined implements HookPort {\n static readonly [PARTS] = flat;\n\n private readonly parts = flat.map((Hook) => new Hook());\n\n async notify(event: TaskEvent) {\n await Promise.all(\n this.parts.map((hook, i) => deadline(() => hook.notify(event), HOOK_TIMEOUT, `#${i}`)),\n );\n }\n };\n },\n};\n"],"names":["LoggerHook","logger","event","took","deadline","work","ms","label","resolve","timer","error","PARTS","Hook","hooks","_a","_b","flat","hook","__publicField","i"],"mappings":"4JAeO,SAASA,EACdC,EAAwE,QACnD,CACrB,OAAO,KAA0B,CAC/B,OAAOC,EAAkB,CACvB,MAAMC,EAAO,GAAGD,EAAM,UAAU,KAEhC,GAAIA,EAAM,GAAI,CACZD,EAAO,KAAK,cAAcC,EAAM,GAAG,UAAUC,CAAI,EAAE,EACnD,MACF,CAEAF,EAAO,MACL,cAAcC,EAAM,GAAG,YAAYA,EAAM,MAAM,WAAWC,CAAI,GACzDD,EAAM,QAAU,EAAI,eAAeA,EAAM,OAAO,GAAK,EAAE,KAAKA,EAAM,QAAU,EAAE,EACrF,CACF,CACF,CACF,CAWA,SAASE,EAASC,EAA8BC,EAAYC,EAA8B,CACxF,OAAO,IAAI,QAASC,GAAY,CAC9B,MAAMC,EAAQ,WAAW,IAAM,CAC7B,QAAQ,KAAK,mBAAmBF,CAAK,aAAaD,CAAE,qBAAgB,EACpEE,EAAAA,CACF,EAAGF,CAAE,EAEL,QAAQ,UACL,KAAKD,CAAI,EACT,KACC,IAAM,CAAE,aAAaI,CAAK,EAAGD,GAAW,EACvCE,GAAU,CACT,aAAaD,CAAK,EAClB,QAAQ,KAAK,mBAAmBF,CAAK,UAAWG,CAAK,EACrDF,GACF,CACF,CACJ,CAAC,CACH,CAGA,MAAMG,EAAQ,OAAO,IAAI,4BAA4B,EAiBxCC,EAAoB,CAC/B,WAAWC,EAAmD,CAnFhE,IAAAC,EAAAC,EAoFI,MAAMC,EAAOH,EAAM,QAChBI,GAAUA,EAA6CN,CAAK,GAAK,CAACM,CAAI,CACzE,EAEA,OACmBH,EAAAH,EADZI,EAAA,KAAmC,CAAnC,aAAA,CAGLG,EAAA,KAAiB,QAAQF,EAAK,IAAKJ,GAAS,IAAIA,CAAM,GAEtD,MAAM,OAAOV,EAAkB,CAC7B,MAAM,QAAQ,IACZ,KAAK,MAAM,IAAI,CAACe,EAAME,IAAMf,EAAS,IAAMa,EAAK,OAAOf,CAAK,EAAG,IAAc,IAAIiB,CAAC,EAAE,CAAC,CACvF,CACF,CACF,EATED,EADKH,EACYD,EAASE,CAAAA,EADrBD,CAWT,CACF"}
@@ -4,3 +4,4 @@ export * from "./registry";
4
4
  export * from "./runner";
5
5
  export * from "./task";
6
6
  export * from "./schedule";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var e=require("./cron.js"),n=require("./registry.js"),r=require("./runner.js"),a=require("./task.js"),u=require("./schedule.js");exports.nextFire=e.nextFire,exports.parseCron=e.parseCron,exports.Registry=n.Registry,exports.DEFAULT_TIMEOUT=r.DEFAULT_TIMEOUT,exports.runTarget=r.runTarget,exports.Task=a.Task,exports.Schedule=u.Schedule,exports.ScheduleRunner=u.ScheduleRunner;
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import{nextFire as o,parseCron as t}from"./cron.mjs";import{Registry as x}from"./registry.mjs";import{DEFAULT_TIMEOUT as m,runTarget as n}from"./runner.mjs";import{Task as u}from"./task.mjs";import{Schedule as s,ScheduleRunner as c}from"./schedule.mjs";export{m as DEFAULT_TIMEOUT,x as Registry,s as Schedule,c as ScheduleRunner,u as Task,o as nextFire,t as parseCron,n as runTarget};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import type { ClassType, InjectMap, Injected, Promisable } from "./types";
2
+ /**
3
+ * Handlers, held by name.
4
+ *
5
+ * A source entry is text, so it cannot carry a function — only a key that
6
+ * points at one. This is the other half of that: the code declares handlers
7
+ * under names, and the source names them.
8
+ *
9
+ * An entry is a class, not an object, because the scheduler constructs it —
10
+ * once per run, so a handler never holds a connection open between fires. The
11
+ * key lives on the static side because the lookup happens before anything is
12
+ * built.
13
+ */
14
+ export type HandlerFn<Context = unknown> = (context: Context) => Promisable<unknown>;
15
+ export interface RegistryEntry<Context = unknown> {
16
+ run(context: Context): Promisable<unknown>;
17
+ }
18
+ /** A handler class: `key` to find it by, `run` once it exists. */
19
+ export interface RegistryEntryClass<Context = unknown> extends ClassType<RegistryEntry<Context>> {
20
+ readonly key: string;
21
+ }
22
+ export interface IRegistry<Context = unknown> {
23
+ add(entry: RegistryEntryClass<Context>): IRegistry<Context>;
24
+ /** The handler under `key`, or undefined — which is what `notFound` reports on. */
25
+ get(key: string): RegistryEntryClass<Context> | undefined;
26
+ keys(): string[];
27
+ }
28
+ interface RegistryFactory {
29
+ /** A handler with no dependencies. */
30
+ <Context = unknown>(key: string, handler: HandlerFn<Context>): RegistryEntryClass<Context>;
31
+ /** A handler whose context carries the given tokens, constructed per run. */
32
+ <Injects extends InjectMap, Context = unknown>(key: string, injects: Injects, handler: HandlerFn<Injected<Context, Injects>>): RegistryEntryClass<Context>;
33
+ /** An empty registry to chain `.add()` onto. */
34
+ empty<Context = unknown>(): IRegistry<Context>;
35
+ add<Context = unknown>(entry: RegistryEntryClass<Context>): IRegistry<Context>;
36
+ }
37
+ export declare const Registry: RegistryFactory;
38
+ export {};
39
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1E;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,SAAS,CAAC,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,EAAE,OAAO,KAAK,UAAU,CAAC,OAAO,CAAC,CAAC;AAErF,MAAM,WAAW,aAAa,CAAC,OAAO,GAAG,OAAO;IAC9C,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;CAC5C;AAED,kEAAkE;AAClE,MAAM,WAAW,kBAAkB,CAAC,OAAO,GAAG,OAAO,CAAE,SAAQ,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9F,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS,CAAC,OAAO,GAAG,OAAO;IAC1C,GAAG,CAAC,KAAK,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAC5D,mFAAmF;IACnF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IAC1D,IAAI,IAAI,MAAM,EAAE,CAAC;CAClB;AAwBD,UAAU,eAAe;IACvB,sCAAsC;IACtC,CAAC,OAAO,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC3F,6EAA6E;IAC7E,CAAC,OAAO,SAAS,SAAS,EAAE,OAAO,GAAG,OAAO,EAC3C,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,GAC7C,kBAAkB,CAAC,OAAO,CAAC,CAAC;IAC/B,gDAAgD;IAChD,KAAK,CAAC,OAAO,GAAG,OAAO,KAAK,SAAS,CAAC,OAAO,CAAC,CAAC;IAC/C,GAAG,CAAC,OAAO,GAAG,OAAO,EAAE,KAAK,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;CAChF;AAiCD,eAAO,MAAM,QAAQ,iBAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ "use strict";var d=Object.defineProperty,g=(r,e,t)=>e in r?d(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,p=(r,e,t)=>g(r,e+"",t);function c(r){return{add(e){const t=new Map(r);return t.set(e.key,e),c(t)},get:e=>r.get(e),keys:()=>[...r.keys()]}}const n=function(r,e,t){var o;const u=typeof e=="function"?{}:e,s=typeof e=="function"?e:t;if(typeof s!="function")throw new Error(`Registry: handler for "${r}" is missing`);return o=class{run(i){const a=i;for(const[f,y]of Object.entries(u))a[f]=new y;return s(i)}},p(o,"key",r),o};n.empty=()=>c(new Map),n.add=r=>n.empty().add(r);const l=n;exports.Registry=l;
2
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sources":["../src/registry.ts"],"sourcesContent":["import type { ClassType, InjectMap, Injected, Promisable } from \"./types\";\n\n/**\n * Handlers, held by name.\n *\n * A source entry is text, so it cannot carry a function — only a key that\n * points at one. This is the other half of that: the code declares handlers\n * under names, and the source names them.\n *\n * An entry is a class, not an object, because the scheduler constructs it —\n * once per run, so a handler never holds a connection open between fires. The\n * key lives on the static side because the lookup happens before anything is\n * built.\n */\nexport type HandlerFn<Context = unknown> = (context: Context) => Promisable<unknown>;\n\nexport interface RegistryEntry<Context = unknown> {\n run(context: Context): Promisable<unknown>;\n}\n\n/** A handler class: `key` to find it by, `run` once it exists. */\nexport interface RegistryEntryClass<Context = unknown> extends ClassType<RegistryEntry<Context>> {\n readonly key: string;\n}\n\nexport interface IRegistry<Context = unknown> {\n add(entry: RegistryEntryClass<Context>): IRegistry<Context>;\n /** The handler under `key`, or undefined — which is what `notFound` reports on. */\n get(key: string): RegistryEntryClass<Context> | undefined;\n keys(): string[];\n}\n\n/**\n * The registry itself stays a plain collection.\n *\n * Nothing ever constructs it — it is read, not built — so making it a class\n * would be ceremony without a job. The rule is about what the scheduler\n * instantiates, and that is the entries, not the box holding them.\n */\nfunction build<Context>(entries: ReadonlyMap<string, RegistryEntryClass<Context>>): IRegistry<Context> {\n return {\n add(entry) {\n // Last registration wins rather than throwing: a hot reload re-runs the\n // declaration, and refusing the second one would leave the first, stale\n // closure in place — the opposite of what an edit is asking for.\n const next = new Map(entries);\n next.set(entry.key, entry);\n return build(next);\n },\n get: (key) => entries.get(key),\n keys: () => [...entries.keys()],\n };\n}\n\ninterface RegistryFactory {\n /** A handler with no dependencies. */\n <Context = unknown>(key: string, handler: HandlerFn<Context>): RegistryEntryClass<Context>;\n /** A handler whose context carries the given tokens, constructed per run. */\n <Injects extends InjectMap, Context = unknown>(\n key: string,\n injects: Injects,\n handler: HandlerFn<Injected<Context, Injects>>,\n ): RegistryEntryClass<Context>;\n /** An empty registry to chain `.add()` onto. */\n empty<Context = unknown>(): IRegistry<Context>;\n add<Context = unknown>(entry: RegistryEntryClass<Context>): IRegistry<Context>;\n}\n\nconst RegistryImpl = function (\n key: string,\n second: InjectMap | HandlerFn<never>,\n third?: HandlerFn<never>,\n): RegistryEntryClass<never> {\n const injects = typeof second === \"function\" ? {} : second;\n const handler = (typeof second === \"function\" ? second : third) as HandlerFn<never>;\n\n if (typeof handler !== \"function\") {\n throw new Error(`Registry: handler for \"${key}\" is missing`);\n }\n\n return class Entry {\n static readonly key = key;\n\n run(context: never) {\n // Tokens are constructed here, per run, so a handler never holds a\n // connection open between fires.\n const scoped = context as Record<string, unknown>;\n for (const [name, Token] of Object.entries(injects)) {\n scoped[name] = new (Token as ClassType)();\n }\n return handler(context);\n }\n };\n} as unknown as RegistryFactory;\n\nRegistryImpl.empty = <Context>() => build<Context>(new Map());\nRegistryImpl.add = <Context>(entry: RegistryEntryClass<Context>) =>\n RegistryImpl.empty<Context>().add(entry);\n\nexport const Registry = RegistryImpl;\n"],"names":["l","d","n","t","e","a","build","entries","entry","next","key","RegistryImpl","second","third","_a","injects","handler","context","scoped","name","Token","__publicField","Registry"],"mappings":"aAuCA,IAAAA,EAAA,OAAA,eAAAC,EAAA,CAAAC,EAAAC,EAAAC,IAAAD,KAAAD,EAAAF,EAAAE,EAAAC,EAAA,CAAA,WAAA,GAAA,aAAA,GAAA,SAAA,GAAA,MAAAC,CAAA,CAAA,EAAAF,EAAAC,CAAA,EAAAC,EAAAC,EAAA,CAAAH,EAAAC,EAAAC,IAAAH,EAAAC,EAAAC,EAAA,GAAAC,CAAA,EAAA,SAASE,EAAeC,EAA+E,CACrG,MAAO,CACL,IAAIC,EAAO,CAIT,MAAMC,EAAO,IAAI,IAAIF,CAAO,EAC5B,OAAAE,EAAK,IAAID,EAAM,IAAKA,CAAK,EAClBF,EAAMG,CAAI,CACnB,EACA,IAAMC,GAAQH,EAAQ,IAAIG,CAAG,EAC7B,KAAM,IAAM,CAAC,GAAGH,EAAQ,KAAA,CAAM,CAChC,CACF,CAgBA,MAAMI,EAAe,SACnBD,EACAE,EACAC,EAC2B,CAxE7B,IAAAC,EAyEE,MAAMC,EAAU,OAAOH,GAAW,WAAa,CAAA,EAAKA,EAC9CI,EAAW,OAAOJ,GAAW,WAAaA,EAASC,EAEzD,GAAI,OAAOG,GAAY,WACrB,MAAM,IAAI,MAAM,0BAA0BN,CAAG,cAAc,EAG7D,OAAOI,EAAA,KAAY,CAGjB,IAAIG,EAAgB,CAGlB,MAAMC,EAASD,EACf,SAAW,CAACE,EAAMC,CAAK,IAAK,OAAO,QAAQL,CAAO,EAChDG,EAAOC,CAAI,EAAI,IAAKC,EAEtB,OAAOJ,EAAQC,CAAO,CACxB,CACF,EAXEI,EADKP,EACW,MAAMJ,CAAAA,EADjBI,CAaT,EAEAH,EAAa,MAAQ,IAAeL,EAAe,IAAI,GAAK,EAC5DK,EAAa,IAAgBH,GAC3BG,EAAa,MAAA,EAAiB,IAAIH,CAAK,QAE5Bc,EAAWX"}
@@ -0,0 +1,2 @@
1
+ var d=Object.defineProperty,p=(r,e,t)=>e in r?d(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,g=(r,e,t)=>p(r,e+"",t);function a(r){return{add(e){const t=new Map(r);return t.set(e.key,e),a(t)},get:e=>r.get(e),keys:()=>[...r.keys()]}}const n=function(r,e,t){var o;const c=typeof e=="function"?{}:e,s=typeof e=="function"?e:t;if(typeof s!="function")throw new Error(`Registry: handler for "${r}" is missing`);return o=class{run(i){const u=i;for(const[f,y]of Object.entries(c))u[f]=new y;return s(i)}},g(o,"key",r),o};n.empty=()=>a(new Map),n.add=r=>n.empty().add(r);const l=n;export{l as Registry};
2
+ //# sourceMappingURL=registry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.mjs","sources":["../src/registry.ts"],"sourcesContent":["import type { ClassType, InjectMap, Injected, Promisable } from \"./types\";\n\n/**\n * Handlers, held by name.\n *\n * A source entry is text, so it cannot carry a function — only a key that\n * points at one. This is the other half of that: the code declares handlers\n * under names, and the source names them.\n *\n * An entry is a class, not an object, because the scheduler constructs it —\n * once per run, so a handler never holds a connection open between fires. The\n * key lives on the static side because the lookup happens before anything is\n * built.\n */\nexport type HandlerFn<Context = unknown> = (context: Context) => Promisable<unknown>;\n\nexport interface RegistryEntry<Context = unknown> {\n run(context: Context): Promisable<unknown>;\n}\n\n/** A handler class: `key` to find it by, `run` once it exists. */\nexport interface RegistryEntryClass<Context = unknown> extends ClassType<RegistryEntry<Context>> {\n readonly key: string;\n}\n\nexport interface IRegistry<Context = unknown> {\n add(entry: RegistryEntryClass<Context>): IRegistry<Context>;\n /** The handler under `key`, or undefined — which is what `notFound` reports on. */\n get(key: string): RegistryEntryClass<Context> | undefined;\n keys(): string[];\n}\n\n/**\n * The registry itself stays a plain collection.\n *\n * Nothing ever constructs it — it is read, not built — so making it a class\n * would be ceremony without a job. The rule is about what the scheduler\n * instantiates, and that is the entries, not the box holding them.\n */\nfunction build<Context>(entries: ReadonlyMap<string, RegistryEntryClass<Context>>): IRegistry<Context> {\n return {\n add(entry) {\n // Last registration wins rather than throwing: a hot reload re-runs the\n // declaration, and refusing the second one would leave the first, stale\n // closure in place — the opposite of what an edit is asking for.\n const next = new Map(entries);\n next.set(entry.key, entry);\n return build(next);\n },\n get: (key) => entries.get(key),\n keys: () => [...entries.keys()],\n };\n}\n\ninterface RegistryFactory {\n /** A handler with no dependencies. */\n <Context = unknown>(key: string, handler: HandlerFn<Context>): RegistryEntryClass<Context>;\n /** A handler whose context carries the given tokens, constructed per run. */\n <Injects extends InjectMap, Context = unknown>(\n key: string,\n injects: Injects,\n handler: HandlerFn<Injected<Context, Injects>>,\n ): RegistryEntryClass<Context>;\n /** An empty registry to chain `.add()` onto. */\n empty<Context = unknown>(): IRegistry<Context>;\n add<Context = unknown>(entry: RegistryEntryClass<Context>): IRegistry<Context>;\n}\n\nconst RegistryImpl = function (\n key: string,\n second: InjectMap | HandlerFn<never>,\n third?: HandlerFn<never>,\n): RegistryEntryClass<never> {\n const injects = typeof second === \"function\" ? {} : second;\n const handler = (typeof second === \"function\" ? second : third) as HandlerFn<never>;\n\n if (typeof handler !== \"function\") {\n throw new Error(`Registry: handler for \"${key}\" is missing`);\n }\n\n return class Entry {\n static readonly key = key;\n\n run(context: never) {\n // Tokens are constructed here, per run, so a handler never holds a\n // connection open between fires.\n const scoped = context as Record<string, unknown>;\n for (const [name, Token] of Object.entries(injects)) {\n scoped[name] = new (Token as ClassType)();\n }\n return handler(context);\n }\n };\n} as unknown as RegistryFactory;\n\nRegistryImpl.empty = <Context>() => build<Context>(new Map());\nRegistryImpl.add = <Context>(entry: RegistryEntryClass<Context>) =>\n RegistryImpl.empty<Context>().add(entry);\n\nexport const Registry = RegistryImpl;\n"],"names":["l","d","n","t","e","a","build","entries","entry","next","key","RegistryImpl","second","third","_a","injects","handler","context","scoped","name","Token","__publicField","Registry"],"mappings":"AAuCA,IAAAA,EAAA,OAAA,eAAAC,EAAA,CAAAC,EAAAC,EAAAC,IAAAD,KAAAD,EAAAF,EAAAE,EAAAC,EAAA,CAAA,WAAA,GAAA,aAAA,GAAA,SAAA,GAAA,MAAAC,CAAA,CAAA,EAAAF,EAAAC,CAAA,EAAAC,EAAAC,EAAA,CAAAH,EAAAC,EAAAC,IAAAH,EAAAC,EAAAC,EAAA,GAAAC,CAAA,EAAA,SAASE,EAAeC,EAA+E,CACrG,MAAO,CACL,IAAIC,EAAO,CAIT,MAAMC,EAAO,IAAI,IAAIF,CAAO,EAC5B,OAAAE,EAAK,IAAID,EAAM,IAAKA,CAAK,EAClBF,EAAMG,CAAI,CACnB,EACA,IAAMC,GAAQH,EAAQ,IAAIG,CAAG,EAC7B,KAAM,IAAM,CAAC,GAAGH,EAAQ,KAAA,CAAM,CAChC,CACF,CAgBA,MAAMI,EAAe,SACnBD,EACAE,EACAC,EAC2B,CAxE7B,IAAAC,EAyEE,MAAMC,EAAU,OAAOH,GAAW,WAAa,CAAA,EAAKA,EAC9CI,EAAW,OAAOJ,GAAW,WAAaA,EAASC,EAEzD,GAAI,OAAOG,GAAY,WACrB,MAAM,IAAI,MAAM,0BAA0BN,CAAG,cAAc,EAG7D,OAAOI,EAAA,KAAY,CAGjB,IAAIG,EAAgB,CAGlB,MAAMC,EAASD,EACf,SAAW,CAACE,EAAMC,CAAK,IAAK,OAAO,QAAQL,CAAO,EAChDG,EAAOC,CAAI,EAAI,IAAKC,EAEtB,OAAOJ,EAAQC,CAAO,CACxB,CACF,EAXEI,EADKP,EACW,MAAMJ,CAAAA,EADjBI,CAaT,EAEAH,EAAa,MAAQ,IAAeL,EAAe,IAAI,GAAK,EAC5DK,EAAa,IAAgBH,GAC3BG,EAAa,MAAA,EAAiB,IAAIH,CAAK,QAE5Bc,EAAWX"}
@@ -0,0 +1,12 @@
1
+ import type { IRegistry } from "./registry";
2
+ import type { NotFoundHandler, TaskDefinition, TaskFailure } from "./types";
3
+ export interface RunOutcome {
4
+ ok: boolean;
5
+ reason?: TaskFailure;
6
+ detail?: string;
7
+ raw?: unknown;
8
+ }
9
+ /** Milliseconds after which a run is abandoned. */
10
+ export declare const DEFAULT_TIMEOUT = 30000;
11
+ export declare function runTarget(task: TaskDefinition, registry: IRegistry<unknown>, context: unknown, timeout?: number, notFound?: NotFoundHandler): Promise<RunOutcome>;
12
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE5E,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,mDAAmD;AACnD,eAAO,MAAM,eAAe,QAAS,CAAC;AA8HtC,wBAAgB,SAAS,CACvB,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,SAAS,CAAC,OAAO,CAAC,EAC5B,OAAO,EAAE,OAAO,EAChB,OAAO,SAAkB,EACzB,QAAQ,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,UAAU,CAAC,CAMrB"}
package/dist/runner.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var w=Object.create;var g=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var p=Object.getPrototypeOf,y=Object.prototype.hasOwnProperty;var k=(t,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of h(e))!y.call(t,n)&&n!==r&&g(t,n,{get:()=>e[n],enumerable:!(i=T(e,n))||i.enumerable});return t};var E=(t,e,r)=>(r=t!=null?w(p(t)):{},k(e||!t||!t.__esModule?g(r,"default",{value:t,enumerable:!0}):r,t));const L=3e4;function u(t,e,r){return{ok:!1,reason:t,detail:e,raw:r}}function b(t,e){return new Promise((r,i)=>{const n=setTimeout(()=>r(f),e);t.then(s=>{clearTimeout(n),r(s)},s=>{clearTimeout(n),i(s)})})}const f=Symbol("timed-out");async function $(t,e,r,i,n){if(t.target.type!=="handler")return u("unknown","not a handler target");const s=e.get(t.target.key);if(!s)return await n?.(t.target.key,t),u("unknown",`no handler registered for "${t.target.key}"`);try{const a=await b(Promise.resolve(new s().run(r)),i);return a===f?u("timeout",`handler exceeded ${i}ms (still running \u2014 a function cannot be interrupted)`):{ok:!0,raw:a}}catch(a){return u("throw",a instanceof Error?a.message:String(a),a)}}async function x(t,e){if(t.target.type!=="api")return u("unknown","not an api target");const{url:r,method:i="POST",headers:n,body:s}=t.target,a=new AbortController,m=setTimeout(()=>a.abort(),e);try{const o=await fetch(r,{method:i,headers:n,body:s,signal:a.signal}),l=await o.text();return o.ok?{ok:!0,raw:l}:u("status",`${o.status} ${o.statusText}`,l)}catch(o){return o instanceof Error&&o.name==="AbortError"?u("timeout",`request exceeded ${e}ms (abandoned \u2014 the server may still be working)`):u("throw",o instanceof Error?o.message:String(o),o)}finally{clearTimeout(m)}}async function U(t,e){if(t.target.type!=="file")return u("unknown","not a file target");const{spawn:r}=await import("node:child_process"),{path:i,args:n=[]}=t.target;return new Promise(s=>{const a=r(process.execPath,[i,...n],{stdio:["ignore","pipe","pipe"]});let m="",o="",l=!1;const d=setTimeout(()=>{l=!0,a.kill("SIGKILL")},e);a.stdout?.on("data",c=>{m+=c}),a.stderr?.on("data",c=>{o+=c}),a.on("error",c=>{clearTimeout(d),s(u("throw",c.message,c))}),a.on("close",c=>{if(clearTimeout(d),l)return s(u("timeout",`killed after ${e}ms`,o));if(c!==0)return s(u("exit",`exit ${c}: ${o.trim().slice(0,500)}`,o));s({ok:!0,raw:m})})})}function A(t,e,r,i=3e4,n){switch(t.target.type){case"handler":return $(t,e,r,i,n);case"api":return x(t,i);case"file":return U(t,i)}}exports.DEFAULT_TIMEOUT=3e4,exports.runTarget=A;
2
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sources":["../src/runner.ts"],"sourcesContent":["import type { IRegistry } from \"./registry\";\nimport type { NotFoundHandler, TaskDefinition, TaskFailure } from \"./types\";\n\nexport interface RunOutcome {\n ok: boolean;\n reason?: TaskFailure;\n detail?: string;\n raw?: unknown;\n}\n\n/** Milliseconds after which a run is abandoned. */\nexport const DEFAULT_TIMEOUT = 30_000;\n\nfunction failure(reason: TaskFailure, detail: string, raw?: unknown): RunOutcome {\n return { ok: false, reason, detail, raw };\n}\n\n/**\n * Races a promise against a deadline.\n *\n * The loser is not cancelled — nothing here can cancel a promise. Each target\n * kind does what it can on top of this: an HTTP request is aborted, a child\n * process is killed, and an in-process handler simply keeps running with\n * nobody listening. That last one is a real limit, not an oversight: a\n * function already executing cannot be interrupted in JavaScript.\n */\nfunction withDeadline<T>(work: Promise<T>, ms: number): Promise<T | typeof TIMED_OUT> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => resolve(TIMED_OUT), ms);\n work.then(\n (value) => { clearTimeout(timer); resolve(value); },\n (error) => { clearTimeout(timer); reject(error); },\n );\n });\n}\n\nconst TIMED_OUT = Symbol(\"timed-out\");\n\nasync function runHandler(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: unknown,\n timeout: number,\n notFound?: NotFoundHandler,\n): Promise<RunOutcome> {\n if (task.target.type !== \"handler\") return failure(\"unknown\", \"not a handler target\");\n\n const Entry = registry.get(task.target.key);\n\n if (!Entry) {\n // Loud, not silent: a row that is enabled but points nowhere would\n // otherwise look like a task that simply never fires.\n await notFound?.(task.target.key, task);\n return failure(\"unknown\", `no handler registered for \"${task.target.key}\"`);\n }\n\n try {\n const result = await withDeadline(Promise.resolve(new Entry().run(context)), timeout);\n\n if (result === TIMED_OUT) {\n return failure(\"timeout\", `handler exceeded ${timeout}ms (still running — a function cannot be interrupted)`);\n }\n\n return { ok: true, raw: result };\n } catch (error) {\n return failure(\"throw\", error instanceof Error ? error.message : String(error), error);\n }\n}\n\nasync function runApi(task: TaskDefinition, timeout: number): Promise<RunOutcome> {\n if (task.target.type !== \"api\") return failure(\"unknown\", \"not an api target\");\n\n const { url, method = \"POST\", headers, body } = task.target;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(url, { method, headers, body, signal: controller.signal });\n const text = await response.text();\n\n // Only the status decides. A 200 carrying `{ ok: false }` is the caller's\n // business — the scheduler has no way to know what a body means, and\n // guessing would make it wrong for somebody.\n return response.ok\n ? { ok: true, raw: text }\n : failure(\"status\", `${response.status} ${response.statusText}`, text);\n } catch (error) {\n const aborted = error instanceof Error && error.name === \"AbortError\";\n\n return aborted\n ? failure(\"timeout\", `request exceeded ${timeout}ms (abandoned — the server may still be working)`)\n : failure(\"throw\", error instanceof Error ? error.message : String(error), error);\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function runFile(task: TaskDefinition, timeout: number): Promise<RunOutcome> {\n if (task.target.type !== \"file\") return failure(\"unknown\", \"not a file target\");\n\n // Imported here rather than at the top so that a consumer using only\n // handler and api targets is not forced onto Node.\n const { spawn } = await import(\"node:child_process\");\n const { path, args = [] } = task.target;\n\n return new Promise<RunOutcome>((resolve) => {\n const child = spawn(process.execPath, [path, ...args], { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let killed = false;\n\n const timer = setTimeout(() => {\n killed = true;\n // The one target kind that can actually be stopped.\n child.kill(\"SIGKILL\");\n }, timeout);\n\n child.stdout?.on(\"data\", (chunk) => { stdout += chunk; });\n child.stderr?.on(\"data\", (chunk) => { stderr += chunk; });\n\n child.on(\"error\", (error) => {\n clearTimeout(timer);\n resolve(failure(\"throw\", error.message, error));\n });\n\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n\n if (killed) return resolve(failure(\"timeout\", `killed after ${timeout}ms`, stderr));\n if (code !== 0) return resolve(failure(\"exit\", `exit ${code}: ${stderr.trim().slice(0, 500)}`, stderr));\n\n resolve({ ok: true, raw: stdout });\n });\n });\n}\n\nexport function runTarget(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: unknown,\n timeout = DEFAULT_TIMEOUT,\n notFound?: NotFoundHandler,\n): Promise<RunOutcome> {\n switch (task.target.type) {\n case \"handler\": return runHandler(task, registry, context, timeout, notFound);\n case \"api\": return runApi(task, timeout);\n case \"file\": return runFile(task, timeout);\n }\n}\n"],"names":["DEFAULT_TIMEOUT","failure","reason","detail","raw","withDeadline","work","ms","resolve","reject","timer","TIMED_OUT","value","error","runHandler","task","registry","context","timeout","notFound","Entry","result","runApi","url","method","headers","body","controller","response","text","runFile","spawn","path","args","child","stdout","stderr","killed","chunk","code","runTarget"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const f=3e4;function s(t,n,o){return{ok:!1,reason:t,detail:n,raw:o}}function w(t,n){return new Promise((o,i)=>{const u=setTimeout(()=>o(g),n);t.then(a=>{clearTimeout(u),o(a)},a=>{clearTimeout(u),i(a)})})}const g=Symbol("timed-out");async function h(t,n,o,i,u){if(t.target.type!=="handler")return s("unknown","not a handler target");const a=n.get(t.target.key);if(!a)return await u?.(t.target.key,t),s("unknown",`no handler registered for "${t.target.key}"`);try{const e=await w(Promise.resolve(new a().run(o)),i);return e===g?s("timeout",`handler exceeded ${i}ms (still running \u2014 a function cannot be interrupted)`):{ok:!0,raw:e}}catch(e){return s("throw",e instanceof Error?e.message:String(e),e)}}async function p(t,n){if(t.target.type!=="api")return s("unknown","not an api target");const{url:o,method:i="POST",headers:u,body:a}=t.target,e=new AbortController,m=setTimeout(()=>e.abort(),n);try{const r=await fetch(o,{method:i,headers:u,body:a,signal:e.signal}),l=await r.text();return r.ok?{ok:!0,raw:l}:s("status",`${r.status} ${r.statusText}`,l)}catch(r){return r instanceof Error&&r.name==="AbortError"?s("timeout",`request exceeded ${n}ms (abandoned \u2014 the server may still be working)`):s("throw",r instanceof Error?r.message:String(r),r)}finally{clearTimeout(m)}}async function y(t,n){if(t.target.type!=="file")return s("unknown","not a file target");const{spawn:o}=await import("node:child_process"),{path:i,args:u=[]}=t.target;return new Promise(a=>{const e=o(process.execPath,[i,...u],{stdio:["ignore","pipe","pipe"]});let m="",r="",l=!1;const d=setTimeout(()=>{l=!0,e.kill("SIGKILL")},n);e.stdout?.on("data",c=>{m+=c}),e.stderr?.on("data",c=>{r+=c}),e.on("error",c=>{clearTimeout(d),a(s("throw",c.message,c))}),e.on("close",c=>{if(clearTimeout(d),l)return a(s("timeout",`killed after ${n}ms`,r));if(c!==0)return a(s("exit",`exit ${c}: ${r.trim().slice(0,500)}`,r));a({ok:!0,raw:m})})})}function k(t,n,o,i=3e4,u){switch(t.target.type){case"handler":return h(t,n,o,i,u);case"api":return p(t,i);case"file":return y(t,i)}}export{f as DEFAULT_TIMEOUT,k as runTarget};
2
+ //# sourceMappingURL=runner.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.mjs","sources":["../src/runner.ts"],"sourcesContent":["import type { IRegistry } from \"./registry\";\nimport type { NotFoundHandler, TaskDefinition, TaskFailure } from \"./types\";\n\nexport interface RunOutcome {\n ok: boolean;\n reason?: TaskFailure;\n detail?: string;\n raw?: unknown;\n}\n\n/** Milliseconds after which a run is abandoned. */\nexport const DEFAULT_TIMEOUT = 30_000;\n\nfunction failure(reason: TaskFailure, detail: string, raw?: unknown): RunOutcome {\n return { ok: false, reason, detail, raw };\n}\n\n/**\n * Races a promise against a deadline.\n *\n * The loser is not cancelled — nothing here can cancel a promise. Each target\n * kind does what it can on top of this: an HTTP request is aborted, a child\n * process is killed, and an in-process handler simply keeps running with\n * nobody listening. That last one is a real limit, not an oversight: a\n * function already executing cannot be interrupted in JavaScript.\n */\nfunction withDeadline<T>(work: Promise<T>, ms: number): Promise<T | typeof TIMED_OUT> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => resolve(TIMED_OUT), ms);\n work.then(\n (value) => { clearTimeout(timer); resolve(value); },\n (error) => { clearTimeout(timer); reject(error); },\n );\n });\n}\n\nconst TIMED_OUT = Symbol(\"timed-out\");\n\nasync function runHandler(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: unknown,\n timeout: number,\n notFound?: NotFoundHandler,\n): Promise<RunOutcome> {\n if (task.target.type !== \"handler\") return failure(\"unknown\", \"not a handler target\");\n\n const Entry = registry.get(task.target.key);\n\n if (!Entry) {\n // Loud, not silent: a row that is enabled but points nowhere would\n // otherwise look like a task that simply never fires.\n await notFound?.(task.target.key, task);\n return failure(\"unknown\", `no handler registered for \"${task.target.key}\"`);\n }\n\n try {\n const result = await withDeadline(Promise.resolve(new Entry().run(context)), timeout);\n\n if (result === TIMED_OUT) {\n return failure(\"timeout\", `handler exceeded ${timeout}ms (still running — a function cannot be interrupted)`);\n }\n\n return { ok: true, raw: result };\n } catch (error) {\n return failure(\"throw\", error instanceof Error ? error.message : String(error), error);\n }\n}\n\nasync function runApi(task: TaskDefinition, timeout: number): Promise<RunOutcome> {\n if (task.target.type !== \"api\") return failure(\"unknown\", \"not an api target\");\n\n const { url, method = \"POST\", headers, body } = task.target;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetch(url, { method, headers, body, signal: controller.signal });\n const text = await response.text();\n\n // Only the status decides. A 200 carrying `{ ok: false }` is the caller's\n // business — the scheduler has no way to know what a body means, and\n // guessing would make it wrong for somebody.\n return response.ok\n ? { ok: true, raw: text }\n : failure(\"status\", `${response.status} ${response.statusText}`, text);\n } catch (error) {\n const aborted = error instanceof Error && error.name === \"AbortError\";\n\n return aborted\n ? failure(\"timeout\", `request exceeded ${timeout}ms (abandoned — the server may still be working)`)\n : failure(\"throw\", error instanceof Error ? error.message : String(error), error);\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function runFile(task: TaskDefinition, timeout: number): Promise<RunOutcome> {\n if (task.target.type !== \"file\") return failure(\"unknown\", \"not a file target\");\n\n // Imported here rather than at the top so that a consumer using only\n // handler and api targets is not forced onto Node.\n const { spawn } = await import(\"node:child_process\");\n const { path, args = [] } = task.target;\n\n return new Promise<RunOutcome>((resolve) => {\n const child = spawn(process.execPath, [path, ...args], { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let killed = false;\n\n const timer = setTimeout(() => {\n killed = true;\n // The one target kind that can actually be stopped.\n child.kill(\"SIGKILL\");\n }, timeout);\n\n child.stdout?.on(\"data\", (chunk) => { stdout += chunk; });\n child.stderr?.on(\"data\", (chunk) => { stderr += chunk; });\n\n child.on(\"error\", (error) => {\n clearTimeout(timer);\n resolve(failure(\"throw\", error.message, error));\n });\n\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n\n if (killed) return resolve(failure(\"timeout\", `killed after ${timeout}ms`, stderr));\n if (code !== 0) return resolve(failure(\"exit\", `exit ${code}: ${stderr.trim().slice(0, 500)}`, stderr));\n\n resolve({ ok: true, raw: stdout });\n });\n });\n}\n\nexport function runTarget(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: unknown,\n timeout = DEFAULT_TIMEOUT,\n notFound?: NotFoundHandler,\n): Promise<RunOutcome> {\n switch (task.target.type) {\n case \"handler\": return runHandler(task, registry, context, timeout, notFound);\n case \"api\": return runApi(task, timeout);\n case \"file\": return runFile(task, timeout);\n }\n}\n"],"names":["DEFAULT_TIMEOUT","failure","reason","detail","raw","withDeadline","work","ms","resolve","reject","timer","TIMED_OUT","value","error","runHandler","task","registry","context","timeout","notFound","Entry","result","runApi","url","method","headers","body","controller","response","text","runFile","spawn","path","args","child","stdout","stderr","killed","chunk","code","runTarget"],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ import { type IRegistry } from "./registry";
2
+ import { type TaskStatus } from "./task";
3
+ import type { ClassType, Coordinator, ErrorHandler, Hook, InjectMap, Injected, NotFoundHandler, Source, TaskParser } from "./types";
4
+ /** What happens to a task that disappears from the source. */
5
+ export type CascadePolicy =
6
+ /** Stop scheduling, let the run in flight finish, then drop it. */
7
+ "drain"
8
+ /** Stop scheduling now. A file target is killed; the other two are let go — see `runner`. */
9
+ | "stop"
10
+ /** Leave it running. For when the source is not the only authority. */
11
+ | "keep";
12
+ interface Descriptor<Entry, Context> {
13
+ injects: InjectMap;
14
+ source?: Source<Entry, Context>;
15
+ parser?: ClassType<TaskParser<Entry>>;
16
+ registry: IRegistry<Context>;
17
+ coordinator?: ClassType<Coordinator>;
18
+ hook?: ClassType<Hook>;
19
+ retry: number;
20
+ timeout: number;
21
+ syncMs: number | false;
22
+ tickMs: number;
23
+ cascade: CascadePolicy;
24
+ onError?: ErrorHandler;
25
+ notFound?: NotFoundHandler;
26
+ }
27
+ export interface ScheduleStatus {
28
+ running: boolean;
29
+ lastSyncAt: Date | null;
30
+ lastSyncOk: boolean | null;
31
+ tasks: TaskStatus[];
32
+ }
33
+ /**
34
+ * A configured scheduler.
35
+ *
36
+ * A plain instance with no global state: constructing two gives two, and
37
+ * whether that should happen is the caller's business. Whatever runs this once
38
+ * — a bootstrap, a framework's init — already owns that question, and
39
+ * answering it here as well would only take the choice away.
40
+ */
41
+ export declare class ScheduleRunner<Entry = string, Context = unknown> {
42
+ private readonly d;
43
+ private readonly tasks;
44
+ private context;
45
+ private parser;
46
+ private coordinator?;
47
+ private hook?;
48
+ private tickTimer;
49
+ private syncTimer;
50
+ private inFlight;
51
+ private started;
52
+ private lastSyncAt;
53
+ private lastSyncOk;
54
+ constructor(descriptor: Descriptor<Entry, Context>);
55
+ start(): Promise<void>;
56
+ /** Clears both timers and waits for runs already in flight. */
57
+ stop(): Promise<void>;
58
+ status(): ScheduleStatus;
59
+ private read;
60
+ /**
61
+ * Re-reads the source and reconciles.
62
+ *
63
+ * Two failure modes are kept apart on purpose. A source that throws leaves
64
+ * the schedule exactly as it was — a database blip is not an instruction to
65
+ * cancel everything. A source that returns nothing while tasks exist is
66
+ * treated the same way: it is far more likely to be a partial read than a
67
+ * deliberate deletion of every job at once, and getting that wrong wipes a
68
+ * schedule during an incident, which is the worst possible moment.
69
+ */
70
+ sync(): Promise<void>;
71
+ private tick;
72
+ private run;
73
+ private safely;
74
+ private report;
75
+ }
76
+ export interface IScheduleBuilder<Entry = string, Context = unknown> extends ClassType<ScheduleRunner<Entry, Context>> {
77
+ source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;
78
+ task(parser: ClassType<TaskParser<Entry>>): IScheduleBuilder<Entry, Context>;
79
+ registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;
80
+ coordinator(coordinator: ClassType<Coordinator>): IScheduleBuilder<Entry, Context>;
81
+ hook(hook: ClassType<Hook>): IScheduleBuilder<Entry, Context>;
82
+ retry(times: number): IScheduleBuilder<Entry, Context>;
83
+ timeout(ms: number): IScheduleBuilder<Entry, Context>;
84
+ /**
85
+ * Whether, and how often, the source is re-read and reconciled against the
86
+ * tasks already running: a source entry that is new becomes a task, one that
87
+ * changed is updated, one that disappeared is handled by `cascade`.
88
+ *
89
+ * `false` turns the repeat off — read once at start and never again.
90
+ * `true` uses the default interval. A number sets it.
91
+ */
92
+ sync(every: number | boolean): IScheduleBuilder<Entry, Context>;
93
+ tick(ms: number): IScheduleBuilder<Entry, Context>;
94
+ cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;
95
+ onError(handler: ErrorHandler): IScheduleBuilder<Entry, Context>;
96
+ notFound(handler: NotFoundHandler): IScheduleBuilder<Entry, Context>;
97
+ }
98
+ /**
99
+ * Chain-style configuration for a scheduler.
100
+ *
101
+ * ```ts
102
+ * export const AppSchedule = Schedule({ db: DataSource })
103
+ * .source((ctx) => ctx.db.query("select * from crons"))
104
+ * .task(RowParser)
105
+ * .registry(Registry.add(Registry("session.cleanup", (ctx) => ...)))
106
+ * .coordinator(PgCoordinator)
107
+ * .hook(Hook.combine(LoggerHook, TelegramHook))
108
+ * .retry(2)
109
+ * .sync(30_000)
110
+ * .cascade("drain");
111
+ *
112
+ * const schedule = new AppSchedule();
113
+ * await schedule.start();
114
+ * ```
115
+ *
116
+ * The chain is the class — there is no terminal to call, and whoever holds the
117
+ * instance decides when it starts and stops.
118
+ */
119
+ export declare function Schedule<Injects extends InjectMap = Record<string, never>>(injects?: Injects): IScheduleBuilder<string, Injected<unknown, Injects>>;
120
+ export {};
121
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtD,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,KAAK,EACV,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAChF,MAAM,EAA6B,UAAU,EAC9C,MAAM,SAAS,CAAC;AAOjB,8DAA8D;AAC9D,MAAM,MAAM,aAAa;AACvB,mEAAmE;AACjE,OAAO;AACT,6FAA6F;GAC3F,MAAM;AACR,uEAAuE;GACrE,MAAM,CAAC;AAEX,UAAU,UAAU,CAAC,KAAK,EAAE,OAAO;IACjC,OAAO,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IACtC,QAAQ,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC;IACrC,IAAI,CAAC,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,OAAO,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,qBAAa,cAAc,CAAC,KAAK,GAAG,MAAM,EAAE,OAAO,GAAG,OAAO;IAC3D,OAAO,CAAC,QAAQ,CAAC,CAAC,CAA6B;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA2B;IAEjD,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,WAAW,CAAC,CAAc;IAClC,OAAO,CAAC,IAAI,CAAC,CAAO;IAEpB,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,QAAQ,CAA4B;IAE5C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,UAAU,CAAwB;gBAE9B,UAAU,EAAE,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC;IAI5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkC5B,+DAA+D;IACzD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAS3B,MAAM,IAAI,cAAc;YAWV,IAAI;IAIlB;;;;;;;;;OASG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA+D3B,OAAO,CAAC,IAAI;YAiBE,GAAG;YAuDH,MAAM;YAQN,MAAM;CAUrB;AAID,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,MAAM,EAAE,OAAO,GAAG,OAAO,CACjE,SAAQ,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACpE,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC7E,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACzE,WAAW,CAAC,WAAW,EAAE,SAAS,CAAC,WAAW,CAAC,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACnF,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC9D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACvD,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACtD;;;;;;;OAOG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAChE,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACnD,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjE,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACjE,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;CACtE;AA+BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,QAAQ,CAAC,OAAO,SAAS,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACxE,OAAO,GAAE,OAAuB,GAC/B,gBAAgB,CAAC,MAAM,EAAE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAUtD"}
@@ -0,0 +1,2 @@
1
+ "use strict";var f=require("./registry.js"),l=require("./runner.js"),p=require("./task.js"),m=Object.defineProperty,g=(n,t,e)=>t in n?m(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,i=(n,t,e)=>g(n,typeof t!="symbol"?t+"":t,e);const S=1e3,d=6e4,T=0;class y{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(o){await this.report(`unparseable entry: ${o instanceof Error?o.message:String(o)}`);continue}e.add(a.key);const c=this.tasks.get(a.key);c?c.update(a,s):this.tasks.set(a.key,new p.Task(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 c;for(let o=1;o<=r;o++){const u=new Date,h=await l.runTarget(s,this.d.registry,this.context,a,this.d.notFound);if(c={key:t.key,scheduledFor:e,startedAt:u,durationMs:Date.now()-u.getTime(),attempt:o,ok:h.ok,reason:h.reason,detail:h.detail,raw:h.raw},h.ok)break;await this.d.onError?.(c,s),o<r&&await new Promise(w=>setTimeout(w,Math.min(2**o*1e3,3e4)))}t.record(c.ok,c.startedAt),await this.safely(()=>this.hook?.notify(c)),await this.safely(()=>this.coordinator?.release(t.key,e,c))}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 k(n){const t=e=>k({...n,...e});return class extends y{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?d: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 v(n={}){return k({injects:n,registry:f.Registry.empty(),retry:T,timeout:l.DEFAULT_TIMEOUT,syncMs:d,tickMs:S,cascade:"drain"})}exports.Schedule=v,exports.ScheduleRunner=y;
2
+ //# sourceMappingURL=schedule.js.map