@keystrokehq/scheduler 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/contract-C5JvbyQ-.cjs.map +1 -1
- package/dist/{contract-M5IpV90X.d.cts → contract-DyhRC8AI.d.cts} +32 -9
- package/dist/contract-DyhRC8AI.d.cts.map +1 -0
- package/dist/{contract-M5IpV90X.d.mts → contract-DyhRC8AI.d.mts} +32 -9
- 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 -107
- 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 -109
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/dist/contract-M5IpV90X.d.cts.map +0 -1
- package/dist/contract-M5IpV90X.d.mts.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,66 +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.attachmentSlug,
|
|
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.claimDueTriggerSchedulesForOrg)(asOf, resolveNextRunAt, limit);
|
|
23
|
-
return (0, _keystrokehq_database.claimDueTriggerSchedules)(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) await enqueueTriggerJob(ctx.jobQueue, row, asOf);
|
|
34
|
-
return claimed.length;
|
|
35
|
-
}
|
|
36
|
-
async function startScheduleTicker(options = {}) {
|
|
37
|
-
const intervalMs = options.pollIntervalMs ?? pollIntervalMs;
|
|
38
|
-
const limit = options.batchSize ?? batchSize;
|
|
39
|
-
const scope = options.scope ?? ctx.scope ?? "project";
|
|
40
|
-
let running = true;
|
|
41
|
-
const loop = async () => {
|
|
42
|
-
while (running) {
|
|
43
|
-
try {
|
|
44
|
-
const claimed = await claimDue(scope, /* @__PURE__ */ new Date(), (schedule) => (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, /* @__PURE__ */ new Date()), limit);
|
|
45
|
-
for (const row of claimed) await enqueueTriggerJob(ctx.jobQueue, row);
|
|
46
|
-
} catch {}
|
|
47
|
-
await sleep$1(intervalMs);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
loop();
|
|
51
|
-
return async () => {
|
|
52
|
-
running = false;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
fireDueSchedules,
|
|
57
|
-
startScheduleTicker
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function sleep$1(ms) {
|
|
61
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
-
}
|
|
63
|
-
//#endregion
|
|
64
8
|
//#region src/cancel-channel.ts
|
|
65
9
|
/** In-process cancel pub/sub for single-process queues (memory, db polling). */
|
|
66
10
|
function createInProcessCancelChannel() {
|
|
@@ -113,7 +57,7 @@ function createDatabaseJobQueue() {
|
|
|
113
57
|
while (running) try {
|
|
114
58
|
const job = await (0, _keystrokehq_database.claimNextJob)(workerId);
|
|
115
59
|
if (!job) {
|
|
116
|
-
await sleep(pollIntervalMs);
|
|
60
|
+
await sleep$1(pollIntervalMs);
|
|
117
61
|
continue;
|
|
118
62
|
}
|
|
119
63
|
try {
|
|
@@ -127,7 +71,7 @@ function createDatabaseJobQueue() {
|
|
|
127
71
|
}
|
|
128
72
|
}
|
|
129
73
|
} catch {
|
|
130
|
-
await sleep(pollIntervalMs);
|
|
74
|
+
await sleep$1(pollIntervalMs);
|
|
131
75
|
}
|
|
132
76
|
};
|
|
133
77
|
loop();
|
|
@@ -140,10 +84,135 @@ function createDatabaseJobQueue() {
|
|
|
140
84
|
subscribeCancel: (handler) => cancelChannel.subscribeCancel(handler)
|
|
141
85
|
};
|
|
142
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
|
+
}
|
|
143
157
|
function sleep(ms) {
|
|
144
158
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
145
159
|
}
|
|
146
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
|
|
147
216
|
//#region src/pg-boss-client.ts
|
|
148
217
|
let boss;
|
|
149
218
|
function resolveDatabaseUrl(url) {
|
|
@@ -206,6 +275,8 @@ function buildPgBossJobQueue(boss, options) {
|
|
|
206
275
|
const queueName = options.queueName ?? "keystroke";
|
|
207
276
|
const cancelChannel = (0, _keystrokehq_database.createPostgresCancelChannel)(options.connectionString, pgCancelChannelName(options.cancelScope ?? "platform", options.cancelScope === "organization" ? options.organizationId : options.projectId));
|
|
208
277
|
return {
|
|
278
|
+
boss,
|
|
279
|
+
queueName,
|
|
209
280
|
async enqueue(input) {
|
|
210
281
|
const jobId = await boss.send(queueName, {
|
|
211
282
|
...input,
|
|
@@ -288,10 +359,132 @@ async function createSharedPgBossJobQueue(boss, options) {
|
|
|
288
359
|
});
|
|
289
360
|
}
|
|
290
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
|
|
291
468
|
//#region src/plugin.ts
|
|
292
469
|
function resolveUrl(ctx) {
|
|
293
470
|
return ctx.url ?? (0, _keystrokehq_database.resolveProjectDatabaseUrlFromEnv)(process.env) ?? _keystrokehq_database.DEFAULT_DATABASE_URL;
|
|
294
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
|
+
}
|
|
295
488
|
/** Postgres pg-boss backend. Platform scope shares one queue; org scope gets one queue per org. */
|
|
296
489
|
function pgBossSchedulerPlugin() {
|
|
297
490
|
return require_contract.defineSchedulerPlugin({
|
|
@@ -318,7 +511,8 @@ function pgBossSchedulerPlugin() {
|
|
|
318
511
|
queueName: pgBossProjectQueueName(projectId),
|
|
319
512
|
projectId
|
|
320
513
|
});
|
|
321
|
-
}
|
|
514
|
+
},
|
|
515
|
+
createTriggerScheduler: createPgBossTriggerSchedulerForPlugin
|
|
322
516
|
});
|
|
323
517
|
}
|
|
324
518
|
/** DB-table queue (Drizzle `jobs`), for SQLite / self-host. */
|
|
@@ -327,7 +521,8 @@ function pollingSchedulerPlugin() {
|
|
|
327
521
|
name: "polling",
|
|
328
522
|
async createJobQueue() {
|
|
329
523
|
return createDatabaseJobQueue();
|
|
330
|
-
}
|
|
524
|
+
},
|
|
525
|
+
createTriggerScheduler: createPollingTriggerScheduler
|
|
331
526
|
});
|
|
332
527
|
}
|
|
333
528
|
/** Auto-selects pg-boss (postgres) or polling (sqlite) by dialect. */
|
|
@@ -337,45 +532,14 @@ function defaultSchedulerPlugin() {
|
|
|
337
532
|
async createJobQueue(ctx) {
|
|
338
533
|
if ((0, _keystrokehq_database.inferDialect)(resolveUrl(ctx), ctx.dialect) === "postgres") return pgBossSchedulerPlugin().createJobQueue(ctx);
|
|
339
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);
|
|
340
539
|
}
|
|
341
540
|
});
|
|
342
541
|
}
|
|
343
542
|
//#endregion
|
|
344
|
-
//#region src/resolve-schedule.ts
|
|
345
|
-
function resolveTriggerSchedule(attachmentKey, schedule, overrides) {
|
|
346
|
-
return (0, _keystrokehq_trigger.resolveCronSchedule)(attachmentKey, schedule, {
|
|
347
|
-
cronScheduleOverride: overrides?.global,
|
|
348
|
-
attachmentScheduleOverrides: overrides?.byAttachment
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
//#endregion
|
|
352
|
-
//#region src/sync-trigger-schedules.ts
|
|
353
|
-
async function syncTriggerSchedules(options) {
|
|
354
|
-
const now = /* @__PURE__ */ new Date();
|
|
355
|
-
const projectSlugs = options.schedules.map((schedule) => schedule.attachmentKey);
|
|
356
|
-
const ephemeralSlugs = await (0, _keystrokehq_database.selectActiveEphemeralScheduledAttachmentSlugs)();
|
|
357
|
-
const slugs = [...projectSlugs, ...ephemeralSlugs];
|
|
358
|
-
if (slugs.length === 0) {
|
|
359
|
-
await (0, _keystrokehq_database.disableAllTriggerSchedules)(now);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
await (0, _keystrokehq_database.disableTriggerSchedulesNotInSlugs)(slugs, now);
|
|
363
|
-
for (const spec of options.schedules) {
|
|
364
|
-
const schedule = resolveTriggerSchedule(spec.attachmentKey, spec.schedule, options.scheduleOverrides);
|
|
365
|
-
const existing = await (0, _keystrokehq_database.selectTriggerScheduleBySlug)(spec.attachmentKey);
|
|
366
|
-
const scheduleChanged = existing?.schedule !== schedule;
|
|
367
|
-
const nextRunAt = existing && !scheduleChanged && existing.enabled === 1 ? existing.nextRunAt : (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, now);
|
|
368
|
-
await (0, _keystrokehq_database.upsertTriggerSchedule)({
|
|
369
|
-
attachmentSlug: spec.attachmentKey,
|
|
370
|
-
kind: spec.kind,
|
|
371
|
-
schedule,
|
|
372
|
-
nextRunAt,
|
|
373
|
-
enabled: true,
|
|
374
|
-
updatedAt: now
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
//#endregion
|
|
379
543
|
//#region src/create-scheduler.ts
|
|
380
544
|
async function createUnderlyingJobQueue(options = {}) {
|
|
381
545
|
if (options.adapter) return options.adapter;
|
|
@@ -387,29 +551,50 @@ async function createUnderlyingJobQueue(options = {}) {
|
|
|
387
551
|
organizationId: options.organizationId
|
|
388
552
|
});
|
|
389
553
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
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({
|
|
392
569
|
jobQueue,
|
|
393
570
|
scope
|
|
394
571
|
});
|
|
572
|
+
}
|
|
573
|
+
function wrapScheduler(jobQueue, triggerScheduler) {
|
|
395
574
|
return {
|
|
396
575
|
enqueue: (input) => jobQueue.enqueue(input),
|
|
397
|
-
startWorker: (handler,
|
|
576
|
+
startWorker: (handler, workerOptions) => jobQueue.startWorker(handler, workerOptions),
|
|
398
577
|
publishCancel: (runId) => jobQueue.publishCancel(runId),
|
|
399
578
|
subscribeCancel: (handler) => jobQueue.subscribeCancel(handler),
|
|
400
|
-
syncTriggerSchedules: (options) =>
|
|
401
|
-
startScheduleTicker: (options) =>
|
|
402
|
-
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) } : {}
|
|
403
584
|
};
|
|
404
585
|
}
|
|
405
586
|
async function createScheduler(options = {}) {
|
|
406
|
-
|
|
587
|
+
const jobQueue = await createUnderlyingJobQueue(options);
|
|
588
|
+
return wrapScheduler(jobQueue, await createTriggerScheduler(options, jobQueue));
|
|
407
589
|
}
|
|
408
590
|
async function createJobQueue(options = {}) {
|
|
409
591
|
return createUnderlyingJobQueue(options);
|
|
410
592
|
}
|
|
411
|
-
function wrapJobQueueAsScheduler(jobQueue, scope) {
|
|
412
|
-
return wrapScheduler(jobQueue,
|
|
593
|
+
async function wrapJobQueueAsScheduler(jobQueue, scope) {
|
|
594
|
+
return wrapScheduler(jobQueue, createDatabaseTriggerScheduler({
|
|
595
|
+
jobQueue,
|
|
596
|
+
scope
|
|
597
|
+
}));
|
|
413
598
|
}
|
|
414
599
|
//#endregion
|
|
415
600
|
//#region src/memory.ts
|
|
@@ -468,7 +653,7 @@ function createMemoryJobQueue(options = {}) {
|
|
|
468
653
|
//#region src/shared-pgboss-queue.ts
|
|
469
654
|
let sharedJobQueue$1;
|
|
470
655
|
async function createSharedPgBossScheduler() {
|
|
471
|
-
return wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
|
|
656
|
+
return await wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
|
|
472
657
|
}
|
|
473
658
|
async function getSharedPgBossJobQueue() {
|
|
474
659
|
if (sharedJobQueue$1) return sharedJobQueue$1;
|
|
@@ -492,7 +677,7 @@ async function createSharedScheduler() {
|
|
|
492
677
|
const plugin = configuredPlugin ?? defaultSchedulerPlugin();
|
|
493
678
|
if (plugin.name === "default") return createSharedPgBossScheduler();
|
|
494
679
|
if (!sharedJobQueue) sharedJobQueue = await plugin.createJobQueue({ scope: "platform" });
|
|
495
|
-
return wrapJobQueueAsScheduler(sharedJobQueue);
|
|
680
|
+
return await wrapJobQueueAsScheduler(sharedJobQueue);
|
|
496
681
|
}
|
|
497
682
|
function resetSharedSchedulerForTests() {
|
|
498
683
|
configuredPlugin = void 0;
|
|
@@ -504,8 +689,10 @@ exports.DEFAULT_QUEUE_NAME = DEFAULT_QUEUE_NAME;
|
|
|
504
689
|
exports.DEFAULT_RETRY_DELAY_MS = require_contract.DEFAULT_RETRY_DELAY_MS;
|
|
505
690
|
exports.buildPgBossJobQueue = buildPgBossJobQueue;
|
|
506
691
|
exports.configureSharedScheduler = configureSharedScheduler;
|
|
692
|
+
exports.createDatabaseTriggerScheduler = createDatabaseTriggerScheduler;
|
|
507
693
|
exports.createJobQueue = createJobQueue;
|
|
508
694
|
exports.createMemoryJobQueue = createMemoryJobQueue;
|
|
695
|
+
exports.createPgBossTriggerScheduler = createPgBossTriggerScheduler;
|
|
509
696
|
exports.createScheduler = createScheduler;
|
|
510
697
|
exports.createSharedPgBossJobQueue = createSharedPgBossJobQueue;
|
|
511
698
|
exports.createSharedPgBossScheduler = createSharedPgBossScheduler;
|
|
@@ -516,12 +703,16 @@ exports.getPgBoss = getPgBoss;
|
|
|
516
703
|
exports.getSharedPgBossJobQueue = getSharedPgBossJobQueue;
|
|
517
704
|
exports.pgBossProjectQueueName = pgBossProjectQueueName;
|
|
518
705
|
exports.pgBossSchedulerPlugin = pgBossSchedulerPlugin;
|
|
706
|
+
exports.pgBossTriggerScheduleKey = pgBossTriggerScheduleKey;
|
|
707
|
+
exports.pgBossTriggerScheduleKeyPrefix = pgBossTriggerScheduleKeyPrefix;
|
|
519
708
|
exports.pollingSchedulerPlugin = pollingSchedulerPlugin;
|
|
520
709
|
exports.resetSharedPgBossJobQueueForTests = resetSharedPgBossJobQueueForTests;
|
|
521
710
|
exports.resetSharedSchedulerForTests = resetSharedSchedulerForTests;
|
|
711
|
+
exports.resolveTriggerSchedule = resolveTriggerSchedule;
|
|
522
712
|
exports.retryDelayMs = require_contract.retryDelayMs;
|
|
523
713
|
exports.startPgBoss = startPgBoss;
|
|
524
714
|
exports.stopPgBoss = stopPgBoss;
|
|
715
|
+
exports.syncTriggerSchedules = syncTriggerSchedules;
|
|
525
716
|
exports.wrapJobQueueAsScheduler = wrapJobQueueAsScheduler;
|
|
526
717
|
|
|
527
718
|
//# sourceMappingURL=index.cjs.map
|