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