@ecosy/schedule 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/dist/hook/index.d.ts +13 -6
- package/dist/hook/index.d.ts.map +1 -1
- package/dist/hook/index.js +1 -1
- package/dist/hook/index.js.map +1 -1
- package/dist/hook/index.mjs +1 -1
- package/dist/hook/index.mjs.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/registry.js.map +1 -1
- package/dist/registry.mjs +1 -1
- package/dist/registry.mjs.map +1 -1
- package/dist/runner.d.ts +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +1 -1
- package/dist/runner.js.map +1 -1
- package/dist/runner.mjs +1 -1
- package/dist/runner.mjs.map +1 -1
- package/dist/schedule.d.ts +12 -10
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +1 -1
- package/dist/schedule.js.map +1 -1
- package/dist/schedule.mjs +1 -1
- package/dist/schedule.mjs.map +1 -1
- package/dist/types.d.ts +33 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1 +1,25 @@
|
|
|
1
|
-
# ecosy-schedule
|
|
1
|
+
# ecosy-schedule
|
|
2
|
+
## Injected context
|
|
3
|
+
|
|
4
|
+
`Schedule({ … })` takes an injection map. Each entry is constructed once at
|
|
5
|
+
`start()`, and the resulting object is the context every callback receives:
|
|
6
|
+
a registry handler's `run(ctx)`, a source's `read(ctx)`, and — since 0.2.0 —
|
|
7
|
+
the third argument of `notFound` and `onError`.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export const AppSchedule = Schedule({ logger: AppLogger })
|
|
11
|
+
.source(ApiSource)
|
|
12
|
+
.task(CronRowParser)
|
|
13
|
+
.registry(registry)
|
|
14
|
+
.notFound((key, task, ctx) => {
|
|
15
|
+
ctx.logger.warn(`[cron] "${task.key}" names handler "${key}", which is not registered`);
|
|
16
|
+
})
|
|
17
|
+
.onError((event, task, ctx) => {
|
|
18
|
+
ctx.logger.error(`[cron] ${task.key} failed (${event.reason}): ${event.detail ?? ""}`);
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Those two are plain functions rather than classes, so they cannot inject for
|
|
23
|
+
themselves; before 0.2.0 an app had to reach its logger around the scheduler.
|
|
24
|
+
The context is the last argument, so a handler written for `(key, task)` or
|
|
25
|
+
`(event, task)` still fits.
|
package/dist/hook/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { ClassType, Hook as HookPort } from "../types";
|
|
2
|
+
type LineLogger = {
|
|
3
|
+
info(...a: unknown[]): void;
|
|
4
|
+
error(...a: unknown[]): void;
|
|
5
|
+
};
|
|
2
6
|
/**
|
|
3
7
|
* Builds a hook class that writes each outcome as a log line.
|
|
4
8
|
*
|
|
@@ -7,12 +11,15 @@ import type { ClassType, Hook as HookPort } from "../types";
|
|
|
7
11
|
* nothing has to be told apart at runtime.
|
|
8
12
|
*
|
|
9
13
|
* Takes anything console-shaped, which `console` itself already is, so it costs
|
|
10
|
-
* no dependency and works before real logging is wired up
|
|
14
|
+
* no dependency and works before real logging is wired up — or a function of
|
|
15
|
+
* the injected context, to write through the logger `Schedule({ … })` injected
|
|
16
|
+
* instead of one reached around it:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* Schedule({ logger: AppLogger }).hook(LoggerHook((ctx) => ctx.logger))
|
|
20
|
+
* ```
|
|
11
21
|
*/
|
|
12
|
-
export declare function LoggerHook(logger?:
|
|
13
|
-
info(...a: unknown[]): void;
|
|
14
|
-
error(...a: unknown[]): void;
|
|
15
|
-
}): ClassType<HookPort>;
|
|
22
|
+
export declare function LoggerHook<Context = unknown>(logger?: LineLogger | ((context: Context) => LineLogger)): ClassType<HookPort<Context>>;
|
|
16
23
|
interface HookFactory {
|
|
17
24
|
/**
|
|
18
25
|
* Fans one event out to several hooks, as a single hook class.
|
|
@@ -25,7 +32,7 @@ interface HookFactory {
|
|
|
25
32
|
* same. With no arguments it is a valid no-op, which makes it a usable
|
|
26
33
|
* default rather than something callers must guard against.
|
|
27
34
|
*/
|
|
28
|
-
combine(...hooks: ClassType<HookPort
|
|
35
|
+
combine<Context = unknown>(...hooks: ClassType<HookPort<Context>>[]): ClassType<HookPort<Context>>;
|
|
29
36
|
}
|
|
30
37
|
export declare const Hook: HookFactory;
|
|
31
38
|
export {};
|
package/dist/hook/index.d.ts.map
CHANGED
|
@@ -1 +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
|
|
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,KAAK,UAAU,GAAG;IAAE,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAAC,KAAK,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;CAAE,CAAC;AAEhF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAG,OAAO,EAC1C,MAAM,GAAE,UAAU,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,KAAK,UAAU,CAAW,GAChE,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAiB9B;AAkCD,UAAU,WAAW;IACnB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,OAAO,GAAG,OAAO,EAAE,GAAG,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,GAAG,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;CACpG;AAED,eAAO,MAAM,IAAI,EAAE,WAkBlB,CAAC"}
|
package/dist/hook/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var l=Object.defineProperty,
|
|
1
|
+
"use strict";var l=Object.defineProperty,m=(t,e,o)=>e in t?l(t,e,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[e]=o,a=(t,e,o)=>m(t,typeof e!="symbol"?e+"":e,o);function f(t=console){return class{notify(e,o){const n=typeof t=="function"?t(o):t,r=`${e.durationMs}ms`;if(e.ok){n.info(`[schedule] ${e.key} ok in ${r}`);return}n.error(`[schedule] ${e.key} failed (${e.reason}) after ${r}${e.attempt>1?` on attempt ${e.attempt}`:""}: ${e.detail??""}`)}}}function d(t,e,o){return new Promise(n=>{const r=setTimeout(()=>{console.warn(`[schedule] hook ${o} exceeded ${e}ms \u2014 abandoned`),n()},e);Promise.resolve().then(t).then(()=>{clearTimeout(r),n()},s=>{clearTimeout(r),console.warn(`[schedule] hook ${o} threw:`,s),n()})})}const c=Symbol.for("@ecosy/schedule:hook-parts"),h={combine(...t){var e,o;const n=t.flatMap(r=>r[c]??[r]);return e=c,o=class{constructor(){a(this,"parts",n.map(r=>new r))}async notify(r,s){await Promise.all(this.parts.map((i,u)=>d(()=>i.notify(r,s),1e4,`#${u}`)))}},a(o,e,n),o}};exports.Hook=h,exports.LoggerHook=f;
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/hook/index.js.map
CHANGED
|
@@ -1 +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
|
|
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\ntype LineLogger = { info(...a: unknown[]): void; error(...a: unknown[]): void };\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 — or a function of\n * the injected context, to write through the logger `Schedule({ … })` injected\n * instead of one reached around it:\n *\n * ```ts\n * Schedule({ logger: AppLogger }).hook(LoggerHook((ctx) => ctx.logger))\n * ```\n */\nexport function LoggerHook<Context = unknown>(\n logger: LineLogger | ((context: Context) => LineLogger) = console,\n): ClassType<HookPort<Context>> {\n return class implements HookPort<Context> {\n notify(event: TaskEvent, context: Context) {\n const log = typeof logger === \"function\" ? logger(context) : logger;\n const took = `${event.durationMs}ms`;\n\n if (event.ok) {\n log.info(`[schedule] ${event.key} ok in ${took}`);\n return;\n }\n\n log.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<Context = unknown>(...hooks: ClassType<HookPort<Context>>[]): ClassType<HookPort<Context>>;\n}\n\nexport const Hook: HookFactory = {\n combine<Context = unknown>(...hooks: ClassType<HookPort<Context>>[]): ClassType<HookPort<Context>> {\n const flat = hooks.flatMap(\n (hook) => (hook as { [PARTS]?: ClassType<HookPort<Context>>[] })[PARTS] ?? [hook],\n );\n\n return class Combined implements HookPort<Context> {\n static readonly [PARTS] = flat;\n\n private readonly parts = flat.map((Hook) => new Hook());\n\n async notify(event: TaskEvent, context: Context) {\n await Promise.all(\n this.parts.map((hook, i) => deadline(() => hook.notify(event, context), HOOK_TIMEOUT, `#${i}`)),\n );\n }\n };\n },\n};\n"],"names":["LoggerHook","logger","event","context","log","took","deadline","work","ms","label","resolve","timer","error","PARTS","Hook","hooks","_a","_b","flat","hook","__publicField","i"],"mappings":"yKAuBO,SAASA,EACdC,EAA0D,QAC5B,CAC9B,OAAO,KAAmC,CACxC,OAAOC,EAAkBC,EAAkB,CACzC,MAAMC,EAAM,OAAOH,GAAW,WAAaA,EAAOE,CAAO,EAAIF,EACvDI,EAAO,GAAGH,EAAM,UAAU,KAEhC,GAAIA,EAAM,GAAI,CACZE,EAAI,KAAK,cAAcF,EAAM,GAAG,UAAUG,CAAI,EAAE,EAChD,MACF,CAEAD,EAAI,MACF,cAAcF,EAAM,GAAG,YAAYA,EAAM,MAAM,WAAWG,CAAI,GACzDH,EAAM,QAAU,EAAI,eAAeA,EAAM,OAAO,GAAK,EAAE,KAAKA,EAAM,QAAU,EAAE,EACrF,CACF,CACF,CACF,CAWA,SAASI,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,QAAA,EACL,KAAKD,CAAI,EACT,KACC,IAAM,CAAE,aAAaI,CAAK,EAAGD,GAAW,EACvCE,GAAU,CACT,aAAaD,CAAK,EAClB,QAAQ,KAAK,mBAAmBF,CAAK,UAAWG,CAAK,EACrDF,EAAAA,CACF,CACF,CACJ,CAAC,CACH,CAGA,MAAMG,EAAQ,OAAO,IAAI,4BAA4B,EAiBxCC,EAAoB,CAC/B,WAA8BC,EAAqE,CA5FrG,IAAAC,EAAAC,EA6FI,MAAMC,EAAOH,EAAM,QAChBI,GAAUA,EAAsDN,CAAK,GAAK,CAACM,CAAI,CAClF,EAEA,OACmBH,EAAAH,EADZI,EAAA,KAA4C,CAA5C,aAAA,CAGLG,EAAA,KAAiB,QAAQF,EAAK,IAAKJ,GAAS,IAAIA,CAAM,CAAA,CAAA,CAEtD,MAAM,OAAOZ,EAAkBC,EAAkB,CAC/C,MAAM,QAAQ,IACZ,KAAK,MAAM,IAAI,CAACgB,EAAME,IAAMf,EAAS,IAAMa,EAAK,OAAOjB,EAAOC,CAAO,EAAG,IAAc,IAAIkB,CAAC,EAAE,CAAC,CAChG,CACF,CACF,EATED,EADKH,EACYD,EAASE,CAAAA,EADrBD,CAWT,CACF"}
|
package/dist/hook/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
1
|
+
var u=Object.defineProperty,m=(t,e,o)=>e in t?u(t,e,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[e]=o,s=(t,e,o)=>m(t,typeof e!="symbol"?e+"":e,o);function f(t=console){return class{notify(e,o){const n=typeof t=="function"?t(o):t,r=`${e.durationMs}ms`;if(e.ok){n.info(`[schedule] ${e.key} ok in ${r}`);return}n.error(`[schedule] ${e.key} failed (${e.reason}) after ${r}${e.attempt>1?` on attempt ${e.attempt}`:""}: ${e.detail??""}`)}}}function p(t,e,o){return new Promise(n=>{const r=setTimeout(()=>{console.warn(`[schedule] hook ${o} exceeded ${e}ms \u2014 abandoned`),n()},e);Promise.resolve().then(t).then(()=>{clearTimeout(r),n()},a=>{clearTimeout(r),console.warn(`[schedule] hook ${o} threw:`,a),n()})})}const c=Symbol.for("@ecosy/schedule:hook-parts"),d={combine(...t){var e,o;const n=t.flatMap(r=>r[c]??[r]);return e=c,o=class{constructor(){s(this,"parts",n.map(r=>new r))}async notify(r,a){await Promise.all(this.parts.map((i,l)=>p(()=>i.notify(r,a),1e4,`#${l}`)))}},s(o,e,n),o}};export{d as Hook,f as LoggerHook};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/hook/index.mjs.map
CHANGED
|
@@ -1 +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
|
|
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\ntype LineLogger = { info(...a: unknown[]): void; error(...a: unknown[]): void };\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 — or a function of\n * the injected context, to write through the logger `Schedule({ … })` injected\n * instead of one reached around it:\n *\n * ```ts\n * Schedule({ logger: AppLogger }).hook(LoggerHook((ctx) => ctx.logger))\n * ```\n */\nexport function LoggerHook<Context = unknown>(\n logger: LineLogger | ((context: Context) => LineLogger) = console,\n): ClassType<HookPort<Context>> {\n return class implements HookPort<Context> {\n notify(event: TaskEvent, context: Context) {\n const log = typeof logger === \"function\" ? logger(context) : logger;\n const took = `${event.durationMs}ms`;\n\n if (event.ok) {\n log.info(`[schedule] ${event.key} ok in ${took}`);\n return;\n }\n\n log.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<Context = unknown>(...hooks: ClassType<HookPort<Context>>[]): ClassType<HookPort<Context>>;\n}\n\nexport const Hook: HookFactory = {\n combine<Context = unknown>(...hooks: ClassType<HookPort<Context>>[]): ClassType<HookPort<Context>> {\n const flat = hooks.flatMap(\n (hook) => (hook as { [PARTS]?: ClassType<HookPort<Context>>[] })[PARTS] ?? [hook],\n );\n\n return class Combined implements HookPort<Context> {\n static readonly [PARTS] = flat;\n\n private readonly parts = flat.map((Hook) => new Hook());\n\n async notify(event: TaskEvent, context: Context) {\n await Promise.all(\n this.parts.map((hook, i) => deadline(() => hook.notify(event, context), HOOK_TIMEOUT, `#${i}`)),\n );\n }\n };\n },\n};\n"],"names":["LoggerHook","logger","event","context","log","took","deadline","work","ms","label","resolve","timer","error","PARTS","Hook","hooks","_a","_b","flat","hook","__publicField","i"],"mappings":"4JAuBO,SAASA,EACdC,EAA0D,QAC5B,CAC9B,OAAO,KAAmC,CACxC,OAAOC,EAAkBC,EAAkB,CACzC,MAAMC,EAAM,OAAOH,GAAW,WAAaA,EAAOE,CAAO,EAAIF,EACvDI,EAAO,GAAGH,EAAM,UAAU,KAEhC,GAAIA,EAAM,GAAI,CACZE,EAAI,KAAK,cAAcF,EAAM,GAAG,UAAUG,CAAI,EAAE,EAChD,MACF,CAEAD,EAAI,MACF,cAAcF,EAAM,GAAG,YAAYA,EAAM,MAAM,WAAWG,CAAI,GACzDH,EAAM,QAAU,EAAI,eAAeA,EAAM,OAAO,GAAK,EAAE,KAAKA,EAAM,QAAU,EAAE,EACrF,CACF,CACF,CACF,CAWA,SAASI,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,QAAA,EACL,KAAKD,CAAI,EACT,KACC,IAAM,CAAE,aAAaI,CAAK,EAAGD,GAAW,EACvCE,GAAU,CACT,aAAaD,CAAK,EAClB,QAAQ,KAAK,mBAAmBF,CAAK,UAAWG,CAAK,EACrDF,EAAAA,CACF,CACF,CACJ,CAAC,CACH,CAGA,MAAMG,EAAQ,OAAO,IAAI,4BAA4B,EAiBxCC,EAAoB,CAC/B,WAA8BC,EAAqE,CA5FrG,IAAAC,EAAAC,EA6FI,MAAMC,EAAOH,EAAM,QAChBI,GAAUA,EAAsDN,CAAK,GAAK,CAACM,CAAI,CAClF,EAEA,OACmBH,EAAAH,EADZI,EAAA,KAA4C,CAA5C,aAAA,CAGLG,EAAA,KAAiB,QAAQF,EAAK,IAAKJ,GAAS,IAAIA,CAAM,CAAA,CAAA,CAEtD,MAAM,OAAOZ,EAAkBC,EAAkB,CAC/C,MAAM,QAAQ,IACZ,KAAK,MAAM,IAAI,CAACgB,EAAME,IAAMf,EAAS,IAAMa,EAAK,OAAOjB,EAAOC,CAAO,EAAG,IAAc,IAAIkB,CAAC,EAAE,CAAC,CAChG,CACF,CACF,EATED,EADKH,EACYD,EAASE,CAAAA,EADrBD,CAWT,CACF"}
|
package/dist/registry.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAoCD,eAAO,MAAM,QAAQ,iBAAe,CAAC"}
|
package/dist/registry.js
CHANGED
|
@@ -1,2 +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(
|
|
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(a){const i={...a};for(const[f,y]of Object.entries(u))i[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
2
|
//# sourceMappingURL=registry.js.map
|
package/dist/registry.js.map
CHANGED
|
@@ -1 +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
|
|
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 — and onto a copy of the scheduler's\n // context, not the context itself. Written onto the shared object they\n // outlived the run, showed up in every other handler and in notFound /\n // onError, and two runs at once overwrote each other's.\n const scoped: Record<string, unknown> = { ...(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(scoped as never);\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","t","n","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,CAMlB,MAAMC,EAAkC,CAAE,GAAID,CAAoC,EAClF,SAAW,CAACE,EAAMC,CAAK,IAAK,OAAO,QAAQL,CAAO,EAChDG,EAAOC,CAAI,EAAI,IAAKC,EAEtB,OAAOJ,EAAQE,CAAe,CAChC,CACF,EAdEG,EADKP,EACW,MAAMJ,CAAAA,EADjBI,CAgBT,EAEAH,EAAa,MAAQ,IAAeL,EAAe,IAAI,GAAK,EAC5DK,EAAa,IAAgBH,GAC3BG,EAAa,MAAA,EAAiB,IAAIH,CAAK,QAE5Bc,EAAWX"}
|
package/dist/registry.mjs
CHANGED
|
@@ -1,2 +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(
|
|
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(u){const i={...u};for(const[f,y]of Object.entries(c))i[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
2
|
//# sourceMappingURL=registry.mjs.map
|
package/dist/registry.mjs.map
CHANGED
|
@@ -1 +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
|
|
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 — and onto a copy of the scheduler's\n // context, not the context itself. Written onto the shared object they\n // outlived the run, showed up in every other handler and in notFound /\n // onError, and two runs at once overwrote each other's.\n const scoped: Record<string, unknown> = { ...(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(scoped as never);\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","t","n","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,CAMlB,MAAMC,EAAkC,CAAE,GAAID,CAAoC,EAClF,SAAW,CAACE,EAAMC,CAAK,IAAK,OAAO,QAAQL,CAAO,EAChDG,EAAOC,CAAI,EAAI,IAAKC,EAEtB,OAAOJ,EAAQE,CAAe,CAChC,CACF,EAdEG,EADKP,EACW,MAAMJ,CAAAA,EADjBI,CAgBT,EAEAH,EAAa,MAAQ,IAAeL,EAAe,IAAI,GAAK,EAC5DK,EAAa,IAAgBH,GAC3BG,EAAa,MAAA,EAAiB,IAAIH,CAAK,QAE5Bc,EAAWX"}
|
package/dist/runner.d.ts
CHANGED
|
@@ -8,5 +8,5 @@ export interface RunOutcome {
|
|
|
8
8
|
}
|
|
9
9
|
/** Milliseconds after which a run is abandoned. */
|
|
10
10
|
export declare const DEFAULT_TIMEOUT = 30000;
|
|
11
|
-
export declare function runTarget(task: TaskDefinition, registry: IRegistry<unknown>, context:
|
|
11
|
+
export declare function runTarget<Context>(task: TaskDefinition, registry: IRegistry<unknown>, context: Context, timeout?: number, notFound?: NotFoundHandler<Context>): Promise<RunOutcome>;
|
|
12
12
|
//# sourceMappingURL=runner.d.ts.map
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,CAAC,OAAO,EAC/B,IAAI,EAAE,cAAc,EACpB,QAAQ,EAAE,SAAS,CAAC,OAAO,CAAC,EAC5B,OAAO,EAAE,OAAO,EAChB,OAAO,SAAkB,EACzB,QAAQ,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,OAAO,CAAC,UAAU,CAAC,CAMrB"}
|
package/dist/runner.js
CHANGED
|
@@ -1,2 +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,
|
|
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,s)=>{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:!(s=T(e,n))||s.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,s)=>{const n=setTimeout(()=>r(f),e);t.then(i=>{clearTimeout(n),r(i)},i=>{clearTimeout(n),s(i)})})}const f=Symbol("timed-out");async function $(t,e,r,s,n){if(t.target.type!=="handler")return u("unknown","not a handler target");const i=e.get(t.target.key);if(!i)return await n?.(t.target.key,t,r),u("unknown",`no handler registered for "${t.target.key}"`);try{const a=await b(Promise.resolve(new i().run(r)),s);return a===f?u("timeout",`handler exceeded ${s}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:s="POST",headers:n,body:i}=t.target,a=new AbortController,m=setTimeout(()=>a.abort(),e);try{const o=await fetch(r,{method:s,headers:n,body:i,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:s,args:n=[]}=t.target;return new Promise(i=>{const a=r(process.execPath,[s,...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),i(u("throw",c.message,c))}),a.on("close",c=>{if(clearTimeout(d),l)return i(u("timeout",`killed after ${e}ms`,o));if(c!==0)return i(u("exit",`exit ${c}: ${o.trim().slice(0,500)}`,o));i({ok:!0,raw:m})})})}function A(t,e,r,s=3e4,n){switch(t.target.type){case"handler":return $(t,e,r,s,n);case"api":return x(t,s);case"file":return U(t,s)}}exports.DEFAULT_TIMEOUT=3e4,exports.runTarget=A;
|
|
2
2
|
//# sourceMappingURL=runner.js.map
|
package/dist/runner.js.map
CHANGED
|
@@ -1 +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:
|
|
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<Context>(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: Context,\n timeout: number,\n notFound?: NotFoundHandler<Context>,\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, context);\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<Context>(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: Context,\n timeout = DEFAULT_TIMEOUT,\n notFound?: NotFoundHandler<Context>,\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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"}
|
package/dist/runner.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const f=3e4;function
|
|
1
|
+
const f=3e4;function i(t,n,a){return{ok:!1,reason:t,detail:n,raw:a}}function w(t,n){return new Promise((a,s)=>{const u=setTimeout(()=>a(g),n);t.then(o=>{clearTimeout(u),a(o)},o=>{clearTimeout(u),s(o)})})}const g=Symbol("timed-out");async function h(t,n,a,s,u){if(t.target.type!=="handler")return i("unknown","not a handler target");const o=n.get(t.target.key);if(!o)return await u?.(t.target.key,t,a),i("unknown",`no handler registered for "${t.target.key}"`);try{const e=await w(Promise.resolve(new o().run(a)),s);return e===g?i("timeout",`handler exceeded ${s}ms (still running \u2014 a function cannot be interrupted)`):{ok:!0,raw:e}}catch(e){return i("throw",e instanceof Error?e.message:String(e),e)}}async function p(t,n){if(t.target.type!=="api")return i("unknown","not an api target");const{url:a,method:s="POST",headers:u,body:o}=t.target,e=new AbortController,m=setTimeout(()=>e.abort(),n);try{const r=await fetch(a,{method:s,headers:u,body:o,signal:e.signal}),l=await r.text();return r.ok?{ok:!0,raw:l}:i("status",`${r.status} ${r.statusText}`,l)}catch(r){return r instanceof Error&&r.name==="AbortError"?i("timeout",`request exceeded ${n}ms (abandoned \u2014 the server may still be working)`):i("throw",r instanceof Error?r.message:String(r),r)}finally{clearTimeout(m)}}async function y(t,n){if(t.target.type!=="file")return i("unknown","not a file target");const{spawn:a}=await import("node:child_process"),{path:s,args:u=[]}=t.target;return new Promise(o=>{const e=a(process.execPath,[s,...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),o(i("throw",c.message,c))}),e.on("close",c=>{if(clearTimeout(d),l)return o(i("timeout",`killed after ${n}ms`,r));if(c!==0)return o(i("exit",`exit ${c}: ${r.trim().slice(0,500)}`,r));o({ok:!0,raw:m})})})}function k(t,n,a,s=3e4,u){switch(t.target.type){case"handler":return h(t,n,a,s,u);case"api":return p(t,s);case"file":return y(t,s)}}export{f as DEFAULT_TIMEOUT,k as runTarget};
|
|
2
2
|
//# sourceMappingURL=runner.mjs.map
|
package/dist/runner.mjs.map
CHANGED
|
@@ -1 +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:
|
|
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<Context>(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: Context,\n timeout: number,\n notFound?: NotFoundHandler<Context>,\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, context);\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<Context>(\n task: TaskDefinition,\n registry: IRegistry<unknown>,\n context: Context,\n timeout = DEFAULT_TIMEOUT,\n notFound?: NotFoundHandler<Context>,\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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"}
|
package/dist/schedule.d.ts
CHANGED
|
@@ -12,17 +12,17 @@ export type CascadePolicy =
|
|
|
12
12
|
interface Descriptor<Entry, Context> {
|
|
13
13
|
injects: InjectMap;
|
|
14
14
|
source?: Source<Entry, Context>;
|
|
15
|
-
parser?: ClassType<TaskParser<Entry>>;
|
|
15
|
+
parser?: ClassType<TaskParser<Entry, Context>>;
|
|
16
16
|
registry: IRegistry<Context>;
|
|
17
|
-
coordinator?: ClassType<Coordinator
|
|
18
|
-
hook?: ClassType<Hook
|
|
17
|
+
coordinator?: ClassType<Coordinator<Context>>;
|
|
18
|
+
hook?: ClassType<Hook<Context>>;
|
|
19
19
|
retry: number;
|
|
20
20
|
timeout: number;
|
|
21
21
|
syncMs: number | false;
|
|
22
22
|
tickMs: number;
|
|
23
23
|
cascade: CascadePolicy;
|
|
24
|
-
onError?: ErrorHandler
|
|
25
|
-
notFound?: NotFoundHandler
|
|
24
|
+
onError?: ErrorHandler<Context>;
|
|
25
|
+
notFound?: NotFoundHandler<Context>;
|
|
26
26
|
}
|
|
27
27
|
export interface ScheduleStatus {
|
|
28
28
|
running: boolean;
|
|
@@ -75,10 +75,10 @@ export declare class ScheduleRunner<Entry = string, Context = unknown> {
|
|
|
75
75
|
}
|
|
76
76
|
export interface IScheduleBuilder<Entry = string, Context = unknown> extends ClassType<ScheduleRunner<Entry, Context>> {
|
|
77
77
|
source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;
|
|
78
|
-
task(parser: ClassType<TaskParser<Entry>>): IScheduleBuilder<Entry, Context>;
|
|
78
|
+
task(parser: ClassType<TaskParser<Entry, Context>>): IScheduleBuilder<Entry, Context>;
|
|
79
79
|
registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;
|
|
80
|
-
coordinator(coordinator: ClassType<Coordinator
|
|
81
|
-
hook(hook: ClassType<Hook
|
|
80
|
+
coordinator(coordinator: ClassType<Coordinator<Context>>): IScheduleBuilder<Entry, Context>;
|
|
81
|
+
hook(hook: ClassType<Hook<Context>>): IScheduleBuilder<Entry, Context>;
|
|
82
82
|
retry(times: number): IScheduleBuilder<Entry, Context>;
|
|
83
83
|
timeout(ms: number): IScheduleBuilder<Entry, Context>;
|
|
84
84
|
/**
|
|
@@ -92,8 +92,10 @@ export interface IScheduleBuilder<Entry = string, Context = unknown> extends Cla
|
|
|
92
92
|
sync(every: number | boolean): IScheduleBuilder<Entry, Context>;
|
|
93
93
|
tick(ms: number): IScheduleBuilder<Entry, Context>;
|
|
94
94
|
cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
/** `handler` gets the injected context as its third argument, as a registry handler does. */
|
|
96
|
+
onError(handler: ErrorHandler<Context>): IScheduleBuilder<Entry, Context>;
|
|
97
|
+
/** `handler` gets the injected context as its third argument, as a registry handler does. */
|
|
98
|
+
notFound(handler: NotFoundHandler<Context>): IScheduleBuilder<Entry, Context>;
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
99
101
|
* Chain-style configuration for a scheduler.
|
package/dist/schedule.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"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"}
|
package/dist/schedule.js
CHANGED
|
@@ -1,2 +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
|
|
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,this.context)}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,this.context))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,this.context),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,this.context)),await this.safely(()=>this.coordinator?.release(t.key,e,c,this.context))}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"}},this.context))}}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 x(n={}){return k({injects:n,registry:f.Registry.empty(),retry:T,timeout:l.DEFAULT_TIMEOUT,syncMs:d,tickMs:S,cascade:"drain"})}exports.Schedule=x,exports.ScheduleRunner=y;
|
|
2
2
|
//# sourceMappingURL=schedule.js.map
|
package/dist/schedule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schedule.js","sources":["../src/schedule.ts"],"sourcesContent":["import { Registry, type IRegistry } from \"./registry\";\nimport { DEFAULT_TIMEOUT, runTarget } from \"./runner\";\nimport { Task, type TaskStatus } from \"./task\";\nimport type {\n ClassType, Coordinator, ErrorHandler, Hook, InjectMap, Injected, NotFoundHandler,\n Source, TaskDefinition, TaskEvent, TaskParser,\n} from \"./types\";\n\n/** How often due tasks are checked. One second, because expressions have a seconds field. */\nconst DEFAULT_TICK = 1_000;\nconst DEFAULT_SYNC = 60_000;\nconst DEFAULT_RETRY = 0;\n\n/** What happens to a task that disappears from the source. */\nexport type CascadePolicy =\n /** Stop scheduling, let the run in flight finish, then drop it. */\n | \"drain\"\n /** Stop scheduling now. A file target is killed; the other two are let go — see `runner`. */\n | \"stop\"\n /** Leave it running. For when the source is not the only authority. */\n | \"keep\";\n\ninterface Descriptor<Entry, Context> {\n injects: InjectMap;\n source?: Source<Entry, Context>;\n parser?: ClassType<TaskParser<Entry>>;\n registry: IRegistry<Context>;\n coordinator?: ClassType<Coordinator>;\n hook?: ClassType<Hook>;\n retry: number;\n timeout: number;\n syncMs: number | false;\n tickMs: number;\n cascade: CascadePolicy;\n onError?: ErrorHandler;\n notFound?: NotFoundHandler;\n}\n\nexport interface ScheduleStatus {\n running: boolean;\n lastSyncAt: Date | null;\n lastSyncOk: boolean | null;\n tasks: TaskStatus[];\n}\n\n/**\n * A configured scheduler.\n *\n * A plain instance with no global state: constructing two gives two, and\n * whether that should happen is the caller's business. Whatever runs this once\n * — a bootstrap, a framework's init — already owns that question, and\n * answering it here as well would only take the choice away.\n */\nexport class ScheduleRunner<Entry = string, Context = unknown> {\n private readonly d: Descriptor<Entry, Context>;\n private readonly tasks = new Map<string, Task>();\n\n private context!: Injected<Context, InjectMap>;\n private parser!: TaskParser<Entry>;\n private coordinator?: Coordinator;\n private hook?: Hook;\n\n private tickTimer: ReturnType<typeof setInterval> | null = null;\n private syncTimer: ReturnType<typeof setInterval> | null = null;\n private inFlight = new Set<Promise<void>>();\n\n private started = false;\n private lastSyncAt: Date | null = null;\n private lastSyncOk: boolean | null = null;\n\n constructor(descriptor: Descriptor<Entry, Context>) {\n this.d = descriptor;\n }\n\n async start(): Promise<void> {\n // Idempotent on a single instance: starting twice must not produce two\n // sets of timers. This says nothing about two separate instances.\n if (this.started) return;\n if (!this.d.source) throw new Error(\"Schedule: a source is required — call .source(...)\");\n if (!this.d.parser) throw new Error(\"Schedule: a task parser is required — call .task(...)\");\n\n this.started = true;\n\n const context = {} as Record<string, unknown>;\n for (const [name, Token] of Object.entries(this.d.injects)) context[name] = new Token();\n this.context = context as Injected<Context, InjectMap>;\n\n this.parser = new this.d.parser();\n this.coordinator = this.d.coordinator && new this.d.coordinator();\n this.hook = this.d.hook && new this.d.hook();\n\n await this.sync();\n\n this.tickTimer = setInterval(() => this.tick(), this.d.tickMs);\n\n /* `false` means the source is read once, at start, and never reconciled\n again — right for a task list baked into the deployment, wrong for one an\n operator edits. The reconcile still happened above, so the tasks are\n loaded either way. */\n if (this.d.syncMs !== false) {\n this.syncTimer = setInterval(() => {\n this.sync().catch((error) => {\n console.warn(\"[schedule] sync failed:\", error);\n });\n }, this.d.syncMs);\n }\n }\n\n /** Clears both timers and waits for runs already in flight. */\n async stop(): Promise<void> {\n if (this.tickTimer) clearInterval(this.tickTimer);\n if (this.syncTimer) clearInterval(this.syncTimer);\n this.tickTimer = this.syncTimer = null;\n this.started = false;\n\n await Promise.allSettled([...this.inFlight]);\n }\n\n status(): ScheduleStatus {\n return {\n running: this.started,\n lastSyncAt: this.lastSyncAt,\n lastSyncOk: this.lastSyncOk,\n tasks: [...this.tasks.values()].map((task) => task.status()),\n };\n }\n\n /* ------------------------------------------------------------ syncing */\n\n private async read(): Promise<Entry[]> {\n return new this.d.source!().read(this.context as Context);\n }\n\n /**\n * Re-reads the source and reconciles.\n *\n * Two failure modes are kept apart on purpose. A source that throws leaves\n * the schedule exactly as it was — a database blip is not an instruction to\n * cancel everything. A source that returns nothing while tasks exist is\n * treated the same way: it is far more likely to be a partial read than a\n * deliberate deletion of every job at once, and getting that wrong wipes a\n * schedule during an incident, which is the worst possible moment.\n */\n async sync(): Promise<void> {\n let entries: Entry[];\n\n try {\n entries = await this.read();\n\n /* A source that answers with something other than a list is a broken\n source, not an empty schedule. Checked here so it goes down the same\n path as a failed read — report it, keep the tasks already running —\n rather than throwing past this try and out of a timer callback. */\n if (!Array.isArray(entries)) {\n throw new Error(`source returned ${typeof entries}, expected an array`);\n }\n\n this.lastSyncOk = true;\n } catch (error) {\n this.lastSyncOk = false;\n this.lastSyncAt = new Date();\n await this.report(`source read failed: ${error instanceof Error ? error.message : String(error)}`);\n return;\n }\n\n this.lastSyncAt = new Date();\n\n const seen = new Set<string>();\n const now = new Date();\n\n for (const entry of entries) {\n let definition: TaskDefinition;\n\n try {\n definition = this.parser.parse(entry);\n } catch (error) {\n await this.report(`unparseable entry: ${error instanceof Error ? error.message : String(error)}`);\n continue;\n }\n\n seen.add(definition.key);\n const existing = this.tasks.get(definition.key);\n\n if (existing) existing.update(definition, now);\n else this.tasks.set(definition.key, new Task(definition, now));\n }\n\n if (entries.length === 0 && this.tasks.size > 0) {\n await this.report(`source returned nothing while ${this.tasks.size} tasks are live — keeping them`);\n return;\n }\n\n for (const [key, task] of this.tasks) {\n if (seen.has(key)) continue;\n if (this.d.cascade === \"keep\") continue;\n\n task.nextAt = null;\n\n // `drain` leaves the entry until the run in flight settles; the tick\n // loop drops it once `running` clears.\n if (this.d.cascade === \"stop\" || !task.running) this.tasks.delete(key);\n }\n }\n\n /* ------------------------------------------------------------ running */\n\n private tick(): void {\n const now = new Date();\n\n for (const [key, task] of this.tasks) {\n // A drained task keeps its slot only until its run settles.\n if (task.nextAt === null && !task.running) {\n this.tasks.delete(key);\n continue;\n }\n\n if (!task.due(now)) continue;\n\n const run = this.run(task).finally(() => this.inFlight.delete(run));\n this.inFlight.add(run);\n }\n }\n\n private async run(task: Task): Promise<void> {\n // Advanced before the run, so a slow task does not drag its own schedule\n // along behind it. Overlap is held off by `running`, not by the clock.\n const scheduledFor = task.advance();\n task.running = true;\n\n try {\n if (this.coordinator && !(await this.coordinator.claim(task.key, scheduledFor))) {\n return; // another instance owns this fire\n }\n\n const definition = task.current;\n const attempts = (definition.retry ?? this.d.retry) + 1;\n const timeout = definition.timeout ?? this.d.timeout;\n\n let event!: TaskEvent;\n\n for (let attempt = 1; attempt <= attempts; attempt++) {\n const startedAt = new Date();\n const outcome = await runTarget(definition, this.d.registry, this.context, timeout, this.d.notFound);\n\n event = {\n key: task.key,\n scheduledFor,\n startedAt,\n durationMs: Date.now() - startedAt.getTime(),\n attempt,\n ok: outcome.ok,\n reason: outcome.reason,\n detail: outcome.detail,\n raw: outcome.raw,\n };\n\n if (outcome.ok) break;\n\n await this.d.onError?.(event, definition);\n\n // Backoff between attempts: retrying a failing endpoint three times in\n // the same millisecond is three failures, not three chances.\n if (attempt < attempts) {\n await new Promise((r) => setTimeout(r, Math.min(2 ** attempt * 1000, 30_000)));\n }\n }\n\n task.record(event.ok, event.startedAt);\n\n // A hook that throws or hangs must not take the run down with it — the\n // work already happened, and reporting is a separate concern.\n await this.safely(() => this.hook?.notify(event));\n await this.safely(() => this.coordinator?.release(task.key, scheduledFor, event));\n } finally {\n task.running = false;\n }\n }\n\n private async safely(work: () => unknown): Promise<void> {\n try {\n await work();\n } catch (error) {\n console.warn(\"[schedule] reporting failed:\", error);\n }\n }\n\n private async report(detail: string): Promise<void> {\n console.warn(`[schedule] ${detail}`);\n\n await this.safely(() =>\n this.d.onError?.(\n { key: \"@schedule\", scheduledFor: new Date(), startedAt: new Date(), durationMs: 0, attempt: 1, ok: false, reason: \"unknown\", detail },\n { key: \"@schedule\", expression: \"\", target: { type: \"handler\", key: \"@schedule\" } },\n ),\n );\n }\n}\n\n/* -------------------------------------------------------------- the chain */\n\nexport interface IScheduleBuilder<Entry = string, Context = unknown>\n extends ClassType<ScheduleRunner<Entry, Context>> {\n source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;\n task(parser: ClassType<TaskParser<Entry>>): IScheduleBuilder<Entry, Context>;\n registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;\n coordinator(coordinator: ClassType<Coordinator>): IScheduleBuilder<Entry, Context>;\n hook(hook: ClassType<Hook>): IScheduleBuilder<Entry, Context>;\n retry(times: number): IScheduleBuilder<Entry, Context>;\n timeout(ms: number): IScheduleBuilder<Entry, Context>;\n /**\n * Whether, and how often, the source is re-read and reconciled against the\n * tasks already running: a source entry that is new becomes a task, one that\n * changed is updated, one that disappeared is handled by `cascade`.\n *\n * `false` turns the repeat off — read once at start and never again.\n * `true` uses the default interval. A number sets it.\n */\n sync(every: number | boolean): IScheduleBuilder<Entry, Context>;\n tick(ms: number): IScheduleBuilder<Entry, Context>;\n cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;\n onError(handler: ErrorHandler): IScheduleBuilder<Entry, Context>;\n notFound(handler: NotFoundHandler): IScheduleBuilder<Entry, Context>;\n}\n\nfunction chain<Entry, Context>(d: Descriptor<Entry, Context>): IScheduleBuilder<Entry, Context> {\n // Each step returns a new class rather than mutating one, so a\n // half-configured chain can be shared as a base and branched.\n const step = <E>(patch: Partial<Descriptor<E, Context>>) =>\n chain({ ...(d as unknown as Descriptor<E, Context>), ...patch });\n\n return class extends ScheduleRunner<Entry, Context> {\n constructor() {\n super(d);\n }\n\n static source<E>(source: Source<E, Context>) { return step<E>({ source }); }\n static task(parser: ClassType<TaskParser<Entry>>) { return step<Entry>({ parser }); }\n static registry(registry: IRegistry<Context>) { return step<Entry>({ registry }); }\n static coordinator(coordinator: ClassType<Coordinator>) { return step<Entry>({ coordinator }); }\n static hook(hook: ClassType<Hook>) { return step<Entry>({ hook }); }\n static retry(times: number) { return step<Entry>({ retry: times }); }\n static timeout(ms: number) { return step<Entry>({ timeout: ms }); }\n static sync(every: number | boolean) {\n const syncMs = every === true ? DEFAULT_SYNC : every;\n return step<Entry>({ syncMs });\n }\n static tick(ms: number) { return step<Entry>({ tickMs: ms }); }\n static cascade(policy: CascadePolicy) { return step<Entry>({ cascade: policy }); }\n static onError(handler: ErrorHandler) { return step<Entry>({ onError: handler }); }\n static notFound(handler: NotFoundHandler) { return step<Entry>({ notFound: handler }); }\n } as unknown as IScheduleBuilder<Entry, Context>;\n}\n\n/**\n * Chain-style configuration for a scheduler.\n *\n * ```ts\n * export const AppSchedule = Schedule({ db: DataSource })\n * .source((ctx) => ctx.db.query(\"select * from crons\"))\n * .task(RowParser)\n * .registry(Registry.add(Registry(\"session.cleanup\", (ctx) => ...)))\n * .coordinator(PgCoordinator)\n * .hook(Hook.combine(LoggerHook, TelegramHook))\n * .retry(2)\n * .sync(30_000)\n * .cascade(\"drain\");\n *\n * const schedule = new AppSchedule();\n * await schedule.start();\n * ```\n *\n * The chain is the class — there is no terminal to call, and whoever holds the\n * instance decides when it starts and stops.\n */\nexport function Schedule<Injects extends InjectMap = Record<string, never>>(\n injects: Injects = {} as Injects,\n): IScheduleBuilder<string, Injected<unknown, Injects>> {\n return chain({\n injects,\n registry: Registry.empty(),\n retry: DEFAULT_RETRY,\n timeout: DEFAULT_TIMEOUT,\n syncMs: DEFAULT_SYNC,\n tickMs: DEFAULT_TICK,\n cascade: \"drain\",\n });\n}\n"],"names":["DEFAULT_TICK","DEFAULT_SYNC","DEFAULT_RETRY","ScheduleRunner","descriptor","__publicField","context","name","Token","error","task","entries","seen","now","entry","definition","existing","Task","key","run","scheduledFor","attempts","timeout","event","attempt","startedAt","outcome","runTarget","r","work","detail","chain","d","step","patch","source","parser","registry","coordinator","hook","times","ms","every","policy","handler","Schedule","injects","Registry","DEFAULT_TIMEOUT"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schedule.js","sources":["../src/schedule.ts"],"sourcesContent":["import { Registry, type IRegistry } from \"./registry\";\nimport { DEFAULT_TIMEOUT, runTarget } from \"./runner\";\nimport { Task, type TaskStatus } from \"./task\";\nimport type {\n ClassType, Coordinator, ErrorHandler, Hook, InjectMap, Injected, NotFoundHandler,\n Source, TaskDefinition, TaskEvent, TaskParser,\n} from \"./types\";\n\n/** How often due tasks are checked. One second, because expressions have a seconds field. */\nconst DEFAULT_TICK = 1_000;\nconst DEFAULT_SYNC = 60_000;\nconst DEFAULT_RETRY = 0;\n\n/** What happens to a task that disappears from the source. */\nexport type CascadePolicy =\n /** Stop scheduling, let the run in flight finish, then drop it. */\n | \"drain\"\n /** Stop scheduling now. A file target is killed; the other two are let go — see `runner`. */\n | \"stop\"\n /** Leave it running. For when the source is not the only authority. */\n | \"keep\";\n\ninterface Descriptor<Entry, Context> {\n injects: InjectMap;\n source?: Source<Entry, Context>;\n parser?: ClassType<TaskParser<Entry, Context>>;\n registry: IRegistry<Context>;\n coordinator?: ClassType<Coordinator<Context>>;\n hook?: ClassType<Hook<Context>>;\n retry: number;\n timeout: number;\n syncMs: number | false;\n tickMs: number;\n cascade: CascadePolicy;\n onError?: ErrorHandler<Context>;\n notFound?: NotFoundHandler<Context>;\n}\n\nexport interface ScheduleStatus {\n running: boolean;\n lastSyncAt: Date | null;\n lastSyncOk: boolean | null;\n tasks: TaskStatus[];\n}\n\n/**\n * A configured scheduler.\n *\n * A plain instance with no global state: constructing two gives two, and\n * whether that should happen is the caller's business. Whatever runs this once\n * — a bootstrap, a framework's init — already owns that question, and\n * answering it here as well would only take the choice away.\n */\nexport class ScheduleRunner<Entry = string, Context = unknown> {\n private readonly d: Descriptor<Entry, Context>;\n private readonly tasks = new Map<string, Task>();\n\n private context!: Injected<Context, InjectMap>;\n private parser!: TaskParser<Entry, Context>;\n private coordinator?: Coordinator<Context>;\n private hook?: Hook<Context>;\n\n private tickTimer: ReturnType<typeof setInterval> | null = null;\n private syncTimer: ReturnType<typeof setInterval> | null = null;\n private inFlight = new Set<Promise<void>>();\n\n private started = false;\n private lastSyncAt: Date | null = null;\n private lastSyncOk: boolean | null = null;\n\n constructor(descriptor: Descriptor<Entry, Context>) {\n this.d = descriptor;\n }\n\n async start(): Promise<void> {\n // Idempotent on a single instance: starting twice must not produce two\n // sets of timers. This says nothing about two separate instances.\n if (this.started) return;\n if (!this.d.source) throw new Error(\"Schedule: a source is required — call .source(...)\");\n if (!this.d.parser) throw new Error(\"Schedule: a task parser is required — call .task(...)\");\n\n this.started = true;\n\n const context = {} as Record<string, unknown>;\n for (const [name, Token] of Object.entries(this.d.injects)) context[name] = new Token();\n this.context = context as Injected<Context, InjectMap>;\n\n this.parser = new this.d.parser();\n this.coordinator = this.d.coordinator && new this.d.coordinator();\n this.hook = this.d.hook && new this.d.hook();\n\n await this.sync();\n\n this.tickTimer = setInterval(() => this.tick(), this.d.tickMs);\n\n /* `false` means the source is read once, at start, and never reconciled\n again — right for a task list baked into the deployment, wrong for one an\n operator edits. The reconcile still happened above, so the tasks are\n loaded either way. */\n if (this.d.syncMs !== false) {\n this.syncTimer = setInterval(() => {\n this.sync().catch((error) => {\n console.warn(\"[schedule] sync failed:\", error);\n });\n }, this.d.syncMs);\n }\n }\n\n /** Clears both timers and waits for runs already in flight. */\n async stop(): Promise<void> {\n if (this.tickTimer) clearInterval(this.tickTimer);\n if (this.syncTimer) clearInterval(this.syncTimer);\n this.tickTimer = this.syncTimer = null;\n this.started = false;\n\n await Promise.allSettled([...this.inFlight]);\n }\n\n status(): ScheduleStatus {\n return {\n running: this.started,\n lastSyncAt: this.lastSyncAt,\n lastSyncOk: this.lastSyncOk,\n tasks: [...this.tasks.values()].map((task) => task.status()),\n };\n }\n\n /* ------------------------------------------------------------ syncing */\n\n private async read(): Promise<Entry[]> {\n return new this.d.source!().read(this.context as Context);\n }\n\n /**\n * Re-reads the source and reconciles.\n *\n * Two failure modes are kept apart on purpose. A source that throws leaves\n * the schedule exactly as it was — a database blip is not an instruction to\n * cancel everything. A source that returns nothing while tasks exist is\n * treated the same way: it is far more likely to be a partial read than a\n * deliberate deletion of every job at once, and getting that wrong wipes a\n * schedule during an incident, which is the worst possible moment.\n */\n async sync(): Promise<void> {\n let entries: Entry[];\n\n try {\n entries = await this.read();\n\n /* A source that answers with something other than a list is a broken\n source, not an empty schedule. Checked here so it goes down the same\n path as a failed read — report it, keep the tasks already running —\n rather than throwing past this try and out of a timer callback. */\n if (!Array.isArray(entries)) {\n throw new Error(`source returned ${typeof entries}, expected an array`);\n }\n\n this.lastSyncOk = true;\n } catch (error) {\n this.lastSyncOk = false;\n this.lastSyncAt = new Date();\n await this.report(`source read failed: ${error instanceof Error ? error.message : String(error)}`);\n return;\n }\n\n this.lastSyncAt = new Date();\n\n const seen = new Set<string>();\n const now = new Date();\n\n for (const entry of entries) {\n let definition: TaskDefinition;\n\n try {\n definition = this.parser.parse(entry, this.context as Context);\n } catch (error) {\n await this.report(`unparseable entry: ${error instanceof Error ? error.message : String(error)}`);\n continue;\n }\n\n seen.add(definition.key);\n const existing = this.tasks.get(definition.key);\n\n if (existing) existing.update(definition, now);\n else this.tasks.set(definition.key, new Task(definition, now));\n }\n\n if (entries.length === 0 && this.tasks.size > 0) {\n await this.report(`source returned nothing while ${this.tasks.size} tasks are live — keeping them`);\n return;\n }\n\n for (const [key, task] of this.tasks) {\n if (seen.has(key)) continue;\n if (this.d.cascade === \"keep\") continue;\n\n task.nextAt = null;\n\n // `drain` leaves the entry until the run in flight settles; the tick\n // loop drops it once `running` clears.\n if (this.d.cascade === \"stop\" || !task.running) this.tasks.delete(key);\n }\n }\n\n /* ------------------------------------------------------------ running */\n\n private tick(): void {\n const now = new Date();\n\n for (const [key, task] of this.tasks) {\n // A drained task keeps its slot only until its run settles.\n if (task.nextAt === null && !task.running) {\n this.tasks.delete(key);\n continue;\n }\n\n if (!task.due(now)) continue;\n\n const run = this.run(task).finally(() => this.inFlight.delete(run));\n this.inFlight.add(run);\n }\n }\n\n private async run(task: Task): Promise<void> {\n // Advanced before the run, so a slow task does not drag its own schedule\n // along behind it. Overlap is held off by `running`, not by the clock.\n const scheduledFor = task.advance();\n task.running = true;\n\n try {\n if (this.coordinator && !(await this.coordinator.claim(task.key, scheduledFor, this.context as Context))) {\n return; // another instance owns this fire\n }\n\n const definition = task.current;\n const attempts = (definition.retry ?? this.d.retry) + 1;\n const timeout = definition.timeout ?? this.d.timeout;\n\n let event!: TaskEvent;\n\n for (let attempt = 1; attempt <= attempts; attempt++) {\n const startedAt = new Date();\n const outcome = await runTarget(definition, this.d.registry as IRegistry<unknown>, this.context as Context, timeout, this.d.notFound);\n\n event = {\n key: task.key,\n scheduledFor,\n startedAt,\n durationMs: Date.now() - startedAt.getTime(),\n attempt,\n ok: outcome.ok,\n reason: outcome.reason,\n detail: outcome.detail,\n raw: outcome.raw,\n };\n\n if (outcome.ok) break;\n\n await this.d.onError?.(event, definition, this.context as Context);\n\n // Backoff between attempts: retrying a failing endpoint three times in\n // the same millisecond is three failures, not three chances.\n if (attempt < attempts) {\n await new Promise((r) => setTimeout(r, Math.min(2 ** attempt * 1000, 30_000)));\n }\n }\n\n task.record(event.ok, event.startedAt);\n\n // A hook that throws or hangs must not take the run down with it — the\n // work already happened, and reporting is a separate concern.\n await this.safely(() => this.hook?.notify(event, this.context as Context));\n await this.safely(() => this.coordinator?.release(task.key, scheduledFor, event, this.context as Context));\n } finally {\n task.running = false;\n }\n }\n\n private async safely(work: () => unknown): Promise<void> {\n try {\n await work();\n } catch (error) {\n console.warn(\"[schedule] reporting failed:\", error);\n }\n }\n\n private async report(detail: string): Promise<void> {\n console.warn(`[schedule] ${detail}`);\n\n await this.safely(() =>\n this.d.onError?.(\n { key: \"@schedule\", scheduledFor: new Date(), startedAt: new Date(), durationMs: 0, attempt: 1, ok: false, reason: \"unknown\", detail },\n { key: \"@schedule\", expression: \"\", target: { type: \"handler\", key: \"@schedule\" } },\n this.context as Context,\n ),\n );\n }\n}\n\n/* -------------------------------------------------------------- the chain */\n\nexport interface IScheduleBuilder<Entry = string, Context = unknown>\n extends ClassType<ScheduleRunner<Entry, Context>> {\n source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;\n task(parser: ClassType<TaskParser<Entry, Context>>): IScheduleBuilder<Entry, Context>;\n registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;\n coordinator(coordinator: ClassType<Coordinator<Context>>): IScheduleBuilder<Entry, Context>;\n hook(hook: ClassType<Hook<Context>>): IScheduleBuilder<Entry, Context>;\n retry(times: number): IScheduleBuilder<Entry, Context>;\n timeout(ms: number): IScheduleBuilder<Entry, Context>;\n /**\n * Whether, and how often, the source is re-read and reconciled against the\n * tasks already running: a source entry that is new becomes a task, one that\n * changed is updated, one that disappeared is handled by `cascade`.\n *\n * `false` turns the repeat off — read once at start and never again.\n * `true` uses the default interval. A number sets it.\n */\n sync(every: number | boolean): IScheduleBuilder<Entry, Context>;\n tick(ms: number): IScheduleBuilder<Entry, Context>;\n cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;\n /** `handler` gets the injected context as its third argument, as a registry handler does. */\n onError(handler: ErrorHandler<Context>): IScheduleBuilder<Entry, Context>;\n /** `handler` gets the injected context as its third argument, as a registry handler does. */\n notFound(handler: NotFoundHandler<Context>): IScheduleBuilder<Entry, Context>;\n}\n\nfunction chain<Entry, Context>(d: Descriptor<Entry, Context>): IScheduleBuilder<Entry, Context> {\n // Each step returns a new class rather than mutating one, so a\n // half-configured chain can be shared as a base and branched.\n const step = <E>(patch: Partial<Descriptor<E, Context>>) =>\n chain({ ...(d as unknown as Descriptor<E, Context>), ...patch });\n\n return class extends ScheduleRunner<Entry, Context> {\n constructor() {\n super(d);\n }\n\n static source<E>(source: Source<E, Context>) { return step<E>({ source }); }\n static task(parser: ClassType<TaskParser<Entry, Context>>) { return step<Entry>({ parser }); }\n static registry(registry: IRegistry<Context>) { return step<Entry>({ registry }); }\n static coordinator(coordinator: ClassType<Coordinator<Context>>) { return step<Entry>({ coordinator }); }\n static hook(hook: ClassType<Hook<Context>>) { return step<Entry>({ hook }); }\n static retry(times: number) { return step<Entry>({ retry: times }); }\n static timeout(ms: number) { return step<Entry>({ timeout: ms }); }\n static sync(every: number | boolean) {\n const syncMs = every === true ? DEFAULT_SYNC : every;\n return step<Entry>({ syncMs });\n }\n static tick(ms: number) { return step<Entry>({ tickMs: ms }); }\n static cascade(policy: CascadePolicy) { return step<Entry>({ cascade: policy }); }\n static onError(handler: ErrorHandler<Context>) { return step<Entry>({ onError: handler }); }\n static notFound(handler: NotFoundHandler<Context>) { return step<Entry>({ notFound: handler }); }\n } as unknown as IScheduleBuilder<Entry, Context>;\n}\n\n/**\n * Chain-style configuration for a scheduler.\n *\n * ```ts\n * export const AppSchedule = Schedule({ db: DataSource })\n * .source((ctx) => ctx.db.query(\"select * from crons\"))\n * .task(RowParser)\n * .registry(Registry.add(Registry(\"session.cleanup\", (ctx) => ...)))\n * .coordinator(PgCoordinator)\n * .hook(Hook.combine(LoggerHook, TelegramHook))\n * .retry(2)\n * .sync(30_000)\n * .cascade(\"drain\");\n *\n * const schedule = new AppSchedule();\n * await schedule.start();\n * ```\n *\n * The chain is the class — there is no terminal to call, and whoever holds the\n * instance decides when it starts and stops.\n */\nexport function Schedule<Injects extends InjectMap = Record<string, never>>(\n injects: Injects = {} as Injects,\n): IScheduleBuilder<string, Injected<unknown, Injects>> {\n return chain({\n injects,\n registry: Registry.empty(),\n retry: DEFAULT_RETRY,\n timeout: DEFAULT_TIMEOUT,\n syncMs: DEFAULT_SYNC,\n tickMs: DEFAULT_TICK,\n cascade: \"drain\",\n });\n}\n"],"names":["DEFAULT_TICK","DEFAULT_SYNC","DEFAULT_RETRY","ScheduleRunner","descriptor","__publicField","context","name","Token","error","task","entries","seen","now","entry","definition","existing","Task","key","run","scheduledFor","attempts","timeout","event","attempt","startedAt","outcome","runTarget","r","work","detail","chain","d","step","patch","source","parser","registry","coordinator","hook","times","ms","every","policy","handler","Schedule","injects","Registry","DEFAULT_TIMEOUT"],"mappings":"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"}
|
package/dist/schedule.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{Registry as w}from"./registry.mjs";import{runTarget as f,DEFAULT_TIMEOUT as m}from"./runner.mjs";import{Task as p}from"./task.mjs";var g=Object.defineProperty,S=(n,t,e)=>t in n?g(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,i=(n,t,e)=>S(n,typeof t!="symbol"?t+"":t,e);const T=1e3,l=6e4,
|
|
1
|
+
import{Registry as w}from"./registry.mjs";import{runTarget as f,DEFAULT_TIMEOUT as m}from"./runner.mjs";import{Task as p}from"./task.mjs";var g=Object.defineProperty,S=(n,t,e)=>t in n?g(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e,i=(n,t,e)=>S(n,typeof t!="symbol"?t+"":t,e);const T=1e3,l=6e4,x=0;class d{constructor(t){i(this,"d"),i(this,"tasks",new Map),i(this,"context"),i(this,"parser"),i(this,"coordinator"),i(this,"hook"),i(this,"tickTimer",null),i(this,"syncTimer",null),i(this,"inFlight",new Set),i(this,"started",!1),i(this,"lastSyncAt",null),i(this,"lastSyncOk",null),this.d=t}async start(){if(this.started)return;if(!this.d.source)throw new Error("Schedule: a source is required \u2014 call .source(...)");if(!this.d.parser)throw new Error("Schedule: a task parser is required \u2014 call .task(...)");this.started=!0;const t={};for(const[e,s]of Object.entries(this.d.injects))t[e]=new s;this.context=t,this.parser=new this.d.parser,this.coordinator=this.d.coordinator&&new this.d.coordinator,this.hook=this.d.hook&&new this.d.hook,await this.sync(),this.tickTimer=setInterval(()=>this.tick(),this.d.tickMs),this.d.syncMs!==!1&&(this.syncTimer=setInterval(()=>{this.sync().catch(e=>{console.warn("[schedule] sync failed:",e)})},this.d.syncMs))}async stop(){this.tickTimer&&clearInterval(this.tickTimer),this.syncTimer&&clearInterval(this.syncTimer),this.tickTimer=this.syncTimer=null,this.started=!1,await Promise.allSettled([...this.inFlight])}status(){return{running:this.started,lastSyncAt:this.lastSyncAt,lastSyncOk:this.lastSyncOk,tasks:[...this.tasks.values()].map(t=>t.status())}}async read(){return new this.d.source().read(this.context)}async sync(){let t;try{if(t=await this.read(),!Array.isArray(t))throw new Error(`source returned ${typeof t}, expected an array`);this.lastSyncOk=!0}catch(r){this.lastSyncOk=!1,this.lastSyncAt=new Date,await this.report(`source read failed: ${r instanceof Error?r.message:String(r)}`);return}this.lastSyncAt=new Date;const e=new Set,s=new Date;for(const r of t){let a;try{a=this.parser.parse(r,this.context)}catch(c){await this.report(`unparseable entry: ${c instanceof Error?c.message:String(c)}`);continue}e.add(a.key);const o=this.tasks.get(a.key);o?o.update(a,s):this.tasks.set(a.key,new p(a,s))}if(t.length===0&&this.tasks.size>0){await this.report(`source returned nothing while ${this.tasks.size} tasks are live \u2014 keeping them`);return}for(const[r,a]of this.tasks)e.has(r)||this.d.cascade!=="keep"&&(a.nextAt=null,(this.d.cascade==="stop"||!a.running)&&this.tasks.delete(r))}tick(){const t=new Date;for(const[e,s]of this.tasks){if(s.nextAt===null&&!s.running){this.tasks.delete(e);continue}if(!s.due(t))continue;const r=this.run(s).finally(()=>this.inFlight.delete(r));this.inFlight.add(r)}}async run(t){const e=t.advance();t.running=!0;try{if(this.coordinator&&!await this.coordinator.claim(t.key,e,this.context))return;const s=t.current,r=(s.retry??this.d.retry)+1,a=s.timeout??this.d.timeout;let o;for(let c=1;c<=r;c++){const u=new Date,h=await f(s,this.d.registry,this.context,a,this.d.notFound);if(o={key:t.key,scheduledFor:e,startedAt:u,durationMs:Date.now()-u.getTime(),attempt:c,ok:h.ok,reason:h.reason,detail:h.detail,raw:h.raw},h.ok)break;await this.d.onError?.(o,s,this.context),c<r&&await new Promise(k=>setTimeout(k,Math.min(2**c*1e3,3e4)))}t.record(o.ok,o.startedAt),await this.safely(()=>this.hook?.notify(o,this.context)),await this.safely(()=>this.coordinator?.release(t.key,e,o,this.context))}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"}},this.context))}}function y(n){const t=e=>y({...n,...e});return class extends d{constructor(){super(n)}static source(e){return t({source:e})}static task(e){return t({parser:e})}static registry(e){return t({registry:e})}static coordinator(e){return t({coordinator:e})}static hook(e){return t({hook:e})}static retry(e){return t({retry:e})}static timeout(e){return t({timeout:e})}static sync(e){return t({syncMs:e===!0?l:e})}static tick(e){return t({tickMs:e})}static cascade(e){return t({cascade:e})}static onError(e){return t({onError:e})}static notFound(e){return t({notFound:e})}}}function A(n={}){return y({injects:n,registry:w.empty(),retry:x,timeout:m,syncMs:l,tickMs:T,cascade:"drain"})}export{A as Schedule,d as ScheduleRunner};
|
|
2
2
|
//# sourceMappingURL=schedule.mjs.map
|
package/dist/schedule.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schedule.mjs","sources":["../src/schedule.ts"],"sourcesContent":["import { Registry, type IRegistry } from \"./registry\";\nimport { DEFAULT_TIMEOUT, runTarget } from \"./runner\";\nimport { Task, type TaskStatus } from \"./task\";\nimport type {\n ClassType, Coordinator, ErrorHandler, Hook, InjectMap, Injected, NotFoundHandler,\n Source, TaskDefinition, TaskEvent, TaskParser,\n} from \"./types\";\n\n/** How often due tasks are checked. One second, because expressions have a seconds field. */\nconst DEFAULT_TICK = 1_000;\nconst DEFAULT_SYNC = 60_000;\nconst DEFAULT_RETRY = 0;\n\n/** What happens to a task that disappears from the source. */\nexport type CascadePolicy =\n /** Stop scheduling, let the run in flight finish, then drop it. */\n | \"drain\"\n /** Stop scheduling now. A file target is killed; the other two are let go — see `runner`. */\n | \"stop\"\n /** Leave it running. For when the source is not the only authority. */\n | \"keep\";\n\ninterface Descriptor<Entry, Context> {\n injects: InjectMap;\n source?: Source<Entry, Context>;\n parser?: ClassType<TaskParser<Entry>>;\n registry: IRegistry<Context>;\n coordinator?: ClassType<Coordinator>;\n hook?: ClassType<Hook>;\n retry: number;\n timeout: number;\n syncMs: number | false;\n tickMs: number;\n cascade: CascadePolicy;\n onError?: ErrorHandler;\n notFound?: NotFoundHandler;\n}\n\nexport interface ScheduleStatus {\n running: boolean;\n lastSyncAt: Date | null;\n lastSyncOk: boolean | null;\n tasks: TaskStatus[];\n}\n\n/**\n * A configured scheduler.\n *\n * A plain instance with no global state: constructing two gives two, and\n * whether that should happen is the caller's business. Whatever runs this once\n * — a bootstrap, a framework's init — already owns that question, and\n * answering it here as well would only take the choice away.\n */\nexport class ScheduleRunner<Entry = string, Context = unknown> {\n private readonly d: Descriptor<Entry, Context>;\n private readonly tasks = new Map<string, Task>();\n\n private context!: Injected<Context, InjectMap>;\n private parser!: TaskParser<Entry>;\n private coordinator?: Coordinator;\n private hook?: Hook;\n\n private tickTimer: ReturnType<typeof setInterval> | null = null;\n private syncTimer: ReturnType<typeof setInterval> | null = null;\n private inFlight = new Set<Promise<void>>();\n\n private started = false;\n private lastSyncAt: Date | null = null;\n private lastSyncOk: boolean | null = null;\n\n constructor(descriptor: Descriptor<Entry, Context>) {\n this.d = descriptor;\n }\n\n async start(): Promise<void> {\n // Idempotent on a single instance: starting twice must not produce two\n // sets of timers. This says nothing about two separate instances.\n if (this.started) return;\n if (!this.d.source) throw new Error(\"Schedule: a source is required — call .source(...)\");\n if (!this.d.parser) throw new Error(\"Schedule: a task parser is required — call .task(...)\");\n\n this.started = true;\n\n const context = {} as Record<string, unknown>;\n for (const [name, Token] of Object.entries(this.d.injects)) context[name] = new Token();\n this.context = context as Injected<Context, InjectMap>;\n\n this.parser = new this.d.parser();\n this.coordinator = this.d.coordinator && new this.d.coordinator();\n this.hook = this.d.hook && new this.d.hook();\n\n await this.sync();\n\n this.tickTimer = setInterval(() => this.tick(), this.d.tickMs);\n\n /* `false` means the source is read once, at start, and never reconciled\n again — right for a task list baked into the deployment, wrong for one an\n operator edits. The reconcile still happened above, so the tasks are\n loaded either way. */\n if (this.d.syncMs !== false) {\n this.syncTimer = setInterval(() => {\n this.sync().catch((error) => {\n console.warn(\"[schedule] sync failed:\", error);\n });\n }, this.d.syncMs);\n }\n }\n\n /** Clears both timers and waits for runs already in flight. */\n async stop(): Promise<void> {\n if (this.tickTimer) clearInterval(this.tickTimer);\n if (this.syncTimer) clearInterval(this.syncTimer);\n this.tickTimer = this.syncTimer = null;\n this.started = false;\n\n await Promise.allSettled([...this.inFlight]);\n }\n\n status(): ScheduleStatus {\n return {\n running: this.started,\n lastSyncAt: this.lastSyncAt,\n lastSyncOk: this.lastSyncOk,\n tasks: [...this.tasks.values()].map((task) => task.status()),\n };\n }\n\n /* ------------------------------------------------------------ syncing */\n\n private async read(): Promise<Entry[]> {\n return new this.d.source!().read(this.context as Context);\n }\n\n /**\n * Re-reads the source and reconciles.\n *\n * Two failure modes are kept apart on purpose. A source that throws leaves\n * the schedule exactly as it was — a database blip is not an instruction to\n * cancel everything. A source that returns nothing while tasks exist is\n * treated the same way: it is far more likely to be a partial read than a\n * deliberate deletion of every job at once, and getting that wrong wipes a\n * schedule during an incident, which is the worst possible moment.\n */\n async sync(): Promise<void> {\n let entries: Entry[];\n\n try {\n entries = await this.read();\n\n /* A source that answers with something other than a list is a broken\n source, not an empty schedule. Checked here so it goes down the same\n path as a failed read — report it, keep the tasks already running —\n rather than throwing past this try and out of a timer callback. */\n if (!Array.isArray(entries)) {\n throw new Error(`source returned ${typeof entries}, expected an array`);\n }\n\n this.lastSyncOk = true;\n } catch (error) {\n this.lastSyncOk = false;\n this.lastSyncAt = new Date();\n await this.report(`source read failed: ${error instanceof Error ? error.message : String(error)}`);\n return;\n }\n\n this.lastSyncAt = new Date();\n\n const seen = new Set<string>();\n const now = new Date();\n\n for (const entry of entries) {\n let definition: TaskDefinition;\n\n try {\n definition = this.parser.parse(entry);\n } catch (error) {\n await this.report(`unparseable entry: ${error instanceof Error ? error.message : String(error)}`);\n continue;\n }\n\n seen.add(definition.key);\n const existing = this.tasks.get(definition.key);\n\n if (existing) existing.update(definition, now);\n else this.tasks.set(definition.key, new Task(definition, now));\n }\n\n if (entries.length === 0 && this.tasks.size > 0) {\n await this.report(`source returned nothing while ${this.tasks.size} tasks are live — keeping them`);\n return;\n }\n\n for (const [key, task] of this.tasks) {\n if (seen.has(key)) continue;\n if (this.d.cascade === \"keep\") continue;\n\n task.nextAt = null;\n\n // `drain` leaves the entry until the run in flight settles; the tick\n // loop drops it once `running` clears.\n if (this.d.cascade === \"stop\" || !task.running) this.tasks.delete(key);\n }\n }\n\n /* ------------------------------------------------------------ running */\n\n private tick(): void {\n const now = new Date();\n\n for (const [key, task] of this.tasks) {\n // A drained task keeps its slot only until its run settles.\n if (task.nextAt === null && !task.running) {\n this.tasks.delete(key);\n continue;\n }\n\n if (!task.due(now)) continue;\n\n const run = this.run(task).finally(() => this.inFlight.delete(run));\n this.inFlight.add(run);\n }\n }\n\n private async run(task: Task): Promise<void> {\n // Advanced before the run, so a slow task does not drag its own schedule\n // along behind it. Overlap is held off by `running`, not by the clock.\n const scheduledFor = task.advance();\n task.running = true;\n\n try {\n if (this.coordinator && !(await this.coordinator.claim(task.key, scheduledFor))) {\n return; // another instance owns this fire\n }\n\n const definition = task.current;\n const attempts = (definition.retry ?? this.d.retry) + 1;\n const timeout = definition.timeout ?? this.d.timeout;\n\n let event!: TaskEvent;\n\n for (let attempt = 1; attempt <= attempts; attempt++) {\n const startedAt = new Date();\n const outcome = await runTarget(definition, this.d.registry, this.context, timeout, this.d.notFound);\n\n event = {\n key: task.key,\n scheduledFor,\n startedAt,\n durationMs: Date.now() - startedAt.getTime(),\n attempt,\n ok: outcome.ok,\n reason: outcome.reason,\n detail: outcome.detail,\n raw: outcome.raw,\n };\n\n if (outcome.ok) break;\n\n await this.d.onError?.(event, definition);\n\n // Backoff between attempts: retrying a failing endpoint three times in\n // the same millisecond is three failures, not three chances.\n if (attempt < attempts) {\n await new Promise((r) => setTimeout(r, Math.min(2 ** attempt * 1000, 30_000)));\n }\n }\n\n task.record(event.ok, event.startedAt);\n\n // A hook that throws or hangs must not take the run down with it — the\n // work already happened, and reporting is a separate concern.\n await this.safely(() => this.hook?.notify(event));\n await this.safely(() => this.coordinator?.release(task.key, scheduledFor, event));\n } finally {\n task.running = false;\n }\n }\n\n private async safely(work: () => unknown): Promise<void> {\n try {\n await work();\n } catch (error) {\n console.warn(\"[schedule] reporting failed:\", error);\n }\n }\n\n private async report(detail: string): Promise<void> {\n console.warn(`[schedule] ${detail}`);\n\n await this.safely(() =>\n this.d.onError?.(\n { key: \"@schedule\", scheduledFor: new Date(), startedAt: new Date(), durationMs: 0, attempt: 1, ok: false, reason: \"unknown\", detail },\n { key: \"@schedule\", expression: \"\", target: { type: \"handler\", key: \"@schedule\" } },\n ),\n );\n }\n}\n\n/* -------------------------------------------------------------- the chain */\n\nexport interface IScheduleBuilder<Entry = string, Context = unknown>\n extends ClassType<ScheduleRunner<Entry, Context>> {\n source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;\n task(parser: ClassType<TaskParser<Entry>>): IScheduleBuilder<Entry, Context>;\n registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;\n coordinator(coordinator: ClassType<Coordinator>): IScheduleBuilder<Entry, Context>;\n hook(hook: ClassType<Hook>): IScheduleBuilder<Entry, Context>;\n retry(times: number): IScheduleBuilder<Entry, Context>;\n timeout(ms: number): IScheduleBuilder<Entry, Context>;\n /**\n * Whether, and how often, the source is re-read and reconciled against the\n * tasks already running: a source entry that is new becomes a task, one that\n * changed is updated, one that disappeared is handled by `cascade`.\n *\n * `false` turns the repeat off — read once at start and never again.\n * `true` uses the default interval. A number sets it.\n */\n sync(every: number | boolean): IScheduleBuilder<Entry, Context>;\n tick(ms: number): IScheduleBuilder<Entry, Context>;\n cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;\n onError(handler: ErrorHandler): IScheduleBuilder<Entry, Context>;\n notFound(handler: NotFoundHandler): IScheduleBuilder<Entry, Context>;\n}\n\nfunction chain<Entry, Context>(d: Descriptor<Entry, Context>): IScheduleBuilder<Entry, Context> {\n // Each step returns a new class rather than mutating one, so a\n // half-configured chain can be shared as a base and branched.\n const step = <E>(patch: Partial<Descriptor<E, Context>>) =>\n chain({ ...(d as unknown as Descriptor<E, Context>), ...patch });\n\n return class extends ScheduleRunner<Entry, Context> {\n constructor() {\n super(d);\n }\n\n static source<E>(source: Source<E, Context>) { return step<E>({ source }); }\n static task(parser: ClassType<TaskParser<Entry>>) { return step<Entry>({ parser }); }\n static registry(registry: IRegistry<Context>) { return step<Entry>({ registry }); }\n static coordinator(coordinator: ClassType<Coordinator>) { return step<Entry>({ coordinator }); }\n static hook(hook: ClassType<Hook>) { return step<Entry>({ hook }); }\n static retry(times: number) { return step<Entry>({ retry: times }); }\n static timeout(ms: number) { return step<Entry>({ timeout: ms }); }\n static sync(every: number | boolean) {\n const syncMs = every === true ? DEFAULT_SYNC : every;\n return step<Entry>({ syncMs });\n }\n static tick(ms: number) { return step<Entry>({ tickMs: ms }); }\n static cascade(policy: CascadePolicy) { return step<Entry>({ cascade: policy }); }\n static onError(handler: ErrorHandler) { return step<Entry>({ onError: handler }); }\n static notFound(handler: NotFoundHandler) { return step<Entry>({ notFound: handler }); }\n } as unknown as IScheduleBuilder<Entry, Context>;\n}\n\n/**\n * Chain-style configuration for a scheduler.\n *\n * ```ts\n * export const AppSchedule = Schedule({ db: DataSource })\n * .source((ctx) => ctx.db.query(\"select * from crons\"))\n * .task(RowParser)\n * .registry(Registry.add(Registry(\"session.cleanup\", (ctx) => ...)))\n * .coordinator(PgCoordinator)\n * .hook(Hook.combine(LoggerHook, TelegramHook))\n * .retry(2)\n * .sync(30_000)\n * .cascade(\"drain\");\n *\n * const schedule = new AppSchedule();\n * await schedule.start();\n * ```\n *\n * The chain is the class — there is no terminal to call, and whoever holds the\n * instance decides when it starts and stops.\n */\nexport function Schedule<Injects extends InjectMap = Record<string, never>>(\n injects: Injects = {} as Injects,\n): IScheduleBuilder<string, Injected<unknown, Injects>> {\n return chain({\n injects,\n registry: Registry.empty(),\n retry: DEFAULT_RETRY,\n timeout: DEFAULT_TIMEOUT,\n syncMs: DEFAULT_SYNC,\n tickMs: DEFAULT_TICK,\n cascade: \"drain\",\n });\n}\n"],"names":["DEFAULT_TICK","DEFAULT_SYNC","DEFAULT_RETRY","ScheduleRunner","descriptor","__publicField","context","name","Token","error","task","entries","seen","now","entry","definition","existing","Task","key","run","scheduledFor","attempts","timeout","event","attempt","startedAt","outcome","runTarget","r","work","detail","chain","d","step","patch","source","parser","registry","coordinator","hook","times","ms","every","policy","handler","Schedule","injects","Registry","DEFAULT_TIMEOUT"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schedule.mjs","sources":["../src/schedule.ts"],"sourcesContent":["import { Registry, type IRegistry } from \"./registry\";\nimport { DEFAULT_TIMEOUT, runTarget } from \"./runner\";\nimport { Task, type TaskStatus } from \"./task\";\nimport type {\n ClassType, Coordinator, ErrorHandler, Hook, InjectMap, Injected, NotFoundHandler,\n Source, TaskDefinition, TaskEvent, TaskParser,\n} from \"./types\";\n\n/** How often due tasks are checked. One second, because expressions have a seconds field. */\nconst DEFAULT_TICK = 1_000;\nconst DEFAULT_SYNC = 60_000;\nconst DEFAULT_RETRY = 0;\n\n/** What happens to a task that disappears from the source. */\nexport type CascadePolicy =\n /** Stop scheduling, let the run in flight finish, then drop it. */\n | \"drain\"\n /** Stop scheduling now. A file target is killed; the other two are let go — see `runner`. */\n | \"stop\"\n /** Leave it running. For when the source is not the only authority. */\n | \"keep\";\n\ninterface Descriptor<Entry, Context> {\n injects: InjectMap;\n source?: Source<Entry, Context>;\n parser?: ClassType<TaskParser<Entry, Context>>;\n registry: IRegistry<Context>;\n coordinator?: ClassType<Coordinator<Context>>;\n hook?: ClassType<Hook<Context>>;\n retry: number;\n timeout: number;\n syncMs: number | false;\n tickMs: number;\n cascade: CascadePolicy;\n onError?: ErrorHandler<Context>;\n notFound?: NotFoundHandler<Context>;\n}\n\nexport interface ScheduleStatus {\n running: boolean;\n lastSyncAt: Date | null;\n lastSyncOk: boolean | null;\n tasks: TaskStatus[];\n}\n\n/**\n * A configured scheduler.\n *\n * A plain instance with no global state: constructing two gives two, and\n * whether that should happen is the caller's business. Whatever runs this once\n * — a bootstrap, a framework's init — already owns that question, and\n * answering it here as well would only take the choice away.\n */\nexport class ScheduleRunner<Entry = string, Context = unknown> {\n private readonly d: Descriptor<Entry, Context>;\n private readonly tasks = new Map<string, Task>();\n\n private context!: Injected<Context, InjectMap>;\n private parser!: TaskParser<Entry, Context>;\n private coordinator?: Coordinator<Context>;\n private hook?: Hook<Context>;\n\n private tickTimer: ReturnType<typeof setInterval> | null = null;\n private syncTimer: ReturnType<typeof setInterval> | null = null;\n private inFlight = new Set<Promise<void>>();\n\n private started = false;\n private lastSyncAt: Date | null = null;\n private lastSyncOk: boolean | null = null;\n\n constructor(descriptor: Descriptor<Entry, Context>) {\n this.d = descriptor;\n }\n\n async start(): Promise<void> {\n // Idempotent on a single instance: starting twice must not produce two\n // sets of timers. This says nothing about two separate instances.\n if (this.started) return;\n if (!this.d.source) throw new Error(\"Schedule: a source is required — call .source(...)\");\n if (!this.d.parser) throw new Error(\"Schedule: a task parser is required — call .task(...)\");\n\n this.started = true;\n\n const context = {} as Record<string, unknown>;\n for (const [name, Token] of Object.entries(this.d.injects)) context[name] = new Token();\n this.context = context as Injected<Context, InjectMap>;\n\n this.parser = new this.d.parser();\n this.coordinator = this.d.coordinator && new this.d.coordinator();\n this.hook = this.d.hook && new this.d.hook();\n\n await this.sync();\n\n this.tickTimer = setInterval(() => this.tick(), this.d.tickMs);\n\n /* `false` means the source is read once, at start, and never reconciled\n again — right for a task list baked into the deployment, wrong for one an\n operator edits. The reconcile still happened above, so the tasks are\n loaded either way. */\n if (this.d.syncMs !== false) {\n this.syncTimer = setInterval(() => {\n this.sync().catch((error) => {\n console.warn(\"[schedule] sync failed:\", error);\n });\n }, this.d.syncMs);\n }\n }\n\n /** Clears both timers and waits for runs already in flight. */\n async stop(): Promise<void> {\n if (this.tickTimer) clearInterval(this.tickTimer);\n if (this.syncTimer) clearInterval(this.syncTimer);\n this.tickTimer = this.syncTimer = null;\n this.started = false;\n\n await Promise.allSettled([...this.inFlight]);\n }\n\n status(): ScheduleStatus {\n return {\n running: this.started,\n lastSyncAt: this.lastSyncAt,\n lastSyncOk: this.lastSyncOk,\n tasks: [...this.tasks.values()].map((task) => task.status()),\n };\n }\n\n /* ------------------------------------------------------------ syncing */\n\n private async read(): Promise<Entry[]> {\n return new this.d.source!().read(this.context as Context);\n }\n\n /**\n * Re-reads the source and reconciles.\n *\n * Two failure modes are kept apart on purpose. A source that throws leaves\n * the schedule exactly as it was — a database blip is not an instruction to\n * cancel everything. A source that returns nothing while tasks exist is\n * treated the same way: it is far more likely to be a partial read than a\n * deliberate deletion of every job at once, and getting that wrong wipes a\n * schedule during an incident, which is the worst possible moment.\n */\n async sync(): Promise<void> {\n let entries: Entry[];\n\n try {\n entries = await this.read();\n\n /* A source that answers with something other than a list is a broken\n source, not an empty schedule. Checked here so it goes down the same\n path as a failed read — report it, keep the tasks already running —\n rather than throwing past this try and out of a timer callback. */\n if (!Array.isArray(entries)) {\n throw new Error(`source returned ${typeof entries}, expected an array`);\n }\n\n this.lastSyncOk = true;\n } catch (error) {\n this.lastSyncOk = false;\n this.lastSyncAt = new Date();\n await this.report(`source read failed: ${error instanceof Error ? error.message : String(error)}`);\n return;\n }\n\n this.lastSyncAt = new Date();\n\n const seen = new Set<string>();\n const now = new Date();\n\n for (const entry of entries) {\n let definition: TaskDefinition;\n\n try {\n definition = this.parser.parse(entry, this.context as Context);\n } catch (error) {\n await this.report(`unparseable entry: ${error instanceof Error ? error.message : String(error)}`);\n continue;\n }\n\n seen.add(definition.key);\n const existing = this.tasks.get(definition.key);\n\n if (existing) existing.update(definition, now);\n else this.tasks.set(definition.key, new Task(definition, now));\n }\n\n if (entries.length === 0 && this.tasks.size > 0) {\n await this.report(`source returned nothing while ${this.tasks.size} tasks are live — keeping them`);\n return;\n }\n\n for (const [key, task] of this.tasks) {\n if (seen.has(key)) continue;\n if (this.d.cascade === \"keep\") continue;\n\n task.nextAt = null;\n\n // `drain` leaves the entry until the run in flight settles; the tick\n // loop drops it once `running` clears.\n if (this.d.cascade === \"stop\" || !task.running) this.tasks.delete(key);\n }\n }\n\n /* ------------------------------------------------------------ running */\n\n private tick(): void {\n const now = new Date();\n\n for (const [key, task] of this.tasks) {\n // A drained task keeps its slot only until its run settles.\n if (task.nextAt === null && !task.running) {\n this.tasks.delete(key);\n continue;\n }\n\n if (!task.due(now)) continue;\n\n const run = this.run(task).finally(() => this.inFlight.delete(run));\n this.inFlight.add(run);\n }\n }\n\n private async run(task: Task): Promise<void> {\n // Advanced before the run, so a slow task does not drag its own schedule\n // along behind it. Overlap is held off by `running`, not by the clock.\n const scheduledFor = task.advance();\n task.running = true;\n\n try {\n if (this.coordinator && !(await this.coordinator.claim(task.key, scheduledFor, this.context as Context))) {\n return; // another instance owns this fire\n }\n\n const definition = task.current;\n const attempts = (definition.retry ?? this.d.retry) + 1;\n const timeout = definition.timeout ?? this.d.timeout;\n\n let event!: TaskEvent;\n\n for (let attempt = 1; attempt <= attempts; attempt++) {\n const startedAt = new Date();\n const outcome = await runTarget(definition, this.d.registry as IRegistry<unknown>, this.context as Context, timeout, this.d.notFound);\n\n event = {\n key: task.key,\n scheduledFor,\n startedAt,\n durationMs: Date.now() - startedAt.getTime(),\n attempt,\n ok: outcome.ok,\n reason: outcome.reason,\n detail: outcome.detail,\n raw: outcome.raw,\n };\n\n if (outcome.ok) break;\n\n await this.d.onError?.(event, definition, this.context as Context);\n\n // Backoff between attempts: retrying a failing endpoint three times in\n // the same millisecond is three failures, not three chances.\n if (attempt < attempts) {\n await new Promise((r) => setTimeout(r, Math.min(2 ** attempt * 1000, 30_000)));\n }\n }\n\n task.record(event.ok, event.startedAt);\n\n // A hook that throws or hangs must not take the run down with it — the\n // work already happened, and reporting is a separate concern.\n await this.safely(() => this.hook?.notify(event, this.context as Context));\n await this.safely(() => this.coordinator?.release(task.key, scheduledFor, event, this.context as Context));\n } finally {\n task.running = false;\n }\n }\n\n private async safely(work: () => unknown): Promise<void> {\n try {\n await work();\n } catch (error) {\n console.warn(\"[schedule] reporting failed:\", error);\n }\n }\n\n private async report(detail: string): Promise<void> {\n console.warn(`[schedule] ${detail}`);\n\n await this.safely(() =>\n this.d.onError?.(\n { key: \"@schedule\", scheduledFor: new Date(), startedAt: new Date(), durationMs: 0, attempt: 1, ok: false, reason: \"unknown\", detail },\n { key: \"@schedule\", expression: \"\", target: { type: \"handler\", key: \"@schedule\" } },\n this.context as Context,\n ),\n );\n }\n}\n\n/* -------------------------------------------------------------- the chain */\n\nexport interface IScheduleBuilder<Entry = string, Context = unknown>\n extends ClassType<ScheduleRunner<Entry, Context>> {\n source<E>(source: Source<E, Context>): IScheduleBuilder<E, Context>;\n task(parser: ClassType<TaskParser<Entry, Context>>): IScheduleBuilder<Entry, Context>;\n registry(registry: IRegistry<Context>): IScheduleBuilder<Entry, Context>;\n coordinator(coordinator: ClassType<Coordinator<Context>>): IScheduleBuilder<Entry, Context>;\n hook(hook: ClassType<Hook<Context>>): IScheduleBuilder<Entry, Context>;\n retry(times: number): IScheduleBuilder<Entry, Context>;\n timeout(ms: number): IScheduleBuilder<Entry, Context>;\n /**\n * Whether, and how often, the source is re-read and reconciled against the\n * tasks already running: a source entry that is new becomes a task, one that\n * changed is updated, one that disappeared is handled by `cascade`.\n *\n * `false` turns the repeat off — read once at start and never again.\n * `true` uses the default interval. A number sets it.\n */\n sync(every: number | boolean): IScheduleBuilder<Entry, Context>;\n tick(ms: number): IScheduleBuilder<Entry, Context>;\n cascade(policy: CascadePolicy): IScheduleBuilder<Entry, Context>;\n /** `handler` gets the injected context as its third argument, as a registry handler does. */\n onError(handler: ErrorHandler<Context>): IScheduleBuilder<Entry, Context>;\n /** `handler` gets the injected context as its third argument, as a registry handler does. */\n notFound(handler: NotFoundHandler<Context>): IScheduleBuilder<Entry, Context>;\n}\n\nfunction chain<Entry, Context>(d: Descriptor<Entry, Context>): IScheduleBuilder<Entry, Context> {\n // Each step returns a new class rather than mutating one, so a\n // half-configured chain can be shared as a base and branched.\n const step = <E>(patch: Partial<Descriptor<E, Context>>) =>\n chain({ ...(d as unknown as Descriptor<E, Context>), ...patch });\n\n return class extends ScheduleRunner<Entry, Context> {\n constructor() {\n super(d);\n }\n\n static source<E>(source: Source<E, Context>) { return step<E>({ source }); }\n static task(parser: ClassType<TaskParser<Entry, Context>>) { return step<Entry>({ parser }); }\n static registry(registry: IRegistry<Context>) { return step<Entry>({ registry }); }\n static coordinator(coordinator: ClassType<Coordinator<Context>>) { return step<Entry>({ coordinator }); }\n static hook(hook: ClassType<Hook<Context>>) { return step<Entry>({ hook }); }\n static retry(times: number) { return step<Entry>({ retry: times }); }\n static timeout(ms: number) { return step<Entry>({ timeout: ms }); }\n static sync(every: number | boolean) {\n const syncMs = every === true ? DEFAULT_SYNC : every;\n return step<Entry>({ syncMs });\n }\n static tick(ms: number) { return step<Entry>({ tickMs: ms }); }\n static cascade(policy: CascadePolicy) { return step<Entry>({ cascade: policy }); }\n static onError(handler: ErrorHandler<Context>) { return step<Entry>({ onError: handler }); }\n static notFound(handler: NotFoundHandler<Context>) { return step<Entry>({ notFound: handler }); }\n } as unknown as IScheduleBuilder<Entry, Context>;\n}\n\n/**\n * Chain-style configuration for a scheduler.\n *\n * ```ts\n * export const AppSchedule = Schedule({ db: DataSource })\n * .source((ctx) => ctx.db.query(\"select * from crons\"))\n * .task(RowParser)\n * .registry(Registry.add(Registry(\"session.cleanup\", (ctx) => ...)))\n * .coordinator(PgCoordinator)\n * .hook(Hook.combine(LoggerHook, TelegramHook))\n * .retry(2)\n * .sync(30_000)\n * .cascade(\"drain\");\n *\n * const schedule = new AppSchedule();\n * await schedule.start();\n * ```\n *\n * The chain is the class — there is no terminal to call, and whoever holds the\n * instance decides when it starts and stops.\n */\nexport function Schedule<Injects extends InjectMap = Record<string, never>>(\n injects: Injects = {} as Injects,\n): IScheduleBuilder<string, Injected<unknown, Injects>> {\n return chain({\n injects,\n registry: Registry.empty(),\n retry: DEFAULT_RETRY,\n timeout: DEFAULT_TIMEOUT,\n syncMs: DEFAULT_SYNC,\n tickMs: DEFAULT_TICK,\n cascade: \"drain\",\n });\n}\n"],"names":["DEFAULT_TICK","DEFAULT_SYNC","DEFAULT_RETRY","ScheduleRunner","descriptor","__publicField","context","name","Token","error","task","entries","seen","now","entry","definition","existing","Task","key","run","scheduledFor","attempts","timeout","event","attempt","startedAt","outcome","runTarget","r","work","detail","chain","d","step","patch","source","parser","registry","coordinator","hook","times","ms","every","policy","handler","Schedule","injects","Registry","DEFAULT_TIMEOUT"],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -64,9 +64,16 @@ export interface TaskDefinition {
|
|
|
64
64
|
/** Milliseconds. Overrides the schedule-level default. */
|
|
65
65
|
timeout?: number;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Turns one entry from a source into a task. Implement this to accept your own format.
|
|
69
|
+
*
|
|
70
|
+
* Every port the scheduler constructs — parser, coordinator, hook — is handed
|
|
71
|
+
* the injected context as its last argument, the same object a handler's
|
|
72
|
+
* `run(ctx)` gets. They are classes built with no arguments, so this is how
|
|
73
|
+
* they reach what `Schedule({ … })` injected.
|
|
74
|
+
*/
|
|
75
|
+
export interface TaskParser<Entry = string, Context = unknown> {
|
|
76
|
+
parse(entry: Entry, context: Context): TaskDefinition;
|
|
70
77
|
}
|
|
71
78
|
/**
|
|
72
79
|
* Why a run failed, normalised across the three target kinds.
|
|
@@ -112,11 +119,15 @@ export type Source<Entry = string, Context = unknown> = ClassType<SourceAdapter<
|
|
|
112
119
|
* `(key, scheduledFor)` is a stronger guarantee than a distributed lock, and
|
|
113
120
|
* it leaves a run history behind for free.
|
|
114
121
|
*/
|
|
115
|
-
export interface Coordinator {
|
|
116
|
-
/**
|
|
117
|
-
|
|
122
|
+
export interface Coordinator<Context = unknown> {
|
|
123
|
+
/**
|
|
124
|
+
* True when this instance won the right to run. False when another already
|
|
125
|
+
* has it. `context` carries what was injected — the pool or repository the
|
|
126
|
+
* claim is written through, typically.
|
|
127
|
+
*/
|
|
128
|
+
claim(key: string, scheduledFor: Date, context: Context): Promisable<boolean>;
|
|
118
129
|
/** Called once the run ends, so an abandoned claim can be told from a live one. */
|
|
119
|
-
release(key: string, scheduledFor: Date, event: TaskEvent): Promisable<void>;
|
|
130
|
+
release(key: string, scheduledFor: Date, event: TaskEvent, context: Context): Promisable<void>;
|
|
120
131
|
}
|
|
121
132
|
/**
|
|
122
133
|
* Where run outcomes go. Optional — without one, a finished task reports
|
|
@@ -125,11 +136,20 @@ export interface Coordinator {
|
|
|
125
136
|
* One hook, not a list: `combine` turns several into one, so nothing
|
|
126
137
|
* downstream ever branches on how many there are.
|
|
127
138
|
*/
|
|
128
|
-
export interface Hook {
|
|
129
|
-
notify(event: TaskEvent): Promisable<void>;
|
|
139
|
+
export interface Hook<Context = unknown> {
|
|
140
|
+
notify(event: TaskEvent, context: Context): Promisable<void>;
|
|
130
141
|
}
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Called when a source entry names a handler that was never registered.
|
|
144
|
+
*
|
|
145
|
+
* `context` is the same object a handler's `run` receives — the injection map
|
|
146
|
+
* `Schedule(…)` was built with, constructed. These two callbacks are plain
|
|
147
|
+
* functions rather than classes, so they cannot inject for themselves, and
|
|
148
|
+
* without it an app had to reach its logger around the scheduler instead of
|
|
149
|
+
* through it. Last in the list, so a handler written for two arguments still
|
|
150
|
+
* fits.
|
|
151
|
+
*/
|
|
152
|
+
export type NotFoundHandler<Context = unknown> = (key: string, task: TaskDefinition, context: Context) => Promisable<void>;
|
|
153
|
+
/** Called when a run fails, before any retry decision. `context` as for {@link NotFoundHandler}. */
|
|
154
|
+
export type ErrorHandler<Context = unknown> = (event: TaskEvent, task: TaskDefinition, context: Context) => Promisable<void>;
|
|
135
155
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,CAAC,QAAQ,GAAG,OAAO,IAAI,UAAU,QAAQ,CAAC;AAE/D,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAElD,MAAM,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,SAAS,SAAS,IAAI,OAAO,GAAG;KAClE,CAAC,IAAI,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,QAAQ,CAAC,GAAG,QAAQ,GAAG,KAAK;CACtF,CAAC;AAEF,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAI3C;;;;GAIG;AACH,MAAM,MAAM,UAAU;AACpB,4EAA4E;AAC1E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAClC,qGAAqG;GACnG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;AAChG,mEAAmE;GACjE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEpD,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC;IACnB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecosy/schedule",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Cron scheduling with pluggable sources, coordination and reporting — no dependencies, runs anywhere Node does",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "tsc && rollup -c",
|
|
30
|
-
"
|
|
29
|
+
"build": "tsc && yarn types:test && rollup -c",
|
|
30
|
+
"test": "yarn build && yarn test:run",
|
|
31
|
+
"test:run": "node --test \"tests/*.test.mjs\"",
|
|
32
|
+
"types:test": "tsc --noEmit --strict --skipLibCheck --target ES2022 --module esnext --moduleResolution bundler types-test/handlers.ts",
|
|
33
|
+
"prepublishOnly": "yarn clean && yarn build && yarn test:run",
|
|
31
34
|
"clean": "rimraf dist"
|
|
32
35
|
},
|
|
33
36
|
"publishConfig": {
|