@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.mjs
CHANGED
|
@@ -1,65 +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,
|
|
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.attachmentSlug,
|
|
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 claimDueTriggerSchedulesForOrg(asOf, resolveNextRunAt, limit);
|
|
22
|
-
return claimDueTriggerSchedules(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) await enqueueTriggerJob(ctx.jobQueue, row, asOf);
|
|
33
|
-
return claimed.length;
|
|
34
|
-
}
|
|
35
|
-
async function startScheduleTicker(options = {}) {
|
|
36
|
-
const intervalMs = options.pollIntervalMs ?? pollIntervalMs;
|
|
37
|
-
const limit = options.batchSize ?? batchSize;
|
|
38
|
-
const scope = options.scope ?? ctx.scope ?? "project";
|
|
39
|
-
let running = true;
|
|
40
|
-
const loop = async () => {
|
|
41
|
-
while (running) {
|
|
42
|
-
try {
|
|
43
|
-
const claimed = await claimDue(scope, /* @__PURE__ */ new Date(), (schedule) => nextTriggerRunAt(schedule, /* @__PURE__ */ new Date()), limit);
|
|
44
|
-
for (const row of claimed) await enqueueTriggerJob(ctx.jobQueue, row);
|
|
45
|
-
} catch {}
|
|
46
|
-
await sleep$1(intervalMs);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
loop();
|
|
50
|
-
return async () => {
|
|
51
|
-
running = false;
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
fireDueSchedules,
|
|
56
|
-
startScheduleTicker
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function sleep$1(ms) {
|
|
60
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
7
|
//#region src/cancel-channel.ts
|
|
64
8
|
/** In-process cancel pub/sub for single-process queues (memory, db polling). */
|
|
65
9
|
function createInProcessCancelChannel() {
|
|
@@ -112,7 +56,7 @@ function createDatabaseJobQueue() {
|
|
|
112
56
|
while (running) try {
|
|
113
57
|
const job = await claimNextJob(workerId);
|
|
114
58
|
if (!job) {
|
|
115
|
-
await sleep(pollIntervalMs);
|
|
59
|
+
await sleep$1(pollIntervalMs);
|
|
116
60
|
continue;
|
|
117
61
|
}
|
|
118
62
|
try {
|
|
@@ -126,7 +70,7 @@ function createDatabaseJobQueue() {
|
|
|
126
70
|
}
|
|
127
71
|
}
|
|
128
72
|
} catch {
|
|
129
|
-
await sleep(pollIntervalMs);
|
|
73
|
+
await sleep$1(pollIntervalMs);
|
|
130
74
|
}
|
|
131
75
|
};
|
|
132
76
|
loop();
|
|
@@ -139,10 +83,135 @@ function createDatabaseJobQueue() {
|
|
|
139
83
|
subscribeCancel: (handler) => cancelChannel.subscribeCancel(handler)
|
|
140
84
|
};
|
|
141
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
|
+
}
|
|
142
156
|
function sleep(ms) {
|
|
143
157
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
144
158
|
}
|
|
145
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
|
|
146
215
|
//#region src/pg-boss-client.ts
|
|
147
216
|
let boss;
|
|
148
217
|
function resolveDatabaseUrl(url) {
|
|
@@ -205,6 +274,8 @@ function buildPgBossJobQueue(boss, options) {
|
|
|
205
274
|
const queueName = options.queueName ?? "keystroke";
|
|
206
275
|
const cancelChannel = createPgCancelChannel(options.connectionString, pgCancelChannelName(options.cancelScope ?? "platform", options.cancelScope === "organization" ? options.organizationId : options.projectId));
|
|
207
276
|
return {
|
|
277
|
+
boss,
|
|
278
|
+
queueName,
|
|
208
279
|
async enqueue(input) {
|
|
209
280
|
const jobId = await boss.send(queueName, {
|
|
210
281
|
...input,
|
|
@@ -287,10 +358,132 @@ async function createSharedPgBossJobQueue(boss, options) {
|
|
|
287
358
|
});
|
|
288
359
|
}
|
|
289
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
|
|
290
467
|
//#region src/plugin.ts
|
|
291
468
|
function resolveUrl(ctx) {
|
|
292
469
|
return ctx.url ?? resolveProjectDatabaseUrlFromEnv(process.env) ?? DEFAULT_DATABASE_URL;
|
|
293
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
|
+
}
|
|
294
487
|
/** Postgres pg-boss backend. Platform scope shares one queue; org scope gets one queue per org. */
|
|
295
488
|
function pgBossSchedulerPlugin() {
|
|
296
489
|
return defineSchedulerPlugin({
|
|
@@ -317,7 +510,8 @@ function pgBossSchedulerPlugin() {
|
|
|
317
510
|
queueName: pgBossProjectQueueName(projectId),
|
|
318
511
|
projectId
|
|
319
512
|
});
|
|
320
|
-
}
|
|
513
|
+
},
|
|
514
|
+
createTriggerScheduler: createPgBossTriggerSchedulerForPlugin
|
|
321
515
|
});
|
|
322
516
|
}
|
|
323
517
|
/** DB-table queue (Drizzle `jobs`), for SQLite / self-host. */
|
|
@@ -326,7 +520,8 @@ function pollingSchedulerPlugin() {
|
|
|
326
520
|
name: "polling",
|
|
327
521
|
async createJobQueue() {
|
|
328
522
|
return createDatabaseJobQueue();
|
|
329
|
-
}
|
|
523
|
+
},
|
|
524
|
+
createTriggerScheduler: createPollingTriggerScheduler
|
|
330
525
|
});
|
|
331
526
|
}
|
|
332
527
|
/** Auto-selects pg-boss (postgres) or polling (sqlite) by dialect. */
|
|
@@ -336,45 +531,14 @@ function defaultSchedulerPlugin() {
|
|
|
336
531
|
async createJobQueue(ctx) {
|
|
337
532
|
if (inferDialect(resolveUrl(ctx), ctx.dialect) === "postgres") return pgBossSchedulerPlugin().createJobQueue(ctx);
|
|
338
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);
|
|
339
538
|
}
|
|
340
539
|
});
|
|
341
540
|
}
|
|
342
541
|
//#endregion
|
|
343
|
-
//#region src/resolve-schedule.ts
|
|
344
|
-
function resolveTriggerSchedule(attachmentKey, schedule, overrides) {
|
|
345
|
-
return resolveCronSchedule(attachmentKey, schedule, {
|
|
346
|
-
cronScheduleOverride: overrides?.global,
|
|
347
|
-
attachmentScheduleOverrides: overrides?.byAttachment
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
//#endregion
|
|
351
|
-
//#region src/sync-trigger-schedules.ts
|
|
352
|
-
async function syncTriggerSchedules(options) {
|
|
353
|
-
const now = /* @__PURE__ */ new Date();
|
|
354
|
-
const projectSlugs = options.schedules.map((schedule) => schedule.attachmentKey);
|
|
355
|
-
const ephemeralSlugs = await selectActiveEphemeralScheduledAttachmentSlugs();
|
|
356
|
-
const slugs = [...projectSlugs, ...ephemeralSlugs];
|
|
357
|
-
if (slugs.length === 0) {
|
|
358
|
-
await disableAllTriggerSchedules(now);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
await disableTriggerSchedulesNotInSlugs(slugs, now);
|
|
362
|
-
for (const spec of options.schedules) {
|
|
363
|
-
const schedule = resolveTriggerSchedule(spec.attachmentKey, spec.schedule, options.scheduleOverrides);
|
|
364
|
-
const existing = await selectTriggerScheduleBySlug(spec.attachmentKey);
|
|
365
|
-
const scheduleChanged = existing?.schedule !== schedule;
|
|
366
|
-
const nextRunAt = existing && !scheduleChanged && existing.enabled === 1 ? existing.nextRunAt : nextTriggerRunAt(schedule, now);
|
|
367
|
-
await upsertTriggerSchedule({
|
|
368
|
-
attachmentSlug: spec.attachmentKey,
|
|
369
|
-
kind: spec.kind,
|
|
370
|
-
schedule,
|
|
371
|
-
nextRunAt,
|
|
372
|
-
enabled: true,
|
|
373
|
-
updatedAt: now
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
//#endregion
|
|
378
542
|
//#region src/create-scheduler.ts
|
|
379
543
|
async function createUnderlyingJobQueue(options = {}) {
|
|
380
544
|
if (options.adapter) return options.adapter;
|
|
@@ -386,29 +550,50 @@ async function createUnderlyingJobQueue(options = {}) {
|
|
|
386
550
|
organizationId: options.organizationId
|
|
387
551
|
});
|
|
388
552
|
}
|
|
389
|
-
function
|
|
390
|
-
|
|
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({
|
|
391
568
|
jobQueue,
|
|
392
569
|
scope
|
|
393
570
|
});
|
|
571
|
+
}
|
|
572
|
+
function wrapScheduler(jobQueue, triggerScheduler) {
|
|
394
573
|
return {
|
|
395
574
|
enqueue: (input) => jobQueue.enqueue(input),
|
|
396
|
-
startWorker: (handler,
|
|
575
|
+
startWorker: (handler, workerOptions) => jobQueue.startWorker(handler, workerOptions),
|
|
397
576
|
publishCancel: (runId) => jobQueue.publishCancel(runId),
|
|
398
577
|
subscribeCancel: (handler) => jobQueue.subscribeCancel(handler),
|
|
399
|
-
syncTriggerSchedules: (options) =>
|
|
400
|
-
startScheduleTicker: (options) =>
|
|
401
|
-
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) } : {}
|
|
402
583
|
};
|
|
403
584
|
}
|
|
404
585
|
async function createScheduler(options = {}) {
|
|
405
|
-
|
|
586
|
+
const jobQueue = await createUnderlyingJobQueue(options);
|
|
587
|
+
return wrapScheduler(jobQueue, await createTriggerScheduler(options, jobQueue));
|
|
406
588
|
}
|
|
407
589
|
async function createJobQueue(options = {}) {
|
|
408
590
|
return createUnderlyingJobQueue(options);
|
|
409
591
|
}
|
|
410
|
-
function wrapJobQueueAsScheduler(jobQueue, scope) {
|
|
411
|
-
return wrapScheduler(jobQueue,
|
|
592
|
+
async function wrapJobQueueAsScheduler(jobQueue, scope) {
|
|
593
|
+
return wrapScheduler(jobQueue, createDatabaseTriggerScheduler({
|
|
594
|
+
jobQueue,
|
|
595
|
+
scope
|
|
596
|
+
}));
|
|
412
597
|
}
|
|
413
598
|
//#endregion
|
|
414
599
|
//#region src/memory.ts
|
|
@@ -467,7 +652,7 @@ function createMemoryJobQueue(options = {}) {
|
|
|
467
652
|
//#region src/shared-pgboss-queue.ts
|
|
468
653
|
let sharedJobQueue$1;
|
|
469
654
|
async function createSharedPgBossScheduler() {
|
|
470
|
-
return wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
|
|
655
|
+
return await wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
|
|
471
656
|
}
|
|
472
657
|
async function getSharedPgBossJobQueue() {
|
|
473
658
|
if (sharedJobQueue$1) return sharedJobQueue$1;
|
|
@@ -491,7 +676,7 @@ async function createSharedScheduler() {
|
|
|
491
676
|
const plugin = configuredPlugin ?? defaultSchedulerPlugin();
|
|
492
677
|
if (plugin.name === "default") return createSharedPgBossScheduler();
|
|
493
678
|
if (!sharedJobQueue) sharedJobQueue = await plugin.createJobQueue({ scope: "platform" });
|
|
494
|
-
return wrapJobQueueAsScheduler(sharedJobQueue);
|
|
679
|
+
return await wrapJobQueueAsScheduler(sharedJobQueue);
|
|
495
680
|
}
|
|
496
681
|
function resetSharedSchedulerForTests() {
|
|
497
682
|
configuredPlugin = void 0;
|
|
@@ -499,6 +684,6 @@ function resetSharedSchedulerForTests() {
|
|
|
499
684
|
resetSharedPgBossJobQueueForTests();
|
|
500
685
|
}
|
|
501
686
|
//#endregion
|
|
502
|
-
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 };
|
|
503
688
|
|
|
504
689
|
//# sourceMappingURL=index.mjs.map
|