@keystrokehq/scheduler 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/contract-C5JvbyQ-.cjs.map +1 -1
- package/dist/{contract-BGjcxdxT.d.cts → contract-DyhRC8AI.d.cts} +30 -7
- package/dist/contract-DyhRC8AI.d.cts.map +1 -0
- package/dist/{contract-BGjcxdxT.d.mts → contract-DyhRC8AI.d.mts} +30 -7
- package/dist/contract-DyhRC8AI.d.mts.map +1 -0
- package/dist/contract-E1QJBH6_.mjs.map +1 -1
- package/dist/contract.d.cts +2 -2
- package/dist/contract.d.mts +2 -2
- package/dist/index.cjs +298 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +55 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +294 -123
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/dist/contract-BGjcxdxT.d.cts.map +0 -1
- package/dist/contract-BGjcxdxT.d.mts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,80 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_contract = require("./contract-C5JvbyQ-.cjs");
|
|
3
3
|
let _keystrokehq_database = require("@keystrokehq/database");
|
|
4
|
-
let _keystrokehq_trigger = require("@keystrokehq/trigger");
|
|
5
4
|
let node_events = require("node:events");
|
|
5
|
+
let _keystrokehq_trigger = require("@keystrokehq/trigger");
|
|
6
6
|
let pg_boss = require("pg-boss");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
|
-
//#region src/schedule-ticker.ts
|
|
9
|
-
function enqueueTriggerJob(jobQueue, row, scheduledAt) {
|
|
10
|
-
const input = {
|
|
11
|
-
kind: "trigger",
|
|
12
|
-
targetId: row.slug,
|
|
13
|
-
runId: crypto.randomUUID(),
|
|
14
|
-
trigger: row.kind,
|
|
15
|
-
payload: {},
|
|
16
|
-
projectId: row.projectId
|
|
17
|
-
};
|
|
18
|
-
if (scheduledAt) input.scheduledAt = scheduledAt;
|
|
19
|
-
return jobQueue.enqueue(input);
|
|
20
|
-
}
|
|
21
|
-
async function claimDue(scope, asOf, resolveNextRunAt, limit) {
|
|
22
|
-
if (scope === "organization") return (0, _keystrokehq_database.claimDueTriggersForOrg)(asOf, resolveNextRunAt, limit);
|
|
23
|
-
return (0, _keystrokehq_database.claimDueTriggers)(asOf, resolveNextRunAt, limit).then((rows) => rows.map((row) => ({
|
|
24
|
-
...row,
|
|
25
|
-
projectId: (0, _keystrokehq_database.getProjectScopeId)()
|
|
26
|
-
})));
|
|
27
|
-
}
|
|
28
|
-
function createScheduleTicker(ctx) {
|
|
29
|
-
const pollIntervalMs = ctx.pollIntervalMs ?? 1e3;
|
|
30
|
-
const batchSize = ctx.batchSize ?? 10;
|
|
31
|
-
async function fireDueSchedules(asOf = /* @__PURE__ */ new Date()) {
|
|
32
|
-
const claimed = await claimDue(ctx.scope ?? "project", asOf, (schedule) => (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, asOf), batchSize);
|
|
33
|
-
for (const row of claimed) {
|
|
34
|
-
if (row.kind !== "cron" && row.kind !== "poll") continue;
|
|
35
|
-
await enqueueTriggerJob(ctx.jobQueue, {
|
|
36
|
-
slug: row.slug,
|
|
37
|
-
kind: row.kind,
|
|
38
|
-
projectId: row.projectId
|
|
39
|
-
}, asOf);
|
|
40
|
-
}
|
|
41
|
-
return claimed.length;
|
|
42
|
-
}
|
|
43
|
-
async function startScheduleTicker(options = {}) {
|
|
44
|
-
const intervalMs = options.pollIntervalMs ?? pollIntervalMs;
|
|
45
|
-
const limit = options.batchSize ?? batchSize;
|
|
46
|
-
const scope = options.scope ?? ctx.scope ?? "project";
|
|
47
|
-
let running = true;
|
|
48
|
-
const loop = async () => {
|
|
49
|
-
while (running) {
|
|
50
|
-
try {
|
|
51
|
-
const claimed = await claimDue(scope, /* @__PURE__ */ new Date(), (schedule) => (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, /* @__PURE__ */ new Date()), limit);
|
|
52
|
-
for (const row of claimed) {
|
|
53
|
-
if (row.kind !== "cron" && row.kind !== "poll") continue;
|
|
54
|
-
await enqueueTriggerJob(ctx.jobQueue, {
|
|
55
|
-
slug: row.slug,
|
|
56
|
-
kind: row.kind,
|
|
57
|
-
projectId: row.projectId
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
} catch {}
|
|
61
|
-
await sleep$1(intervalMs);
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
loop();
|
|
65
|
-
return async () => {
|
|
66
|
-
running = false;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
fireDueSchedules,
|
|
71
|
-
startScheduleTicker
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
function sleep$1(ms) {
|
|
75
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
76
|
-
}
|
|
77
|
-
//#endregion
|
|
78
8
|
//#region src/cancel-channel.ts
|
|
79
9
|
/** In-process cancel pub/sub for single-process queues (memory, db polling). */
|
|
80
10
|
function createInProcessCancelChannel() {
|
|
@@ -127,7 +57,7 @@ function createDatabaseJobQueue() {
|
|
|
127
57
|
while (running) try {
|
|
128
58
|
const job = await (0, _keystrokehq_database.claimNextJob)(workerId);
|
|
129
59
|
if (!job) {
|
|
130
|
-
await sleep(pollIntervalMs);
|
|
60
|
+
await sleep$1(pollIntervalMs);
|
|
131
61
|
continue;
|
|
132
62
|
}
|
|
133
63
|
try {
|
|
@@ -141,7 +71,7 @@ function createDatabaseJobQueue() {
|
|
|
141
71
|
}
|
|
142
72
|
}
|
|
143
73
|
} catch {
|
|
144
|
-
await sleep(pollIntervalMs);
|
|
74
|
+
await sleep$1(pollIntervalMs);
|
|
145
75
|
}
|
|
146
76
|
};
|
|
147
77
|
loop();
|
|
@@ -154,10 +84,135 @@ function createDatabaseJobQueue() {
|
|
|
154
84
|
subscribeCancel: (handler) => cancelChannel.subscribeCancel(handler)
|
|
155
85
|
};
|
|
156
86
|
}
|
|
87
|
+
function sleep$1(ms) {
|
|
88
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/schedule-ticker.ts
|
|
92
|
+
function enqueueTriggerJob(jobQueue, row, scheduledAt) {
|
|
93
|
+
const input = {
|
|
94
|
+
kind: "trigger",
|
|
95
|
+
targetId: row.slug,
|
|
96
|
+
runId: crypto.randomUUID(),
|
|
97
|
+
trigger: row.kind,
|
|
98
|
+
payload: {},
|
|
99
|
+
projectId: row.projectId
|
|
100
|
+
};
|
|
101
|
+
if (scheduledAt) input.scheduledAt = scheduledAt;
|
|
102
|
+
return jobQueue.enqueue(input);
|
|
103
|
+
}
|
|
104
|
+
async function claimDue(scope, asOf, resolveNextRunAt, limit) {
|
|
105
|
+
if (scope === "organization") return (0, _keystrokehq_database.claimDueTriggersForOrg)(asOf, resolveNextRunAt, limit);
|
|
106
|
+
return (0, _keystrokehq_database.claimDueTriggers)(asOf, resolveNextRunAt, limit).then((rows) => rows.map((row) => ({
|
|
107
|
+
...row,
|
|
108
|
+
projectId: (0, _keystrokehq_database.getProjectScopeId)()
|
|
109
|
+
})));
|
|
110
|
+
}
|
|
111
|
+
function createScheduleTicker(ctx) {
|
|
112
|
+
const pollIntervalMs = ctx.pollIntervalMs ?? 1e3;
|
|
113
|
+
const batchSize = ctx.batchSize ?? 10;
|
|
114
|
+
async function fireDueSchedules(asOf = /* @__PURE__ */ new Date()) {
|
|
115
|
+
const claimed = await claimDue(ctx.scope ?? "project", asOf, (schedule) => (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, asOf), batchSize);
|
|
116
|
+
for (const row of claimed) {
|
|
117
|
+
if (row.kind !== "cron" && row.kind !== "poll") continue;
|
|
118
|
+
await enqueueTriggerJob(ctx.jobQueue, {
|
|
119
|
+
slug: row.slug,
|
|
120
|
+
kind: row.kind,
|
|
121
|
+
projectId: row.projectId
|
|
122
|
+
}, asOf);
|
|
123
|
+
}
|
|
124
|
+
return claimed.length;
|
|
125
|
+
}
|
|
126
|
+
async function startScheduleTicker(options = {}) {
|
|
127
|
+
const intervalMs = options.pollIntervalMs ?? pollIntervalMs;
|
|
128
|
+
const limit = options.batchSize ?? batchSize;
|
|
129
|
+
const scope = options.scope ?? ctx.scope ?? "project";
|
|
130
|
+
let running = true;
|
|
131
|
+
const loop = async () => {
|
|
132
|
+
while (running) {
|
|
133
|
+
try {
|
|
134
|
+
const claimed = await claimDue(scope, /* @__PURE__ */ new Date(), (schedule) => (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, /* @__PURE__ */ new Date()), limit);
|
|
135
|
+
for (const row of claimed) {
|
|
136
|
+
if (row.kind !== "cron" && row.kind !== "poll") continue;
|
|
137
|
+
await enqueueTriggerJob(ctx.jobQueue, {
|
|
138
|
+
slug: row.slug,
|
|
139
|
+
kind: row.kind,
|
|
140
|
+
projectId: row.projectId
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} catch {}
|
|
144
|
+
await sleep(intervalMs);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
loop();
|
|
148
|
+
return async () => {
|
|
149
|
+
running = false;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
fireDueSchedules,
|
|
154
|
+
startScheduleTicker
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
157
|
function sleep(ms) {
|
|
158
158
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
159
159
|
}
|
|
160
160
|
//#endregion
|
|
161
|
+
//#region src/resolve-schedule.ts
|
|
162
|
+
function resolveTriggerSchedule(triggerSlug, schedule, overrides) {
|
|
163
|
+
return (0, _keystrokehq_trigger.resolveCronSchedule)(triggerSlug, schedule, {
|
|
164
|
+
cronScheduleOverride: overrides?.global,
|
|
165
|
+
attachmentScheduleOverrides: overrides?.byTrigger
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/sync-trigger-schedules.ts
|
|
170
|
+
async function syncTriggerSchedules(options) {
|
|
171
|
+
const now = /* @__PURE__ */ new Date();
|
|
172
|
+
const projectSlugs = options.schedules.map((schedule) => schedule.triggerSlug);
|
|
173
|
+
const ephemeralSlugs = await (0, _keystrokehq_database.selectActiveEphemeralScheduledTriggerSlugs)();
|
|
174
|
+
const slugs = [...projectSlugs, ...ephemeralSlugs];
|
|
175
|
+
if (slugs.length === 0) {
|
|
176
|
+
await (0, _keystrokehq_database.disableAllTriggerSchedules)(now);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await (0, _keystrokehq_database.disableTriggerSchedulesNotInSlugs)(slugs, now);
|
|
180
|
+
for (const spec of options.schedules) {
|
|
181
|
+
const schedule = resolveTriggerSchedule(spec.triggerSlug, spec.schedule, options.scheduleOverrides);
|
|
182
|
+
const existing = await (0, _keystrokehq_database.selectTriggerBySlug)(spec.triggerSlug);
|
|
183
|
+
if (!existing) continue;
|
|
184
|
+
const nextRunAt = !(existing.schedule !== schedule) && existing.enabled === 1 ? existing.nextRunAt : (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, now);
|
|
185
|
+
const enabled = (await (0, _keystrokehq_database.listExecutableAttachmentsByTriggerIds)([existing.id])).length > 0;
|
|
186
|
+
await (0, _keystrokehq_database.upsertTriggerScheduleFields)({
|
|
187
|
+
triggerSlug: spec.triggerSlug,
|
|
188
|
+
schedule,
|
|
189
|
+
nextRunAt: nextRunAt ?? now,
|
|
190
|
+
enabled,
|
|
191
|
+
updatedAt: now
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/database-trigger-scheduler.ts
|
|
197
|
+
/** Postgres poll loop — default trigger scheduler for pg-boss, polling, and memory paths. */
|
|
198
|
+
function createDatabaseTriggerScheduler(options) {
|
|
199
|
+
const ticker = createScheduleTicker({
|
|
200
|
+
jobQueue: options.jobQueue,
|
|
201
|
+
scope: options.scope,
|
|
202
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
203
|
+
batchSize: options.batchSize
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
sync: (syncOptions) => syncTriggerSchedules(syncOptions),
|
|
207
|
+
start: (startOptions) => ticker.startScheduleTicker(startOptions),
|
|
208
|
+
fireDue: (asOf) => ticker.fireDueSchedules(asOf),
|
|
209
|
+
async remove(triggerSlug) {
|
|
210
|
+
await (0, _keystrokehq_database.setTriggerScheduleEnabled)(triggerSlug, false, /* @__PURE__ */ new Date());
|
|
211
|
+
},
|
|
212
|
+
async upsert(_spec) {}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
161
216
|
//#region src/pg-boss-client.ts
|
|
162
217
|
let boss;
|
|
163
218
|
function resolveDatabaseUrl(url) {
|
|
@@ -220,6 +275,8 @@ function buildPgBossJobQueue(boss, options) {
|
|
|
220
275
|
const queueName = options.queueName ?? "keystroke";
|
|
221
276
|
const cancelChannel = (0, _keystrokehq_database.createPostgresCancelChannel)(options.connectionString, pgCancelChannelName(options.cancelScope ?? "platform", options.cancelScope === "organization" ? options.organizationId : options.projectId));
|
|
222
277
|
return {
|
|
278
|
+
boss,
|
|
279
|
+
queueName,
|
|
223
280
|
async enqueue(input) {
|
|
224
281
|
const jobId = await boss.send(queueName, {
|
|
225
282
|
...input,
|
|
@@ -302,10 +359,132 @@ async function createSharedPgBossJobQueue(boss, options) {
|
|
|
302
359
|
});
|
|
303
360
|
}
|
|
304
361
|
//#endregion
|
|
362
|
+
//#region src/pg-boss-trigger-scheduler.ts
|
|
363
|
+
/**
|
|
364
|
+
* pg-boss schedule key for one trigger. Mirrors {@link bullmqTriggerSchedulerId}
|
|
365
|
+
* so the firing backend is interchangeable: `trigger-<projectId>-<slug>`. pg-boss
|
|
366
|
+
* keys allow hyphens, so colons (legal in ids) are swapped for `-`.
|
|
367
|
+
*/
|
|
368
|
+
function pgBossTriggerScheduleKey(spec) {
|
|
369
|
+
return `trigger-${(spec.projectId ?? (0, _keystrokehq_database.getProjectScopeId)()).replaceAll(":", "-")}-${spec.triggerSlug.replaceAll(":", "-")}`;
|
|
370
|
+
}
|
|
371
|
+
/** Key prefix scoping reconcile cleanup to the active project (org queues are shared). */
|
|
372
|
+
function pgBossTriggerScheduleKeyPrefix(projectId) {
|
|
373
|
+
return `trigger-${(projectId ?? (0, _keystrokehq_database.getProjectScopeId)()).replaceAll(":", "-")}-`;
|
|
374
|
+
}
|
|
375
|
+
function triggerJobData(spec) {
|
|
376
|
+
return {
|
|
377
|
+
kind: "trigger",
|
|
378
|
+
targetId: spec.triggerSlug,
|
|
379
|
+
runId: "",
|
|
380
|
+
trigger: spec.kind,
|
|
381
|
+
payload: {},
|
|
382
|
+
projectId: spec.projectId ?? (0, _keystrokehq_database.getProjectScopeId)()
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function rowToSpec(row) {
|
|
386
|
+
if (row.kind !== "cron" && row.kind !== "poll") return;
|
|
387
|
+
if (!row.schedule) return;
|
|
388
|
+
return {
|
|
389
|
+
triggerSlug: row.slug,
|
|
390
|
+
kind: row.kind,
|
|
391
|
+
schedule: row.schedule,
|
|
392
|
+
projectId: row.projectId
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Trigger scheduler backed by pg-boss native cron scheduling. Mirrors
|
|
397
|
+
* {@link createBullmqTriggerScheduler}: `boss.schedule` registers a recurring job
|
|
398
|
+
* onto the project/org queue (no per-second DB poll), and reconcile keeps the
|
|
399
|
+
* `pgboss.schedule` table in sync with the `triggers` table (source of truth).
|
|
400
|
+
*
|
|
401
|
+
* Granularity: pg-boss cron is minute-level (it checks schedules every ~30s and
|
|
402
|
+
* dedupes per minute), so sub-minute schedules effectively fire at most once per
|
|
403
|
+
* minute — unlike the DB ticker's 1s loop.
|
|
404
|
+
*/
|
|
405
|
+
function createPgBossTriggerScheduler(_ctx, queue) {
|
|
406
|
+
async function upsertScheduler(spec, overrides) {
|
|
407
|
+
const schedule = resolveTriggerSchedule(spec.triggerSlug, spec.schedule, overrides);
|
|
408
|
+
const key = pgBossTriggerScheduleKey(spec);
|
|
409
|
+
await queue.boss.schedule(queue.queueName, schedule, triggerJobData({
|
|
410
|
+
...spec,
|
|
411
|
+
schedule
|
|
412
|
+
}), { key });
|
|
413
|
+
}
|
|
414
|
+
async function reconcileFromDatabase(overrides) {
|
|
415
|
+
const projectPrefix = pgBossTriggerScheduleKeyPrefix();
|
|
416
|
+
const rows = await (0, _keystrokehq_database.listEnabledScheduledTriggers)();
|
|
417
|
+
const expectedKeys = /* @__PURE__ */ new Set();
|
|
418
|
+
const failures = [];
|
|
419
|
+
for (const row of rows) {
|
|
420
|
+
const spec = rowToSpec(row);
|
|
421
|
+
if (!spec) continue;
|
|
422
|
+
expectedKeys.add(pgBossTriggerScheduleKey(spec));
|
|
423
|
+
try {
|
|
424
|
+
await upsertScheduler(spec, overrides);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
failures.push({
|
|
427
|
+
triggerSlug: spec.triggerSlug,
|
|
428
|
+
error
|
|
429
|
+
});
|
|
430
|
+
console.error(`[pg-boss-trigger-scheduler] failed to schedule ${spec.triggerSlug}`, error);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const schedules = await queue.boss.getSchedules(queue.queueName);
|
|
434
|
+
for (const schedule of schedules) {
|
|
435
|
+
const key = schedule.key;
|
|
436
|
+
if (!key || !key.startsWith(projectPrefix)) continue;
|
|
437
|
+
if (!expectedKeys.has(key)) try {
|
|
438
|
+
await queue.boss.unschedule(queue.queueName, key);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
failures.push({
|
|
441
|
+
triggerSlug: key,
|
|
442
|
+
error
|
|
443
|
+
});
|
|
444
|
+
console.error(`[pg-boss-trigger-scheduler] failed to unschedule stale ${key}`, error);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (failures.length > 0) throw new Error(`pg-boss trigger scheduler reconcile failed for ${failures.length} trigger(s)`);
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
async sync(syncOptions) {
|
|
451
|
+
await syncTriggerSchedules(syncOptions);
|
|
452
|
+
await reconcileFromDatabase(syncOptions.scheduleOverrides);
|
|
453
|
+
},
|
|
454
|
+
async start(_options) {
|
|
455
|
+
return async () => void 0;
|
|
456
|
+
},
|
|
457
|
+
fireDue: async () => 0,
|
|
458
|
+
upsert: upsertScheduler,
|
|
459
|
+
async remove(triggerSlug, projectId) {
|
|
460
|
+
await queue.boss.unschedule(queue.queueName, pgBossTriggerScheduleKey({
|
|
461
|
+
triggerSlug,
|
|
462
|
+
projectId
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
305
468
|
//#region src/plugin.ts
|
|
306
469
|
function resolveUrl(ctx) {
|
|
307
470
|
return ctx.url ?? (0, _keystrokehq_database.resolveProjectDatabaseUrlFromEnv)(process.env) ?? _keystrokehq_database.DEFAULT_DATABASE_URL;
|
|
308
471
|
}
|
|
472
|
+
/** DB poll-loop trigger scheduler — used by the SQLite/self-host polling backend. */
|
|
473
|
+
function createPollingTriggerScheduler(ctx, queue) {
|
|
474
|
+
const scope = ctx.scope === "organization" ? "organization" : "project";
|
|
475
|
+
return Promise.resolve(createDatabaseTriggerScheduler({
|
|
476
|
+
jobQueue: queue,
|
|
477
|
+
scope
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
function isPgBossJobQueue(queue) {
|
|
481
|
+
return "boss" in queue && "queueName" in queue;
|
|
482
|
+
}
|
|
483
|
+
/** Native pg-boss cron scheduler; falls back to the DB ticker if the queue isn't pg-boss. */
|
|
484
|
+
function createPgBossTriggerSchedulerForPlugin(ctx, queue) {
|
|
485
|
+
if (!isPgBossJobQueue(queue)) return createPollingTriggerScheduler(ctx, queue);
|
|
486
|
+
return Promise.resolve(createPgBossTriggerScheduler(ctx, queue));
|
|
487
|
+
}
|
|
309
488
|
/** Postgres pg-boss backend. Platform scope shares one queue; org scope gets one queue per org. */
|
|
310
489
|
function pgBossSchedulerPlugin() {
|
|
311
490
|
return require_contract.defineSchedulerPlugin({
|
|
@@ -332,7 +511,8 @@ function pgBossSchedulerPlugin() {
|
|
|
332
511
|
queueName: pgBossProjectQueueName(projectId),
|
|
333
512
|
projectId
|
|
334
513
|
});
|
|
335
|
-
}
|
|
514
|
+
},
|
|
515
|
+
createTriggerScheduler: createPgBossTriggerSchedulerForPlugin
|
|
336
516
|
});
|
|
337
517
|
}
|
|
338
518
|
/** DB-table queue (Drizzle `jobs`), for SQLite / self-host. */
|
|
@@ -341,7 +521,8 @@ function pollingSchedulerPlugin() {
|
|
|
341
521
|
name: "polling",
|
|
342
522
|
async createJobQueue() {
|
|
343
523
|
return createDatabaseJobQueue();
|
|
344
|
-
}
|
|
524
|
+
},
|
|
525
|
+
createTriggerScheduler: createPollingTriggerScheduler
|
|
345
526
|
});
|
|
346
527
|
}
|
|
347
528
|
/** Auto-selects pg-boss (postgres) or polling (sqlite) by dialect. */
|
|
@@ -351,45 +532,14 @@ function defaultSchedulerPlugin() {
|
|
|
351
532
|
async createJobQueue(ctx) {
|
|
352
533
|
if ((0, _keystrokehq_database.inferDialect)(resolveUrl(ctx), ctx.dialect) === "postgres") return pgBossSchedulerPlugin().createJobQueue(ctx);
|
|
353
534
|
return pollingSchedulerPlugin().createJobQueue(ctx);
|
|
535
|
+
},
|
|
536
|
+
async createTriggerScheduler(ctx, queue) {
|
|
537
|
+
if ((0, _keystrokehq_database.inferDialect)(resolveUrl(ctx), ctx.dialect) === "postgres") return pgBossSchedulerPlugin().createTriggerScheduler(ctx, queue);
|
|
538
|
+
return pollingSchedulerPlugin().createTriggerScheduler(ctx, queue);
|
|
354
539
|
}
|
|
355
540
|
});
|
|
356
541
|
}
|
|
357
542
|
//#endregion
|
|
358
|
-
//#region src/resolve-schedule.ts
|
|
359
|
-
function resolveTriggerSchedule(triggerSlug, schedule, overrides) {
|
|
360
|
-
return (0, _keystrokehq_trigger.resolveCronSchedule)(triggerSlug, schedule, {
|
|
361
|
-
cronScheduleOverride: overrides?.global,
|
|
362
|
-
attachmentScheduleOverrides: overrides?.byTrigger
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
//#endregion
|
|
366
|
-
//#region src/sync-trigger-schedules.ts
|
|
367
|
-
async function syncTriggerSchedules(options) {
|
|
368
|
-
const now = /* @__PURE__ */ new Date();
|
|
369
|
-
const projectSlugs = options.schedules.map((schedule) => schedule.triggerSlug);
|
|
370
|
-
const ephemeralSlugs = await (0, _keystrokehq_database.selectActiveEphemeralScheduledTriggerSlugs)();
|
|
371
|
-
const slugs = [...projectSlugs, ...ephemeralSlugs];
|
|
372
|
-
if (slugs.length === 0) {
|
|
373
|
-
await (0, _keystrokehq_database.disableAllTriggerSchedules)(now);
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
await (0, _keystrokehq_database.disableTriggerSchedulesNotInSlugs)(slugs, now);
|
|
377
|
-
for (const spec of options.schedules) {
|
|
378
|
-
const schedule = resolveTriggerSchedule(spec.triggerSlug, spec.schedule, options.scheduleOverrides);
|
|
379
|
-
const existing = await (0, _keystrokehq_database.selectTriggerBySlug)(spec.triggerSlug);
|
|
380
|
-
if (!existing) continue;
|
|
381
|
-
const nextRunAt = !(existing.schedule !== schedule) && existing.enabled === 1 ? existing.nextRunAt : (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, now);
|
|
382
|
-
const enabled = (await (0, _keystrokehq_database.listExecutableAttachmentsByTriggerIds)([existing.id])).length > 0;
|
|
383
|
-
await (0, _keystrokehq_database.upsertTriggerScheduleFields)({
|
|
384
|
-
triggerSlug: spec.triggerSlug,
|
|
385
|
-
schedule,
|
|
386
|
-
nextRunAt: nextRunAt ?? now,
|
|
387
|
-
enabled,
|
|
388
|
-
updatedAt: now
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
//#endregion
|
|
393
543
|
//#region src/create-scheduler.ts
|
|
394
544
|
async function createUnderlyingJobQueue(options = {}) {
|
|
395
545
|
if (options.adapter) return options.adapter;
|
|
@@ -401,29 +551,50 @@ async function createUnderlyingJobQueue(options = {}) {
|
|
|
401
551
|
organizationId: options.organizationId
|
|
402
552
|
});
|
|
403
553
|
}
|
|
404
|
-
function
|
|
405
|
-
|
|
554
|
+
function pluginContext(options = {}) {
|
|
555
|
+
return {
|
|
556
|
+
scope: options.scope ?? "project",
|
|
557
|
+
url: options.url,
|
|
558
|
+
dialect: options.dialect,
|
|
559
|
+
projectId: options.projectId,
|
|
560
|
+
organizationId: options.organizationId
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
async function createTriggerScheduler(options, jobQueue) {
|
|
564
|
+
const scope = options.scope === "organization" ? "organization" : "project";
|
|
565
|
+
const plugin = options.plugin ?? defaultSchedulerPlugin();
|
|
566
|
+
const ctx = pluginContext(options);
|
|
567
|
+
if (plugin.createTriggerScheduler) return plugin.createTriggerScheduler(ctx, jobQueue);
|
|
568
|
+
return createDatabaseTriggerScheduler({
|
|
406
569
|
jobQueue,
|
|
407
570
|
scope
|
|
408
571
|
});
|
|
572
|
+
}
|
|
573
|
+
function wrapScheduler(jobQueue, triggerScheduler) {
|
|
409
574
|
return {
|
|
410
575
|
enqueue: (input) => jobQueue.enqueue(input),
|
|
411
|
-
startWorker: (handler,
|
|
576
|
+
startWorker: (handler, workerOptions) => jobQueue.startWorker(handler, workerOptions),
|
|
412
577
|
publishCancel: (runId) => jobQueue.publishCancel(runId),
|
|
413
578
|
subscribeCancel: (handler) => jobQueue.subscribeCancel(handler),
|
|
414
|
-
syncTriggerSchedules: (options) =>
|
|
415
|
-
startScheduleTicker: (options) =>
|
|
416
|
-
fireDueSchedules: (asOf) =>
|
|
579
|
+
syncTriggerSchedules: (options) => triggerScheduler.sync(options),
|
|
580
|
+
startScheduleTicker: (options) => triggerScheduler.start(options),
|
|
581
|
+
fireDueSchedules: (asOf) => triggerScheduler.fireDue?.(asOf) ?? Promise.resolve(0),
|
|
582
|
+
...triggerScheduler.upsert ? { upsertTriggerSchedule: (spec, overrides) => triggerScheduler.upsert(spec, overrides) } : {},
|
|
583
|
+
...triggerScheduler.remove ? { removeTriggerSchedule: (triggerSlug, projectId) => triggerScheduler.remove(triggerSlug, projectId) } : {}
|
|
417
584
|
};
|
|
418
585
|
}
|
|
419
586
|
async function createScheduler(options = {}) {
|
|
420
|
-
|
|
587
|
+
const jobQueue = await createUnderlyingJobQueue(options);
|
|
588
|
+
return wrapScheduler(jobQueue, await createTriggerScheduler(options, jobQueue));
|
|
421
589
|
}
|
|
422
590
|
async function createJobQueue(options = {}) {
|
|
423
591
|
return createUnderlyingJobQueue(options);
|
|
424
592
|
}
|
|
425
|
-
function wrapJobQueueAsScheduler(jobQueue, scope) {
|
|
426
|
-
return wrapScheduler(jobQueue,
|
|
593
|
+
async function wrapJobQueueAsScheduler(jobQueue, scope) {
|
|
594
|
+
return wrapScheduler(jobQueue, createDatabaseTriggerScheduler({
|
|
595
|
+
jobQueue,
|
|
596
|
+
scope
|
|
597
|
+
}));
|
|
427
598
|
}
|
|
428
599
|
//#endregion
|
|
429
600
|
//#region src/memory.ts
|
|
@@ -482,7 +653,7 @@ function createMemoryJobQueue(options = {}) {
|
|
|
482
653
|
//#region src/shared-pgboss-queue.ts
|
|
483
654
|
let sharedJobQueue$1;
|
|
484
655
|
async function createSharedPgBossScheduler() {
|
|
485
|
-
return wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
|
|
656
|
+
return await wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
|
|
486
657
|
}
|
|
487
658
|
async function getSharedPgBossJobQueue() {
|
|
488
659
|
if (sharedJobQueue$1) return sharedJobQueue$1;
|
|
@@ -506,7 +677,7 @@ async function createSharedScheduler() {
|
|
|
506
677
|
const plugin = configuredPlugin ?? defaultSchedulerPlugin();
|
|
507
678
|
if (plugin.name === "default") return createSharedPgBossScheduler();
|
|
508
679
|
if (!sharedJobQueue) sharedJobQueue = await plugin.createJobQueue({ scope: "platform" });
|
|
509
|
-
return wrapJobQueueAsScheduler(sharedJobQueue);
|
|
680
|
+
return await wrapJobQueueAsScheduler(sharedJobQueue);
|
|
510
681
|
}
|
|
511
682
|
function resetSharedSchedulerForTests() {
|
|
512
683
|
configuredPlugin = void 0;
|
|
@@ -518,8 +689,10 @@ exports.DEFAULT_QUEUE_NAME = DEFAULT_QUEUE_NAME;
|
|
|
518
689
|
exports.DEFAULT_RETRY_DELAY_MS = require_contract.DEFAULT_RETRY_DELAY_MS;
|
|
519
690
|
exports.buildPgBossJobQueue = buildPgBossJobQueue;
|
|
520
691
|
exports.configureSharedScheduler = configureSharedScheduler;
|
|
692
|
+
exports.createDatabaseTriggerScheduler = createDatabaseTriggerScheduler;
|
|
521
693
|
exports.createJobQueue = createJobQueue;
|
|
522
694
|
exports.createMemoryJobQueue = createMemoryJobQueue;
|
|
695
|
+
exports.createPgBossTriggerScheduler = createPgBossTriggerScheduler;
|
|
523
696
|
exports.createScheduler = createScheduler;
|
|
524
697
|
exports.createSharedPgBossJobQueue = createSharedPgBossJobQueue;
|
|
525
698
|
exports.createSharedPgBossScheduler = createSharedPgBossScheduler;
|
|
@@ -530,12 +703,16 @@ exports.getPgBoss = getPgBoss;
|
|
|
530
703
|
exports.getSharedPgBossJobQueue = getSharedPgBossJobQueue;
|
|
531
704
|
exports.pgBossProjectQueueName = pgBossProjectQueueName;
|
|
532
705
|
exports.pgBossSchedulerPlugin = pgBossSchedulerPlugin;
|
|
706
|
+
exports.pgBossTriggerScheduleKey = pgBossTriggerScheduleKey;
|
|
707
|
+
exports.pgBossTriggerScheduleKeyPrefix = pgBossTriggerScheduleKeyPrefix;
|
|
533
708
|
exports.pollingSchedulerPlugin = pollingSchedulerPlugin;
|
|
534
709
|
exports.resetSharedPgBossJobQueueForTests = resetSharedPgBossJobQueueForTests;
|
|
535
710
|
exports.resetSharedSchedulerForTests = resetSharedSchedulerForTests;
|
|
711
|
+
exports.resolveTriggerSchedule = resolveTriggerSchedule;
|
|
536
712
|
exports.retryDelayMs = require_contract.retryDelayMs;
|
|
537
713
|
exports.startPgBoss = startPgBoss;
|
|
538
714
|
exports.stopPgBoss = stopPgBoss;
|
|
715
|
+
exports.syncTriggerSchedules = syncTriggerSchedules;
|
|
539
716
|
exports.wrapJobQueueAsScheduler = wrapJobQueueAsScheduler;
|
|
540
717
|
|
|
541
718
|
//# sourceMappingURL=index.cjs.map
|