@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.
package/dist/index.cjs ADDED
@@ -0,0 +1,465 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_contract = require("./contract-C5JvbyQ-.cjs");
3
+ let _keystrokehq_database = require("@keystrokehq/database");
4
+ let _keystrokehq_trigger = require("@keystrokehq/trigger");
5
+ let pg_boss = require("pg-boss");
6
+ let node_crypto = require("node:crypto");
7
+ //#region src/schedule-ticker.ts
8
+ function createScheduleTicker(ctx) {
9
+ const pollIntervalMs = ctx.pollIntervalMs ?? 1e3;
10
+ const batchSize = ctx.batchSize ?? 10;
11
+ async function fireDueSchedules(asOf = /* @__PURE__ */ new Date()) {
12
+ const claimed = await (0, _keystrokehq_database.claimDueTriggerSchedules)(asOf, (schedule) => (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, asOf), batchSize);
13
+ for (const row of claimed) await ctx.jobQueue.enqueue({
14
+ kind: "trigger",
15
+ targetId: row.attachmentKey,
16
+ runId: crypto.randomUUID(),
17
+ trigger: row.kind,
18
+ payload: {},
19
+ scheduledAt: asOf
20
+ });
21
+ return claimed.length;
22
+ }
23
+ async function startScheduleTicker(options = {}) {
24
+ const intervalMs = options.pollIntervalMs ?? pollIntervalMs;
25
+ const limit = options.batchSize ?? batchSize;
26
+ let running = true;
27
+ const loop = async () => {
28
+ while (running) {
29
+ try {
30
+ await (0, _keystrokehq_database.claimDueTriggerSchedules)(/* @__PURE__ */ new Date(), (schedule) => (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, /* @__PURE__ */ new Date()), limit).then(async (claimed) => {
31
+ for (const row of claimed) await ctx.jobQueue.enqueue({
32
+ kind: "trigger",
33
+ targetId: row.attachmentKey,
34
+ runId: crypto.randomUUID(),
35
+ trigger: row.kind,
36
+ payload: {}
37
+ });
38
+ });
39
+ } catch {}
40
+ await sleep$1(intervalMs);
41
+ }
42
+ };
43
+ loop();
44
+ return async () => {
45
+ running = false;
46
+ };
47
+ }
48
+ return {
49
+ fireDueSchedules,
50
+ startScheduleTicker
51
+ };
52
+ }
53
+ function sleep$1(ms) {
54
+ return new Promise((resolve) => setTimeout(resolve, ms));
55
+ }
56
+ //#endregion
57
+ //#region src/database-queue.ts
58
+ function toJobPayload(job) {
59
+ return {
60
+ jobId: job.id,
61
+ kind: job.kind,
62
+ targetId: job.targetId,
63
+ runId: job.runId,
64
+ trigger: job.trigger,
65
+ payload: job.payload,
66
+ attempt: job.attempt,
67
+ maxAttempts: job.maxAttempts,
68
+ scheduledAt: job.scheduledAt
69
+ };
70
+ }
71
+ function createDatabaseJobQueue() {
72
+ return {
73
+ async enqueue(input) {
74
+ return (0, _keystrokehq_database.enqueueJob)(input);
75
+ },
76
+ async startWorker(handler, options = {}) {
77
+ const workerId = options.workerId ?? crypto.randomUUID();
78
+ const pollIntervalMs = options.pollIntervalMs ?? 250;
79
+ const leaseSweepIntervalMs = options.leaseSweepIntervalMs ?? 3e4;
80
+ let running = true;
81
+ const leaseTimer = setInterval(() => {
82
+ (0, _keystrokehq_database.requeueExpiredLeases)();
83
+ }, leaseSweepIntervalMs);
84
+ const loop = async () => {
85
+ while (running) try {
86
+ const job = await (0, _keystrokehq_database.claimNextJob)(workerId);
87
+ if (!job) {
88
+ await sleep(pollIntervalMs);
89
+ continue;
90
+ }
91
+ try {
92
+ await handler(toJobPayload(job));
93
+ await (0, _keystrokehq_database.markJobComplete)(job.id);
94
+ } catch (error) {
95
+ if (job.attempt < job.maxAttempts) await (0, _keystrokehq_database.scheduleJobRetry)(job.id, job.attempt + 1, require_contract.retryDelayMs(job.attempt));
96
+ else {
97
+ await (0, _keystrokehq_database.markJobFailed)(job.id, error);
98
+ if (job.kind === "workflow") await (0, _keystrokehq_database.failWorkflowRun)(job.runId, error);
99
+ }
100
+ }
101
+ } catch {
102
+ await sleep(pollIntervalMs);
103
+ }
104
+ };
105
+ loop();
106
+ return async () => {
107
+ running = false;
108
+ clearInterval(leaseTimer);
109
+ };
110
+ }
111
+ };
112
+ }
113
+ function sleep(ms) {
114
+ return new Promise((resolve) => setTimeout(resolve, ms));
115
+ }
116
+ //#endregion
117
+ //#region src/pg-boss-client.ts
118
+ let boss;
119
+ function resolveDatabaseUrl(url) {
120
+ const resolved = url ?? (0, _keystrokehq_database.resolvePostgresUrlFromEnv)(process.env);
121
+ if (!resolved) throw new Error("DATABASE_URL or POSTGRES_HOST/POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB is required");
122
+ return resolved;
123
+ }
124
+ async function startPgBoss(url) {
125
+ if (boss) return boss;
126
+ const next = new pg_boss.PgBoss({
127
+ connectionString: resolveDatabaseUrl(url),
128
+ schema: "pgboss"
129
+ });
130
+ next.on("error", (error) => {
131
+ console.error("[pg-boss]", error);
132
+ });
133
+ await next.start();
134
+ boss = next;
135
+ return next;
136
+ }
137
+ function getPgBoss() {
138
+ if (!boss) throw new Error("PgBoss not started. Call startPgBoss() first.");
139
+ return boss;
140
+ }
141
+ async function stopPgBoss() {
142
+ if (!boss) return;
143
+ await boss.stop();
144
+ boss = void 0;
145
+ }
146
+ //#endregion
147
+ //#region src/pg-boss-queue.ts
148
+ /** Shared queue name for the platform control-plane scope. */
149
+ const DEFAULT_QUEUE_NAME = "keystroke";
150
+ const PGBOSS_SCHEMA = "pgboss";
151
+ /**
152
+ * Derive a per-project pg-boss queue name. pg-boss queue names must be <= 50
153
+ * chars, contain only [A-Za-z0-9_], and not start with a digit. Project ids are
154
+ * usually UUIDs (hyphens, may start with a digit), so we sanitize, and fall back
155
+ * to a hash when the sanitized name would exceed the length limit.
156
+ */
157
+ function pgBossProjectQueueName(projectId) {
158
+ const name = `${DEFAULT_QUEUE_NAME}_${projectId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
159
+ if (name.length <= 50) return name;
160
+ return `${DEFAULT_QUEUE_NAME}_${(0, node_crypto.createHash)("sha1").update(projectId).digest("hex").slice(0, 40)}`;
161
+ }
162
+ function buildPgBossJobQueue(boss, options = {}) {
163
+ const queueName = options.queueName ?? "keystroke";
164
+ return {
165
+ async enqueue(input) {
166
+ const jobId = await boss.send(queueName, input, {
167
+ retryLimit: (input.maxAttempts ?? 3) - 1,
168
+ retryDelay: Math.ceil(require_contract.retryDelayMs(1) / 1e3),
169
+ startAfter: input.scheduledAt
170
+ });
171
+ if (!jobId) throw new Error("Failed to enqueue job");
172
+ return jobId;
173
+ },
174
+ async startWorker(handler) {
175
+ let stopped = false;
176
+ const workerId = await boss.work(queueName, { batchSize: 1 }, async (jobs) => {
177
+ if (stopped) return;
178
+ const job = jobs[0];
179
+ if (!job) return;
180
+ const data = job.data;
181
+ await handler({
182
+ jobId: job.id,
183
+ kind: data.kind,
184
+ targetId: data.targetId,
185
+ runId: data.runId,
186
+ trigger: data.trigger,
187
+ payload: data.payload ?? {},
188
+ attempt: data.attempt ?? 1,
189
+ maxAttempts: data.maxAttempts ?? 3,
190
+ scheduledAt: data.scheduledAt ? new Date(data.scheduledAt) : /* @__PURE__ */ new Date()
191
+ });
192
+ });
193
+ return async () => {
194
+ stopped = true;
195
+ await boss.offWork(queueName, { id: workerId });
196
+ if (options.stopBossOnWorkerStop) await boss.stop({
197
+ graceful: true,
198
+ timeout: 5e3
199
+ });
200
+ };
201
+ }
202
+ };
203
+ }
204
+ async function createPgBossQueue(options) {
205
+ const queueName = options.queueName ?? "keystroke";
206
+ const boss = new pg_boss.PgBoss({
207
+ connectionString: options.connectionString,
208
+ schema: PGBOSS_SCHEMA
209
+ });
210
+ await boss.start();
211
+ await boss.createQueue(queueName);
212
+ return buildPgBossJobQueue(boss, {
213
+ stopBossOnWorkerStop: true,
214
+ queueName
215
+ });
216
+ }
217
+ async function createSharedPgBossJobQueue(boss, queueName = DEFAULT_QUEUE_NAME) {
218
+ await boss.createQueue(queueName);
219
+ return buildPgBossJobQueue(boss, { queueName });
220
+ }
221
+ //#endregion
222
+ //#region src/plugin.ts
223
+ function resolveUrl(ctx) {
224
+ return ctx.url ?? (0, _keystrokehq_database.resolveProjectDatabaseUrlFromEnv)(process.env) ?? _keystrokehq_database.DEFAULT_DATABASE_URL;
225
+ }
226
+ /** Postgres pg-boss backend. Platform scope shares one queue; project scope gets a per-project queue. */
227
+ function pgBossSchedulerPlugin() {
228
+ return require_contract.defineSchedulerPlugin({
229
+ name: "pg-boss",
230
+ async createJobQueue(ctx) {
231
+ const url = resolveUrl(ctx);
232
+ if (ctx.scope === "platform") {
233
+ await startPgBoss(url);
234
+ return createSharedPgBossJobQueue(getPgBoss());
235
+ }
236
+ return createPgBossQueue({
237
+ connectionString: url,
238
+ queueName: pgBossProjectQueueName(ctx.projectId ?? (0, _keystrokehq_database.getProjectScopeId)())
239
+ });
240
+ }
241
+ });
242
+ }
243
+ /** DB-table queue (Drizzle `jobs`), for SQLite / self-host. */
244
+ function pollingSchedulerPlugin() {
245
+ return require_contract.defineSchedulerPlugin({
246
+ name: "polling",
247
+ async createJobQueue() {
248
+ return createDatabaseJobQueue();
249
+ }
250
+ });
251
+ }
252
+ /** Auto-selects pg-boss (postgres) or polling (sqlite) by dialect. */
253
+ function defaultSchedulerPlugin() {
254
+ return require_contract.defineSchedulerPlugin({
255
+ name: "default",
256
+ async createJobQueue(ctx) {
257
+ if ((0, _keystrokehq_database.inferDialect)(resolveUrl(ctx), ctx.dialect) === "postgres") return pgBossSchedulerPlugin().createJobQueue(ctx);
258
+ return pollingSchedulerPlugin().createJobQueue(ctx);
259
+ }
260
+ });
261
+ }
262
+ //#endregion
263
+ //#region src/resolve-schedule.ts
264
+ function resolveTriggerSchedule(attachmentKey, schedule, overrides) {
265
+ return (0, _keystrokehq_trigger.resolveCronSchedule)(attachmentKey, schedule, {
266
+ cronScheduleOverride: overrides?.global,
267
+ attachmentScheduleOverrides: overrides?.byAttachment
268
+ });
269
+ }
270
+ //#endregion
271
+ //#region src/sync-trigger-schedules.ts
272
+ async function syncTriggerSchedules(options) {
273
+ const now = /* @__PURE__ */ new Date();
274
+ const keys = options.schedules.map((schedule) => schedule.attachmentKey);
275
+ if (keys.length === 0) {
276
+ await (0, _keystrokehq_database.disableAllTriggerSchedules)(now);
277
+ return;
278
+ }
279
+ await (0, _keystrokehq_database.disableTriggerSchedulesNotInKeys)(keys, now);
280
+ for (const spec of options.schedules) {
281
+ const schedule = resolveTriggerSchedule(spec.attachmentKey, spec.schedule, options.scheduleOverrides);
282
+ const existing = await (0, _keystrokehq_database.selectTriggerScheduleByKey)(spec.attachmentKey);
283
+ const scheduleChanged = existing?.schedule !== schedule;
284
+ const nextRunAt = existing && !scheduleChanged && existing.enabled === 1 ? existing.nextRunAt : (0, _keystrokehq_trigger.nextTriggerRunAt)(schedule, now);
285
+ await (0, _keystrokehq_database.upsertTriggerSchedule)({
286
+ attachmentKey: spec.attachmentKey,
287
+ kind: spec.kind,
288
+ schedule,
289
+ nextRunAt,
290
+ enabled: true,
291
+ updatedAt: now
292
+ });
293
+ }
294
+ }
295
+ //#endregion
296
+ //#region src/create-scheduler.ts
297
+ async function createUnderlyingJobQueue(options = {}) {
298
+ if (options.adapter) return options.adapter;
299
+ return (options.plugin ?? defaultSchedulerPlugin()).createJobQueue({
300
+ scope: options.scope ?? "project",
301
+ url: options.url,
302
+ dialect: options.dialect,
303
+ projectId: options.projectId,
304
+ organizationId: options.organizationId
305
+ });
306
+ }
307
+ function wrapScheduler(jobQueue) {
308
+ const ticker = createScheduleTicker({ jobQueue });
309
+ return {
310
+ enqueue: (input) => jobQueue.enqueue(input),
311
+ startWorker: (handler, options) => jobQueue.startWorker(handler, options),
312
+ syncTriggerSchedules: (options) => syncTriggerSchedules(options),
313
+ startScheduleTicker: (options) => ticker.startScheduleTicker(options),
314
+ fireDueSchedules: (asOf) => ticker.fireDueSchedules(asOf)
315
+ };
316
+ }
317
+ async function createScheduler(options = {}) {
318
+ return wrapScheduler(await createUnderlyingJobQueue(options));
319
+ }
320
+ async function createJobQueue(options = {}) {
321
+ return createUnderlyingJobQueue(options);
322
+ }
323
+ function wrapJobQueueAsScheduler(jobQueue) {
324
+ return wrapScheduler(jobQueue);
325
+ }
326
+ //#endregion
327
+ //#region src/memory.ts
328
+ function createMemoryJobQueue(options = {}) {
329
+ const pending = [];
330
+ let handler;
331
+ let draining = false;
332
+ async function drain() {
333
+ if (!handler || draining) return;
334
+ draining = true;
335
+ try {
336
+ while (pending.length > 0) {
337
+ const next = pending.shift();
338
+ if (!next) break;
339
+ const input = next.input;
340
+ await handler({
341
+ jobId: next.id,
342
+ kind: input.kind,
343
+ targetId: input.targetId,
344
+ runId: input.runId,
345
+ trigger: input.trigger,
346
+ payload: input.payload ?? {},
347
+ attempt: input.attempt ?? 1,
348
+ maxAttempts: input.maxAttempts ?? 3,
349
+ scheduledAt: input.scheduledAt ?? next.enqueuedAt
350
+ });
351
+ }
352
+ } finally {
353
+ draining = false;
354
+ }
355
+ }
356
+ return {
357
+ async enqueue(input) {
358
+ const id = crypto.randomUUID();
359
+ pending.push({
360
+ id,
361
+ input,
362
+ enqueuedAt: /* @__PURE__ */ new Date()
363
+ });
364
+ if (options.sync) await drain();
365
+ return id;
366
+ },
367
+ async startWorker(nextHandler, _opts = {}) {
368
+ handler = nextHandler;
369
+ if (!options.sync) drain();
370
+ return async () => {
371
+ handler = void 0;
372
+ };
373
+ }
374
+ };
375
+ }
376
+ //#endregion
377
+ //#region src/shared-pgboss-queue.ts
378
+ let sharedJobQueue$1;
379
+ async function createSharedPgBossScheduler() {
380
+ return wrapJobQueueAsScheduler(await getSharedPgBossJobQueue());
381
+ }
382
+ async function getSharedPgBossJobQueue() {
383
+ if (sharedJobQueue$1) return sharedJobQueue$1;
384
+ sharedJobQueue$1 = await createSharedPgBossJobQueue(getPgBoss());
385
+ return sharedJobQueue$1;
386
+ }
387
+ function resetSharedPgBossJobQueueForTests() {
388
+ sharedJobQueue$1 = void 0;
389
+ }
390
+ //#endregion
391
+ //#region src/shared-scheduler.ts
392
+ let configuredPlugin;
393
+ let sharedJobQueue;
394
+ function configureSharedScheduler(plugin) {
395
+ configuredPlugin = plugin;
396
+ sharedJobQueue = void 0;
397
+ }
398
+ async function createSharedScheduler() {
399
+ const plugin = configuredPlugin ?? defaultSchedulerPlugin();
400
+ if (plugin.name === "default") return createSharedPgBossScheduler();
401
+ if (!sharedJobQueue) sharedJobQueue = await plugin.createJobQueue({ scope: "platform" });
402
+ return wrapJobQueueAsScheduler(sharedJobQueue);
403
+ }
404
+ function resetSharedSchedulerForTests() {
405
+ configuredPlugin = void 0;
406
+ sharedJobQueue = void 0;
407
+ resetSharedPgBossJobQueueForTests();
408
+ }
409
+ //#endregion
410
+ //#region src/resolve-plugin-from-env.ts
411
+ /** Env var naming a module that exports `createSchedulerPlugin(env): SchedulerPlugin`. */
412
+ const SCHEDULER_MODULE_ENV = "KEYSTROKE_SCHEDULER_MODULE";
413
+ function isSchedulerPlugin(value) {
414
+ if (typeof value !== "object" || value === null) return false;
415
+ const candidate = value;
416
+ return typeof candidate.name === "string" && typeof candidate.createJobQueue === "function";
417
+ }
418
+ function getCreateSchedulerPlugin(mod) {
419
+ if (typeof mod !== "object" || mod === null) return;
420
+ const factory = mod.createSchedulerPlugin;
421
+ return typeof factory === "function" ? factory : void 0;
422
+ }
423
+ /**
424
+ * Resolve a scheduler plugin selected via env so a producer (platform) and a
425
+ * consumer (container worker) can never diverge on backend. When
426
+ * `KEYSTROKE_SCHEDULER_MODULE` is set, dynamically import that module and call
427
+ * its `createSchedulerPlugin(env)`
428
+ */
429
+ async function resolveSchedulerPluginFromEnv(env = process.env) {
430
+ const moduleSpecifier = env[SCHEDULER_MODULE_ENV]?.trim();
431
+ if (!moduleSpecifier) return;
432
+ const createSchedulerPlugin = getCreateSchedulerPlugin(await import(moduleSpecifier));
433
+ if (!createSchedulerPlugin) throw new Error(`Scheduler module "${moduleSpecifier}" must export createSchedulerPlugin(env): SchedulerPlugin`);
434
+ const plugin = createSchedulerPlugin(env);
435
+ if (!isSchedulerPlugin(plugin)) throw new Error(`Scheduler module "${moduleSpecifier}" createSchedulerPlugin(env) did not return a valid SchedulerPlugin (expected { name: string, createJobQueue: function })`);
436
+ return plugin;
437
+ }
438
+ //#endregion
439
+ exports.DEFAULT_QUEUE_NAME = DEFAULT_QUEUE_NAME;
440
+ exports.DEFAULT_RETRY_DELAY_MS = require_contract.DEFAULT_RETRY_DELAY_MS;
441
+ exports.SCHEDULER_MODULE_ENV = SCHEDULER_MODULE_ENV;
442
+ exports.buildPgBossJobQueue = buildPgBossJobQueue;
443
+ exports.configureSharedScheduler = configureSharedScheduler;
444
+ exports.createJobQueue = createJobQueue;
445
+ exports.createMemoryJobQueue = createMemoryJobQueue;
446
+ exports.createScheduler = createScheduler;
447
+ exports.createSharedPgBossJobQueue = createSharedPgBossJobQueue;
448
+ exports.createSharedPgBossScheduler = createSharedPgBossScheduler;
449
+ exports.createSharedScheduler = createSharedScheduler;
450
+ exports.defaultSchedulerPlugin = defaultSchedulerPlugin;
451
+ exports.defineSchedulerPlugin = require_contract.defineSchedulerPlugin;
452
+ exports.getPgBoss = getPgBoss;
453
+ exports.getSharedPgBossJobQueue = getSharedPgBossJobQueue;
454
+ exports.pgBossProjectQueueName = pgBossProjectQueueName;
455
+ exports.pgBossSchedulerPlugin = pgBossSchedulerPlugin;
456
+ exports.pollingSchedulerPlugin = pollingSchedulerPlugin;
457
+ exports.resetSharedPgBossJobQueueForTests = resetSharedPgBossJobQueueForTests;
458
+ exports.resetSharedSchedulerForTests = resetSharedSchedulerForTests;
459
+ exports.resolveSchedulerPluginFromEnv = resolveSchedulerPluginFromEnv;
460
+ exports.retryDelayMs = require_contract.retryDelayMs;
461
+ exports.startPgBoss = startPgBoss;
462
+ exports.stopPgBoss = stopPgBoss;
463
+ exports.wrapJobQueueAsScheduler = wrapJobQueueAsScheduler;
464
+
465
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["sleep","retryDelayMs","PgBoss","retryDelayMs","PgBoss","DEFAULT_DATABASE_URL","defineSchedulerPlugin","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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"}
@@ -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.cjs";
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.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","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"}