@graphorin/triggers 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,39 @@
1
1
  # @graphorin/triggers
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Patch Changes
6
+
7
+ - [#135](https://github.com/o-stepper/graphorin/pull/135) [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3) Thanks [@o-stepper](https://github.com/o-stepper)! - Server / client / periphery correctness (audit 2026-07-04 Wave B, cluster B6).
8
+
9
+ - periphery-01: `POST /v1/workflows/:id/resume` actually resumes - it background-iterates `workflow.resume(threadId, {resume})` and emits every event on the `workflow:<id>/runs/<runId>/events` subject (mirroring execute); the fictional unmounted SSE URL is gone and the run completes. `POST /v1/workflows/:id/fork` returns an honest 501 instead of a 202 that forked nothing.
10
+ - periphery-02: `standalone-server.md` now states plainly that `graphorin start` serves health/metrics/tokens/tickets/WS-SSE only and that the domain routes mount only when adapters are composed programmatically via `createServer({...})`.
11
+ - periphery-03: the client SSE fallback survives reconnects - `#reconnect` no longer attempts the RPC resubscribe on the read-only SSE transport (which threw and permanently killed the subscription); the reconnect carries the subscription's cursor as a `Last-Event-ID` header so the server replays only missed events; a closed bound subscription is recreated on the next `subscribe()`.
12
+ - periphery-08: the idempotency middleware tracks in-flight keyed executions - a concurrent duplicate gets `409 idempotency-in-flight` + `Retry-After` instead of double-executing (per draft-ietf-httpapi-idempotency-key-header).
13
+ - periphery-09: `subscribe({ target: 'run' })` without `sessionId` throws a clear client-side error instead of building a `run:` subject the server grammar can never accept.
14
+ - periphery-10: session-stream subjects gate on `sessions:read:<sessionId>` (read-only streams, sessionId resource slot) instead of `agents:invoke:<sessionId>`, aligning with the SSE route's requirement.
15
+ - periphery-11: interval triggers under `catchupPolicy: 'none'` advance to the next FUTURE boundary after downtime instead of firing immediately on restart; `recordActivity()` no longer arms idle timers on a stopped scheduler (P-14).
16
+ - periphery-05: the TransformersJS embedder throws for an unknown model with no `dim` hint (the PS-11 fix ported from the Ollama embedder) instead of assuming 768 - a wrong assumed width baked a wrong-width id + vec0 table and the id changed after the first embed; a width drift against a bound dim now throws too.
17
+ - P-05: the WS upgrade bearer verification passes the client IP so the per-IP failure lockout engages for upgrade attempts (previously a lockout-free brute-force surface).
18
+ - Docs: the fictional "disconnect policy" section replaced with the real reconnect-and-replay behaviour; the WS ticket endpoint path corrected to `/v1/session/ws-ticket`.
19
+
20
+ - Updated dependencies [[`32f20c1`](https://github.com/o-stepper/graphorin/commit/32f20c110f184f8cef7eec85bf39f5f07c886cb6), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`4f850d9`](https://github.com/o-stepper/graphorin/commit/4f850d9bc0a05d6256c59c5117b010336fcb41d3), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`17a2d30`](https://github.com/o-stepper/graphorin/commit/17a2d30564154ca2ab87473335cdef43a5089c84), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627), [`ce06b47`](https://github.com/o-stepper/graphorin/commit/ce06b472af9e30ac5d0792f7a8b6f42170a94627)]:
21
+ - @graphorin/core@0.6.0
22
+
23
+ ## 0.5.0
24
+
25
+ First version published to the npm registry (with Sigstore build
26
+ provenance). The 0.2.0, 0.3.0, and 0.4.0 versions were internal lockstep
27
+ milestones and were never published. All `@graphorin/*` packages release
28
+ lockstep at the same version; the full release notes for 0.2.0-0.5.0 live
29
+ in the repository-level
30
+ [CHANGELOG](https://github.com/o-stepper/graphorin/blob/main/CHANGELOG.md).
31
+
3
32
  ## 0.1.0
4
33
 
5
34
  ### Minor Changes
6
35
 
7
36
  - Initial release. Background-task scheduler for the Graphorin
8
- framework durable cron / interval / idle / event triggers, an
37
+ framework - durable cron / interval / idle / event triggers, an
9
38
  in-tree 5-field cron parser, per-trigger catch-up policies, and a
10
39
  shared code path for library and server modes.
package/README.md CHANGED
@@ -8,7 +8,7 @@ package owns:
8
8
 
9
9
  - An in-tree 5-field **cron parser** (`* * * * *`, ranges, lists,
10
10
  steps; no third-party dependency).
11
- - A `Scheduler` runtime a process-bound loop that fires registered
11
+ - A `Scheduler` runtime - a process-bound loop that fires registered
12
12
  triggers and persists their state via the `TriggerStore` contract
13
13
  from `@graphorin/core/contracts`.
14
14
  - Per-trigger **catch-up policies** (`'none'` default, `'last'`,
@@ -69,4 +69,4 @@ MIT © 2026 Oleksiy Stepurenko.
69
69
 
70
70
  ---
71
71
 
72
- **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
72
+ **Project Graphorin** · v0.6.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
package/dist/cron.d.ts CHANGED
@@ -21,10 +21,10 @@
21
21
  * The parser is intentionally **strict**: any unrecognised character
22
22
  * raises {@link CronParseError} so a typo never silently never-fires.
23
23
  *
24
- * **Day vs. day-of-week semantics AND, not OR.** When *both* `day`
24
+ * **Day vs. day-of-week semantics - AND, not OR.** When *both* `day`
25
25
  * and `dayOfWeek` are restricted (neither is the every-value
26
26
  * wildcard), Graphorin requires **both** to match for a fire to
27
- * happen i.e. `0 12 1-7 * 1` means "noon on the first Monday of
27
+ * happen - i.e. `0 12 1-7 * 1` means "noon on the first Monday of
28
28
  * every month". This differs from Vixie / POSIX cron, which
29
29
  * OR-combines the two restricted fields. AND semantics are easier to
30
30
  * reason about in personal-assistant scenarios; the framework stays
@@ -64,7 +64,7 @@ declare function parseCron(expression: string): ParsedCron;
64
64
  * trigger as UTC; operators that need local time express that in
65
65
  * their cron expression).
66
66
  *
67
- * Returns `null` if no fire happens in the next 4 years (defensive
67
+ * Returns `null` if no fire happens in the next 4 years (defensive -
68
68
  * impossible for a well-formed cron expression except a vacuous
69
69
  * combination that never aligns).
70
70
  *
package/dist/cron.js CHANGED
@@ -21,10 +21,10 @@
21
21
  * The parser is intentionally **strict**: any unrecognised character
22
22
  * raises {@link CronParseError} so a typo never silently never-fires.
23
23
  *
24
- * **Day vs. day-of-week semantics AND, not OR.** When *both* `day`
24
+ * **Day vs. day-of-week semantics - AND, not OR.** When *both* `day`
25
25
  * and `dayOfWeek` are restricted (neither is the every-value
26
26
  * wildcard), Graphorin requires **both** to match for a fire to
27
- * happen i.e. `0 12 1-7 * 1` means "noon on the first Monday of
27
+ * happen - i.e. `0 12 1-7 * 1` means "noon on the first Monday of
28
28
  * every month". This differs from Vixie / POSIX cron, which
29
29
  * OR-combines the two restricted fields. AND semantics are easier to
30
30
  * reason about in personal-assistant scenarios; the framework stays
@@ -130,7 +130,7 @@ function parseNumeric(name, raw, expression) {
130
130
  * trigger as UTC; operators that need local time express that in
131
131
  * their cron expression).
132
132
  *
133
- * Returns `null` if no fire happens in the next 4 years (defensive
133
+ * Returns `null` if no fire happens in the next 4 years (defensive -
134
134
  * impossible for a well-formed cron expression except a vacuous
135
135
  * combination that never aligns).
136
136
  *
package/dist/cron.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cron.js","names":["expression: string","RANGES: Readonly<Record<keyof Omit<ParsedCron, 'expression'>, FieldRange>>","stepStr: string | undefined","step","from: number","to: number"],"sources":["../src/cron.ts"],"sourcesContent":["/**\n * Tiny in-tree 5-field cron parser used by `@graphorin/triggers`.\n *\n * Supported syntax:\n *\n * minute (0-59)\n * hour (0-23)\n * day (1-31)\n * month (1-12)\n * dayOfWeek (0-6; Sunday = 0)\n *\n * Each field accepts:\n * - star (asterisk) for every value\n * - a single number `5`\n * - a comma list `1,2,5`\n * - a range `1-4`\n * - a step expression with the slash operator (e.g. every-5 minutes,\n * `0-30/10` for \"0,10,20,30 minutes\")\n *\n * The parser is intentionally **strict**: any unrecognised character\n * raises {@link CronParseError} so a typo never silently never-fires.\n *\n * **Day vs. day-of-week semantics — AND, not OR.** When *both* `day`\n * and `dayOfWeek` are restricted (neither is the every-value\n * wildcard), Graphorin requires **both** to match for a fire to\n * happen — i.e. `0 12 1-7 * 1` means \"noon on the first Monday of\n * every month\". This differs from Vixie / POSIX cron, which\n * OR-combines the two restricted fields. AND semantics are easier to\n * reason about in personal-assistant scenarios; the framework stays\n * consistent with this rule rather than mixing the two conventions.\n *\n * The scheduler treats every trigger as **UTC**. Operators that need\n * a local-time fire encode the offset directly into their cron\n * expression (e.g. `0 14 * * *` for \"9am Eastern in winter\").\n *\n * @packageDocumentation\n */\n\n/** @stable */\nexport class CronParseError extends Error {\n override readonly name = 'CronParseError';\n constructor(\n public readonly expression: string,\n message: string,\n ) {\n super(`[graphorin/triggers] cron expression '${expression}': ${message}`);\n }\n}\n\n/** @stable */\nexport interface ParsedCron {\n readonly expression: string;\n readonly minute: ReadonlySet<number>;\n readonly hour: ReadonlySet<number>;\n readonly day: ReadonlySet<number>;\n readonly month: ReadonlySet<number>;\n readonly dayOfWeek: ReadonlySet<number>;\n}\n\ninterface FieldRange {\n readonly min: number;\n readonly max: number;\n}\n\nconst RANGES: Readonly<Record<keyof Omit<ParsedCron, 'expression'>, FieldRange>> = {\n minute: { min: 0, max: 59 },\n hour: { min: 0, max: 23 },\n day: { min: 1, max: 31 },\n month: { min: 1, max: 12 },\n dayOfWeek: { min: 0, max: 6 },\n};\n\n/**\n * Parse a 5-field cron expression. Throws {@link CronParseError} on\n * any malformed input.\n *\n * @stable\n */\nexport function parseCron(expression: string): ParsedCron {\n const trimmed = expression.trim();\n if (trimmed.length === 0) {\n throw new CronParseError(expression, 'expression is empty');\n }\n const fields = trimmed.split(/\\s+/);\n if (fields.length !== 5) {\n throw new CronParseError(\n expression,\n `expected 5 whitespace-separated fields (minute hour day month dayOfWeek), got ${fields.length}`,\n );\n }\n const [minuteStr, hourStr, dayStr, monthStr, dowStr] = fields as [\n string,\n string,\n string,\n string,\n string,\n ];\n return {\n expression: trimmed,\n minute: parseField('minute', minuteStr, RANGES.minute, expression),\n hour: parseField('hour', hourStr, RANGES.hour, expression),\n day: parseField('day', dayStr, RANGES.day, expression),\n month: parseField('month', monthStr, RANGES.month, expression),\n dayOfWeek: parseField('dayOfWeek', dowStr, RANGES.dayOfWeek, expression),\n };\n}\n\nfunction parseField(\n name: string,\n raw: string,\n range: FieldRange,\n expression: string,\n): ReadonlySet<number> {\n const out = new Set<number>();\n for (const part of raw.split(',')) {\n if (part.length === 0) {\n throw new CronParseError(expression, `field '${name}' has an empty list element`);\n }\n let stepStr: string | undefined;\n let bodyStr = part;\n if (part.includes('/')) {\n const [body, step] = part.split('/', 2);\n bodyStr = body ?? '';\n stepStr = step;\n }\n const step = stepStr === undefined ? 1 : Number.parseInt(stepStr, 10);\n if (!Number.isFinite(step) || step <= 0) {\n throw new CronParseError(expression, `field '${name}' has invalid step '${stepStr}'`);\n }\n let from: number;\n let to: number;\n if (bodyStr === '*') {\n from = range.min;\n to = range.max;\n } else if (bodyStr.includes('-')) {\n const [a, b] = bodyStr.split('-', 2);\n from = parseNumeric(name, a, expression);\n to = parseNumeric(name, b, expression);\n } else {\n const single = parseNumeric(name, bodyStr, expression);\n from = single;\n to = stepStr === undefined ? single : range.max;\n }\n if (from < range.min || to > range.max || from > to) {\n throw new CronParseError(\n expression,\n `field '${name}' value out of range [${range.min}, ${range.max}]: ${bodyStr}`,\n );\n }\n for (let v = from; v <= to; v += step) {\n out.add(v);\n }\n }\n return out;\n}\n\nfunction parseNumeric(name: string, raw: string | undefined, expression: string): number {\n if (raw === undefined || raw.length === 0) {\n throw new CronParseError(expression, `field '${name}' has empty numeric component`);\n }\n if (!/^\\d+$/.test(raw)) {\n throw new CronParseError(expression, `field '${name}' value '${raw}' is not numeric`);\n }\n return Number.parseInt(raw, 10);\n}\n\n/**\n * Compute the next fire time strictly after `from` for the supplied\n * cron schedule. Returns a UTC `Date` (the scheduler treats every\n * trigger as UTC; operators that need local time express that in\n * their cron expression).\n *\n * Returns `null` if no fire happens in the next 4 years (defensive —\n * impossible for a well-formed cron expression except a vacuous\n * combination that never aligns).\n *\n * @stable\n */\nexport function nextFireAfter(parsed: ParsedCron, from: Date): Date | null {\n const start = new Date(from.getTime() + 60_000);\n start.setUTCSeconds(0, 0);\n const horizon = new Date(start.getTime() + 4 * 365 * 24 * 60 * 60 * 1000);\n\n let cursor = new Date(start.getTime());\n while (cursor.getTime() < horizon.getTime()) {\n const month = cursor.getUTCMonth() + 1;\n if (!parsed.month.has(month)) {\n cursor = new Date(\n Date.UTC(\n cursor.getUTCMonth() === 11 ? cursor.getUTCFullYear() + 1 : cursor.getUTCFullYear(),\n (cursor.getUTCMonth() + 1) % 12,\n 1,\n 0,\n 0,\n 0,\n 0,\n ),\n );\n continue;\n }\n const day = cursor.getUTCDate();\n const dow = cursor.getUTCDay();\n if (!parsed.day.has(day) || !parsed.dayOfWeek.has(dow)) {\n cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);\n cursor.setUTCHours(0, 0, 0, 0);\n continue;\n }\n if (!parsed.hour.has(cursor.getUTCHours())) {\n cursor = new Date(cursor.getTime() + 60 * 60 * 1000);\n cursor.setUTCMinutes(0, 0, 0);\n continue;\n }\n if (!parsed.minute.has(cursor.getUTCMinutes())) {\n cursor = new Date(cursor.getTime() + 60_000);\n cursor.setUTCSeconds(0, 0);\n continue;\n }\n return cursor;\n }\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cron.js","names":["expression: string","RANGES: Readonly<Record<keyof Omit<ParsedCron, 'expression'>, FieldRange>>","stepStr: string | undefined","step","from: number","to: number"],"sources":["../src/cron.ts"],"sourcesContent":["/**\n * Tiny in-tree 5-field cron parser used by `@graphorin/triggers`.\n *\n * Supported syntax:\n *\n * minute (0-59)\n * hour (0-23)\n * day (1-31)\n * month (1-12)\n * dayOfWeek (0-6; Sunday = 0)\n *\n * Each field accepts:\n * - star (asterisk) for every value\n * - a single number `5`\n * - a comma list `1,2,5`\n * - a range `1-4`\n * - a step expression with the slash operator (e.g. every-5 minutes,\n * `0-30/10` for \"0,10,20,30 minutes\")\n *\n * The parser is intentionally **strict**: any unrecognised character\n * raises {@link CronParseError} so a typo never silently never-fires.\n *\n * **Day vs. day-of-week semantics - AND, not OR.** When *both* `day`\n * and `dayOfWeek` are restricted (neither is the every-value\n * wildcard), Graphorin requires **both** to match for a fire to\n * happen - i.e. `0 12 1-7 * 1` means \"noon on the first Monday of\n * every month\". This differs from Vixie / POSIX cron, which\n * OR-combines the two restricted fields. AND semantics are easier to\n * reason about in personal-assistant scenarios; the framework stays\n * consistent with this rule rather than mixing the two conventions.\n *\n * The scheduler treats every trigger as **UTC**. Operators that need\n * a local-time fire encode the offset directly into their cron\n * expression (e.g. `0 14 * * *` for \"9am Eastern in winter\").\n *\n * @packageDocumentation\n */\n\n/** @stable */\nexport class CronParseError extends Error {\n override readonly name = 'CronParseError';\n constructor(\n public readonly expression: string,\n message: string,\n ) {\n super(`[graphorin/triggers] cron expression '${expression}': ${message}`);\n }\n}\n\n/** @stable */\nexport interface ParsedCron {\n readonly expression: string;\n readonly minute: ReadonlySet<number>;\n readonly hour: ReadonlySet<number>;\n readonly day: ReadonlySet<number>;\n readonly month: ReadonlySet<number>;\n readonly dayOfWeek: ReadonlySet<number>;\n}\n\ninterface FieldRange {\n readonly min: number;\n readonly max: number;\n}\n\nconst RANGES: Readonly<Record<keyof Omit<ParsedCron, 'expression'>, FieldRange>> = {\n minute: { min: 0, max: 59 },\n hour: { min: 0, max: 23 },\n day: { min: 1, max: 31 },\n month: { min: 1, max: 12 },\n dayOfWeek: { min: 0, max: 6 },\n};\n\n/**\n * Parse a 5-field cron expression. Throws {@link CronParseError} on\n * any malformed input.\n *\n * @stable\n */\nexport function parseCron(expression: string): ParsedCron {\n const trimmed = expression.trim();\n if (trimmed.length === 0) {\n throw new CronParseError(expression, 'expression is empty');\n }\n const fields = trimmed.split(/\\s+/);\n if (fields.length !== 5) {\n throw new CronParseError(\n expression,\n `expected 5 whitespace-separated fields (minute hour day month dayOfWeek), got ${fields.length}`,\n );\n }\n const [minuteStr, hourStr, dayStr, monthStr, dowStr] = fields as [\n string,\n string,\n string,\n string,\n string,\n ];\n return {\n expression: trimmed,\n minute: parseField('minute', minuteStr, RANGES.minute, expression),\n hour: parseField('hour', hourStr, RANGES.hour, expression),\n day: parseField('day', dayStr, RANGES.day, expression),\n month: parseField('month', monthStr, RANGES.month, expression),\n dayOfWeek: parseField('dayOfWeek', dowStr, RANGES.dayOfWeek, expression),\n };\n}\n\nfunction parseField(\n name: string,\n raw: string,\n range: FieldRange,\n expression: string,\n): ReadonlySet<number> {\n const out = new Set<number>();\n for (const part of raw.split(',')) {\n if (part.length === 0) {\n throw new CronParseError(expression, `field '${name}' has an empty list element`);\n }\n let stepStr: string | undefined;\n let bodyStr = part;\n if (part.includes('/')) {\n const [body, step] = part.split('/', 2);\n bodyStr = body ?? '';\n stepStr = step;\n }\n const step = stepStr === undefined ? 1 : Number.parseInt(stepStr, 10);\n if (!Number.isFinite(step) || step <= 0) {\n throw new CronParseError(expression, `field '${name}' has invalid step '${stepStr}'`);\n }\n let from: number;\n let to: number;\n if (bodyStr === '*') {\n from = range.min;\n to = range.max;\n } else if (bodyStr.includes('-')) {\n const [a, b] = bodyStr.split('-', 2);\n from = parseNumeric(name, a, expression);\n to = parseNumeric(name, b, expression);\n } else {\n const single = parseNumeric(name, bodyStr, expression);\n from = single;\n to = stepStr === undefined ? single : range.max;\n }\n if (from < range.min || to > range.max || from > to) {\n throw new CronParseError(\n expression,\n `field '${name}' value out of range [${range.min}, ${range.max}]: ${bodyStr}`,\n );\n }\n for (let v = from; v <= to; v += step) {\n out.add(v);\n }\n }\n return out;\n}\n\nfunction parseNumeric(name: string, raw: string | undefined, expression: string): number {\n if (raw === undefined || raw.length === 0) {\n throw new CronParseError(expression, `field '${name}' has empty numeric component`);\n }\n if (!/^\\d+$/.test(raw)) {\n throw new CronParseError(expression, `field '${name}' value '${raw}' is not numeric`);\n }\n return Number.parseInt(raw, 10);\n}\n\n/**\n * Compute the next fire time strictly after `from` for the supplied\n * cron schedule. Returns a UTC `Date` (the scheduler treats every\n * trigger as UTC; operators that need local time express that in\n * their cron expression).\n *\n * Returns `null` if no fire happens in the next 4 years (defensive -\n * impossible for a well-formed cron expression except a vacuous\n * combination that never aligns).\n *\n * @stable\n */\nexport function nextFireAfter(parsed: ParsedCron, from: Date): Date | null {\n const start = new Date(from.getTime() + 60_000);\n start.setUTCSeconds(0, 0);\n const horizon = new Date(start.getTime() + 4 * 365 * 24 * 60 * 60 * 1000);\n\n let cursor = new Date(start.getTime());\n while (cursor.getTime() < horizon.getTime()) {\n const month = cursor.getUTCMonth() + 1;\n if (!parsed.month.has(month)) {\n cursor = new Date(\n Date.UTC(\n cursor.getUTCMonth() === 11 ? cursor.getUTCFullYear() + 1 : cursor.getUTCFullYear(),\n (cursor.getUTCMonth() + 1) % 12,\n 1,\n 0,\n 0,\n 0,\n 0,\n ),\n );\n continue;\n }\n const day = cursor.getUTCDate();\n const dow = cursor.getUTCDay();\n if (!parsed.day.has(day) || !parsed.dayOfWeek.has(dow)) {\n cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000);\n cursor.setUTCHours(0, 0, 0, 0);\n continue;\n }\n if (!parsed.hour.has(cursor.getUTCHours())) {\n cursor = new Date(cursor.getTime() + 60 * 60 * 1000);\n cursor.setUTCMinutes(0, 0, 0);\n continue;\n }\n if (!parsed.minute.has(cursor.getUTCMinutes())) {\n cursor = new Date(cursor.getTime() + 60_000);\n cursor.setUTCSeconds(0, 0);\n continue;\n }\n return cursor;\n }\n return null;\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -4,14 +4,14 @@ import { TriggerState, TriggerStore } from "@graphorin/core/contracts";
4
4
  //#region src/index.d.ts
5
5
 
6
6
  /** Canonical version constant. Mirrors the `package.json` version. */
7
- declare const VERSION = "0.5.0";
7
+ declare const VERSION = "0.6.0";
8
8
  /**
9
9
  * Catch-up policy applied when a trigger missed one or more fires
10
10
  * while the scheduler was offline.
11
11
  *
12
- * - `'none'` drop missed fires (default; safest for personal-assistant scenarios).
13
- * - `'last'` fire once on resume (best for cron-style daily jobs).
14
- * - `'all'` fire each missed run up to `maxCatchupRuns` within `catchupWindowMs`.
12
+ * - `'none'` - drop missed fires (default; safest for personal-assistant scenarios).
13
+ * - `'last'` - fire once on resume (best for cron-style daily jobs).
14
+ * - `'all'` - fire each missed run up to `maxCatchupRuns` within `catchupWindowMs`.
15
15
  *
16
16
  * @stable
17
17
  */
@@ -25,7 +25,7 @@ interface TriggerOptions {
25
25
  /**
26
26
  * How far back (from the last successful fire) misses are honored.
27
27
  * Catch-up counts REAL missed fires (RP-12), so the window must
28
- * exceed the trigger period a 24h window can never catch up a
28
+ * exceed the trigger period - a 24h window can never catch up a
29
29
  * daily cron whose boundary is itself 24h after the last fire.
30
30
  * Default 24h.
31
31
  */
@@ -61,7 +61,7 @@ interface TriggerDeclaration {
61
61
  }
62
62
  /**
63
63
  * Build a cron trigger declaration. The expression is validated
64
- * eagerly a malformed cron expression throws at registration time,
64
+ * eagerly - a malformed cron expression throws at registration time,
65
65
  * not at first fire.
66
66
  *
67
67
  * @stable
@@ -117,7 +117,7 @@ interface CreateSchedulerOptions {
117
117
  readonly store: TriggerStore;
118
118
  /** Default `'lib'`. Server mode skips the lib-mode warning. */
119
119
  readonly mode?: 'lib' | 'server';
120
- /** Override the wall clock used by tests. */
120
+ /** Override the wall clock - used by tests. */
121
121
  readonly now?: () => number;
122
122
  /**
123
123
  * Override `setTimeout`. The callback receives the chosen delay in
package/dist/index.js CHANGED
@@ -2,10 +2,10 @@ import { CronParseError, nextFireAfter, parseCron } from "./cron.js";
2
2
 
3
3
  //#region src/index.ts
4
4
  /** Canonical version constant. Mirrors the `package.json` version. */
5
- const VERSION = "0.5.0";
5
+ const VERSION = "0.6.0";
6
6
  /**
7
7
  * Build a cron trigger declaration. The expression is validated
8
- * eagerly a malformed cron expression throws at registration time,
8
+ * eagerly - a malformed cron expression throws at registration time,
9
9
  * not at first fire.
10
10
  *
11
11
  * @stable
@@ -57,7 +57,7 @@ function event(id, eventName, callback, options = {}) {
57
57
  function createScheduler(options) {
58
58
  return new SchedulerImpl(options);
59
59
  }
60
- /** Module-scoped flag one WARN per process. */
60
+ /** Module-scoped flag - one WARN per process. */
61
61
  let LIB_MODE_WARNED = false;
62
62
  /**
63
63
  * Test-only helper. Drops the per-process WARN-once flag so the next
@@ -70,7 +70,7 @@ function _resetLibModeWarningForTesting() {
70
70
  }
71
71
  const DEFAULT_CATCHUP_WINDOW_MS = 1440 * 60 * 1e3;
72
72
  /**
73
- * Node clamps `setTimeout` delays beyond `2^31 - 1` ms to 1 ms a
73
+ * Node clamps `setTimeout` delays beyond `2^31 - 1` ms to 1 ms - a
74
74
  * quarterly cron would fire immediately in a hot loop (RP-15). Delays
75
75
  * above this arm an intermediate wake-up that re-schedules instead.
76
76
  */
@@ -103,7 +103,7 @@ var SchedulerImpl = class {
103
103
  if (this.#mode === "lib" && declaration.options.acknowledgeLibMode !== true) {
104
104
  if (!LIB_MODE_WARNED) {
105
105
  LIB_MODE_WARNED = true;
106
- console.warn("[graphorin/triggers] running in library mode triggers fire only while the parent process is alive. Pass { acknowledgeLibMode: true } to suppress this warning.");
106
+ console.warn("[graphorin/triggers] running in library mode - triggers fire only while the parent process is alive. Pass { acknowledgeLibMode: true } to suppress this warning.");
107
107
  this.#publish({
108
108
  type: "lib-mode-warning",
109
109
  id: declaration.id
@@ -259,6 +259,7 @@ var SchedulerImpl = class {
259
259
  }
260
260
  recordActivity() {
261
261
  this.#lastActivity = this.#now();
262
+ if (!this.#started) return;
262
263
  for (const decl of this.#declarations.values()) {
263
264
  if (decl.kind !== "idle") continue;
264
265
  this.#cancelHandle(decl.id);
@@ -314,7 +315,10 @@ var SchedulerImpl = class {
314
315
  }
315
316
  case "interval": {
316
317
  const intervalMs = Number.parseInt(decl.spec, 10);
317
- return (state?.lastFiredAt !== void 0 ? Date.parse(state.lastFiredAt) : now) + intervalMs;
318
+ const last = state?.lastFiredAt !== void 0 ? Date.parse(state.lastFiredAt) : now;
319
+ const next = last + intervalMs;
320
+ if (next <= now && Number.isFinite(intervalMs) && intervalMs > 0) return last + (Math.floor((now - last) / intervalMs) + 1) * intervalMs;
321
+ return next;
318
322
  }
319
323
  case "idle": {
320
324
  const idleMs = Number.parseInt(decl.spec, 10);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#store","#mode","#now","#setTimeout","#clearTimeout","#lastActivity","#publish","#parsedCron","#declarations","#computeNextFire","state: TriggerState","#applyCatchup","#started","#schedule","#cancelHandle","#disposed","#handles","#closedEvents","#eventResolvers","updated: TriggerState","updated","#eventQueue","event","delay: number | null","handle"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/triggers — durable cron / interval / idle / event\n * trigger scheduler for the Graphorin framework.\n *\n * @packageDocumentation\n */\n\nimport type { TriggerState, TriggerStore } from '@graphorin/core/contracts';\nimport { nextFireAfter, type ParsedCron, parseCron } from './cron.js';\n\nexport { CronParseError, nextFireAfter, type ParsedCron, parseCron } from './cron.js';\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.5.0';\n\n/**\n * Catch-up policy applied when a trigger missed one or more fires\n * while the scheduler was offline.\n *\n * - `'none'` — drop missed fires (default; safest for personal-assistant scenarios).\n * - `'last'` — fire once on resume (best for cron-style daily jobs).\n * - `'all'` — fire each missed run up to `maxCatchupRuns` within `catchupWindowMs`.\n *\n * @stable\n */\nexport type CatchupPolicy = 'none' | 'last' | 'all';\n\n/** @stable */\nexport type TriggerKind = 'cron' | 'interval' | 'idle' | 'event';\n\n/** @stable */\nexport interface TriggerOptions {\n readonly catchupPolicy?: CatchupPolicy;\n readonly maxCatchupRuns?: number;\n /**\n * How far back (from the last successful fire) misses are honored.\n * Catch-up counts REAL missed fires (RP-12), so the window must\n * exceed the trigger period — a 24h window can never catch up a\n * daily cron whose boundary is itself 24h after the last fire.\n * Default 24h.\n */\n readonly catchupWindowMs?: number;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Suppress the one-time per-process library-mode WARN. Library\n * callers acknowledging that triggers fire only as long as the\n * process lives pass `true` here.\n */\n readonly acknowledgeLibMode?: boolean;\n}\n\n/**\n * Trigger callback. Receives an optional `payload` for `event`\n * triggers; for cron / interval / idle triggers `payload` is\n * `undefined`.\n *\n * @stable\n */\nexport type TriggerCallback = (payload?: unknown) => void | Promise<void>;\n\n/**\n * Public trigger declaration emitted by the helper functions\n * (`cron(...)`, `interval(...)`, `idle(...)`, `event(...)`).\n *\n * @stable\n */\nexport interface TriggerDeclaration {\n readonly id: string;\n readonly kind: TriggerKind;\n readonly spec: string;\n readonly callback: TriggerCallback;\n readonly options: TriggerOptions;\n}\n\n/**\n * Build a cron trigger declaration. The expression is validated\n * eagerly — a malformed cron expression throws at registration time,\n * not at first fire.\n *\n * @stable\n */\nexport function cron(\n id: string,\n expression: string,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n parseCron(expression);\n return { id, kind: 'cron', spec: expression, callback, options };\n}\n\n/** @stable */\nexport function interval(\n id: string,\n intervalMs: number,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n if (!Number.isFinite(intervalMs) || intervalMs <= 0) {\n throw new Error(`[graphorin/triggers] interval(${id}): intervalMs must be > 0`);\n }\n return {\n id,\n kind: 'interval',\n spec: String(intervalMs),\n callback,\n options,\n };\n}\n\n/** @stable */\nexport function idle(\n id: string,\n idleMs: number,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n if (!Number.isFinite(idleMs) || idleMs <= 0) {\n throw new Error(`[graphorin/triggers] idle(${id}): idleMs must be > 0`);\n }\n return {\n id,\n kind: 'idle',\n spec: String(idleMs),\n callback,\n options,\n };\n}\n\n/** @stable */\nexport function event(\n id: string,\n eventName: string,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n if (eventName.length === 0) {\n throw new Error(`[graphorin/triggers] event(${id}): eventName must not be empty`);\n }\n return {\n id,\n kind: 'event',\n spec: eventName,\n callback,\n options,\n };\n}\n\n/**\n * Lifecycle event emitted by {@link Scheduler.events}. Useful for\n * tests and for piping into observability without monkey-patching.\n *\n * @stable\n */\nexport type SchedulerEvent =\n | { readonly type: 'started' }\n | { readonly type: 'stopped' }\n | { readonly type: 'registered'; readonly id: string; readonly kind: TriggerKind }\n | { readonly type: 'fire-start'; readonly id: string; readonly firedAt: number }\n | { readonly type: 'fire-end'; readonly id: string; readonly durationMs: number }\n | {\n readonly type: 'fire-error';\n readonly id: string;\n readonly error: unknown;\n readonly durationMs: number;\n }\n | { readonly type: 'catchup-applied'; readonly id: string; readonly missed: number }\n | { readonly type: 'lib-mode-warning'; readonly id: string };\n\n/**\n * Options for {@link createScheduler}.\n *\n * @stable\n */\nexport interface CreateSchedulerOptions {\n readonly store: TriggerStore;\n /** Default `'lib'`. Server mode skips the lib-mode warning. */\n readonly mode?: 'lib' | 'server';\n /** Override the wall clock — used by tests. */\n readonly now?: () => number;\n /**\n * Override `setTimeout`. The callback receives the chosen delay in\n * milliseconds; the return value is the handle the scheduler\n * later passes to `clearTimeout`. Tests inject a controllable timer.\n */\n readonly setTimeout?: (cb: () => void, ms: number) => unknown;\n readonly clearTimeout?: (handle: unknown) => void;\n /**\n * Resets the per-process WARN-once flag. Used by the test suite to\n * verify the warning fires exactly once per run.\n *\n * @internal\n */\n readonly _resetLibModeFlag?: boolean;\n}\n\n/**\n * Public Scheduler surface.\n *\n * @stable\n */\nexport interface Scheduler {\n register(declaration: TriggerDeclaration): Promise<TriggerState>;\n unregister(id: string): Promise<void>;\n list(): Promise<readonly TriggerState[]>;\n start(): Promise<void>;\n stop(): Promise<void>;\n /** Emit `eventName` to every registered `event` trigger. */\n emit(eventName: string, payload?: unknown): Promise<void>;\n /** Manually fire `id` (used by `graphorin triggers fire`, Phase 15). */\n fire(id: string, payload?: unknown): Promise<void>;\n /**\n * Flip the persistent `disabled` flag (IP-17). Disabling cancels the\n * armed timer but keeps the trigger registered + persisted; enabling\n * recomputes the next fire from now and re-arms. The destructive\n * removal is `unregister(...)`.\n */\n setDisabled(id: string, disabled: boolean): Promise<TriggerState>;\n /** AsyncIterable lifecycle event stream. */\n events(): AsyncIterable<SchedulerEvent>;\n /** Notify the scheduler that the user / runtime is no longer idle. */\n recordActivity(): void;\n}\n\n/** @stable */\nexport function createScheduler(options: CreateSchedulerOptions): Scheduler {\n return new SchedulerImpl(options);\n}\n\n/** Module-scoped flag — one WARN per process. */\nlet LIB_MODE_WARNED = false;\n\n/**\n * Test-only helper. Drops the per-process WARN-once flag so the next\n * `register(...)` call in lib mode emits the warning again.\n *\n * @internal\n */\nexport function _resetLibModeWarningForTesting(): void {\n LIB_MODE_WARNED = false;\n}\n\nconst DEFAULT_CATCHUP_WINDOW_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Node clamps `setTimeout` delays beyond `2^31 - 1` ms to 1 ms — a\n * quarterly cron would fire immediately in a hot loop (RP-15). Delays\n * above this arm an intermediate wake-up that re-schedules instead.\n */\nconst MAX_TIMEOUT_MS = 2 ** 31 - 1;\n\nclass SchedulerImpl implements Scheduler {\n readonly #store: TriggerStore;\n readonly #mode: 'lib' | 'server';\n readonly #now: () => number;\n readonly #setTimeout: (cb: () => void, ms: number) => unknown;\n readonly #clearTimeout: (handle: unknown) => void;\n #started = false;\n #disposed = false;\n #lastActivity: number;\n #handles: Map<string, unknown> = new Map();\n #parsedCron: Map<string, ParsedCron> = new Map();\n #declarations: Map<string, TriggerDeclaration> = new Map();\n #eventQueue: SchedulerEvent[] = [];\n #eventResolvers: Array<(value: IteratorResult<SchedulerEvent>) => void> = [];\n #closedEvents = false;\n\n constructor(options: CreateSchedulerOptions) {\n this.#store = options.store;\n this.#mode = options.mode ?? 'lib';\n this.#now = options.now ?? Date.now;\n this.#setTimeout = options.setTimeout ?? ((cb, ms) => globalThis.setTimeout(cb, ms));\n this.#clearTimeout =\n options.clearTimeout ?? ((h) => globalThis.clearTimeout(h as ReturnType<typeof setTimeout>));\n this.#lastActivity = this.#now();\n if (options._resetLibModeFlag) LIB_MODE_WARNED = false;\n }\n\n async register(declaration: TriggerDeclaration): Promise<TriggerState> {\n if (this.#mode === 'lib' && declaration.options.acknowledgeLibMode !== true) {\n if (!LIB_MODE_WARNED) {\n LIB_MODE_WARNED = true;\n console.warn(\n `[graphorin/triggers] running in library mode — triggers fire only while the parent process is alive. ` +\n `Pass { acknowledgeLibMode: true } to suppress this warning.`,\n );\n this.#publish({ type: 'lib-mode-warning', id: declaration.id });\n }\n }\n\n if (declaration.kind === 'cron') {\n this.#parsedCron.set(declaration.id, parseCron(declaration.spec));\n }\n this.#declarations.set(declaration.id, declaration);\n\n const now = this.#now();\n const existing = await this.#store.get(declaration.id);\n const nextFireMs = this.#computeNextFire(declaration, existing, now);\n const state: TriggerState = {\n id: declaration.id,\n kind: declaration.kind,\n spec: declaration.spec,\n callbackRef: declaration.id,\n ...(existing?.lastFiredAt !== undefined ? { lastFiredAt: existing.lastFiredAt } : {}),\n ...(nextFireMs !== null ? { nextFireAt: new Date(nextFireMs).toISOString() } : {}),\n missedFires: 0,\n disabled: existing?.disabled === true,\n catchupPolicy: declaration.options.catchupPolicy ?? 'none',\n maxCatchupRuns: declaration.options.maxCatchupRuns ?? 1,\n catchupWindowMs: declaration.options.catchupWindowMs ?? DEFAULT_CATCHUP_WINDOW_MS,\n ...(declaration.options.tags !== undefined ? { tags: declaration.options.tags } : {}),\n createdAt: existing !== null ? existing.createdAt : new Date(now).toISOString(),\n ...(existing !== null ? { updatedAt: new Date(now).toISOString() } : {}),\n };\n await this.#store.upsert(state);\n\n this.#publish({ type: 'registered', id: declaration.id, kind: declaration.kind });\n\n if (existing !== null && (declaration.kind === 'cron' || declaration.kind === 'interval')) {\n await this.#applyCatchup(state, existing, now);\n }\n\n if (this.#started) this.#schedule(declaration.id, state);\n return state;\n }\n\n async unregister(id: string): Promise<void> {\n this.#cancelHandle(id);\n this.#declarations.delete(id);\n this.#parsedCron.delete(id);\n await this.#store.remove(id);\n }\n\n async list(): Promise<readonly TriggerState[]> {\n return this.#store.list();\n }\n\n async start(): Promise<void> {\n if (this.#started || this.#disposed) return;\n this.#started = true;\n this.#publish({ type: 'started' });\n const states = await this.#store.list();\n for (const state of states) {\n if (state.disabled) continue;\n this.#schedule(state.id, state);\n }\n }\n\n async stop(): Promise<void> {\n if (!this.#started) return;\n this.#started = false;\n for (const id of [...this.#handles.keys()]) this.#cancelHandle(id);\n this.#publish({ type: 'stopped' });\n this.#closedEvents = true;\n while (this.#eventResolvers.length > 0) {\n const resolver = this.#eventResolvers.shift();\n resolver?.({ done: true, value: undefined });\n }\n }\n\n async emit(eventName: string, payload?: unknown): Promise<void> {\n for (const decl of this.#declarations.values()) {\n if (decl.kind === 'event' && decl.spec === eventName) {\n await this.fire(decl.id, payload);\n }\n }\n }\n\n async fire(id: string, payload?: unknown): Promise<void> {\n const decl = this.#declarations.get(id);\n if (decl === undefined) {\n throw new Error(`[graphorin/triggers] no trigger registered with id '${id}'`);\n }\n const firedAt = this.#now();\n this.#publish({ type: 'fire-start', id, firedAt });\n const start = firedAt;\n try {\n await decl.callback(payload);\n const durationMs = this.#now() - start;\n this.#publish({ type: 'fire-end', id, durationMs });\n const state = await this.#store.get(id);\n // RP-13: compute the next fire from THIS fire's timestamp — the\n // persisted state still carries the PREVIOUS lastFiredAt, which\n // for interval triggers yields `prev + interval ≈ now` and an\n // immediate duplicate via the clamped-to-0 delay. Idle triggers\n // never self-reschedule: their next window starts only when\n // `recordActivity()` observes new activity (a fire-driven re-arm\n // would compute a stale `lastActivity + idleMs`, clamp to 0 and\n // refire in a loop).\n const nextFireMs =\n state !== null && decl.kind !== 'idle'\n ? this.#computeNextFire(\n decl,\n { ...state, lastFiredAt: new Date(firedAt).toISOString() },\n this.#now(),\n )\n : null;\n await this.#store.recordFire(\n id,\n new Date(firedAt).toISOString(),\n nextFireMs !== null ? new Date(nextFireMs).toISOString() : undefined,\n );\n if (this.#started && decl.kind !== 'idle') {\n const refreshed = await this.#store.get(id);\n if (refreshed !== null) this.#schedule(id, refreshed);\n }\n } catch (err) {\n const durationMs = this.#now() - start;\n this.#publish({ type: 'fire-error', id, error: err, durationMs });\n // RP-14: the one-shot timer is consumed by this fire — recompute\n // and persist the next fire WITHOUT recording a successful fire\n // (lastFiredAt stays put), and re-arm, so a single callback\n // failure cannot permanently silence a daily cron.\n try {\n const state = await this.#store.get(id);\n if (state !== null && decl.kind !== 'idle') {\n const nextFireMs = this.#computeNextFire(\n decl,\n { ...state, lastFiredAt: new Date(firedAt).toISOString() },\n this.#now(),\n );\n const updated: TriggerState = {\n ...state,\n ...(nextFireMs !== null ? { nextFireAt: new Date(nextFireMs).toISOString() } : {}),\n updatedAt: new Date(this.#now()).toISOString(),\n };\n await this.#store.upsert(updated);\n if (this.#started) this.#schedule(id, updated);\n }\n } catch {\n // Best-effort re-arm — a store failure here must not become an\n // unhandled rejection out of the timer callback.\n }\n }\n }\n\n async setDisabled(id: string, disabled: boolean): Promise<TriggerState> {\n const state = await this.#store.get(id);\n if (state === null) {\n throw new Error(`[graphorin/triggers] no trigger registered with id '${id}'`);\n }\n const nowIso = new Date(this.#now()).toISOString();\n if (disabled) {\n this.#cancelHandle(id);\n const updated: TriggerState = { ...state, disabled: true, updatedAt: nowIso };\n await this.#store.upsert(updated);\n return updated;\n }\n // Re-enable: recompute the next fire from NOW (a stale persisted\n // nextFireAt from before the disable would otherwise clamp to 0 and\n // fire immediately), then re-arm.\n const decl = this.#declarations.get(id);\n const nextMs =\n decl !== undefined\n ? this.#computeNextFire(decl, { ...state, lastFiredAt: nowIso }, this.#now())\n : null;\n const updated: TriggerState = {\n ...state,\n disabled: false,\n ...(nextMs !== null ? { nextFireAt: new Date(nextMs).toISOString() } : {}),\n updatedAt: nowIso,\n };\n await this.#store.upsert(updated);\n if (this.#started) this.#schedule(id, updated);\n return updated;\n }\n\n recordActivity(): void {\n this.#lastActivity = this.#now();\n // Reschedule idle triggers so they treat \"now\" as the start of\n // their idle window.\n for (const decl of this.#declarations.values()) {\n if (decl.kind !== 'idle') continue;\n this.#cancelHandle(decl.id);\n const idleMs = Number.parseInt(decl.spec, 10);\n const handle = this.#setTimeout(() => {\n void this.fire(decl.id);\n }, idleMs);\n this.#handles.set(decl.id, handle);\n }\n }\n\n events(): AsyncIterable<SchedulerEvent> {\n const queue = this.#eventQueue;\n const resolvers = this.#eventResolvers;\n const me = this;\n return {\n [Symbol.asyncIterator](): AsyncIterator<SchedulerEvent> {\n return {\n async next(): Promise<IteratorResult<SchedulerEvent>> {\n if (queue.length > 0) {\n const value = queue.shift() as SchedulerEvent;\n return { done: false, value };\n }\n if (me.#closedEvents) return { done: true, value: undefined };\n return new Promise((resolve) => resolvers.push(resolve));\n },\n async return(): Promise<IteratorResult<SchedulerEvent>> {\n return { done: true, value: undefined };\n },\n };\n },\n };\n }\n\n // ---------------------------------------------------------------------------\n\n #publish(event: SchedulerEvent): void {\n if (this.#closedEvents) return;\n if (this.#eventResolvers.length > 0) {\n const resolver = this.#eventResolvers.shift();\n resolver?.({ done: false, value: event });\n return;\n }\n this.#eventQueue.push(event);\n }\n\n #computeNextFire(\n decl: TriggerDeclaration,\n state: TriggerState | null,\n now: number,\n ): number | null {\n switch (decl.kind) {\n case 'cron': {\n const parsed = this.#parsedCron.get(decl.id) ?? parseCron(decl.spec);\n const next = nextFireAfter(parsed, new Date(now));\n return next === null ? null : next.getTime();\n }\n case 'interval': {\n const intervalMs = Number.parseInt(decl.spec, 10);\n const last = state?.lastFiredAt !== undefined ? Date.parse(state.lastFiredAt) : now;\n return last + intervalMs;\n }\n case 'idle': {\n const idleMs = Number.parseInt(decl.spec, 10);\n return this.#lastActivity + idleMs;\n }\n case 'event':\n return null;\n }\n }\n\n async #applyCatchup(state: TriggerState, existing: TriggerState, now: number): Promise<void> {\n if (state.catchupPolicy === 'none') return;\n const decl = this.#declarations.get(state.id);\n if (decl === undefined || (decl.kind !== 'cron' && decl.kind !== 'interval')) return;\n const lastFired = existing.lastFiredAt !== undefined ? Date.parse(existing.lastFiredAt) : null;\n if (lastFired === null) return;\n if (lastFired < now - state.catchupWindowMs) return;\n\n // RP-12: count the REAL missed fires by walking the schedule from\n // the last successful fire to now — a restart with zero crossed\n // boundaries must apply zero catch-up (the old code fired `1` /\n // `maxCatchupRuns` times unconditionally).\n const SCAN_CAP = 1000;\n let missed = 0;\n if (decl.kind === 'cron') {\n const parsed = this.#parsedCron.get(decl.id) ?? parseCron(decl.spec);\n let cursor = lastFired;\n while (missed < SCAN_CAP) {\n const next = nextFireAfter(parsed, new Date(cursor));\n if (next === null || next.getTime() > now) break;\n cursor = next.getTime();\n missed += 1;\n }\n } else {\n const intervalMs = Number.parseInt(decl.spec, 10);\n missed = Math.floor((now - lastFired) / intervalMs);\n }\n if (missed <= 0) return;\n\n const toFire = state.catchupPolicy === 'last' ? 1 : Math.min(missed, state.maxCatchupRuns);\n for (let i = 0; i < toFire; i++) {\n await this.fire(state.id);\n }\n // The overflow beyond what we re-ran is recorded for health/CLI.\n const excess = missed - toFire;\n if (excess > 0) {\n const refreshed = await this.#store.get(state.id);\n if (refreshed !== null) {\n await this.#store.upsert({ ...refreshed, missedFires: excess });\n }\n }\n this.#publish({ type: 'catchup-applied', id: state.id, missed });\n }\n\n #schedule(id: string, state: TriggerState): void {\n this.#cancelHandle(id);\n if (state.disabled) return;\n const decl = this.#declarations.get(id);\n if (decl === undefined) return;\n if (decl.kind === 'event') return;\n\n let delay: number | null = null;\n if (state.nextFireAt !== undefined) {\n delay = Date.parse(state.nextFireAt) - this.#now();\n } else {\n const next = this.#computeNextFire(decl, state, this.#now());\n delay = next === null ? null : next - this.#now();\n }\n if (delay === null) return;\n if (delay < 0) delay = 0;\n\n if (delay > MAX_TIMEOUT_MS) {\n // RP-15: chunk the wait — the intermediate wake-up re-reads the\n // freshest state and re-schedules (it never fires the callback).\n const handle = this.#setTimeout(() => {\n void this.#store\n .get(id)\n .then((s) => {\n if (s !== null) this.#schedule(id, s);\n })\n .catch(() => {});\n }, MAX_TIMEOUT_MS);\n this.#handles.set(id, handle);\n return;\n }\n\n const handle = this.#setTimeout(() => {\n void this.fire(id);\n }, delay);\n this.#handles.set(id, handle);\n }\n\n #cancelHandle(id: string): void {\n const handle = this.#handles.get(id);\n if (handle !== undefined) {\n this.#clearTimeout(handle);\n this.#handles.delete(id);\n }\n }\n}\n"],"mappings":";;;;AAaA,MAAa,UAAU;;;;;;;;AAoEvB,SAAgB,KACd,IACA,YACA,UACA,UAA0B,EAAE,EACR;AACpB,WAAU,WAAW;AACrB,QAAO;EAAE;EAAI,MAAM;EAAQ,MAAM;EAAY;EAAU;EAAS;;;AAIlE,SAAgB,SACd,IACA,YACA,UACA,UAA0B,EAAE,EACR;AACpB,KAAI,CAAC,OAAO,SAAS,WAAW,IAAI,cAAc,EAChD,OAAM,IAAI,MAAM,iCAAiC,GAAG,2BAA2B;AAEjF,QAAO;EACL;EACA,MAAM;EACN,MAAM,OAAO,WAAW;EACxB;EACA;EACD;;;AAIH,SAAgB,KACd,IACA,QACA,UACA,UAA0B,EAAE,EACR;AACpB,KAAI,CAAC,OAAO,SAAS,OAAO,IAAI,UAAU,EACxC,OAAM,IAAI,MAAM,6BAA6B,GAAG,uBAAuB;AAEzE,QAAO;EACL;EACA,MAAM;EACN,MAAM,OAAO,OAAO;EACpB;EACA;EACD;;;AAIH,SAAgB,MACd,IACA,WACA,UACA,UAA0B,EAAE,EACR;AACpB,KAAI,UAAU,WAAW,EACvB,OAAM,IAAI,MAAM,8BAA8B,GAAG,gCAAgC;AAEnF,QAAO;EACL;EACA,MAAM;EACN,MAAM;EACN;EACA;EACD;;;AAgFH,SAAgB,gBAAgB,SAA4C;AAC1E,QAAO,IAAI,cAAc,QAAQ;;;AAInC,IAAI,kBAAkB;;;;;;;AAQtB,SAAgB,iCAAuC;AACrD,mBAAkB;;AAGpB,MAAM,4BAA4B,OAAU,KAAK;;;;;;AAOjD,MAAM,iBAAiB,KAAK,KAAK;AAEjC,IAAM,gBAAN,MAAyC;CACvC,CAASA;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,CAASC;CACT,WAAW;CACX,YAAY;CACZ;CACA,2BAAiC,IAAI,KAAK;CAC1C,8BAAuC,IAAI,KAAK;CAChD,gCAAiD,IAAI,KAAK;CAC1D,cAAgC,EAAE;CAClC,kBAA0E,EAAE;CAC5E,gBAAgB;CAEhB,YAAY,SAAiC;AAC3C,QAAKJ,QAAS,QAAQ;AACtB,QAAKC,OAAQ,QAAQ,QAAQ;AAC7B,QAAKC,MAAO,QAAQ,OAAO,KAAK;AAChC,QAAKC,aAAc,QAAQ,gBAAgB,IAAI,OAAO,WAAW,WAAW,IAAI,GAAG;AACnF,QAAKC,eACH,QAAQ,kBAAkB,MAAM,WAAW,aAAa,EAAmC;AAC7F,QAAKC,eAAgB,MAAKH,KAAM;AAChC,MAAI,QAAQ,kBAAmB,mBAAkB;;CAGnD,MAAM,SAAS,aAAwD;AACrE,MAAI,MAAKD,SAAU,SAAS,YAAY,QAAQ,uBAAuB,MACrE;OAAI,CAAC,iBAAiB;AACpB,sBAAkB;AAClB,YAAQ,KACN,mKAED;AACD,UAAKK,QAAS;KAAE,MAAM;KAAoB,IAAI,YAAY;KAAI,CAAC;;;AAInE,MAAI,YAAY,SAAS,OACvB,OAAKC,WAAY,IAAI,YAAY,IAAI,UAAU,YAAY,KAAK,CAAC;AAEnE,QAAKC,aAAc,IAAI,YAAY,IAAI,YAAY;EAEnD,MAAM,MAAM,MAAKN,KAAM;EACvB,MAAM,WAAW,MAAM,MAAKF,MAAO,IAAI,YAAY,GAAG;EACtD,MAAM,aAAa,MAAKS,gBAAiB,aAAa,UAAU,IAAI;EACpE,MAAMC,QAAsB;GAC1B,IAAI,YAAY;GAChB,MAAM,YAAY;GAClB,MAAM,YAAY;GAClB,aAAa,YAAY;GACzB,GAAI,UAAU,gBAAgB,SAAY,EAAE,aAAa,SAAS,aAAa,GAAG,EAAE;GACpF,GAAI,eAAe,OAAO,EAAE,YAAY,IAAI,KAAK,WAAW,CAAC,aAAa,EAAE,GAAG,EAAE;GACjF,aAAa;GACb,UAAU,UAAU,aAAa;GACjC,eAAe,YAAY,QAAQ,iBAAiB;GACpD,gBAAgB,YAAY,QAAQ,kBAAkB;GACtD,iBAAiB,YAAY,QAAQ,mBAAmB;GACxD,GAAI,YAAY,QAAQ,SAAS,SAAY,EAAE,MAAM,YAAY,QAAQ,MAAM,GAAG,EAAE;GACpF,WAAW,aAAa,OAAO,SAAS,YAAY,IAAI,KAAK,IAAI,CAAC,aAAa;GAC/E,GAAI,aAAa,OAAO,EAAE,WAAW,IAAI,KAAK,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE;GACxE;AACD,QAAM,MAAKV,MAAO,OAAO,MAAM;AAE/B,QAAKM,QAAS;GAAE,MAAM;GAAc,IAAI,YAAY;GAAI,MAAM,YAAY;GAAM,CAAC;AAEjF,MAAI,aAAa,SAAS,YAAY,SAAS,UAAU,YAAY,SAAS,YAC5E,OAAM,MAAKK,aAAc,OAAO,UAAU,IAAI;AAGhD,MAAI,MAAKC,QAAU,OAAKC,SAAU,YAAY,IAAI,MAAM;AACxD,SAAO;;CAGT,MAAM,WAAW,IAA2B;AAC1C,QAAKC,aAAc,GAAG;AACtB,QAAKN,aAAc,OAAO,GAAG;AAC7B,QAAKD,WAAY,OAAO,GAAG;AAC3B,QAAM,MAAKP,MAAO,OAAO,GAAG;;CAG9B,MAAM,OAAyC;AAC7C,SAAO,MAAKA,MAAO,MAAM;;CAG3B,MAAM,QAAuB;AAC3B,MAAI,MAAKY,WAAY,MAAKG,SAAW;AACrC,QAAKH,UAAW;AAChB,QAAKN,QAAS,EAAE,MAAM,WAAW,CAAC;EAClC,MAAM,SAAS,MAAM,MAAKN,MAAO,MAAM;AACvC,OAAK,MAAM,SAAS,QAAQ;AAC1B,OAAI,MAAM,SAAU;AACpB,SAAKa,SAAU,MAAM,IAAI,MAAM;;;CAInC,MAAM,OAAsB;AAC1B,MAAI,CAAC,MAAKD,QAAU;AACpB,QAAKA,UAAW;AAChB,OAAK,MAAM,MAAM,CAAC,GAAG,MAAKI,QAAS,MAAM,CAAC,CAAE,OAAKF,aAAc,GAAG;AAClE,QAAKR,QAAS,EAAE,MAAM,WAAW,CAAC;AAClC,QAAKW,eAAgB;AACrB,SAAO,MAAKC,eAAgB,SAAS,EAEnC,CADiB,MAAKA,eAAgB,OAAO,GAClC;GAAE,MAAM;GAAM,OAAO;GAAW,CAAC;;CAIhD,MAAM,KAAK,WAAmB,SAAkC;AAC9D,OAAK,MAAM,QAAQ,MAAKV,aAAc,QAAQ,CAC5C,KAAI,KAAK,SAAS,WAAW,KAAK,SAAS,UACzC,OAAM,KAAK,KAAK,KAAK,IAAI,QAAQ;;CAKvC,MAAM,KAAK,IAAY,SAAkC;EACvD,MAAM,OAAO,MAAKA,aAAc,IAAI,GAAG;AACvC,MAAI,SAAS,OACX,OAAM,IAAI,MAAM,uDAAuD,GAAG,GAAG;EAE/E,MAAM,UAAU,MAAKN,KAAM;AAC3B,QAAKI,QAAS;GAAE,MAAM;GAAc;GAAI;GAAS,CAAC;EAClD,MAAM,QAAQ;AACd,MAAI;AACF,SAAM,KAAK,SAAS,QAAQ;GAC5B,MAAM,aAAa,MAAKJ,KAAM,GAAG;AACjC,SAAKI,QAAS;IAAE,MAAM;IAAY;IAAI;IAAY,CAAC;GACnD,MAAM,QAAQ,MAAM,MAAKN,MAAO,IAAI,GAAG;GASvC,MAAM,aACJ,UAAU,QAAQ,KAAK,SAAS,SAC5B,MAAKS,gBACH,MACA;IAAE,GAAG;IAAO,aAAa,IAAI,KAAK,QAAQ,CAAC,aAAa;IAAE,EAC1D,MAAKP,KAAM,CACZ,GACD;AACN,SAAM,MAAKF,MAAO,WAChB,IACA,IAAI,KAAK,QAAQ,CAAC,aAAa,EAC/B,eAAe,OAAO,IAAI,KAAK,WAAW,CAAC,aAAa,GAAG,OAC5D;AACD,OAAI,MAAKY,WAAY,KAAK,SAAS,QAAQ;IACzC,MAAM,YAAY,MAAM,MAAKZ,MAAO,IAAI,GAAG;AAC3C,QAAI,cAAc,KAAM,OAAKa,SAAU,IAAI,UAAU;;WAEhD,KAAK;GACZ,MAAM,aAAa,MAAKX,KAAM,GAAG;AACjC,SAAKI,QAAS;IAAE,MAAM;IAAc;IAAI,OAAO;IAAK;IAAY,CAAC;AAKjE,OAAI;IACF,MAAM,QAAQ,MAAM,MAAKN,MAAO,IAAI,GAAG;AACvC,QAAI,UAAU,QAAQ,KAAK,SAAS,QAAQ;KAC1C,MAAM,aAAa,MAAKS,gBACtB,MACA;MAAE,GAAG;MAAO,aAAa,IAAI,KAAK,QAAQ,CAAC,aAAa;MAAE,EAC1D,MAAKP,KAAM,CACZ;KACD,MAAMiB,UAAwB;MAC5B,GAAG;MACH,GAAI,eAAe,OAAO,EAAE,YAAY,IAAI,KAAK,WAAW,CAAC,aAAa,EAAE,GAAG,EAAE;MACjF,WAAW,IAAI,KAAK,MAAKjB,KAAM,CAAC,CAAC,aAAa;MAC/C;AACD,WAAM,MAAKF,MAAO,OAAO,QAAQ;AACjC,SAAI,MAAKY,QAAU,OAAKC,SAAU,IAAI,QAAQ;;WAE1C;;;CAOZ,MAAM,YAAY,IAAY,UAA0C;EACtE,MAAM,QAAQ,MAAM,MAAKb,MAAO,IAAI,GAAG;AACvC,MAAI,UAAU,KACZ,OAAM,IAAI,MAAM,uDAAuD,GAAG,GAAG;EAE/E,MAAM,SAAS,IAAI,KAAK,MAAKE,KAAM,CAAC,CAAC,aAAa;AAClD,MAAI,UAAU;AACZ,SAAKY,aAAc,GAAG;GACtB,MAAMK,YAAwB;IAAE,GAAG;IAAO,UAAU;IAAM,WAAW;IAAQ;AAC7E,SAAM,MAAKnB,MAAO,OAAOoB,UAAQ;AACjC,UAAOA;;EAKT,MAAM,OAAO,MAAKZ,aAAc,IAAI,GAAG;EACvC,MAAM,SACJ,SAAS,SACL,MAAKC,gBAAiB,MAAM;GAAE,GAAG;GAAO,aAAa;GAAQ,EAAE,MAAKP,KAAM,CAAC,GAC3E;EACN,MAAMiB,UAAwB;GAC5B,GAAG;GACH,UAAU;GACV,GAAI,WAAW,OAAO,EAAE,YAAY,IAAI,KAAK,OAAO,CAAC,aAAa,EAAE,GAAG,EAAE;GACzE,WAAW;GACZ;AACD,QAAM,MAAKnB,MAAO,OAAO,QAAQ;AACjC,MAAI,MAAKY,QAAU,OAAKC,SAAU,IAAI,QAAQ;AAC9C,SAAO;;CAGT,iBAAuB;AACrB,QAAKR,eAAgB,MAAKH,KAAM;AAGhC,OAAK,MAAM,QAAQ,MAAKM,aAAc,QAAQ,EAAE;AAC9C,OAAI,KAAK,SAAS,OAAQ;AAC1B,SAAKM,aAAc,KAAK,GAAG;GAC3B,MAAM,SAAS,OAAO,SAAS,KAAK,MAAM,GAAG;GAC7C,MAAM,SAAS,MAAKX,iBAAkB;AACpC,IAAK,KAAK,KAAK,KAAK,GAAG;MACtB,OAAO;AACV,SAAKa,QAAS,IAAI,KAAK,IAAI,OAAO;;;CAItC,SAAwC;EACtC,MAAM,QAAQ,MAAKK;EACnB,MAAM,YAAY,MAAKH;EACvB,MAAM,KAAK;AACX,SAAO,EACL,CAAC,OAAO,iBAAgD;AACtD,UAAO;IACL,MAAM,OAAgD;AACpD,SAAI,MAAM,SAAS,EAEjB,QAAO;MAAE,MAAM;MAAO,OADR,MAAM,OAAO;MACE;AAE/B,SAAI,IAAGD,aAAe,QAAO;MAAE,MAAM;MAAM,OAAO;MAAW;AAC7D,YAAO,IAAI,SAAS,YAAY,UAAU,KAAK,QAAQ,CAAC;;IAE1D,MAAM,SAAkD;AACtD,YAAO;MAAE,MAAM;MAAM,OAAO;MAAW;;IAE1C;KAEJ;;CAKH,SAAS,SAA6B;AACpC,MAAI,MAAKA,aAAe;AACxB,MAAI,MAAKC,eAAgB,SAAS,GAAG;AAEnC,GADiB,MAAKA,eAAgB,OAAO,GAClC;IAAE,MAAM;IAAO,OAAOI;IAAO,CAAC;AACzC;;AAEF,QAAKD,WAAY,KAAKC,QAAM;;CAG9B,iBACE,MACA,OACA,KACe;AACf,UAAQ,KAAK,MAAb;GACE,KAAK,QAAQ;IAEX,MAAM,OAAO,cADE,MAAKf,WAAY,IAAI,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK,EACjC,IAAI,KAAK,IAAI,CAAC;AACjD,WAAO,SAAS,OAAO,OAAO,KAAK,SAAS;;GAE9C,KAAK,YAAY;IACf,MAAM,aAAa,OAAO,SAAS,KAAK,MAAM,GAAG;AAEjD,YADa,OAAO,gBAAgB,SAAY,KAAK,MAAM,MAAM,YAAY,GAAG,OAClE;;GAEhB,KAAK,QAAQ;IACX,MAAM,SAAS,OAAO,SAAS,KAAK,MAAM,GAAG;AAC7C,WAAO,MAAKF,eAAgB;;GAE9B,KAAK,QACH,QAAO;;;CAIb,OAAMM,aAAc,OAAqB,UAAwB,KAA4B;AAC3F,MAAI,MAAM,kBAAkB,OAAQ;EACpC,MAAM,OAAO,MAAKH,aAAc,IAAI,MAAM,GAAG;AAC7C,MAAI,SAAS,UAAc,KAAK,SAAS,UAAU,KAAK,SAAS,WAAa;EAC9E,MAAM,YAAY,SAAS,gBAAgB,SAAY,KAAK,MAAM,SAAS,YAAY,GAAG;AAC1F,MAAI,cAAc,KAAM;AACxB,MAAI,YAAY,MAAM,MAAM,gBAAiB;EAM7C,MAAM,WAAW;EACjB,IAAI,SAAS;AACb,MAAI,KAAK,SAAS,QAAQ;GACxB,MAAM,SAAS,MAAKD,WAAY,IAAI,KAAK,GAAG,IAAI,UAAU,KAAK,KAAK;GACpE,IAAI,SAAS;AACb,UAAO,SAAS,UAAU;IACxB,MAAM,OAAO,cAAc,QAAQ,IAAI,KAAK,OAAO,CAAC;AACpD,QAAI,SAAS,QAAQ,KAAK,SAAS,GAAG,IAAK;AAC3C,aAAS,KAAK,SAAS;AACvB,cAAU;;SAEP;GACL,MAAM,aAAa,OAAO,SAAS,KAAK,MAAM,GAAG;AACjD,YAAS,KAAK,OAAO,MAAM,aAAa,WAAW;;AAErD,MAAI,UAAU,EAAG;EAEjB,MAAM,SAAS,MAAM,kBAAkB,SAAS,IAAI,KAAK,IAAI,QAAQ,MAAM,eAAe;AAC1F,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,IAC1B,OAAM,KAAK,KAAK,MAAM,GAAG;EAG3B,MAAM,SAAS,SAAS;AACxB,MAAI,SAAS,GAAG;GACd,MAAM,YAAY,MAAM,MAAKP,MAAO,IAAI,MAAM,GAAG;AACjD,OAAI,cAAc,KAChB,OAAM,MAAKA,MAAO,OAAO;IAAE,GAAG;IAAW,aAAa;IAAQ,CAAC;;AAGnE,QAAKM,QAAS;GAAE,MAAM;GAAmB,IAAI,MAAM;GAAI;GAAQ,CAAC;;CAGlE,UAAU,IAAY,OAA2B;AAC/C,QAAKQ,aAAc,GAAG;AACtB,MAAI,MAAM,SAAU;EACpB,MAAM,OAAO,MAAKN,aAAc,IAAI,GAAG;AACvC,MAAI,SAAS,OAAW;AACxB,MAAI,KAAK,SAAS,QAAS;EAE3B,IAAIe,QAAuB;AAC3B,MAAI,MAAM,eAAe,OACvB,SAAQ,KAAK,MAAM,MAAM,WAAW,GAAG,MAAKrB,KAAM;OAC7C;GACL,MAAM,OAAO,MAAKO,gBAAiB,MAAM,OAAO,MAAKP,KAAM,CAAC;AAC5D,WAAQ,SAAS,OAAO,OAAO,OAAO,MAAKA,KAAM;;AAEnD,MAAI,UAAU,KAAM;AACpB,MAAI,QAAQ,EAAG,SAAQ;AAEvB,MAAI,QAAQ,gBAAgB;GAG1B,MAAMsB,WAAS,MAAKrB,iBAAkB;AACpC,IAAK,MAAKH,MACP,IAAI,GAAG,CACP,MAAM,MAAM;AACX,SAAI,MAAM,KAAM,OAAKa,SAAU,IAAI,EAAE;MACrC,CACD,YAAY,GAAG;MACjB,eAAe;AAClB,SAAKG,QAAS,IAAI,IAAIQ,SAAO;AAC7B;;EAGF,MAAM,SAAS,MAAKrB,iBAAkB;AACpC,GAAK,KAAK,KAAK,GAAG;KACjB,MAAM;AACT,QAAKa,QAAS,IAAI,IAAI,OAAO;;CAG/B,cAAc,IAAkB;EAC9B,MAAM,SAAS,MAAKA,QAAS,IAAI,GAAG;AACpC,MAAI,WAAW,QAAW;AACxB,SAAKZ,aAAc,OAAO;AAC1B,SAAKY,QAAS,OAAO,GAAG"}
1
+ {"version":3,"file":"index.js","names":["#store","#mode","#now","#setTimeout","#clearTimeout","#lastActivity","#publish","#parsedCron","#declarations","#computeNextFire","state: TriggerState","#applyCatchup","#started","#schedule","#cancelHandle","#disposed","#handles","#closedEvents","#eventResolvers","updated: TriggerState","updated","#eventQueue","event","delay: number | null","handle"],"sources":["../src/index.ts"],"sourcesContent":["/**\n * @graphorin/triggers - durable cron / interval / idle / event\n * trigger scheduler for the Graphorin framework.\n *\n * @packageDocumentation\n */\n\nimport type { TriggerState, TriggerStore } from '@graphorin/core/contracts';\nimport { nextFireAfter, type ParsedCron, parseCron } from './cron.js';\n\nexport { CronParseError, nextFireAfter, type ParsedCron, parseCron } from './cron.js';\n\n/** Canonical version constant. Mirrors the `package.json` version. */\nexport const VERSION = '0.6.0';\n\n/**\n * Catch-up policy applied when a trigger missed one or more fires\n * while the scheduler was offline.\n *\n * - `'none'` - drop missed fires (default; safest for personal-assistant scenarios).\n * - `'last'` - fire once on resume (best for cron-style daily jobs).\n * - `'all'` - fire each missed run up to `maxCatchupRuns` within `catchupWindowMs`.\n *\n * @stable\n */\nexport type CatchupPolicy = 'none' | 'last' | 'all';\n\n/** @stable */\nexport type TriggerKind = 'cron' | 'interval' | 'idle' | 'event';\n\n/** @stable */\nexport interface TriggerOptions {\n readonly catchupPolicy?: CatchupPolicy;\n readonly maxCatchupRuns?: number;\n /**\n * How far back (from the last successful fire) misses are honored.\n * Catch-up counts REAL missed fires (RP-12), so the window must\n * exceed the trigger period - a 24h window can never catch up a\n * daily cron whose boundary is itself 24h after the last fire.\n * Default 24h.\n */\n readonly catchupWindowMs?: number;\n readonly tags?: ReadonlyArray<string>;\n /**\n * Suppress the one-time per-process library-mode WARN. Library\n * callers acknowledging that triggers fire only as long as the\n * process lives pass `true` here.\n */\n readonly acknowledgeLibMode?: boolean;\n}\n\n/**\n * Trigger callback. Receives an optional `payload` for `event`\n * triggers; for cron / interval / idle triggers `payload` is\n * `undefined`.\n *\n * @stable\n */\nexport type TriggerCallback = (payload?: unknown) => void | Promise<void>;\n\n/**\n * Public trigger declaration emitted by the helper functions\n * (`cron(...)`, `interval(...)`, `idle(...)`, `event(...)`).\n *\n * @stable\n */\nexport interface TriggerDeclaration {\n readonly id: string;\n readonly kind: TriggerKind;\n readonly spec: string;\n readonly callback: TriggerCallback;\n readonly options: TriggerOptions;\n}\n\n/**\n * Build a cron trigger declaration. The expression is validated\n * eagerly - a malformed cron expression throws at registration time,\n * not at first fire.\n *\n * @stable\n */\nexport function cron(\n id: string,\n expression: string,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n parseCron(expression);\n return { id, kind: 'cron', spec: expression, callback, options };\n}\n\n/** @stable */\nexport function interval(\n id: string,\n intervalMs: number,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n if (!Number.isFinite(intervalMs) || intervalMs <= 0) {\n throw new Error(`[graphorin/triggers] interval(${id}): intervalMs must be > 0`);\n }\n return {\n id,\n kind: 'interval',\n spec: String(intervalMs),\n callback,\n options,\n };\n}\n\n/** @stable */\nexport function idle(\n id: string,\n idleMs: number,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n if (!Number.isFinite(idleMs) || idleMs <= 0) {\n throw new Error(`[graphorin/triggers] idle(${id}): idleMs must be > 0`);\n }\n return {\n id,\n kind: 'idle',\n spec: String(idleMs),\n callback,\n options,\n };\n}\n\n/** @stable */\nexport function event(\n id: string,\n eventName: string,\n callback: TriggerCallback,\n options: TriggerOptions = {},\n): TriggerDeclaration {\n if (eventName.length === 0) {\n throw new Error(`[graphorin/triggers] event(${id}): eventName must not be empty`);\n }\n return {\n id,\n kind: 'event',\n spec: eventName,\n callback,\n options,\n };\n}\n\n/**\n * Lifecycle event emitted by {@link Scheduler.events}. Useful for\n * tests and for piping into observability without monkey-patching.\n *\n * @stable\n */\nexport type SchedulerEvent =\n | { readonly type: 'started' }\n | { readonly type: 'stopped' }\n | { readonly type: 'registered'; readonly id: string; readonly kind: TriggerKind }\n | { readonly type: 'fire-start'; readonly id: string; readonly firedAt: number }\n | { readonly type: 'fire-end'; readonly id: string; readonly durationMs: number }\n | {\n readonly type: 'fire-error';\n readonly id: string;\n readonly error: unknown;\n readonly durationMs: number;\n }\n | { readonly type: 'catchup-applied'; readonly id: string; readonly missed: number }\n | { readonly type: 'lib-mode-warning'; readonly id: string };\n\n/**\n * Options for {@link createScheduler}.\n *\n * @stable\n */\nexport interface CreateSchedulerOptions {\n readonly store: TriggerStore;\n /** Default `'lib'`. Server mode skips the lib-mode warning. */\n readonly mode?: 'lib' | 'server';\n /** Override the wall clock - used by tests. */\n readonly now?: () => number;\n /**\n * Override `setTimeout`. The callback receives the chosen delay in\n * milliseconds; the return value is the handle the scheduler\n * later passes to `clearTimeout`. Tests inject a controllable timer.\n */\n readonly setTimeout?: (cb: () => void, ms: number) => unknown;\n readonly clearTimeout?: (handle: unknown) => void;\n /**\n * Resets the per-process WARN-once flag. Used by the test suite to\n * verify the warning fires exactly once per run.\n *\n * @internal\n */\n readonly _resetLibModeFlag?: boolean;\n}\n\n/**\n * Public Scheduler surface.\n *\n * @stable\n */\nexport interface Scheduler {\n register(declaration: TriggerDeclaration): Promise<TriggerState>;\n unregister(id: string): Promise<void>;\n list(): Promise<readonly TriggerState[]>;\n start(): Promise<void>;\n stop(): Promise<void>;\n /** Emit `eventName` to every registered `event` trigger. */\n emit(eventName: string, payload?: unknown): Promise<void>;\n /** Manually fire `id` (used by `graphorin triggers fire`, Phase 15). */\n fire(id: string, payload?: unknown): Promise<void>;\n /**\n * Flip the persistent `disabled` flag (IP-17). Disabling cancels the\n * armed timer but keeps the trigger registered + persisted; enabling\n * recomputes the next fire from now and re-arms. The destructive\n * removal is `unregister(...)`.\n */\n setDisabled(id: string, disabled: boolean): Promise<TriggerState>;\n /** AsyncIterable lifecycle event stream. */\n events(): AsyncIterable<SchedulerEvent>;\n /** Notify the scheduler that the user / runtime is no longer idle. */\n recordActivity(): void;\n}\n\n/** @stable */\nexport function createScheduler(options: CreateSchedulerOptions): Scheduler {\n return new SchedulerImpl(options);\n}\n\n/** Module-scoped flag - one WARN per process. */\nlet LIB_MODE_WARNED = false;\n\n/**\n * Test-only helper. Drops the per-process WARN-once flag so the next\n * `register(...)` call in lib mode emits the warning again.\n *\n * @internal\n */\nexport function _resetLibModeWarningForTesting(): void {\n LIB_MODE_WARNED = false;\n}\n\nconst DEFAULT_CATCHUP_WINDOW_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Node clamps `setTimeout` delays beyond `2^31 - 1` ms to 1 ms - a\n * quarterly cron would fire immediately in a hot loop (RP-15). Delays\n * above this arm an intermediate wake-up that re-schedules instead.\n */\nconst MAX_TIMEOUT_MS = 2 ** 31 - 1;\n\nclass SchedulerImpl implements Scheduler {\n readonly #store: TriggerStore;\n readonly #mode: 'lib' | 'server';\n readonly #now: () => number;\n readonly #setTimeout: (cb: () => void, ms: number) => unknown;\n readonly #clearTimeout: (handle: unknown) => void;\n #started = false;\n #disposed = false;\n #lastActivity: number;\n #handles: Map<string, unknown> = new Map();\n #parsedCron: Map<string, ParsedCron> = new Map();\n #declarations: Map<string, TriggerDeclaration> = new Map();\n #eventQueue: SchedulerEvent[] = [];\n #eventResolvers: Array<(value: IteratorResult<SchedulerEvent>) => void> = [];\n #closedEvents = false;\n\n constructor(options: CreateSchedulerOptions) {\n this.#store = options.store;\n this.#mode = options.mode ?? 'lib';\n this.#now = options.now ?? Date.now;\n this.#setTimeout = options.setTimeout ?? ((cb, ms) => globalThis.setTimeout(cb, ms));\n this.#clearTimeout =\n options.clearTimeout ?? ((h) => globalThis.clearTimeout(h as ReturnType<typeof setTimeout>));\n this.#lastActivity = this.#now();\n if (options._resetLibModeFlag) LIB_MODE_WARNED = false;\n }\n\n async register(declaration: TriggerDeclaration): Promise<TriggerState> {\n if (this.#mode === 'lib' && declaration.options.acknowledgeLibMode !== true) {\n if (!LIB_MODE_WARNED) {\n LIB_MODE_WARNED = true;\n console.warn(\n `[graphorin/triggers] running in library mode - triggers fire only while the parent process is alive. ` +\n `Pass { acknowledgeLibMode: true } to suppress this warning.`,\n );\n this.#publish({ type: 'lib-mode-warning', id: declaration.id });\n }\n }\n\n if (declaration.kind === 'cron') {\n this.#parsedCron.set(declaration.id, parseCron(declaration.spec));\n }\n this.#declarations.set(declaration.id, declaration);\n\n const now = this.#now();\n const existing = await this.#store.get(declaration.id);\n const nextFireMs = this.#computeNextFire(declaration, existing, now);\n const state: TriggerState = {\n id: declaration.id,\n kind: declaration.kind,\n spec: declaration.spec,\n callbackRef: declaration.id,\n ...(existing?.lastFiredAt !== undefined ? { lastFiredAt: existing.lastFiredAt } : {}),\n ...(nextFireMs !== null ? { nextFireAt: new Date(nextFireMs).toISOString() } : {}),\n missedFires: 0,\n disabled: existing?.disabled === true,\n catchupPolicy: declaration.options.catchupPolicy ?? 'none',\n maxCatchupRuns: declaration.options.maxCatchupRuns ?? 1,\n catchupWindowMs: declaration.options.catchupWindowMs ?? DEFAULT_CATCHUP_WINDOW_MS,\n ...(declaration.options.tags !== undefined ? { tags: declaration.options.tags } : {}),\n createdAt: existing !== null ? existing.createdAt : new Date(now).toISOString(),\n ...(existing !== null ? { updatedAt: new Date(now).toISOString() } : {}),\n };\n await this.#store.upsert(state);\n\n this.#publish({ type: 'registered', id: declaration.id, kind: declaration.kind });\n\n if (existing !== null && (declaration.kind === 'cron' || declaration.kind === 'interval')) {\n await this.#applyCatchup(state, existing, now);\n }\n\n if (this.#started) this.#schedule(declaration.id, state);\n return state;\n }\n\n async unregister(id: string): Promise<void> {\n this.#cancelHandle(id);\n this.#declarations.delete(id);\n this.#parsedCron.delete(id);\n await this.#store.remove(id);\n }\n\n async list(): Promise<readonly TriggerState[]> {\n return this.#store.list();\n }\n\n async start(): Promise<void> {\n if (this.#started || this.#disposed) return;\n this.#started = true;\n this.#publish({ type: 'started' });\n const states = await this.#store.list();\n for (const state of states) {\n if (state.disabled) continue;\n this.#schedule(state.id, state);\n }\n }\n\n async stop(): Promise<void> {\n if (!this.#started) return;\n this.#started = false;\n for (const id of [...this.#handles.keys()]) this.#cancelHandle(id);\n this.#publish({ type: 'stopped' });\n this.#closedEvents = true;\n while (this.#eventResolvers.length > 0) {\n const resolver = this.#eventResolvers.shift();\n resolver?.({ done: true, value: undefined });\n }\n }\n\n async emit(eventName: string, payload?: unknown): Promise<void> {\n for (const decl of this.#declarations.values()) {\n if (decl.kind === 'event' && decl.spec === eventName) {\n await this.fire(decl.id, payload);\n }\n }\n }\n\n async fire(id: string, payload?: unknown): Promise<void> {\n const decl = this.#declarations.get(id);\n if (decl === undefined) {\n throw new Error(`[graphorin/triggers] no trigger registered with id '${id}'`);\n }\n const firedAt = this.#now();\n this.#publish({ type: 'fire-start', id, firedAt });\n const start = firedAt;\n try {\n await decl.callback(payload);\n const durationMs = this.#now() - start;\n this.#publish({ type: 'fire-end', id, durationMs });\n const state = await this.#store.get(id);\n // RP-13: compute the next fire from THIS fire's timestamp - the\n // persisted state still carries the PREVIOUS lastFiredAt, which\n // for interval triggers yields `prev + interval ≈ now` and an\n // immediate duplicate via the clamped-to-0 delay. Idle triggers\n // never self-reschedule: their next window starts only when\n // `recordActivity()` observes new activity (a fire-driven re-arm\n // would compute a stale `lastActivity + idleMs`, clamp to 0 and\n // refire in a loop).\n const nextFireMs =\n state !== null && decl.kind !== 'idle'\n ? this.#computeNextFire(\n decl,\n { ...state, lastFiredAt: new Date(firedAt).toISOString() },\n this.#now(),\n )\n : null;\n await this.#store.recordFire(\n id,\n new Date(firedAt).toISOString(),\n nextFireMs !== null ? new Date(nextFireMs).toISOString() : undefined,\n );\n if (this.#started && decl.kind !== 'idle') {\n const refreshed = await this.#store.get(id);\n if (refreshed !== null) this.#schedule(id, refreshed);\n }\n } catch (err) {\n const durationMs = this.#now() - start;\n this.#publish({ type: 'fire-error', id, error: err, durationMs });\n // RP-14: the one-shot timer is consumed by this fire - recompute\n // and persist the next fire WITHOUT recording a successful fire\n // (lastFiredAt stays put), and re-arm, so a single callback\n // failure cannot permanently silence a daily cron.\n try {\n const state = await this.#store.get(id);\n if (state !== null && decl.kind !== 'idle') {\n const nextFireMs = this.#computeNextFire(\n decl,\n { ...state, lastFiredAt: new Date(firedAt).toISOString() },\n this.#now(),\n );\n const updated: TriggerState = {\n ...state,\n ...(nextFireMs !== null ? { nextFireAt: new Date(nextFireMs).toISOString() } : {}),\n updatedAt: new Date(this.#now()).toISOString(),\n };\n await this.#store.upsert(updated);\n if (this.#started) this.#schedule(id, updated);\n }\n } catch {\n // Best-effort re-arm - a store failure here must not become an\n // unhandled rejection out of the timer callback.\n }\n }\n }\n\n async setDisabled(id: string, disabled: boolean): Promise<TriggerState> {\n const state = await this.#store.get(id);\n if (state === null) {\n throw new Error(`[graphorin/triggers] no trigger registered with id '${id}'`);\n }\n const nowIso = new Date(this.#now()).toISOString();\n if (disabled) {\n this.#cancelHandle(id);\n const updated: TriggerState = { ...state, disabled: true, updatedAt: nowIso };\n await this.#store.upsert(updated);\n return updated;\n }\n // Re-enable: recompute the next fire from NOW (a stale persisted\n // nextFireAt from before the disable would otherwise clamp to 0 and\n // fire immediately), then re-arm.\n const decl = this.#declarations.get(id);\n const nextMs =\n decl !== undefined\n ? this.#computeNextFire(decl, { ...state, lastFiredAt: nowIso }, this.#now())\n : null;\n const updated: TriggerState = {\n ...state,\n disabled: false,\n ...(nextMs !== null ? { nextFireAt: new Date(nextMs).toISOString() } : {}),\n updatedAt: nowIso,\n };\n await this.#store.upsert(updated);\n if (this.#started) this.#schedule(id, updated);\n return updated;\n }\n\n recordActivity(): void {\n this.#lastActivity = this.#now();\n // periphery (P-14): never (re)arm idle timers on a scheduler that\n // is not started - a stopped scheduler firing callbacks is a\n // lifecycle violation. The activity timestamp is still recorded so\n // a later start() computes the idle window correctly.\n if (!this.#started) return;\n // Reschedule idle triggers so they treat \"now\" as the start of\n // their idle window.\n for (const decl of this.#declarations.values()) {\n if (decl.kind !== 'idle') continue;\n this.#cancelHandle(decl.id);\n const idleMs = Number.parseInt(decl.spec, 10);\n const handle = this.#setTimeout(() => {\n void this.fire(decl.id);\n }, idleMs);\n this.#handles.set(decl.id, handle);\n }\n }\n\n events(): AsyncIterable<SchedulerEvent> {\n const queue = this.#eventQueue;\n const resolvers = this.#eventResolvers;\n const me = this;\n return {\n [Symbol.asyncIterator](): AsyncIterator<SchedulerEvent> {\n return {\n async next(): Promise<IteratorResult<SchedulerEvent>> {\n if (queue.length > 0) {\n const value = queue.shift() as SchedulerEvent;\n return { done: false, value };\n }\n if (me.#closedEvents) return { done: true, value: undefined };\n return new Promise((resolve) => resolvers.push(resolve));\n },\n async return(): Promise<IteratorResult<SchedulerEvent>> {\n return { done: true, value: undefined };\n },\n };\n },\n };\n }\n\n // ---------------------------------------------------------------------------\n\n #publish(event: SchedulerEvent): void {\n if (this.#closedEvents) return;\n if (this.#eventResolvers.length > 0) {\n const resolver = this.#eventResolvers.shift();\n resolver?.({ done: false, value: event });\n return;\n }\n this.#eventQueue.push(event);\n }\n\n #computeNextFire(\n decl: TriggerDeclaration,\n state: TriggerState | null,\n now: number,\n ): number | null {\n switch (decl.kind) {\n case 'cron': {\n const parsed = this.#parsedCron.get(decl.id) ?? parseCron(decl.spec);\n const next = nextFireAfter(parsed, new Date(now));\n return next === null ? null : next.getTime();\n }\n case 'interval': {\n const intervalMs = Number.parseInt(decl.spec, 10);\n const last = state?.lastFiredAt !== undefined ? Date.parse(state.lastFiredAt) : now;\n const next = last + intervalMs;\n // periphery-11: after downtime `last + interval` is in the past;\n // the schedule clamp turned that into an IMMEDIATE fire even\n // under `catchupPolicy: 'none'` (\"drop missed fires\"). Advance\n // to the next FUTURE boundary on the original cadence - missed\n // boundaries are the catch-up machinery's business, not the\n // base schedule's. (Cron already behaves this way:\n // `nextFireAfter(now)` is always future.)\n if (next <= now && Number.isFinite(intervalMs) && intervalMs > 0) {\n const missedBoundaries = Math.floor((now - last) / intervalMs) + 1;\n return last + missedBoundaries * intervalMs;\n }\n return next;\n }\n case 'idle': {\n const idleMs = Number.parseInt(decl.spec, 10);\n return this.#lastActivity + idleMs;\n }\n case 'event':\n return null;\n }\n }\n\n async #applyCatchup(state: TriggerState, existing: TriggerState, now: number): Promise<void> {\n if (state.catchupPolicy === 'none') return;\n const decl = this.#declarations.get(state.id);\n if (decl === undefined || (decl.kind !== 'cron' && decl.kind !== 'interval')) return;\n const lastFired = existing.lastFiredAt !== undefined ? Date.parse(existing.lastFiredAt) : null;\n if (lastFired === null) return;\n if (lastFired < now - state.catchupWindowMs) return;\n\n // RP-12: count the REAL missed fires by walking the schedule from\n // the last successful fire to now - a restart with zero crossed\n // boundaries must apply zero catch-up (the old code fired `1` /\n // `maxCatchupRuns` times unconditionally).\n const SCAN_CAP = 1000;\n let missed = 0;\n if (decl.kind === 'cron') {\n const parsed = this.#parsedCron.get(decl.id) ?? parseCron(decl.spec);\n let cursor = lastFired;\n while (missed < SCAN_CAP) {\n const next = nextFireAfter(parsed, new Date(cursor));\n if (next === null || next.getTime() > now) break;\n cursor = next.getTime();\n missed += 1;\n }\n } else {\n const intervalMs = Number.parseInt(decl.spec, 10);\n missed = Math.floor((now - lastFired) / intervalMs);\n }\n if (missed <= 0) return;\n\n const toFire = state.catchupPolicy === 'last' ? 1 : Math.min(missed, state.maxCatchupRuns);\n for (let i = 0; i < toFire; i++) {\n await this.fire(state.id);\n }\n // The overflow beyond what we re-ran is recorded for health/CLI.\n const excess = missed - toFire;\n if (excess > 0) {\n const refreshed = await this.#store.get(state.id);\n if (refreshed !== null) {\n await this.#store.upsert({ ...refreshed, missedFires: excess });\n }\n }\n this.#publish({ type: 'catchup-applied', id: state.id, missed });\n }\n\n #schedule(id: string, state: TriggerState): void {\n this.#cancelHandle(id);\n if (state.disabled) return;\n const decl = this.#declarations.get(id);\n if (decl === undefined) return;\n if (decl.kind === 'event') return;\n\n let delay: number | null = null;\n if (state.nextFireAt !== undefined) {\n delay = Date.parse(state.nextFireAt) - this.#now();\n } else {\n const next = this.#computeNextFire(decl, state, this.#now());\n delay = next === null ? null : next - this.#now();\n }\n if (delay === null) return;\n if (delay < 0) delay = 0;\n\n if (delay > MAX_TIMEOUT_MS) {\n // RP-15: chunk the wait - the intermediate wake-up re-reads the\n // freshest state and re-schedules (it never fires the callback).\n const handle = this.#setTimeout(() => {\n void this.#store\n .get(id)\n .then((s) => {\n if (s !== null) this.#schedule(id, s);\n })\n .catch(() => {});\n }, MAX_TIMEOUT_MS);\n this.#handles.set(id, handle);\n return;\n }\n\n const handle = this.#setTimeout(() => {\n void this.fire(id);\n }, delay);\n this.#handles.set(id, handle);\n }\n\n #cancelHandle(id: string): void {\n const handle = this.#handles.get(id);\n if (handle !== undefined) {\n this.#clearTimeout(handle);\n this.#handles.delete(id);\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphorin/triggers",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Background-task scheduler for the Graphorin framework. Ships durable cron / interval / idle / event triggers with the same code path in library and server modes, an in-tree 5-field cron parser, per-trigger catch-up policies (`'none'` default; `'last'` or `'all'` opt-in), and an AsyncIterable lifecycle event stream so consumers can plug into observability without monkey-patching.",
5
5
  "license": "MIT",
6
6
  "author": "Oleksiy Stepurenko",
@@ -49,14 +49,14 @@
49
49
  "LICENSE"
50
50
  ],
51
51
  "dependencies": {
52
- "@graphorin/core": "0.5.0"
52
+ "@graphorin/core": "0.6.0"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public",
56
56
  "provenance": true
57
57
  },
58
58
  "devDependencies": {
59
- "@graphorin/store-sqlite": "0.5.0"
59
+ "@graphorin/store-sqlite": "0.6.0"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsdown",