@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 +10 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/cron.d.ts +76 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +174 -0
- package/dist/cron.js.map +1 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +404 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
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
|
package/dist/cron.js.map
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|