@graphorin/triggers 0.5.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 ADDED
@@ -0,0 +1,10 @@
1
+ # @graphorin/triggers
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Initial release. Background-task scheduler for the Graphorin
8
+ framework — durable cron / interval / idle / event triggers, an
9
+ in-tree 5-field cron parser, per-trigger catch-up policies, and a
10
+ shared code path for library and server modes.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleksiy Stepurenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # @graphorin/triggers
2
+
3
+ > Background-task scheduler for the Graphorin framework.
4
+
5
+ `@graphorin/triggers` ships durable cron / interval / idle / event
6
+ triggers with the same code path in library and server modes. The
7
+ package owns:
8
+
9
+ - An in-tree 5-field **cron parser** (`* * * * *`, ranges, lists,
10
+ steps; no third-party dependency).
11
+ - A `Scheduler` runtime — a process-bound loop that fires registered
12
+ triggers and persists their state via the `TriggerStore` contract
13
+ from `@graphorin/core/contracts`.
14
+ - Per-trigger **catch-up policies** (`'none'` default, `'last'`,
15
+ `'all'`) plus `maxCatchupRuns` and `catchupWindowMs` knobs.
16
+ - An **AsyncIterable lifecycle event stream** (`scheduler.events()`)
17
+ so observability and tests can subscribe without monkey-patching.
18
+ - A one-time per-process **library-mode WARN** (`acknowledgeLibMode`
19
+ to suppress) reminding library callers that triggers fire as long
20
+ as the parent process lives.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pnpm add @graphorin/triggers @graphorin/store-sqlite
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```ts
31
+ import { createScheduler, cron, interval, idle, event } from '@graphorin/triggers';
32
+ import { createSqliteStore } from '@graphorin/store-sqlite';
33
+
34
+ const store = await createSqliteStore({ path: './assistant.db' });
35
+ await store.init();
36
+
37
+ const scheduler = createScheduler({ store: store.triggers });
38
+
39
+ scheduler.register(
40
+ cron('daily-digest', '0 9 * * *', async () => {
41
+ // …compose and send a daily digest email
42
+ }, { catchupPolicy: 'last' }),
43
+ );
44
+
45
+ scheduler.register(
46
+ interval('poll-mailbox', 60_000, async () => {
47
+ // …poll IMAP every minute
48
+ }),
49
+ );
50
+
51
+ scheduler.register(
52
+ idle('flush-buffer', 5 * 60_000, async () => {
53
+ // …fires after the user has been idle for 5 minutes
54
+ }),
55
+ );
56
+
57
+ scheduler.register(
58
+ event('on-handoff', 'session.handoff', async (payload) => {
59
+ // …fires whenever a handoff event is published
60
+ }),
61
+ );
62
+
63
+ await scheduler.start();
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT © 2026 Oleksiy Stepurenko.
69
+
70
+ ---
71
+
72
+ **Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
package/dist/cron.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ //#region src/cron.d.ts
2
+ /**
3
+ * Tiny in-tree 5-field cron parser used by `@graphorin/triggers`.
4
+ *
5
+ * Supported syntax:
6
+ *
7
+ * minute (0-59)
8
+ * hour (0-23)
9
+ * day (1-31)
10
+ * month (1-12)
11
+ * dayOfWeek (0-6; Sunday = 0)
12
+ *
13
+ * Each field accepts:
14
+ * - star (asterisk) for every value
15
+ * - a single number `5`
16
+ * - a comma list `1,2,5`
17
+ * - a range `1-4`
18
+ * - a step expression with the slash operator (e.g. every-5 minutes,
19
+ * `0-30/10` for "0,10,20,30 minutes")
20
+ *
21
+ * The parser is intentionally **strict**: any unrecognised character
22
+ * raises {@link CronParseError} so a typo never silently never-fires.
23
+ *
24
+ * **Day vs. day-of-week semantics — AND, not OR.** When *both* `day`
25
+ * and `dayOfWeek` are restricted (neither is the every-value
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
28
+ * every month". This differs from Vixie / POSIX cron, which
29
+ * OR-combines the two restricted fields. AND semantics are easier to
30
+ * reason about in personal-assistant scenarios; the framework stays
31
+ * consistent with this rule rather than mixing the two conventions.
32
+ *
33
+ * The scheduler treats every trigger as **UTC**. Operators that need
34
+ * a local-time fire encode the offset directly into their cron
35
+ * expression (e.g. `0 14 * * *` for "9am Eastern in winter").
36
+ *
37
+ * @packageDocumentation
38
+ */
39
+ /** @stable */
40
+ declare class CronParseError extends Error {
41
+ readonly expression: string;
42
+ readonly name = "CronParseError";
43
+ constructor(expression: string, message: string);
44
+ }
45
+ /** @stable */
46
+ interface ParsedCron {
47
+ readonly expression: string;
48
+ readonly minute: ReadonlySet<number>;
49
+ readonly hour: ReadonlySet<number>;
50
+ readonly day: ReadonlySet<number>;
51
+ readonly month: ReadonlySet<number>;
52
+ readonly dayOfWeek: ReadonlySet<number>;
53
+ }
54
+ /**
55
+ * Parse a 5-field cron expression. Throws {@link CronParseError} on
56
+ * any malformed input.
57
+ *
58
+ * @stable
59
+ */
60
+ declare function parseCron(expression: string): ParsedCron;
61
+ /**
62
+ * Compute the next fire time strictly after `from` for the supplied
63
+ * cron schedule. Returns a UTC `Date` (the scheduler treats every
64
+ * trigger as UTC; operators that need local time express that in
65
+ * their cron expression).
66
+ *
67
+ * Returns `null` if no fire happens in the next 4 years (defensive —
68
+ * impossible for a well-formed cron expression except a vacuous
69
+ * combination that never aligns).
70
+ *
71
+ * @stable
72
+ */
73
+ declare function nextFireAfter(parsed: ParsedCron, from: Date): Date | null;
74
+ //#endregion
75
+ export { CronParseError, ParsedCron, nextFireAfter, parseCron };
76
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","names":[],"sources":["../src/cron.ts"],"sourcesContent":[],"mappings":";;AAuCA;AAWA;;;;;;;AA4BA;AAoGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA3Ia,cAAA,SAAuB,KAAA;;;;;;UAWnB,UAAA;;mBAEE;iBACF;gBACD;kBACE;sBACI;;;;;;;;iBAsBN,SAAA,sBAA+B;;;;;;;;;;;;;iBAoG/B,aAAA,SAAsB,kBAAkB,OAAO"}
package/dist/cron.js ADDED
@@ -0,0 +1,174 @@
1
+ //#region src/cron.ts
2
+ /**
3
+ * Tiny in-tree 5-field cron parser used by `@graphorin/triggers`.
4
+ *
5
+ * Supported syntax:
6
+ *
7
+ * minute (0-59)
8
+ * hour (0-23)
9
+ * day (1-31)
10
+ * month (1-12)
11
+ * dayOfWeek (0-6; Sunday = 0)
12
+ *
13
+ * Each field accepts:
14
+ * - star (asterisk) for every value
15
+ * - a single number `5`
16
+ * - a comma list `1,2,5`
17
+ * - a range `1-4`
18
+ * - a step expression with the slash operator (e.g. every-5 minutes,
19
+ * `0-30/10` for "0,10,20,30 minutes")
20
+ *
21
+ * The parser is intentionally **strict**: any unrecognised character
22
+ * raises {@link CronParseError} so a typo never silently never-fires.
23
+ *
24
+ * **Day vs. day-of-week semantics — AND, not OR.** When *both* `day`
25
+ * and `dayOfWeek` are restricted (neither is the every-value
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
28
+ * every month". This differs from Vixie / POSIX cron, which
29
+ * OR-combines the two restricted fields. AND semantics are easier to
30
+ * reason about in personal-assistant scenarios; the framework stays
31
+ * consistent with this rule rather than mixing the two conventions.
32
+ *
33
+ * The scheduler treats every trigger as **UTC**. Operators that need
34
+ * a local-time fire encode the offset directly into their cron
35
+ * expression (e.g. `0 14 * * *` for "9am Eastern in winter").
36
+ *
37
+ * @packageDocumentation
38
+ */
39
+ /** @stable */
40
+ var CronParseError = class extends Error {
41
+ name = "CronParseError";
42
+ constructor(expression, message) {
43
+ super(`[graphorin/triggers] cron expression '${expression}': ${message}`);
44
+ this.expression = expression;
45
+ }
46
+ };
47
+ const RANGES = {
48
+ minute: {
49
+ min: 0,
50
+ max: 59
51
+ },
52
+ hour: {
53
+ min: 0,
54
+ max: 23
55
+ },
56
+ day: {
57
+ min: 1,
58
+ max: 31
59
+ },
60
+ month: {
61
+ min: 1,
62
+ max: 12
63
+ },
64
+ dayOfWeek: {
65
+ min: 0,
66
+ max: 6
67
+ }
68
+ };
69
+ /**
70
+ * Parse a 5-field cron expression. Throws {@link CronParseError} on
71
+ * any malformed input.
72
+ *
73
+ * @stable
74
+ */
75
+ function parseCron(expression) {
76
+ const trimmed = expression.trim();
77
+ if (trimmed.length === 0) throw new CronParseError(expression, "expression is empty");
78
+ const fields = trimmed.split(/\s+/);
79
+ if (fields.length !== 5) throw new CronParseError(expression, `expected 5 whitespace-separated fields (minute hour day month dayOfWeek), got ${fields.length}`);
80
+ const [minuteStr, hourStr, dayStr, monthStr, dowStr] = fields;
81
+ return {
82
+ expression: trimmed,
83
+ minute: parseField("minute", minuteStr, RANGES.minute, expression),
84
+ hour: parseField("hour", hourStr, RANGES.hour, expression),
85
+ day: parseField("day", dayStr, RANGES.day, expression),
86
+ month: parseField("month", monthStr, RANGES.month, expression),
87
+ dayOfWeek: parseField("dayOfWeek", dowStr, RANGES.dayOfWeek, expression)
88
+ };
89
+ }
90
+ function parseField(name, raw, range, expression) {
91
+ const out = /* @__PURE__ */ new Set();
92
+ for (const part of raw.split(",")) {
93
+ if (part.length === 0) throw new CronParseError(expression, `field '${name}' has an empty list element`);
94
+ let stepStr;
95
+ let bodyStr = part;
96
+ if (part.includes("/")) {
97
+ const [body, step$1] = part.split("/", 2);
98
+ bodyStr = body ?? "";
99
+ stepStr = step$1;
100
+ }
101
+ const step = stepStr === void 0 ? 1 : Number.parseInt(stepStr, 10);
102
+ if (!Number.isFinite(step) || step <= 0) throw new CronParseError(expression, `field '${name}' has invalid step '${stepStr}'`);
103
+ let from;
104
+ let to;
105
+ if (bodyStr === "*") {
106
+ from = range.min;
107
+ to = range.max;
108
+ } else if (bodyStr.includes("-")) {
109
+ const [a, b] = bodyStr.split("-", 2);
110
+ from = parseNumeric(name, a, expression);
111
+ to = parseNumeric(name, b, expression);
112
+ } else {
113
+ const single = parseNumeric(name, bodyStr, expression);
114
+ from = single;
115
+ to = stepStr === void 0 ? single : range.max;
116
+ }
117
+ if (from < range.min || to > range.max || from > to) throw new CronParseError(expression, `field '${name}' value out of range [${range.min}, ${range.max}]: ${bodyStr}`);
118
+ for (let v = from; v <= to; v += step) out.add(v);
119
+ }
120
+ return out;
121
+ }
122
+ function parseNumeric(name, raw, expression) {
123
+ if (raw === void 0 || raw.length === 0) throw new CronParseError(expression, `field '${name}' has empty numeric component`);
124
+ if (!/^\d+$/.test(raw)) throw new CronParseError(expression, `field '${name}' value '${raw}' is not numeric`);
125
+ return Number.parseInt(raw, 10);
126
+ }
127
+ /**
128
+ * Compute the next fire time strictly after `from` for the supplied
129
+ * cron schedule. Returns a UTC `Date` (the scheduler treats every
130
+ * trigger as UTC; operators that need local time express that in
131
+ * their cron expression).
132
+ *
133
+ * Returns `null` if no fire happens in the next 4 years (defensive —
134
+ * impossible for a well-formed cron expression except a vacuous
135
+ * combination that never aligns).
136
+ *
137
+ * @stable
138
+ */
139
+ function nextFireAfter(parsed, from) {
140
+ const start = new Date(from.getTime() + 6e4);
141
+ start.setUTCSeconds(0, 0);
142
+ const horizon = new Date(start.getTime() + 4 * 365 * 24 * 60 * 60 * 1e3);
143
+ let cursor = new Date(start.getTime());
144
+ while (cursor.getTime() < horizon.getTime()) {
145
+ const month = cursor.getUTCMonth() + 1;
146
+ if (!parsed.month.has(month)) {
147
+ cursor = new Date(Date.UTC(cursor.getUTCMonth() === 11 ? cursor.getUTCFullYear() + 1 : cursor.getUTCFullYear(), (cursor.getUTCMonth() + 1) % 12, 1, 0, 0, 0, 0));
148
+ continue;
149
+ }
150
+ const day = cursor.getUTCDate();
151
+ const dow = cursor.getUTCDay();
152
+ if (!parsed.day.has(day) || !parsed.dayOfWeek.has(dow)) {
153
+ cursor = new Date(cursor.getTime() + 1440 * 60 * 1e3);
154
+ cursor.setUTCHours(0, 0, 0, 0);
155
+ continue;
156
+ }
157
+ if (!parsed.hour.has(cursor.getUTCHours())) {
158
+ cursor = new Date(cursor.getTime() + 3600 * 1e3);
159
+ cursor.setUTCMinutes(0, 0, 0);
160
+ continue;
161
+ }
162
+ if (!parsed.minute.has(cursor.getUTCMinutes())) {
163
+ cursor = new Date(cursor.getTime() + 6e4);
164
+ cursor.setUTCSeconds(0, 0);
165
+ continue;
166
+ }
167
+ return cursor;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ //#endregion
173
+ export { CronParseError, nextFireAfter, parseCron };
174
+ //# sourceMappingURL=cron.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,175 @@
1
+ import { CronParseError, ParsedCron, nextFireAfter, parseCron } from "./cron.js";
2
+ import { TriggerState, TriggerStore } from "@graphorin/core/contracts";
3
+
4
+ //#region src/index.d.ts
5
+
6
+ /** Canonical version constant. Mirrors the `package.json` version. */
7
+ declare const VERSION = "0.5.0";
8
+ /**
9
+ * Catch-up policy applied when a trigger missed one or more fires
10
+ * while the scheduler was offline.
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`.
15
+ *
16
+ * @stable
17
+ */
18
+ type CatchupPolicy = 'none' | 'last' | 'all';
19
+ /** @stable */
20
+ type TriggerKind = 'cron' | 'interval' | 'idle' | 'event';
21
+ /** @stable */
22
+ interface TriggerOptions {
23
+ readonly catchupPolicy?: CatchupPolicy;
24
+ readonly maxCatchupRuns?: number;
25
+ /**
26
+ * How far back (from the last successful fire) misses are honored.
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
29
+ * daily cron whose boundary is itself 24h after the last fire.
30
+ * Default 24h.
31
+ */
32
+ readonly catchupWindowMs?: number;
33
+ readonly tags?: ReadonlyArray<string>;
34
+ /**
35
+ * Suppress the one-time per-process library-mode WARN. Library
36
+ * callers acknowledging that triggers fire only as long as the
37
+ * process lives pass `true` here.
38
+ */
39
+ readonly acknowledgeLibMode?: boolean;
40
+ }
41
+ /**
42
+ * Trigger callback. Receives an optional `payload` for `event`
43
+ * triggers; for cron / interval / idle triggers `payload` is
44
+ * `undefined`.
45
+ *
46
+ * @stable
47
+ */
48
+ type TriggerCallback = (payload?: unknown) => void | Promise<void>;
49
+ /**
50
+ * Public trigger declaration emitted by the helper functions
51
+ * (`cron(...)`, `interval(...)`, `idle(...)`, `event(...)`).
52
+ *
53
+ * @stable
54
+ */
55
+ interface TriggerDeclaration {
56
+ readonly id: string;
57
+ readonly kind: TriggerKind;
58
+ readonly spec: string;
59
+ readonly callback: TriggerCallback;
60
+ readonly options: TriggerOptions;
61
+ }
62
+ /**
63
+ * Build a cron trigger declaration. The expression is validated
64
+ * eagerly — a malformed cron expression throws at registration time,
65
+ * not at first fire.
66
+ *
67
+ * @stable
68
+ */
69
+ declare function cron(id: string, expression: string, callback: TriggerCallback, options?: TriggerOptions): TriggerDeclaration;
70
+ /** @stable */
71
+ declare function interval(id: string, intervalMs: number, callback: TriggerCallback, options?: TriggerOptions): TriggerDeclaration;
72
+ /** @stable */
73
+ declare function idle(id: string, idleMs: number, callback: TriggerCallback, options?: TriggerOptions): TriggerDeclaration;
74
+ /** @stable */
75
+ declare function event(id: string, eventName: string, callback: TriggerCallback, options?: TriggerOptions): TriggerDeclaration;
76
+ /**
77
+ * Lifecycle event emitted by {@link Scheduler.events}. Useful for
78
+ * tests and for piping into observability without monkey-patching.
79
+ *
80
+ * @stable
81
+ */
82
+ type SchedulerEvent = {
83
+ readonly type: 'started';
84
+ } | {
85
+ readonly type: 'stopped';
86
+ } | {
87
+ readonly type: 'registered';
88
+ readonly id: string;
89
+ readonly kind: TriggerKind;
90
+ } | {
91
+ readonly type: 'fire-start';
92
+ readonly id: string;
93
+ readonly firedAt: number;
94
+ } | {
95
+ readonly type: 'fire-end';
96
+ readonly id: string;
97
+ readonly durationMs: number;
98
+ } | {
99
+ readonly type: 'fire-error';
100
+ readonly id: string;
101
+ readonly error: unknown;
102
+ readonly durationMs: number;
103
+ } | {
104
+ readonly type: 'catchup-applied';
105
+ readonly id: string;
106
+ readonly missed: number;
107
+ } | {
108
+ readonly type: 'lib-mode-warning';
109
+ readonly id: string;
110
+ };
111
+ /**
112
+ * Options for {@link createScheduler}.
113
+ *
114
+ * @stable
115
+ */
116
+ interface CreateSchedulerOptions {
117
+ readonly store: TriggerStore;
118
+ /** Default `'lib'`. Server mode skips the lib-mode warning. */
119
+ readonly mode?: 'lib' | 'server';
120
+ /** Override the wall clock — used by tests. */
121
+ readonly now?: () => number;
122
+ /**
123
+ * Override `setTimeout`. The callback receives the chosen delay in
124
+ * milliseconds; the return value is the handle the scheduler
125
+ * later passes to `clearTimeout`. Tests inject a controllable timer.
126
+ */
127
+ readonly setTimeout?: (cb: () => void, ms: number) => unknown;
128
+ readonly clearTimeout?: (handle: unknown) => void;
129
+ /**
130
+ * Resets the per-process WARN-once flag. Used by the test suite to
131
+ * verify the warning fires exactly once per run.
132
+ *
133
+ * @internal
134
+ */
135
+ readonly _resetLibModeFlag?: boolean;
136
+ }
137
+ /**
138
+ * Public Scheduler surface.
139
+ *
140
+ * @stable
141
+ */
142
+ interface Scheduler {
143
+ register(declaration: TriggerDeclaration): Promise<TriggerState>;
144
+ unregister(id: string): Promise<void>;
145
+ list(): Promise<readonly TriggerState[]>;
146
+ start(): Promise<void>;
147
+ stop(): Promise<void>;
148
+ /** Emit `eventName` to every registered `event` trigger. */
149
+ emit(eventName: string, payload?: unknown): Promise<void>;
150
+ /** Manually fire `id` (used by `graphorin triggers fire`, Phase 15). */
151
+ fire(id: string, payload?: unknown): Promise<void>;
152
+ /**
153
+ * Flip the persistent `disabled` flag (IP-17). Disabling cancels the
154
+ * armed timer but keeps the trigger registered + persisted; enabling
155
+ * recomputes the next fire from now and re-arms. The destructive
156
+ * removal is `unregister(...)`.
157
+ */
158
+ setDisabled(id: string, disabled: boolean): Promise<TriggerState>;
159
+ /** AsyncIterable lifecycle event stream. */
160
+ events(): AsyncIterable<SchedulerEvent>;
161
+ /** Notify the scheduler that the user / runtime is no longer idle. */
162
+ recordActivity(): void;
163
+ }
164
+ /** @stable */
165
+ declare function createScheduler(options: CreateSchedulerOptions): Scheduler;
166
+ /**
167
+ * Test-only helper. Drops the per-process WARN-once flag so the next
168
+ * `register(...)` call in lib mode emits the warning again.
169
+ *
170
+ * @internal
171
+ */
172
+ declare function _resetLibModeWarningForTesting(): void;
173
+ //#endregion
174
+ export { CatchupPolicy, CreateSchedulerOptions, CronParseError, type ParsedCron, Scheduler, SchedulerEvent, TriggerCallback, TriggerDeclaration, TriggerKind, TriggerOptions, VERSION, _resetLibModeWarningForTesting, createScheduler, cron, event, idle, interval, nextFireAfter, parseCron };
175
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;AA0DA;AAQiB,cArDJ,OAAA,GAqDsB,OAAA;;;;;AAenC;;;;;AAWA;AAGY,KAtEA,aAAA,GAsEA,MAAA,GAAA,MAAA,GAAA,KAAA;;AAET,KArES,WAAA,GAqET,MAAA,GAAA,UAAA,GAAA,MAAA,GAAA,OAAA;;AAca,UAhFC,cAAA,CAgFG;EAGR,SAAA,aAAA,CAAA,EAlFe,aAkFf;EACD,SAAA,cAAA,CAAA,EAAA,MAAA;EACR;;AAcH;;;;;EAwBY,SAAA,eAAc,CAAA,EAAA,MAG6C;EAiBtD,SAAA,IAAA,CAAA,EApIC,aAoIqB,CAAA,MAAA,CACrB;EA0BD;;;;;EAGU,SAAA,kBAAA,CAAA,EAAA,OAAA;;;;;;;;;AAef,KAjKA,eAAA,GAiKA,CAAA,OAAA,CAAA,EAAA,OAAA,EAAA,GAAA,IAAA,GAjKgD,OAiKhD,CAAA,IAAA,CAAA;;AAMZ;AAaA;;;;UA5KiB,kBAAA;;iBAEA;;qBAEI;oBACD;;;;;;;;;iBAUJ,IAAA,2CAGJ,2BACD,iBACR;;iBAMa,QAAA,2CAGJ,2BACD,iBACR;;iBAca,IAAA,uCAGJ,2BACD,iBACR;;iBAca,KAAA,0CAGJ,2BACD,iBACR;;;;;;;KAmBS,cAAA;;;;;;;iBAG2D;;;;;;;;;;;;;;;;;;;;;;;;;;;UAiBtD,sBAAA;kBACC;;;;;;;;;;;;;;;;;;;;;;;;;UA0BD,SAAA;wBACO,qBAAqB,QAAQ;0BAC3B;UAChB,iBAAiB;WAChB;UACD;;8CAEoC;;uCAEP;;;;;;;8CAOO,QAAQ;;YAE1C,cAAc;;;;;iBAMV,eAAA,UAAyB,yBAAyB;;;;;;;iBAalD,8BAAA,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,404 @@
1
+ import { CronParseError, nextFireAfter, parseCron } from "./cron.js";
2
+
3
+ //#region src/index.ts
4
+ /** Canonical version constant. Mirrors the `package.json` version. */
5
+ const VERSION = "0.5.0";
6
+ /**
7
+ * Build a cron trigger declaration. The expression is validated
8
+ * eagerly — a malformed cron expression throws at registration time,
9
+ * not at first fire.
10
+ *
11
+ * @stable
12
+ */
13
+ function cron(id, expression, callback, options = {}) {
14
+ parseCron(expression);
15
+ return {
16
+ id,
17
+ kind: "cron",
18
+ spec: expression,
19
+ callback,
20
+ options
21
+ };
22
+ }
23
+ /** @stable */
24
+ function interval(id, intervalMs, callback, options = {}) {
25
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) throw new Error(`[graphorin/triggers] interval(${id}): intervalMs must be > 0`);
26
+ return {
27
+ id,
28
+ kind: "interval",
29
+ spec: String(intervalMs),
30
+ callback,
31
+ options
32
+ };
33
+ }
34
+ /** @stable */
35
+ function idle(id, idleMs, callback, options = {}) {
36
+ if (!Number.isFinite(idleMs) || idleMs <= 0) throw new Error(`[graphorin/triggers] idle(${id}): idleMs must be > 0`);
37
+ return {
38
+ id,
39
+ kind: "idle",
40
+ spec: String(idleMs),
41
+ callback,
42
+ options
43
+ };
44
+ }
45
+ /** @stable */
46
+ function event(id, eventName, callback, options = {}) {
47
+ if (eventName.length === 0) throw new Error(`[graphorin/triggers] event(${id}): eventName must not be empty`);
48
+ return {
49
+ id,
50
+ kind: "event",
51
+ spec: eventName,
52
+ callback,
53
+ options
54
+ };
55
+ }
56
+ /** @stable */
57
+ function createScheduler(options) {
58
+ return new SchedulerImpl(options);
59
+ }
60
+ /** Module-scoped flag — one WARN per process. */
61
+ let LIB_MODE_WARNED = false;
62
+ /**
63
+ * Test-only helper. Drops the per-process WARN-once flag so the next
64
+ * `register(...)` call in lib mode emits the warning again.
65
+ *
66
+ * @internal
67
+ */
68
+ function _resetLibModeWarningForTesting() {
69
+ LIB_MODE_WARNED = false;
70
+ }
71
+ const DEFAULT_CATCHUP_WINDOW_MS = 1440 * 60 * 1e3;
72
+ /**
73
+ * Node clamps `setTimeout` delays beyond `2^31 - 1` ms to 1 ms — a
74
+ * quarterly cron would fire immediately in a hot loop (RP-15). Delays
75
+ * above this arm an intermediate wake-up that re-schedules instead.
76
+ */
77
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
78
+ var SchedulerImpl = class {
79
+ #store;
80
+ #mode;
81
+ #now;
82
+ #setTimeout;
83
+ #clearTimeout;
84
+ #started = false;
85
+ #disposed = false;
86
+ #lastActivity;
87
+ #handles = /* @__PURE__ */ new Map();
88
+ #parsedCron = /* @__PURE__ */ new Map();
89
+ #declarations = /* @__PURE__ */ new Map();
90
+ #eventQueue = [];
91
+ #eventResolvers = [];
92
+ #closedEvents = false;
93
+ constructor(options) {
94
+ this.#store = options.store;
95
+ this.#mode = options.mode ?? "lib";
96
+ this.#now = options.now ?? Date.now;
97
+ this.#setTimeout = options.setTimeout ?? ((cb, ms) => globalThis.setTimeout(cb, ms));
98
+ this.#clearTimeout = options.clearTimeout ?? ((h) => globalThis.clearTimeout(h));
99
+ this.#lastActivity = this.#now();
100
+ if (options._resetLibModeFlag) LIB_MODE_WARNED = false;
101
+ }
102
+ async register(declaration) {
103
+ if (this.#mode === "lib" && declaration.options.acknowledgeLibMode !== true) {
104
+ if (!LIB_MODE_WARNED) {
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.");
107
+ this.#publish({
108
+ type: "lib-mode-warning",
109
+ id: declaration.id
110
+ });
111
+ }
112
+ }
113
+ if (declaration.kind === "cron") this.#parsedCron.set(declaration.id, parseCron(declaration.spec));
114
+ this.#declarations.set(declaration.id, declaration);
115
+ const now = this.#now();
116
+ const existing = await this.#store.get(declaration.id);
117
+ const nextFireMs = this.#computeNextFire(declaration, existing, now);
118
+ const state = {
119
+ id: declaration.id,
120
+ kind: declaration.kind,
121
+ spec: declaration.spec,
122
+ callbackRef: declaration.id,
123
+ ...existing?.lastFiredAt !== void 0 ? { lastFiredAt: existing.lastFiredAt } : {},
124
+ ...nextFireMs !== null ? { nextFireAt: new Date(nextFireMs).toISOString() } : {},
125
+ missedFires: 0,
126
+ disabled: existing?.disabled === true,
127
+ catchupPolicy: declaration.options.catchupPolicy ?? "none",
128
+ maxCatchupRuns: declaration.options.maxCatchupRuns ?? 1,
129
+ catchupWindowMs: declaration.options.catchupWindowMs ?? DEFAULT_CATCHUP_WINDOW_MS,
130
+ ...declaration.options.tags !== void 0 ? { tags: declaration.options.tags } : {},
131
+ createdAt: existing !== null ? existing.createdAt : new Date(now).toISOString(),
132
+ ...existing !== null ? { updatedAt: new Date(now).toISOString() } : {}
133
+ };
134
+ await this.#store.upsert(state);
135
+ this.#publish({
136
+ type: "registered",
137
+ id: declaration.id,
138
+ kind: declaration.kind
139
+ });
140
+ if (existing !== null && (declaration.kind === "cron" || declaration.kind === "interval")) await this.#applyCatchup(state, existing, now);
141
+ if (this.#started) this.#schedule(declaration.id, state);
142
+ return state;
143
+ }
144
+ async unregister(id) {
145
+ this.#cancelHandle(id);
146
+ this.#declarations.delete(id);
147
+ this.#parsedCron.delete(id);
148
+ await this.#store.remove(id);
149
+ }
150
+ async list() {
151
+ return this.#store.list();
152
+ }
153
+ async start() {
154
+ if (this.#started || this.#disposed) return;
155
+ this.#started = true;
156
+ this.#publish({ type: "started" });
157
+ const states = await this.#store.list();
158
+ for (const state of states) {
159
+ if (state.disabled) continue;
160
+ this.#schedule(state.id, state);
161
+ }
162
+ }
163
+ async stop() {
164
+ if (!this.#started) return;
165
+ this.#started = false;
166
+ for (const id of [...this.#handles.keys()]) this.#cancelHandle(id);
167
+ this.#publish({ type: "stopped" });
168
+ this.#closedEvents = true;
169
+ while (this.#eventResolvers.length > 0) this.#eventResolvers.shift()?.({
170
+ done: true,
171
+ value: void 0
172
+ });
173
+ }
174
+ async emit(eventName, payload) {
175
+ for (const decl of this.#declarations.values()) if (decl.kind === "event" && decl.spec === eventName) await this.fire(decl.id, payload);
176
+ }
177
+ async fire(id, payload) {
178
+ const decl = this.#declarations.get(id);
179
+ if (decl === void 0) throw new Error(`[graphorin/triggers] no trigger registered with id '${id}'`);
180
+ const firedAt = this.#now();
181
+ this.#publish({
182
+ type: "fire-start",
183
+ id,
184
+ firedAt
185
+ });
186
+ const start = firedAt;
187
+ try {
188
+ await decl.callback(payload);
189
+ const durationMs = this.#now() - start;
190
+ this.#publish({
191
+ type: "fire-end",
192
+ id,
193
+ durationMs
194
+ });
195
+ const state = await this.#store.get(id);
196
+ const nextFireMs = state !== null && decl.kind !== "idle" ? this.#computeNextFire(decl, {
197
+ ...state,
198
+ lastFiredAt: new Date(firedAt).toISOString()
199
+ }, this.#now()) : null;
200
+ await this.#store.recordFire(id, new Date(firedAt).toISOString(), nextFireMs !== null ? new Date(nextFireMs).toISOString() : void 0);
201
+ if (this.#started && decl.kind !== "idle") {
202
+ const refreshed = await this.#store.get(id);
203
+ if (refreshed !== null) this.#schedule(id, refreshed);
204
+ }
205
+ } catch (err) {
206
+ const durationMs = this.#now() - start;
207
+ this.#publish({
208
+ type: "fire-error",
209
+ id,
210
+ error: err,
211
+ durationMs
212
+ });
213
+ try {
214
+ const state = await this.#store.get(id);
215
+ if (state !== null && decl.kind !== "idle") {
216
+ const nextFireMs = this.#computeNextFire(decl, {
217
+ ...state,
218
+ lastFiredAt: new Date(firedAt).toISOString()
219
+ }, this.#now());
220
+ const updated = {
221
+ ...state,
222
+ ...nextFireMs !== null ? { nextFireAt: new Date(nextFireMs).toISOString() } : {},
223
+ updatedAt: new Date(this.#now()).toISOString()
224
+ };
225
+ await this.#store.upsert(updated);
226
+ if (this.#started) this.#schedule(id, updated);
227
+ }
228
+ } catch {}
229
+ }
230
+ }
231
+ async setDisabled(id, disabled) {
232
+ const state = await this.#store.get(id);
233
+ if (state === null) throw new Error(`[graphorin/triggers] no trigger registered with id '${id}'`);
234
+ const nowIso = new Date(this.#now()).toISOString();
235
+ if (disabled) {
236
+ this.#cancelHandle(id);
237
+ const updated$1 = {
238
+ ...state,
239
+ disabled: true,
240
+ updatedAt: nowIso
241
+ };
242
+ await this.#store.upsert(updated$1);
243
+ return updated$1;
244
+ }
245
+ const decl = this.#declarations.get(id);
246
+ const nextMs = decl !== void 0 ? this.#computeNextFire(decl, {
247
+ ...state,
248
+ lastFiredAt: nowIso
249
+ }, this.#now()) : null;
250
+ const updated = {
251
+ ...state,
252
+ disabled: false,
253
+ ...nextMs !== null ? { nextFireAt: new Date(nextMs).toISOString() } : {},
254
+ updatedAt: nowIso
255
+ };
256
+ await this.#store.upsert(updated);
257
+ if (this.#started) this.#schedule(id, updated);
258
+ return updated;
259
+ }
260
+ recordActivity() {
261
+ this.#lastActivity = this.#now();
262
+ for (const decl of this.#declarations.values()) {
263
+ if (decl.kind !== "idle") continue;
264
+ this.#cancelHandle(decl.id);
265
+ const idleMs = Number.parseInt(decl.spec, 10);
266
+ const handle = this.#setTimeout(() => {
267
+ this.fire(decl.id);
268
+ }, idleMs);
269
+ this.#handles.set(decl.id, handle);
270
+ }
271
+ }
272
+ events() {
273
+ const queue = this.#eventQueue;
274
+ const resolvers = this.#eventResolvers;
275
+ const me = this;
276
+ return { [Symbol.asyncIterator]() {
277
+ return {
278
+ async next() {
279
+ if (queue.length > 0) return {
280
+ done: false,
281
+ value: queue.shift()
282
+ };
283
+ if (me.#closedEvents) return {
284
+ done: true,
285
+ value: void 0
286
+ };
287
+ return new Promise((resolve) => resolvers.push(resolve));
288
+ },
289
+ async return() {
290
+ return {
291
+ done: true,
292
+ value: void 0
293
+ };
294
+ }
295
+ };
296
+ } };
297
+ }
298
+ #publish(event$1) {
299
+ if (this.#closedEvents) return;
300
+ if (this.#eventResolvers.length > 0) {
301
+ this.#eventResolvers.shift()?.({
302
+ done: false,
303
+ value: event$1
304
+ });
305
+ return;
306
+ }
307
+ this.#eventQueue.push(event$1);
308
+ }
309
+ #computeNextFire(decl, state, now) {
310
+ switch (decl.kind) {
311
+ case "cron": {
312
+ const next = nextFireAfter(this.#parsedCron.get(decl.id) ?? parseCron(decl.spec), new Date(now));
313
+ return next === null ? null : next.getTime();
314
+ }
315
+ case "interval": {
316
+ const intervalMs = Number.parseInt(decl.spec, 10);
317
+ return (state?.lastFiredAt !== void 0 ? Date.parse(state.lastFiredAt) : now) + intervalMs;
318
+ }
319
+ case "idle": {
320
+ const idleMs = Number.parseInt(decl.spec, 10);
321
+ return this.#lastActivity + idleMs;
322
+ }
323
+ case "event": return null;
324
+ }
325
+ }
326
+ async #applyCatchup(state, existing, now) {
327
+ if (state.catchupPolicy === "none") return;
328
+ const decl = this.#declarations.get(state.id);
329
+ if (decl === void 0 || decl.kind !== "cron" && decl.kind !== "interval") return;
330
+ const lastFired = existing.lastFiredAt !== void 0 ? Date.parse(existing.lastFiredAt) : null;
331
+ if (lastFired === null) return;
332
+ if (lastFired < now - state.catchupWindowMs) return;
333
+ const SCAN_CAP = 1e3;
334
+ let missed = 0;
335
+ if (decl.kind === "cron") {
336
+ const parsed = this.#parsedCron.get(decl.id) ?? parseCron(decl.spec);
337
+ let cursor = lastFired;
338
+ while (missed < SCAN_CAP) {
339
+ const next = nextFireAfter(parsed, new Date(cursor));
340
+ if (next === null || next.getTime() > now) break;
341
+ cursor = next.getTime();
342
+ missed += 1;
343
+ }
344
+ } else {
345
+ const intervalMs = Number.parseInt(decl.spec, 10);
346
+ missed = Math.floor((now - lastFired) / intervalMs);
347
+ }
348
+ if (missed <= 0) return;
349
+ const toFire = state.catchupPolicy === "last" ? 1 : Math.min(missed, state.maxCatchupRuns);
350
+ for (let i = 0; i < toFire; i++) await this.fire(state.id);
351
+ const excess = missed - toFire;
352
+ if (excess > 0) {
353
+ const refreshed = await this.#store.get(state.id);
354
+ if (refreshed !== null) await this.#store.upsert({
355
+ ...refreshed,
356
+ missedFires: excess
357
+ });
358
+ }
359
+ this.#publish({
360
+ type: "catchup-applied",
361
+ id: state.id,
362
+ missed
363
+ });
364
+ }
365
+ #schedule(id, state) {
366
+ this.#cancelHandle(id);
367
+ if (state.disabled) return;
368
+ const decl = this.#declarations.get(id);
369
+ if (decl === void 0) return;
370
+ if (decl.kind === "event") return;
371
+ let delay = null;
372
+ if (state.nextFireAt !== void 0) delay = Date.parse(state.nextFireAt) - this.#now();
373
+ else {
374
+ const next = this.#computeNextFire(decl, state, this.#now());
375
+ delay = next === null ? null : next - this.#now();
376
+ }
377
+ if (delay === null) return;
378
+ if (delay < 0) delay = 0;
379
+ if (delay > MAX_TIMEOUT_MS) {
380
+ const handle$1 = this.#setTimeout(() => {
381
+ this.#store.get(id).then((s) => {
382
+ if (s !== null) this.#schedule(id, s);
383
+ }).catch(() => {});
384
+ }, MAX_TIMEOUT_MS);
385
+ this.#handles.set(id, handle$1);
386
+ return;
387
+ }
388
+ const handle = this.#setTimeout(() => {
389
+ this.fire(id);
390
+ }, delay);
391
+ this.#handles.set(id, handle);
392
+ }
393
+ #cancelHandle(id) {
394
+ const handle = this.#handles.get(id);
395
+ if (handle !== void 0) {
396
+ this.#clearTimeout(handle);
397
+ this.#handles.delete(id);
398
+ }
399
+ }
400
+ };
401
+
402
+ //#endregion
403
+ export { CronParseError, VERSION, _resetLibModeWarningForTesting, createScheduler, cron, event, idle, interval, nextFireAfter, parseCron };
404
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":"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"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@graphorin/triggers",
3
+ "version": "0.5.0",
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
+ "license": "MIT",
6
+ "author": "Oleksiy Stepurenko",
7
+ "homepage": "https://github.com/o-stepper/graphorin/tree/main/packages/triggers",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/o-stepper/graphorin.git",
11
+ "directory": "packages/triggers"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/o-stepper/graphorin/issues"
15
+ },
16
+ "keywords": [
17
+ "graphorin",
18
+ "ai",
19
+ "agents",
20
+ "framework",
21
+ "triggers",
22
+ "scheduler",
23
+ "cron",
24
+ "background-tasks",
25
+ "durable"
26
+ ],
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=22.0.0"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./cron": {
40
+ "types": "./dist/cron.d.ts",
41
+ "import": "./dist/cron.js"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "README.md",
48
+ "CHANGELOG.md",
49
+ "LICENSE"
50
+ ],
51
+ "dependencies": {
52
+ "@graphorin/core": "0.5.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public",
56
+ "provenance": true
57
+ },
58
+ "devDependencies": {
59
+ "@graphorin/store-sqlite": "0.5.0"
60
+ },
61
+ "scripts": {
62
+ "build": "tsdown",
63
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
64
+ "test": "vitest run",
65
+ "lint": "biome check .",
66
+ "clean": "rimraf dist .turbo *.tsbuildinfo"
67
+ }
68
+ }