@keystrokehq/scheduler 0.0.66

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.
@@ -0,0 +1,68 @@
1
+ import { _ as Scheduler, a as defineSchedulerPlugin, b as retryDelayMs, c as DEFAULT_RETRY_DELAY_MS, f as JobPayload, g as ScheduleTickerOptions, h as ScheduleSyncOptions, i as SchedulerScope, l as EnqueueInput, m as JobTrigger, n as SchedulerPlugin, o as CreateJobQueueOptions, p as JobQueue, r as SchedulerPluginContext, s as CreateSchedulerOptions, u as JobHandler, v as StopFn, y as WorkerOptions } from "./contract-CYKkRmnH.mjs";
2
+ import { PgBoss } from "pg-boss";
3
+
4
+ //#region src/create-scheduler.d.ts
5
+ declare function createScheduler(options?: CreateSchedulerOptions): Promise<Scheduler>;
6
+ declare function createJobQueue(options?: CreateJobQueueOptions): Promise<JobQueue>;
7
+ declare function wrapJobQueueAsScheduler(jobQueue: JobQueue): Scheduler;
8
+ //#endregion
9
+ //#region src/memory.d.ts
10
+ type MemoryJobQueueOptions = {
11
+ sync?: boolean;
12
+ };
13
+ declare function createMemoryJobQueue(options?: MemoryJobQueueOptions): JobQueue;
14
+ //#endregion
15
+ //#region src/shared-scheduler.d.ts
16
+ declare function configureSharedScheduler(plugin: SchedulerPlugin): void;
17
+ declare function createSharedScheduler(): Promise<Scheduler>;
18
+ declare function resetSharedSchedulerForTests(): void;
19
+ //#endregion
20
+ //#region src/plugin.d.ts
21
+ /** Postgres pg-boss backend. Platform scope shares one queue; project scope gets a per-project queue. */
22
+ declare function pgBossSchedulerPlugin(): SchedulerPlugin;
23
+ /** DB-table queue (Drizzle `jobs`), for SQLite / self-host. */
24
+ declare function pollingSchedulerPlugin(): SchedulerPlugin;
25
+ /** Auto-selects pg-boss (postgres) or polling (sqlite) by dialect. */
26
+ declare function defaultSchedulerPlugin(): SchedulerPlugin;
27
+ //#endregion
28
+ //#region src/resolve-plugin-from-env.d.ts
29
+ /** Env var naming a module that exports `createSchedulerPlugin(env): SchedulerPlugin`. */
30
+ declare const SCHEDULER_MODULE_ENV = "KEYSTROKE_SCHEDULER_MODULE";
31
+ type SchedulerEnv = Record<string, string | undefined>;
32
+ /**
33
+ * Resolve a scheduler plugin selected via env so a producer (platform) and a
34
+ * consumer (container worker) can never diverge on backend. When
35
+ * `KEYSTROKE_SCHEDULER_MODULE` is set, dynamically import that module and call
36
+ * its `createSchedulerPlugin(env)`
37
+ */
38
+ declare function resolveSchedulerPluginFromEnv(env?: SchedulerEnv): Promise<SchedulerPlugin | undefined>;
39
+ //#endregion
40
+ //#region src/pg-boss-client.d.ts
41
+ declare function startPgBoss(url?: string): Promise<PgBoss>;
42
+ declare function getPgBoss(): PgBoss;
43
+ declare function stopPgBoss(): Promise<void>;
44
+ //#endregion
45
+ //#region src/shared-pgboss-queue.d.ts
46
+ declare function createSharedPgBossScheduler(): Promise<Scheduler>;
47
+ declare function getSharedPgBossJobQueue(): Promise<JobQueue>;
48
+ declare function resetSharedPgBossJobQueueForTests(): void;
49
+ //#endregion
50
+ //#region src/pg-boss-queue.d.ts
51
+ /** Shared queue name for the platform control-plane scope. */
52
+ declare const DEFAULT_QUEUE_NAME = "keystroke";
53
+ /**
54
+ * Derive a per-project pg-boss queue name. pg-boss queue names must be <= 50
55
+ * chars, contain only [A-Za-z0-9_], and not start with a digit. Project ids are
56
+ * usually UUIDs (hyphens, may start with a digit), so we sanitize, and fall back
57
+ * to a hash when the sanitized name would exceed the length limit.
58
+ */
59
+ declare function pgBossProjectQueueName(projectId: string): string;
60
+ type BuildPgBossJobQueueOptions = {
61
+ stopBossOnWorkerStop?: boolean;
62
+ queueName?: string;
63
+ };
64
+ declare function buildPgBossJobQueue(boss: PgBoss, options?: BuildPgBossJobQueueOptions): JobQueue;
65
+ declare function createSharedPgBossJobQueue(boss: PgBoss, queueName?: string): Promise<JobQueue>;
66
+ //#endregion
67
+ export { type CreateJobQueueOptions, type CreateSchedulerOptions, DEFAULT_QUEUE_NAME, DEFAULT_RETRY_DELAY_MS, type EnqueueInput, type JobHandler, type JobPayload, type JobQueue, type JobTrigger, type MemoryJobQueueOptions, SCHEDULER_MODULE_ENV, type ScheduleSyncOptions, type ScheduleTickerOptions, type Scheduler, type SchedulerPlugin, type SchedulerPluginContext, type SchedulerScope, type StopFn, type WorkerOptions, buildPgBossJobQueue, configureSharedScheduler, createJobQueue, createMemoryJobQueue, createScheduler, createSharedPgBossJobQueue, createSharedPgBossScheduler, createSharedScheduler, defaultSchedulerPlugin, defineSchedulerPlugin, getPgBoss, getSharedPgBossJobQueue, pgBossProjectQueueName, pgBossSchedulerPlugin, pollingSchedulerPlugin, resetSharedPgBossJobQueueForTests, resetSharedSchedulerForTests, resolveSchedulerPluginFromEnv, retryDelayMs, startPgBoss, stopPgBoss, wrapJobQueueAsScheduler };
68
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/create-scheduler.ts","../src/memory.ts","../src/shared-scheduler.ts","../src/plugin.ts","../src/resolve-plugin-from-env.ts","../src/pg-boss-client.ts","../src/shared-pgboss-queue.ts","../src/pg-boss-queue.ts"],"mappings":";;;;iBAwCsB,eAAA,CAAgB,OAAA,GAAS,sBAAA,GAA8B,OAAA,CAAQ,SAAA;AAAA,iBAK/D,cAAA,CAAe,OAAA,GAAS,qBAAA,GAA6B,OAAA,CAAQ,QAAA;AAAA,iBAInE,uBAAA,CAAwB,QAAA,EAAU,QAAA,GAAW,SAAS;;;KC9C1D,qBAAA;EACV,IAAI;AAAA;AAAA,iBAGU,oBAAA,CAAqB,OAAA,GAAS,qBAAA,GAA6B,QAAQ;;;iBCKnE,wBAAA,CAAyB,MAAuB,EAAf,eAAe;AAAA,iBAK1C,qBAAA,CAAA,GAAyB,OAAO,CAAC,SAAA;AAAA,iBAcvC,4BAAA,CAAA;;;;iBCHA,qBAAA,CAAA,GAAyB,eAAe;;iBAqBxC,sBAAA,CAAA,GAA0B,eAAe;;iBAUzC,sBAAA,CAAA,GAA0B,eAAe;;;;cCxD5C,oBAAA;AAAA,KAER,YAAA,GAAe,MAAM;AJmC1B;;;;;;AAAA,iBIXsB,6BAAA,CACpB,GAAA,GAAK,YAAA,GACJ,OAAA,CAAQ,eAAA;;;iBChBW,WAAA,CAAY,GAAA,YAAe,OAAO,CAAC,MAAA;AAAA,iBAkBzC,SAAA,CAAA,GAAa,MAAM;AAAA,iBAOb,UAAA,CAAA,GAAc,OAAO;;;iBCjCrB,2BAAA,CAAA,GAA+B,OAAO,CAAC,SAAA;AAAA,iBAKvC,uBAAA,CAAA,GAA2B,OAAO,CAAC,QAAA;AAAA,iBASzC,iCAAA,CAAA;;;;cCfH,kBAAA;APkCb;;;;;;AAAA,iBOzBgB,sBAAA,CAAuB,SAAiB;AAAA,KAyBnD,0BAAA;EACH,oBAAA;EACA,SAAS;AAAA;AAAA,iBAGK,mBAAA,CACd,IAAA,EAAM,MAAA,EACN,OAAA,GAAS,0BAAA,GACR,QAAA;AAAA,iBAyEmB,0BAAA,CACpB,IAAA,EAAM,MAAA,EACN,SAAA,YACC,OAAA,CAAQ,QAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,440 @@
1
+ import { n as DEFAULT_RETRY_DELAY_MS, r as retryDelayMs, t as defineSchedulerPlugin } from "./contract-E1QJBH6_.mjs";
2
+ import { DEFAULT_DATABASE_URL, claimDueTriggerSchedules, claimNextJob, disableAllTriggerSchedules, disableTriggerSchedulesNotInKeys, enqueueJob, failWorkflowRun, getProjectScopeId, inferDialect, markJobComplete, markJobFailed, requeueExpiredLeases, resolvePostgresUrlFromEnv, resolveProjectDatabaseUrlFromEnv, scheduleJobRetry, selectTriggerScheduleByKey, upsertTriggerSchedule } from "@keystrokehq/database";
3
+ import { nextTriggerRunAt, resolveCronSchedule } from "@keystrokehq/trigger";
4
+ import { PgBoss } from "pg-boss";
5
+ import { createHash } from "node:crypto";
6
+ //#region src/schedule-ticker.ts
7
+ function createScheduleTicker(ctx) {
8
+ const pollIntervalMs = ctx.pollIntervalMs ?? 1e3;
9
+ const batchSize = ctx.batchSize ?? 10;
10
+ async function fireDueSchedules(asOf = /* @__PURE__ */ new Date()) {
11
+ const claimed = await claimDueTriggerSchedules(asOf, (schedule) => nextTriggerRunAt(schedule, asOf), batchSize);
12
+ for (const row of claimed) await ctx.jobQueue.enqueue({
13
+ kind: "trigger",
14
+ targetId: row.attachmentKey,
15
+ runId: crypto.randomUUID(),
16
+ trigger: row.kind,
17
+ payload: {},
18
+ scheduledAt: asOf
19
+ });
20
+ return claimed.length;
21
+ }
22
+ async function startScheduleTicker(options = {}) {
23
+ const intervalMs = options.pollIntervalMs ?? pollIntervalMs;
24
+ const limit = options.batchSize ?? batchSize;
25
+ let running = true;
26
+ const loop = async () => {
27
+ while (running) {
28
+ try {
29
+ await claimDueTriggerSchedules(/* @__PURE__ */ new Date(), (schedule) => nextTriggerRunAt(schedule, /* @__PURE__ */ new Date()), limit).then(async (claimed) => {
30
+ for (const row of claimed) await ctx.jobQueue.enqueue({
31
+ kind: "trigger",
32
+ targetId: row.attachmentKey,
33
+ runId: crypto.randomUUID(),
34
+ trigger: row.kind,
35
+ payload: {}
36
+ });
37
+ });
38
+ } catch {}
39
+ await sleep$1(intervalMs);
40
+ }
41
+ };
42
+ loop();
43
+ return async () => {
44
+ running = false;
45
+ };
46
+ }
47
+ return {
48
+ fireDueSchedules,
49
+ startScheduleTicker
50
+ };
51
+ }
52
+ function sleep$1(ms) {
53
+ return new Promise((resolve) => setTimeout(resolve, ms));
54
+ }
55
+ //#endregion
56
+ //#region src/database-queue.ts
57
+ function toJobPayload(job) {
58
+ return {
59
+ jobId: job.id,
60
+ kind: job.kind,
61
+ targetId: job.targetId,
62
+ runId: job.runId,
63
+ trigger: job.trigger,
64
+ payload: job.payload,
65
+ attempt: job.attempt,
66
+ maxAttempts: job.maxAttempts,
67
+ scheduledAt: job.scheduledAt
68
+ };
69
+ }
70
+ function createDatabaseJobQueue() {
71
+ return {
72
+ async enqueue(input) {
73
+ return enqueueJob(input);
74
+ },
75
+ async startWorker(handler, options = {}) {
76
+ const workerId = options.workerId ?? crypto.randomUUID();
77
+ const pollIntervalMs = options.pollIntervalMs ?? 250;
78
+ const leaseSweepIntervalMs = options.leaseSweepIntervalMs ?? 3e4;
79
+ let running = true;
80
+ const leaseTimer = setInterval(() => {
81
+ requeueExpiredLeases();
82
+ }, leaseSweepIntervalMs);
83
+ const loop = async () => {
84
+ while (running) try {
85
+ const job = await claimNextJob(workerId);
86
+ if (!job) {
87
+ await sleep(pollIntervalMs);
88
+ continue;
89
+ }
90
+ try {
91
+ await handler(toJobPayload(job));
92
+ await markJobComplete(job.id);
93
+ } catch (error) {
94
+ if (job.attempt < job.maxAttempts) await scheduleJobRetry(job.id, job.attempt + 1, retryDelayMs(job.attempt));
95
+ else {
96
+ await markJobFailed(job.id, error);
97
+ if (job.kind === "workflow") await failWorkflowRun(job.runId, error);
98
+ }
99
+ }
100
+ } catch {
101
+ await sleep(pollIntervalMs);
102
+ }
103
+ };
104
+ loop();
105
+ return async () => {
106
+ running = false;
107
+ clearInterval(leaseTimer);
108
+ };
109
+ }
110
+ };
111
+ }
112
+ function sleep(ms) {
113
+ return new Promise((resolve) => setTimeout(resolve, ms));
114
+ }
115
+ //#endregion
116
+ //#region src/pg-boss-client.ts
117
+ let boss;
118
+ function resolveDatabaseUrl(url) {
119
+ const resolved = url ?? resolvePostgresUrlFromEnv(process.env);
120
+ if (!resolved) throw new Error("DATABASE_URL or POSTGRES_HOST/POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB is required");
121
+ return resolved;
122
+ }
123
+ async function startPgBoss(url) {
124
+ if (boss) return boss;
125
+ const next = new PgBoss({
126
+ connectionString: resolveDatabaseUrl(url),
127
+ schema: "pgboss"
128
+ });
129
+ next.on("error", (error) => {
130
+ console.error("[pg-boss]", error);
131
+ });
132
+ await next.start();
133
+ boss = next;
134
+ return next;
135
+ }
136
+ function getPgBoss() {
137
+ if (!boss) throw new Error("PgBoss not started. Call startPgBoss() first.");
138
+ return boss;
139
+ }
140
+ async function stopPgBoss() {
141
+ if (!boss) return;
142
+ await boss.stop();
143
+ boss = void 0;
144
+ }
145
+ //#endregion
146
+ //#region src/pg-boss-queue.ts
147
+ /** Shared queue name for the platform control-plane scope. */
148
+ const DEFAULT_QUEUE_NAME = "keystroke";
149
+ const PGBOSS_SCHEMA = "pgboss";
150
+ /**
151
+ * Derive a per-project pg-boss queue name. pg-boss queue names must be <= 50
152
+ * chars, contain only [A-Za-z0-9_], and not start with a digit. Project ids are
153
+ * usually UUIDs (hyphens, may start with a digit), so we sanitize, and fall back
154
+ * to a hash when the sanitized name would exceed the length limit.
155
+ */
156
+ function pgBossProjectQueueName(projectId) {
157
+ const name = `${DEFAULT_QUEUE_NAME}_${projectId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
158
+ if (name.length <= 50) return name;
159
+ return `${DEFAULT_QUEUE_NAME}_${createHash("sha1").update(projectId).digest("hex").slice(0, 40)}`;
160
+ }
161
+ function buildPgBossJobQueue(boss, options = {}) {
162
+ const queueName = options.queueName ?? "keystroke";
163
+ return {
164
+ async enqueue(input) {
165
+ const jobId = await boss.send(queueName, input, {
166
+ retryLimit: (input.maxAttempts ?? 3) - 1,
167
+ retryDelay: Math.ceil(retryDelayMs(1) / 1e3),
168
+ startAfter: input.scheduledAt
169
+ });
170
+ if (!jobId) throw new Error("Failed to enqueue job");
171
+ return jobId;
172
+ },
173
+ async startWorker(handler) {
174
+ let stopped = false;
175
+ const workerId = await boss.work(queueName, { batchSize: 1 }, async (jobs) => {
176
+ if (stopped) return;
177
+ const job = jobs[0];
178
+ if (!job) return;
179
+ const data = job.data;
180
+ await handler({
181
+ jobId: job.id,
182
+ kind: data.kind,
183
+ targetId: data.targetId,
184
+ runId: data.runId,
185
+ trigger: data.trigger,
186
+ payload: data.payload ?? {},
187
+ attempt: data.attempt ?? 1,
188
+ maxAttempts: data.maxAttempts ?? 3,
189
+ scheduledAt: data.scheduledAt ? new Date(data.scheduledAt) : /* @__PURE__ */ new Date()
190
+ });
191
+ });
192
+ return async () => {
193
+ stopped = true;
194
+ await boss.offWork(queueName, { id: workerId });
195
+ if (options.stopBossOnWorkerStop) await boss.stop({
196
+ graceful: true,
197
+ timeout: 5e3
198
+ });
199
+ };
200
+ }
201
+ };
202
+ }
203
+ async function createPgBossQueue(options) {
204
+ const queueName = options.queueName ?? "keystroke";
205
+ const boss = new PgBoss({
206
+ connectionString: options.connectionString,
207
+ schema: PGBOSS_SCHEMA
208
+ });
209
+ await boss.start();
210
+ await boss.createQueue(queueName);
211
+ return buildPgBossJobQueue(boss, {
212
+ stopBossOnWorkerStop: true,
213
+ queueName
214
+ });
215
+ }
216
+ async function createSharedPgBossJobQueue(boss, queueName = DEFAULT_QUEUE_NAME) {
217
+ await boss.createQueue(queueName);
218
+ return buildPgBossJobQueue(boss, { queueName });
219
+ }
220
+ //#endregion
221
+ //#region src/plugin.ts
222
+ function resolveUrl(ctx) {
223
+ return ctx.url ?? resolveProjectDatabaseUrlFromEnv(process.env) ?? DEFAULT_DATABASE_URL;
224
+ }
225
+ /** Postgres pg-boss backend. Platform scope shares one queue; project scope gets a per-project queue. */
226
+ function pgBossSchedulerPlugin() {
227
+ return defineSchedulerPlugin({
228
+ name: "pg-boss",
229
+ async createJobQueue(ctx) {
230
+ const url = resolveUrl(ctx);
231
+ if (ctx.scope === "platform") {
232
+ await startPgBoss(url);
233
+ return createSharedPgBossJobQueue(getPgBoss());
234
+ }
235
+ return createPgBossQueue({
236
+ connectionString: url,
237
+ queueName: pgBossProjectQueueName(ctx.projectId ?? getProjectScopeId())
238
+ });
239
+ }
240
+ });
241
+ }
242
+ /** DB-table queue (Drizzle `jobs`), for SQLite / self-host. */
243
+ function pollingSchedulerPlugin() {
244
+ return defineSchedulerPlugin({
245
+ name: "polling",
246
+ async createJobQueue() {
247
+ return createDatabaseJobQueue();
248
+ }
249
+ });
250
+ }
251
+ /** Auto-selects pg-boss (postgres) or polling (sqlite) by dialect. */
252
+ function defaultSchedulerPlugin() {
253
+ return defineSchedulerPlugin({
254
+ name: "default",
255
+ async createJobQueue(ctx) {
256
+ if (inferDialect(resolveUrl(ctx), ctx.dialect) === "postgres") return pgBossSchedulerPlugin().createJobQueue(ctx);
257
+ return pollingSchedulerPlugin().createJobQueue(ctx);
258
+ }
259
+ });
260
+ }
261
+ //#endregion
262
+ //#region src/resolve-schedule.ts
263
+ function resolveTriggerSchedule(attachmentKey, schedule, overrides) {
264
+ return resolveCronSchedule(attachmentKey, schedule, {
265
+ cronScheduleOverride: overrides?.global,
266
+ attachmentScheduleOverrides: overrides?.byAttachment
267
+ });
268
+ }
269
+ //#endregion
270
+ //#region src/sync-trigger-schedules.ts
271
+ async function syncTriggerSchedules(options) {
272
+ const now = /* @__PURE__ */ new Date();
273
+ const keys = options.schedules.map((schedule) => schedule.attachmentKey);
274
+ if (keys.length === 0) {
275
+ await disableAllTriggerSchedules(now);
276
+ return;
277
+ }
278
+ await disableTriggerSchedulesNotInKeys(keys, now);
279
+ for (const spec of options.schedules) {
280
+ const schedule = resolveTriggerSchedule(spec.attachmentKey, spec.schedule, options.scheduleOverrides);
281
+ const existing = await selectTriggerScheduleByKey(spec.attachmentKey);
282
+ const scheduleChanged = existing?.schedule !== schedule;
283
+ const nextRunAt = existing && !scheduleChanged && existing.enabled === 1 ? existing.nextRunAt : nextTriggerRunAt(schedule, now);
284
+ await upsertTriggerSchedule({
285
+ attachmentKey: spec.attachmentKey,
286
+ kind: spec.kind,
287
+ schedule,
288
+ nextRunAt,
289
+ enabled: true,
290
+ updatedAt: now
291
+ });
292
+ }
293
+ }
294
+ //#endregion
295
+ //#region src/create-scheduler.ts
296
+ async function createUnderlyingJobQueue(options = {}) {
297
+ if (options.adapter) return options.adapter;
298
+ return (options.plugin ?? defaultSchedulerPlugin()).createJobQueue({
299
+ scope: options.scope ?? "project",
300
+ url: options.url,
301
+ dialect: options.dialect,
302
+ projectId: options.projectId,
303
+ organizationId: options.organizationId
304
+ });
305
+ }
306
+ function wrapScheduler(jobQueue) {
307
+ const ticker = createScheduleTicker({ jobQueue });
308
+ return {
309
+ enqueue: (input) => jobQueue.enqueue(input),
310
+ startWorker: (handler, options) => jobQueue.startWorker(handler, options),
311
+ syncTriggerSchedules: (options) => syncTriggerSchedules(options),
312
+ startScheduleTicker: (options) => ticker.startScheduleTicker(options),
313
+ fireDueSchedules: (asOf) => ticker.fireDueSchedules(asOf)
314
+ };
315
+ }
316
+ async function createScheduler(options = {}) {
317
+ return wrapScheduler(await createUnderlyingJobQueue(options));
318
+ }
319
+ async function createJobQueue(options = {}) {
320
+ return createUnderlyingJobQueue(options);
321
+ }
322
+ function wrapJobQueueAsScheduler(jobQueue) {
323
+ return wrapScheduler(jobQueue);
324
+ }
325
+ //#endregion
326
+ //#region src/memory.ts
327
+ function createMemoryJobQueue(options = {}) {
328
+ const pending = [];
329
+ let handler;
330
+ let draining = false;
331
+ async function drain() {
332
+ if (!handler || draining) return;
333
+ draining = true;
334
+ try {
335
+ while (pending.length > 0) {
336
+ const next = pending.shift();
337
+ if (!next) break;
338
+ const input = next.input;
339
+ await handler({
340
+ jobId: next.id,
341
+ kind: input.kind,
342
+ targetId: input.targetId,
343
+ runId: input.runId,
344
+ trigger: input.trigger,
345
+ payload: input.payload ?? {},
346
+ attempt: input.attempt ?? 1,
347
+ maxAttempts: input.maxAttempts ?? 3,
348
+ scheduledAt: input.scheduledAt ?? next.enqueuedAt
349
+ });
350
+ }
351
+ } finally {
352
+ draining = false;
353
+ }
354
+ }
355
+ return {
356
+ async enqueue(input) {
357
+ const id = crypto.randomUUID();
358
+ pending.push({
359
+ id,
360
+ input,
361
+ enqueuedAt: /* @__PURE__ */ new Date()
362
+ });
363
+ if (options.sync) await drain();
364
+ return id;
365
+ },
366
+ async startWorker(nextHandler, _opts = {}) {
367
+ handler = nextHandler;
368
+ if (!options.sync) drain();
369
+ return async () => {
370
+ handler = void 0;
371
+ };
372
+ }
373
+ };
374
+ }
375
+ //#endregion
376
+ //#region src/shared-pgboss-queue.ts
377
+ let sharedJobQueue$1;
378
+ async function createSharedPgBossScheduler() {
379
+ return wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
380
+ }
381
+ async function getSharedPgBossJobQueue() {
382
+ if (sharedJobQueue$1) return sharedJobQueue$1;
383
+ sharedJobQueue$1 = await createSharedPgBossJobQueue(getPgBoss());
384
+ return sharedJobQueue$1;
385
+ }
386
+ function resetSharedPgBossJobQueueForTests() {
387
+ sharedJobQueue$1 = void 0;
388
+ }
389
+ //#endregion
390
+ //#region src/shared-scheduler.ts
391
+ let configuredPlugin;
392
+ let sharedJobQueue;
393
+ function configureSharedScheduler(plugin) {
394
+ configuredPlugin = plugin;
395
+ sharedJobQueue = void 0;
396
+ }
397
+ async function createSharedScheduler() {
398
+ const plugin = configuredPlugin ?? defaultSchedulerPlugin();
399
+ if (plugin.name === "default") return createSharedPgBossScheduler();
400
+ if (!sharedJobQueue) sharedJobQueue = await plugin.createJobQueue({ scope: "platform" });
401
+ return wrapJobQueueAsScheduler(sharedJobQueue);
402
+ }
403
+ function resetSharedSchedulerForTests() {
404
+ configuredPlugin = void 0;
405
+ sharedJobQueue = void 0;
406
+ resetSharedPgBossJobQueueForTests();
407
+ }
408
+ //#endregion
409
+ //#region src/resolve-plugin-from-env.ts
410
+ /** Env var naming a module that exports `createSchedulerPlugin(env): SchedulerPlugin`. */
411
+ const SCHEDULER_MODULE_ENV = "KEYSTROKE_SCHEDULER_MODULE";
412
+ function isSchedulerPlugin(value) {
413
+ if (typeof value !== "object" || value === null) return false;
414
+ const candidate = value;
415
+ return typeof candidate.name === "string" && typeof candidate.createJobQueue === "function";
416
+ }
417
+ function getCreateSchedulerPlugin(mod) {
418
+ if (typeof mod !== "object" || mod === null) return;
419
+ const factory = mod.createSchedulerPlugin;
420
+ return typeof factory === "function" ? factory : void 0;
421
+ }
422
+ /**
423
+ * Resolve a scheduler plugin selected via env so a producer (platform) and a
424
+ * consumer (container worker) can never diverge on backend. When
425
+ * `KEYSTROKE_SCHEDULER_MODULE` is set, dynamically import that module and call
426
+ * its `createSchedulerPlugin(env)`
427
+ */
428
+ async function resolveSchedulerPluginFromEnv(env = process.env) {
429
+ const moduleSpecifier = env[SCHEDULER_MODULE_ENV]?.trim();
430
+ if (!moduleSpecifier) return;
431
+ const createSchedulerPlugin = getCreateSchedulerPlugin(await import(moduleSpecifier));
432
+ if (!createSchedulerPlugin) throw new Error(`Scheduler module "${moduleSpecifier}" must export createSchedulerPlugin(env): SchedulerPlugin`);
433
+ const plugin = createSchedulerPlugin(env);
434
+ if (!isSchedulerPlugin(plugin)) throw new Error(`Scheduler module "${moduleSpecifier}" createSchedulerPlugin(env) did not return a valid SchedulerPlugin (expected { name: string, createJobQueue: function })`);
435
+ return plugin;
436
+ }
437
+ //#endregion
438
+ export { DEFAULT_QUEUE_NAME, DEFAULT_RETRY_DELAY_MS, SCHEDULER_MODULE_ENV, buildPgBossJobQueue, configureSharedScheduler, createJobQueue, createMemoryJobQueue, createScheduler, createSharedPgBossJobQueue, createSharedPgBossScheduler, createSharedScheduler, defaultSchedulerPlugin, defineSchedulerPlugin, getPgBoss, getSharedPgBossJobQueue, pgBossProjectQueueName, pgBossSchedulerPlugin, pollingSchedulerPlugin, resetSharedPgBossJobQueueForTests, resetSharedSchedulerForTests, resolveSchedulerPluginFromEnv, retryDelayMs, startPgBoss, stopPgBoss, wrapJobQueueAsScheduler };
439
+
440
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["sleep","syncTriggerScheduleRows","sharedJobQueue"],"sources":["../src/schedule-ticker.ts","../src/database-queue.ts","../src/pg-boss-client.ts","../src/pg-boss-queue.ts","../src/plugin.ts","../src/resolve-schedule.ts","../src/sync-trigger-schedules.ts","../src/create-scheduler.ts","../src/memory.ts","../src/shared-pgboss-queue.ts","../src/shared-scheduler.ts","../src/resolve-plugin-from-env.ts"],"sourcesContent":["import { claimDueTriggerSchedules } from \"@keystrokehq/database\";\nimport { nextTriggerRunAt } from \"@keystrokehq/trigger\";\nimport type { JobQueue, ScheduleTickerOptions, StopFn } from \"./types\";\n\nexport type ScheduleTickerContext = {\n jobQueue: JobQueue;\n pollIntervalMs?: number;\n batchSize?: number;\n};\n\nexport function createScheduleTicker(ctx: ScheduleTickerContext) {\n const pollIntervalMs = ctx.pollIntervalMs ?? 1_000;\n const batchSize = ctx.batchSize ?? 10;\n\n async function fireDueSchedules(asOf = new Date()): Promise<number> {\n const claimed = await claimDueTriggerSchedules(\n asOf,\n (schedule) => nextTriggerRunAt(schedule, asOf),\n batchSize,\n );\n\n for (const row of claimed) {\n await ctx.jobQueue.enqueue({\n kind: \"trigger\",\n targetId: row.attachmentKey,\n runId: crypto.randomUUID(),\n trigger: row.kind,\n payload: {},\n scheduledAt: asOf,\n });\n }\n\n return claimed.length;\n }\n\n async function startScheduleTicker(options: ScheduleTickerOptions = {}): Promise<StopFn> {\n const intervalMs = options.pollIntervalMs ?? pollIntervalMs;\n const limit = options.batchSize ?? batchSize;\n let running = true;\n\n const loop = async () => {\n while (running) {\n try {\n await claimDueTriggerSchedules(\n new Date(),\n (schedule) => nextTriggerRunAt(schedule, new Date()),\n limit,\n ).then(async (claimed) => {\n for (const row of claimed) {\n await ctx.jobQueue.enqueue({\n kind: \"trigger\",\n targetId: row.attachmentKey,\n runId: crypto.randomUUID(),\n trigger: row.kind,\n payload: {},\n });\n }\n });\n } catch {\n // keep ticking\n }\n\n await sleep(intervalMs);\n }\n };\n\n void loop();\n\n return async () => {\n running = false;\n };\n }\n\n return { fireDueSchedules, startScheduleTicker };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import {\n claimNextJob,\n enqueueJob,\n failWorkflowRun,\n markJobComplete,\n markJobFailed,\n requeueExpiredLeases,\n scheduleJobRetry,\n} from \"@keystrokehq/database\";\nimport type { ClaimedJob } from \"@keystrokehq/database\";\nimport type { JobHandler, JobQueue, StopFn, WorkerOptions } from \"./types\";\nimport { retryDelayMs } from \"./types\";\n\nfunction toJobPayload(job: ClaimedJob): Parameters<JobHandler>[0] {\n return {\n jobId: job.id,\n kind: job.kind,\n targetId: job.targetId,\n runId: job.runId,\n trigger: job.trigger,\n payload: job.payload,\n attempt: job.attempt,\n maxAttempts: job.maxAttempts,\n scheduledAt: job.scheduledAt,\n };\n}\n\nexport function createDatabaseJobQueue(): JobQueue {\n return {\n async enqueue(input) {\n return enqueueJob(input);\n },\n\n async startWorker(handler: JobHandler, options: WorkerOptions = {}): Promise<StopFn> {\n const workerId = options.workerId ?? crypto.randomUUID();\n const pollIntervalMs = options.pollIntervalMs ?? 250;\n const leaseSweepIntervalMs = options.leaseSweepIntervalMs ?? 30_000;\n let running = true;\n\n const leaseTimer = setInterval(() => {\n void requeueExpiredLeases();\n }, leaseSweepIntervalMs);\n\n const loop = async () => {\n while (running) {\n try {\n const job = await claimNextJob(workerId);\n if (!job) {\n await sleep(pollIntervalMs);\n continue;\n }\n\n try {\n await handler(toJobPayload(job));\n await markJobComplete(job.id);\n } catch (error) {\n if (job.attempt < job.maxAttempts) {\n await scheduleJobRetry(job.id, job.attempt + 1, retryDelayMs(job.attempt));\n } else {\n await markJobFailed(job.id, error);\n if (job.kind === \"workflow\") {\n await failWorkflowRun(job.runId, error);\n }\n }\n }\n } catch {\n await sleep(pollIntervalMs);\n }\n }\n };\n\n void loop();\n\n return async () => {\n running = false;\n clearInterval(leaseTimer);\n };\n },\n };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { resolvePostgresUrlFromEnv } from \"@keystrokehq/database\";\nimport { PgBoss } from \"pg-boss\";\n\nlet boss: PgBoss | undefined;\n\nfunction resolveDatabaseUrl(url?: string): string {\n const resolved = url ?? resolvePostgresUrlFromEnv(process.env);\n if (!resolved) {\n throw new Error(\n \"DATABASE_URL or POSTGRES_HOST/POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB is required\",\n );\n }\n return resolved;\n}\n\nexport async function startPgBoss(url?: string): Promise<PgBoss> {\n if (boss) {\n return boss;\n }\n\n const next = new PgBoss({\n connectionString: resolveDatabaseUrl(url),\n schema: \"pgboss\",\n });\n next.on(\"error\", (error) => {\n console.error(\"[pg-boss]\", error);\n });\n\n await next.start();\n boss = next;\n return next;\n}\n\nexport function getPgBoss(): PgBoss {\n if (!boss) {\n throw new Error(\"PgBoss not started. Call startPgBoss() first.\");\n }\n return boss;\n}\n\nexport async function stopPgBoss(): Promise<void> {\n if (!boss) {\n return;\n }\n\n await boss.stop();\n boss = undefined;\n}\n","import { createHash } from \"node:crypto\";\nimport { PgBoss, type Job } from \"pg-boss\";\nimport type { JobHandler, JobQueue, StopFn } from \"./types\";\nimport { retryDelayMs } from \"./types\";\n\n/** Shared queue name for the platform control-plane scope. */\nexport const DEFAULT_QUEUE_NAME = \"keystroke\";\nconst PGBOSS_SCHEMA = \"pgboss\";\n\n/**\n * Derive a per-project pg-boss queue name. pg-boss queue names must be <= 50\n * chars, contain only [A-Za-z0-9_], and not start with a digit. Project ids are\n * usually UUIDs (hyphens, may start with a digit), so we sanitize, and fall back\n * to a hash when the sanitized name would exceed the length limit.\n */\nexport function pgBossProjectQueueName(projectId: string): string {\n const sanitized = projectId.replace(/[^a-zA-Z0-9_]/g, \"_\");\n const name = `${DEFAULT_QUEUE_NAME}_${sanitized}`;\n if (name.length <= 50) {\n return name;\n }\n return `${DEFAULT_QUEUE_NAME}_${createHash(\"sha1\").update(projectId).digest(\"hex\").slice(0, 40)}`;\n}\n\nexport type PgBossQueueOptions = {\n connectionString: string;\n queueName?: string;\n};\n\ntype PgBossJobData = {\n kind: \"workflow\" | \"agent\" | \"trigger\";\n targetId: string;\n runId: string;\n trigger: \"api\" | \"cron\" | \"webhook\" | \"poll\" | \"retry\" | \"prompt\";\n payload?: unknown;\n attempt?: number;\n maxAttempts?: number;\n scheduledAt?: string;\n};\n\ntype BuildPgBossJobQueueOptions = {\n stopBossOnWorkerStop?: boolean;\n queueName?: string;\n};\n\nexport function buildPgBossJobQueue(\n boss: PgBoss,\n options: BuildPgBossJobQueueOptions = {},\n): JobQueue {\n const queueName = options.queueName ?? DEFAULT_QUEUE_NAME;\n\n return {\n async enqueue(input) {\n const jobId = await boss.send(queueName, input, {\n retryLimit: (input.maxAttempts ?? 3) - 1,\n retryDelay: Math.ceil(retryDelayMs(1) / 1000),\n startAfter: input.scheduledAt,\n });\n\n if (!jobId) {\n throw new Error(\"Failed to enqueue job\");\n }\n\n return jobId;\n },\n\n async startWorker(handler: JobHandler): Promise<StopFn> {\n let stopped = false;\n\n const workerId = await boss.work<PgBossJobData>(\n queueName,\n { batchSize: 1 },\n async (jobs: Job<PgBossJobData>[]) => {\n if (stopped) {\n return;\n }\n\n const job = jobs[0];\n if (!job) {\n return;\n }\n\n const data = job.data;\n\n await handler({\n jobId: job.id,\n kind: data.kind,\n targetId: data.targetId,\n runId: data.runId,\n trigger: data.trigger,\n payload: data.payload ?? {},\n attempt: data.attempt ?? 1,\n maxAttempts: data.maxAttempts ?? 3,\n scheduledAt: data.scheduledAt ? new Date(data.scheduledAt) : new Date(),\n });\n },\n );\n\n return async () => {\n stopped = true;\n await boss.offWork(queueName, { id: workerId });\n if (options.stopBossOnWorkerStop) {\n await boss.stop({ graceful: true, timeout: 5_000 });\n }\n };\n },\n };\n}\n\nexport async function createPgBossQueue(options: PgBossQueueOptions): Promise<JobQueue> {\n const queueName = options.queueName ?? DEFAULT_QUEUE_NAME;\n const boss = new PgBoss({\n connectionString: options.connectionString,\n schema: PGBOSS_SCHEMA,\n });\n await boss.start();\n await boss.createQueue(queueName);\n\n return buildPgBossJobQueue(boss, { stopBossOnWorkerStop: true, queueName });\n}\n\nexport async function createSharedPgBossJobQueue(\n boss: PgBoss,\n queueName: string = DEFAULT_QUEUE_NAME,\n): Promise<JobQueue> {\n await boss.createQueue(queueName);\n return buildPgBossJobQueue(boss, { queueName });\n}\n","import {\n DEFAULT_DATABASE_URL,\n getProjectScopeId,\n inferDialect,\n resolveProjectDatabaseUrlFromEnv,\n} from \"@keystrokehq/database\";\n\nimport {\n defineSchedulerPlugin,\n type SchedulerPlugin,\n type SchedulerPluginContext,\n} from \"./contract\";\nimport { createDatabaseJobQueue } from \"./database-queue\";\nimport { getPgBoss, startPgBoss } from \"./pg-boss-client\";\nimport {\n createPgBossQueue,\n createSharedPgBossJobQueue,\n pgBossProjectQueueName,\n} from \"./pg-boss-queue\";\n\nexport type { SchedulerPlugin, SchedulerPluginContext, SchedulerScope } from \"./contract\";\nexport { defineSchedulerPlugin } from \"./contract\";\n\nfunction resolveUrl(ctx: SchedulerPluginContext): string {\n return ctx.url ?? resolveProjectDatabaseUrlFromEnv(process.env) ?? DEFAULT_DATABASE_URL;\n}\n\n/** Postgres pg-boss backend. Platform scope shares one queue; project scope gets a per-project queue. */\nexport function pgBossSchedulerPlugin(): SchedulerPlugin {\n return defineSchedulerPlugin({\n name: \"pg-boss\",\n async createJobQueue(ctx) {\n const url = resolveUrl(ctx);\n\n if (ctx.scope === \"platform\") {\n await startPgBoss(url);\n return createSharedPgBossJobQueue(getPgBoss());\n }\n\n const projectId = ctx.projectId ?? getProjectScopeId();\n return createPgBossQueue({\n connectionString: url,\n queueName: pgBossProjectQueueName(projectId),\n });\n },\n });\n}\n\n/** DB-table queue (Drizzle `jobs`), for SQLite / self-host. */\nexport function pollingSchedulerPlugin(): SchedulerPlugin {\n return defineSchedulerPlugin({\n name: \"polling\",\n async createJobQueue() {\n return createDatabaseJobQueue();\n },\n });\n}\n\n/** Auto-selects pg-boss (postgres) or polling (sqlite) by dialect. */\nexport function defaultSchedulerPlugin(): SchedulerPlugin {\n return defineSchedulerPlugin({\n name: \"default\",\n async createJobQueue(ctx) {\n const url = resolveUrl(ctx);\n const dialect = inferDialect(url, ctx.dialect);\n\n if (dialect === \"postgres\") {\n return pgBossSchedulerPlugin().createJobQueue(ctx);\n }\n\n return pollingSchedulerPlugin().createJobQueue(ctx);\n },\n });\n}\n","import { resolveCronSchedule } from \"@keystrokehq/trigger\";\n\nexport type ScheduleOverrideOptions = {\n global?: string;\n byAttachment?: Record<string, string>;\n};\n\nexport function resolveTriggerSchedule(\n attachmentKey: string,\n schedule: string,\n overrides?: ScheduleOverrideOptions,\n): string {\n return resolveCronSchedule(attachmentKey, schedule, {\n cronScheduleOverride: overrides?.global,\n attachmentScheduleOverrides: overrides?.byAttachment,\n });\n}\n","import {\n disableAllTriggerSchedules,\n disableTriggerSchedulesNotInKeys,\n selectTriggerScheduleByKey,\n upsertTriggerSchedule,\n} from \"@keystrokehq/database\";\nimport { nextTriggerRunAt } from \"@keystrokehq/trigger\";\nimport type { ScheduleSyncOptions } from \"./types\";\nimport { resolveTriggerSchedule } from \"./resolve-schedule\";\n\nexport async function syncTriggerSchedules(options: ScheduleSyncOptions): Promise<void> {\n const now = new Date();\n const keys = options.schedules.map((schedule) => schedule.attachmentKey);\n\n if (keys.length === 0) {\n await disableAllTriggerSchedules(now);\n return;\n }\n\n await disableTriggerSchedulesNotInKeys(keys, now);\n\n for (const spec of options.schedules) {\n const schedule = resolveTriggerSchedule(\n spec.attachmentKey,\n spec.schedule,\n options.scheduleOverrides,\n );\n const existing = await selectTriggerScheduleByKey(spec.attachmentKey);\n const scheduleChanged = existing?.schedule !== schedule;\n const nextRunAt =\n existing && !scheduleChanged && existing.enabled === 1\n ? existing.nextRunAt\n : nextTriggerRunAt(schedule, now);\n\n await upsertTriggerSchedule({\n attachmentKey: spec.attachmentKey,\n kind: spec.kind,\n schedule,\n nextRunAt,\n enabled: true,\n updatedAt: now,\n });\n }\n}\n","import { createScheduleTicker } from \"./schedule-ticker\";\nimport { defaultSchedulerPlugin } from \"./plugin\";\nimport { syncTriggerSchedules as syncTriggerScheduleRows } from \"./sync-trigger-schedules\";\nimport type {\n CreateJobQueueOptions,\n CreateSchedulerOptions,\n JobQueue,\n ScheduleSyncOptions,\n ScheduleTickerOptions,\n Scheduler,\n StopFn,\n} from \"./types\";\n\nasync function createUnderlyingJobQueue(options: CreateJobQueueOptions = {}): Promise<JobQueue> {\n if (options.adapter) {\n return options.adapter;\n }\n\n const plugin = options.plugin ?? defaultSchedulerPlugin();\n return plugin.createJobQueue({\n scope: options.scope ?? \"project\",\n url: options.url,\n dialect: options.dialect,\n projectId: options.projectId,\n organizationId: options.organizationId,\n });\n}\n\nfunction wrapScheduler(jobQueue: JobQueue): Scheduler {\n const ticker = createScheduleTicker({ jobQueue });\n\n return {\n enqueue: (input) => jobQueue.enqueue(input),\n startWorker: (handler, options) => jobQueue.startWorker(handler, options),\n syncTriggerSchedules: (options: ScheduleSyncOptions) => syncTriggerScheduleRows(options),\n startScheduleTicker: (options?: ScheduleTickerOptions) => ticker.startScheduleTicker(options),\n fireDueSchedules: (asOf?: Date) => ticker.fireDueSchedules(asOf),\n };\n}\n\nexport async function createScheduler(options: CreateSchedulerOptions = {}): Promise<Scheduler> {\n const jobQueue = await createUnderlyingJobQueue(options);\n return wrapScheduler(jobQueue);\n}\n\nexport async function createJobQueue(options: CreateJobQueueOptions = {}): Promise<JobQueue> {\n return createUnderlyingJobQueue(options);\n}\n\nexport function wrapJobQueueAsScheduler(jobQueue: JobQueue): Scheduler {\n return wrapScheduler(jobQueue);\n}\n\nexport type { StopFn };\n","import type { EnqueueInput, JobHandler, JobQueue, StopFn, WorkerOptions } from \"./types\";\nimport { retryDelayMs } from \"./types\";\n\nexport type MemoryJobQueueOptions = {\n sync?: boolean;\n};\n\nexport function createMemoryJobQueue(options: MemoryJobQueueOptions = {}): JobQueue {\n const pending: Array<{ id: string; input: EnqueueInput; enqueuedAt: Date }> = [];\n let handler: JobHandler | undefined;\n let draining = false;\n\n async function drain(): Promise<void> {\n if (!handler || draining) {\n return;\n }\n\n draining = true;\n try {\n while (pending.length > 0) {\n const next = pending.shift();\n if (!next) {\n break;\n }\n\n const input = next.input;\n await handler({\n jobId: next.id,\n kind: input.kind,\n targetId: input.targetId,\n runId: input.runId,\n trigger: input.trigger,\n payload: input.payload ?? {},\n attempt: input.attempt ?? 1,\n maxAttempts: input.maxAttempts ?? 3,\n scheduledAt: input.scheduledAt ?? next.enqueuedAt,\n });\n }\n } finally {\n draining = false;\n }\n }\n\n return {\n async enqueue(input) {\n const id = crypto.randomUUID();\n pending.push({ id, input, enqueuedAt: new Date() });\n\n if (options.sync) {\n await drain();\n }\n\n return id;\n },\n\n async startWorker(nextHandler: JobHandler, _opts: WorkerOptions = {}): Promise<StopFn> {\n handler = nextHandler;\n\n if (!options.sync) {\n void drain();\n }\n\n return async () => {\n handler = undefined;\n };\n },\n };\n}\n\nexport { retryDelayMs };\n","import { getPgBoss } from \"./pg-boss-client\";\nimport { wrapJobQueueAsScheduler } from \"./create-scheduler\";\nimport { createSharedPgBossJobQueue } from \"./pg-boss-queue\";\nimport type { JobQueue, Scheduler } from \"./types\";\n\nlet sharedJobQueue: JobQueue | undefined;\n\nexport async function createSharedPgBossScheduler(): Promise<Scheduler> {\n const jobQueue = await getSharedPgBossJobQueue();\n return wrapJobQueueAsScheduler(jobQueue);\n}\n\nexport async function getSharedPgBossJobQueue(): Promise<JobQueue> {\n if (sharedJobQueue) {\n return sharedJobQueue;\n }\n\n sharedJobQueue = await createSharedPgBossJobQueue(getPgBoss());\n return sharedJobQueue;\n}\n\nexport function resetSharedPgBossJobQueueForTests(): void {\n sharedJobQueue = undefined;\n}\n","import type { SchedulerPlugin } from \"./contract\";\nimport { wrapJobQueueAsScheduler } from \"./create-scheduler\";\nimport { defaultSchedulerPlugin } from \"./plugin\";\nimport {\n createSharedPgBossScheduler,\n resetSharedPgBossJobQueueForTests,\n} from \"./shared-pgboss-queue\";\nimport type { JobQueue, Scheduler } from \"./types\";\n\nlet configuredPlugin: SchedulerPlugin | undefined;\nlet sharedJobQueue: JobQueue | undefined;\n\nexport function configureSharedScheduler(plugin: SchedulerPlugin): void {\n configuredPlugin = plugin;\n sharedJobQueue = undefined;\n}\n\nexport async function createSharedScheduler(): Promise<Scheduler> {\n const plugin = configuredPlugin ?? defaultSchedulerPlugin();\n\n if (plugin.name === \"default\") {\n return createSharedPgBossScheduler();\n }\n\n if (!sharedJobQueue) {\n sharedJobQueue = await plugin.createJobQueue({ scope: \"platform\" });\n }\n\n return wrapJobQueueAsScheduler(sharedJobQueue);\n}\n\nexport function resetSharedSchedulerForTests(): void {\n configuredPlugin = undefined;\n sharedJobQueue = undefined;\n resetSharedPgBossJobQueueForTests();\n}\n","import type { SchedulerPlugin } from \"./contract\";\n\n/** Env var naming a module that exports `createSchedulerPlugin(env): SchedulerPlugin`. */\nexport const SCHEDULER_MODULE_ENV = \"KEYSTROKE_SCHEDULER_MODULE\";\n\ntype SchedulerEnv = Record<string, string | undefined>;\n\nfunction isSchedulerPlugin(value: unknown): value is SchedulerPlugin {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n const candidate = value as Record<string, unknown>;\n return typeof candidate.name === \"string\" && typeof candidate.createJobQueue === \"function\";\n}\n\nfunction getCreateSchedulerPlugin(mod: unknown): ((env: SchedulerEnv) => unknown) | undefined {\n if (typeof mod !== \"object\" || mod === null) {\n return undefined;\n }\n const factory = (mod as Record<string, unknown>).createSchedulerPlugin;\n return typeof factory === \"function\" ? (factory as (env: SchedulerEnv) => unknown) : undefined;\n}\n\n/**\n * Resolve a scheduler plugin selected via env so a producer (platform) and a\n * consumer (container worker) can never diverge on backend. When\n * `KEYSTROKE_SCHEDULER_MODULE` is set, dynamically import that module and call\n * its `createSchedulerPlugin(env)`\n */\nexport async function resolveSchedulerPluginFromEnv(\n env: SchedulerEnv = process.env,\n): Promise<SchedulerPlugin | undefined> {\n const moduleSpecifier = env[SCHEDULER_MODULE_ENV]?.trim();\n if (!moduleSpecifier) {\n return undefined;\n }\n\n const mod: unknown = await import(moduleSpecifier);\n const createSchedulerPlugin = getCreateSchedulerPlugin(mod);\n if (!createSchedulerPlugin) {\n throw new Error(\n `Scheduler module \"${moduleSpecifier}\" must export createSchedulerPlugin(env): SchedulerPlugin`,\n );\n }\n\n const plugin = createSchedulerPlugin(env);\n if (!isSchedulerPlugin(plugin)) {\n throw new Error(\n `Scheduler module \"${moduleSpecifier}\" createSchedulerPlugin(env) did not return a valid SchedulerPlugin (expected { name: string, createJobQueue: function })`,\n );\n }\n\n return plugin;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@keystrokehq/scheduler",
3
+ "version": "0.0.66",
4
+ "private": false,
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/dallinbentley/keystroke.git",
8
+ "directory": "packages/scheduler"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "type": "module",
14
+ "main": "./dist/index.cjs",
15
+ "module": "./dist/index.mjs",
16
+ "types": "./dist/index.d.mts",
17
+ "exports": {
18
+ ".": {
19
+ "development": "./src/index.ts",
20
+ "types": "./dist/index.d.mts",
21
+ "import": "./dist/index.mjs",
22
+ "require": "./dist/index.cjs"
23
+ },
24
+ "./contract": {
25
+ "development": "./src/contract.ts",
26
+ "types": "./dist/contract.d.mts",
27
+ "import": "./dist/contract.mjs",
28
+ "require": "./dist/contract.cjs"
29
+ }
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "provenance": true,
34
+ "registry": "https://registry.npmjs.org"
35
+ },
36
+ "dependencies": {
37
+ "cron-schedule": "^6.0.0",
38
+ "pg-boss": "^12.18.2"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.9.1",
42
+ "oxlint": "^1.66.0",
43
+ "tsdown": "^0.22.0",
44
+ "typescript": "^6.0.3",
45
+ "vitest": "^4.1.7",
46
+ "@keystrokehq/database": "0.0.62",
47
+ "@keystrokehq/trigger": "0.0.64",
48
+ "@keystrokehq/tsdown-config": "0.0.3",
49
+ "@keystrokehq/vitest-config": "0.0.4",
50
+ "@keystrokehq/oxlint-config": "0.0.3",
51
+ "@keystrokehq/tsconfig": "0.0.3"
52
+ },
53
+ "peerDependencies": {
54
+ "@keystrokehq/database": "0.0.62",
55
+ "@keystrokehq/trigger": "0.0.64"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@keystrokehq/database": {
59
+ "optional": true
60
+ },
61
+ "@keystrokehq/trigger": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "scripts": {
66
+ "build": "tsdown",
67
+ "dev": "tsdown --watch --no-clean",
68
+ "lint": "oxlint .",
69
+ "test": "vitest run",
70
+ "test:unit": "vitest run --project unit",
71
+ "test:integration": "vitest run --project integration",
72
+ "typecheck": "tsc --noEmit"
73
+ }
74
+ }