do-jobs 0.0.2 → 0.1.2

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 ADDED
@@ -0,0 +1,121 @@
1
+ # do-jobs
2
+
3
+ Type-safe delayed and interval jobs for Cloudflare Durable Objects.
4
+
5
+ ## Getting Started
6
+
7
+ ### 1. Install
8
+
9
+ ```bash
10
+ pnpm add do-jobs zod
11
+ ```
12
+
13
+ `do-jobs` validates job payloads with the [Standard Schema](https://standardschema.dev/) interface, so you can use libraries like Zod.
14
+
15
+ ### 2. Define jobs and wire your Durable Object
16
+
17
+ ```ts
18
+ import { createDefineJob, setupJobs, type JobRuntime } from "do-jobs";
19
+ import { z } from "zod";
20
+
21
+ type Env = {
22
+ WEBHOOK_URL: string;
23
+ };
24
+
25
+ const defineJob = createDefineJob<{ env: Env }>();
26
+
27
+ const sendWebhook = defineJob({ type: "send-webhook" })
28
+ .input(
29
+ z.object({
30
+ message: z.string().min(1),
31
+ }),
32
+ )
33
+ .handler(async ({ input, context, job }) => {
34
+ await fetch(context.env.WEBHOOK_URL, {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json" },
37
+ body: JSON.stringify({ id: job.id, message: input.message }),
38
+ });
39
+ });
40
+
41
+ export class JobsDO extends DurableObject<Env> {
42
+ private runtimePromise: Promise<JobRuntime>;
43
+
44
+ constructor(ctx: DurableObjectState, env: Env) {
45
+ super(ctx, env);
46
+ this.runtimePromise = setupJobs({
47
+ ctx,
48
+ context: { env },
49
+ jobs: [sendWebhook],
50
+ });
51
+ }
52
+
53
+ async fetch(request: Request): Promise<Response> {
54
+ const runtime = await this.runtimePromise;
55
+ const url = new URL(request.url);
56
+
57
+ if (url.pathname === "/enqueue") {
58
+ await runtime.schedule(sendWebhook, {
59
+ input: { message: "hello" },
60
+ at: Date.now() + 5_000,
61
+ });
62
+
63
+ return new Response("queued", { status: 202 });
64
+ }
65
+
66
+ return new Response("not found", { status: 404 });
67
+ }
68
+
69
+ async alarm(): Promise<void> {
70
+ const runtime = await this.runtimePromise;
71
+ await runtime.onAlarm();
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Docs
77
+
78
+ ### `createDefineJob<TContext>()`
79
+
80
+ Creates a typed builder for declaring jobs.
81
+
82
+ ```ts
83
+ const defineJob = createDefineJob<{ env: Env }>();
84
+ ```
85
+
86
+ A job definition requires:
87
+ - `type`: unique string identifier.
88
+ - `input(schema)`: any Standard Schema compatible validator.
89
+ - `handler(({ input, context, job }) => ...)`: execution function.
90
+
91
+ ### `setupJobs({ jobs, ctx, context, maxJobsPerAlarm? })`
92
+
93
+ Initializes runtime state and returns a `JobRuntime`.
94
+
95
+ - `jobs`: list of registered job definitions.
96
+ - `ctx`: Durable Object state.
97
+ - `context`: object injected into every handler.
98
+ - `maxJobsPerAlarm` (default `50`): upper bound of jobs executed per alarm tick.
99
+
100
+ `setupJobs(...)` also:
101
+ - creates/migrates internal SQLite tables,
102
+ - recalculates the next Durable Object alarm.
103
+
104
+ ### `JobRuntime`
105
+
106
+ `setupJobs(...)` returns methods:
107
+
108
+ - `onAlarm()`: run due jobs and schedule the next alarm.
109
+ - `setNextAlarm()`: force recalculation of next alarm time.
110
+ - `schedule(job, { input, at })`: queue a one-off run.
111
+ - `scheduleInterval(job, { input, dedupeKey, everyMs, startAt? })`: create/update an interval schedule. Safe to call on every Durable Object boot — see the note below.
112
+ - `cancelInterval(job, { dedupeKey })`: cancel a previously scheduled interval.
113
+
114
+ ### Behavior Notes
115
+
116
+ - Inputs are validated before enqueueing and again before handler execution.
117
+ - Persisted payloads must remain valid after JSON serialization.
118
+ - Interval jobs are deduplicated by `(job.type, dedupeKey)`.
119
+ - `scheduleInterval` is idempotent w.r.t. timing: re-registering an unchanged, still-active schedule preserves its pending `next_run_at`. This makes it safe to call from a Durable Object constructor / `onStart`, which run again on every wake from hibernation. The next run is only (re)started when you pass an explicit `startAt`, change `everyMs`, or revive a cancelled schedule.
120
+ - Failed handlers are marked as `failed` and store error message/stack.
121
+ - Job statuses: `queued`, `running`, `completed`, `failed`, `cancelled`.
package/dist/index.mjs CHANGED
@@ -153,6 +153,8 @@ function upsertIntervalSchedule({ ctx, type, dedupeKey, input, everyMs, startAt
153
153
  const now = Date.now();
154
154
  const scheduleId = crypto.randomUUID();
155
155
  const payload = JSON.stringify(input);
156
+ const startAtProvided = startAt !== void 0;
157
+ const nextRunAt = startAtProvided ? startAt : now + everyMs;
156
158
  ctx.storage.transactionSync(() => {
157
159
  execute(ctx.storage, `INSERT INTO "${JOB_SCHEDULES_TABLE}" (
158
160
  "id",
@@ -169,7 +171,12 @@ function upsertIntervalSchedule({ ctx, type, dedupeKey, input, everyMs, startAt
169
171
  ON CONFLICT ("type", "dedupe_key") DO UPDATE SET
170
172
  "payload" = excluded."payload",
171
173
  "interval_ms" = excluded."interval_ms",
172
- "next_run_at" = excluded."next_run_at",
174
+ "next_run_at" = CASE
175
+ WHEN ? = 1 THEN excluded."next_run_at"
176
+ WHEN "${JOB_SCHEDULES_TABLE}"."status" <> 'active' THEN excluded."next_run_at"
177
+ WHEN "${JOB_SCHEDULES_TABLE}"."interval_ms" <> excluded."interval_ms" THEN excluded."next_run_at"
178
+ ELSE "${JOB_SCHEDULES_TABLE}"."next_run_at"
179
+ END,
173
180
  "status" = excluded."status",
174
181
  "updated_at" = excluded."updated_at"`, [
175
182
  scheduleId,
@@ -177,11 +184,12 @@ function upsertIntervalSchedule({ ctx, type, dedupeKey, input, everyMs, startAt
177
184
  dedupeKey,
178
185
  payload,
179
186
  everyMs,
180
- startAt,
187
+ nextRunAt,
181
188
  "active",
182
189
  now,
183
190
  now,
184
- null
191
+ null,
192
+ startAtProvided ? 1 : 0
185
193
  ]);
186
194
  });
187
195
  const [row] = execute(ctx.storage, `SELECT
@@ -489,7 +497,7 @@ async function setupJobs(options) {
489
497
  const schema = requireRegisteredJob(jobsByType, job)[jobDefinitionInternals].schema;
490
498
  validateDedupeKey(scheduleOptions.dedupeKey);
491
499
  const everyMs = normalizeIntervalMs(scheduleOptions.everyMs);
492
- const startAt = normalizeTimestamp(scheduleOptions.startAt ?? Date.now() + everyMs, `"startAt"`);
500
+ const startAt = scheduleOptions.startAt === void 0 ? void 0 : normalizeTimestamp(scheduleOptions.startAt, `"startAt"`);
493
501
  const input = await parseJobInput(schema, scheduleOptions.input);
494
502
  await validatePersistedInput(schema, input);
495
503
  const record = upsertIntervalSchedule({
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/define-job.ts","../src/schema.ts","../src/storage.ts","../src/runtime.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { CreateDefineJobBuilder, DefinedJob, DefineJobBuilder, DefineJobInputBuilder, JobHandler } from \"./types\";\n\nexport const jobDefinitionInternals = Symbol.for(\"cloudflare-do-jobs/definition\");\n\ntype JobDefinitionInternals<TType extends string, TSchema extends StandardSchemaV1> = {\n schema: TSchema;\n handler: JobHandler<StandardSchemaV1.InferOutput<TSchema>, Record<string, unknown>, TType>;\n};\n\nexport type InternalDefinedJob<TType extends string, TSchema extends StandardSchemaV1> = DefinedJob<\n TType,\n TSchema,\n Record<string, unknown>\n> & {\n [jobDefinitionInternals]: JobDefinitionInternals<TType, TSchema>;\n};\n\nexport function createDefineJob<\n TContext extends Record<string, unknown> = Record<string, unknown>,\n>(): CreateDefineJobBuilder<TContext> {\n return function defineJob<TType extends string>(options: { type: TType }): DefineJobBuilder<TType, TContext> {\n return {\n input: <TSchema extends StandardSchemaV1>(schema: TSchema): DefineJobInputBuilder<TType, TSchema, TContext> => ({\n handler: (handler: JobHandler<StandardSchemaV1.InferOutput<TSchema>, TContext, TType>) => {\n const job: InternalDefinedJob<TType, TSchema> = {\n type: options.type,\n [jobDefinitionInternals]: {\n schema,\n handler: handler as JobHandler<StandardSchemaV1.InferOutput<TSchema>, Record<string, unknown>, TType>,\n },\n };\n return job as DefinedJob<TType, TSchema, TContext>;\n },\n }),\n };\n };\n}\n","const JOBS_SCHEMA_VERSION_KEY = \"jobs-schema-version\";\n\nexport const JOBS_TABLE = \"__jobs\";\nexport const JOB_SCHEDULES_TABLE = \"__job_schedules\";\n\ntype JobsSchemaMigration = {\n version: number;\n up: (storage: DurableObjectStorage) => void;\n};\n\nconst jobsSchemaMigrations: JobsSchemaMigration[] = [\n {\n version: 0,\n up: (storage) => {\n storage.sql.exec(`CREATE TABLE IF NOT EXISTS \"${JOBS_TABLE}\" (\n \"id\" TEXT NOT NULL PRIMARY KEY,\n \"type\" TEXT NOT NULL,\n \"status\" TEXT NOT NULL,\n \"payload\" TEXT NOT NULL,\n \"scheduled_at\" INTEGER NOT NULL,\n \"started_at\" INTEGER,\n \"finished_at\" INTEGER,\n \"error_message\" TEXT,\n \"error_stack\" TEXT,\n \"schedule_id\" TEXT,\n \"created_at\" INTEGER NOT NULL,\n \"updated_at\" INTEGER NOT NULL\n )`);\n\n storage.sql.exec(`CREATE INDEX IF NOT EXISTS \"idx_jobs_due\" ON \"${JOBS_TABLE}\" (\"status\", \"scheduled_at\", \"id\")`);\n\n storage.sql.exec(`CREATE TABLE IF NOT EXISTS \"${JOB_SCHEDULES_TABLE}\" (\n \"id\" TEXT NOT NULL PRIMARY KEY,\n \"type\" TEXT NOT NULL,\n \"dedupe_key\" TEXT NOT NULL,\n \"payload\" TEXT NOT NULL,\n \"interval_ms\" INTEGER NOT NULL,\n \"next_run_at\" INTEGER NOT NULL,\n \"status\" TEXT NOT NULL,\n \"created_at\" INTEGER NOT NULL,\n \"updated_at\" INTEGER NOT NULL,\n \"last_run_at\" INTEGER\n )`);\n\n storage.sql.exec(\n `CREATE UNIQUE INDEX IF NOT EXISTS \"idx_job_schedules_type_key\" ON \"${JOB_SCHEDULES_TABLE}\" (\"type\", \"dedupe_key\")`,\n );\n storage.sql.exec(\n `CREATE INDEX IF NOT EXISTS \"idx_job_schedules_due\" ON \"${JOB_SCHEDULES_TABLE}\" (\"status\", \"next_run_at\", \"id\")`,\n );\n },\n },\n];\n\nexport function ensureJobsSchema(ctx: DurableObjectState): void {\n const currentVersion = ctx.storage.kv.get<number>(JOBS_SCHEMA_VERSION_KEY) ?? -1;\n\n for (const migration of jobsSchemaMigrations) {\n if (migration.version <= currentVersion) continue;\n\n ctx.storage.transactionSync(() => {\n migration.up(ctx.storage);\n ctx.storage.kv.put(JOBS_SCHEMA_VERSION_KEY, migration.version);\n });\n }\n}\n","import { JOB_SCHEDULES_TABLE, JOBS_TABLE } from \"./schema\";\nimport type { IntervalScheduleRecord, JobRunRecord } from \"./types\";\n\ntype JobRow = {\n id: string;\n type: string;\n status: \"queued\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n payload: string;\n scheduled_at: number;\n started_at: number | null;\n finished_at: number | null;\n error_message: string | null;\n error_stack: string | null;\n schedule_id: string | null;\n created_at: number;\n updated_at: number;\n};\n\ntype ScheduleRow = {\n id: string;\n type: string;\n dedupe_key: string;\n payload: string;\n interval_ms: number;\n next_run_at: number;\n status: \"active\" | \"cancelled\";\n created_at: number;\n updated_at: number;\n last_run_at: number | null;\n};\n\nfunction execute<TResult = unknown>(\n storage: DurableObjectStorage,\n sql: string,\n parameters: readonly unknown[] = [],\n): TResult[] {\n return storage.sql.exec(sql, ...parameters).toArray() as TResult[];\n}\n\nfunction parsePayload(payload: string): unknown {\n return JSON.parse(payload);\n}\n\nexport function toJobRunRecord<TType extends string = string, TInput = unknown>(\n row: JobRow,\n): JobRunRecord<TType, TInput> {\n return {\n id: row.id,\n type: row.type as TType,\n status: row.status,\n payload: parsePayload(row.payload) as TInput,\n scheduledAt: row.scheduled_at,\n startedAt: row.started_at,\n finishedAt: row.finished_at,\n errorMessage: row.error_message,\n errorStack: row.error_stack,\n scheduleId: row.schedule_id,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toIntervalScheduleRecord<TType extends string = string, TInput = unknown>(\n row: ScheduleRow,\n): IntervalScheduleRecord<TType, TInput> {\n return {\n id: row.id,\n type: row.type as TType,\n dedupeKey: row.dedupe_key,\n payload: parsePayload(row.payload) as TInput,\n intervalMs: row.interval_ms,\n nextRunAt: row.next_run_at,\n status: row.status,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n lastRunAt: row.last_run_at,\n };\n}\n\nexport function insertOneOffJob<TInput>({\n ctx,\n type,\n input,\n at,\n}: {\n ctx: DurableObjectState;\n type: string;\n input: TInput;\n at: number;\n}): JobRunRecord<string, TInput> {\n const now = Date.now();\n const row: JobRow = {\n id: crypto.randomUUID(),\n type,\n status: \"queued\",\n payload: JSON.stringify(input),\n scheduled_at: at,\n started_at: null,\n finished_at: null,\n error_message: null,\n error_stack: null,\n schedule_id: null,\n created_at: now,\n updated_at: now,\n };\n\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `INSERT INTO \"${JOBS_TABLE}\" (\n \"id\",\n \"type\",\n \"status\",\n \"payload\",\n \"scheduled_at\",\n \"started_at\",\n \"finished_at\",\n \"error_message\",\n \"error_stack\",\n \"schedule_id\",\n \"created_at\",\n \"updated_at\"\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n row.id,\n row.type,\n row.status,\n row.payload,\n row.scheduled_at,\n row.started_at,\n row.finished_at,\n row.error_message,\n row.error_stack,\n row.schedule_id,\n row.created_at,\n row.updated_at,\n ],\n );\n });\n\n return toJobRunRecord<string, TInput>(row);\n}\n\nexport function upsertIntervalSchedule<TInput>({\n ctx,\n type,\n dedupeKey,\n input,\n everyMs,\n startAt,\n}: {\n ctx: DurableObjectState;\n type: string;\n dedupeKey: string;\n input: TInput;\n everyMs: number;\n startAt: number;\n}): IntervalScheduleRecord<string, TInput> {\n const now = Date.now();\n const scheduleId = crypto.randomUUID();\n const payload = JSON.stringify(input);\n\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `INSERT INTO \"${JOB_SCHEDULES_TABLE}\" (\n \"id\",\n \"type\",\n \"dedupe_key\",\n \"payload\",\n \"interval_ms\",\n \"next_run_at\",\n \"status\",\n \"created_at\",\n \"updated_at\",\n \"last_run_at\"\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (\"type\", \"dedupe_key\") DO UPDATE SET\n \"payload\" = excluded.\"payload\",\n \"interval_ms\" = excluded.\"interval_ms\",\n \"next_run_at\" = excluded.\"next_run_at\",\n \"status\" = excluded.\"status\",\n \"updated_at\" = excluded.\"updated_at\"`,\n [scheduleId, type, dedupeKey, payload, everyMs, startAt, \"active\", now, now, null],\n );\n });\n\n const [row] = execute<ScheduleRow>(\n ctx.storage,\n `SELECT\n \"id\",\n \"type\",\n \"dedupe_key\",\n \"payload\",\n \"interval_ms\",\n \"next_run_at\",\n \"status\",\n \"created_at\",\n \"updated_at\",\n \"last_run_at\"\n FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"type\" = ? AND \"dedupe_key\" = ?\n LIMIT 1`,\n [type, dedupeKey],\n );\n\n if (!row) {\n throw new Error(`Failed to create schedule for job type \"${type}\"`);\n }\n\n return toIntervalScheduleRecord<string, TInput>(row);\n}\n\nexport function cancelIntervalSchedule({\n ctx,\n type,\n dedupeKey,\n}: {\n ctx: DurableObjectState;\n type: string;\n dedupeKey: string;\n}): boolean {\n const [existing] = execute<Pick<ScheduleRow, \"id\">>(\n ctx.storage,\n `SELECT \"id\" FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"type\" = ? AND \"dedupe_key\" = ? AND \"status\" = 'active'\n LIMIT 1`,\n [type, dedupeKey],\n );\n\n if (!existing) {\n return false;\n }\n\n const now = Date.now();\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOB_SCHEDULES_TABLE}\"\n SET \"status\" = 'cancelled', \"updated_at\" = ?\n WHERE \"id\" = ?`,\n [now, existing.id],\n );\n });\n\n return true;\n}\n\nexport function materializeDueSchedules(ctx: DurableObjectState, now: number): number {\n let insertedJobs = 0;\n\n ctx.storage.transactionSync(() => {\n const dueSchedules = execute<ScheduleRow>(\n ctx.storage,\n `SELECT\n \"id\",\n \"type\",\n \"dedupe_key\",\n \"payload\",\n \"interval_ms\",\n \"next_run_at\",\n \"status\",\n \"created_at\",\n \"updated_at\",\n \"last_run_at\"\n FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"status\" = 'active' AND \"next_run_at\" <= ?\n ORDER BY \"next_run_at\" ASC, \"id\" ASC`,\n [now],\n );\n\n for (const schedule of dueSchedules) {\n const runId = crypto.randomUUID();\n execute(\n ctx.storage,\n `INSERT INTO \"${JOBS_TABLE}\" (\n \"id\",\n \"type\",\n \"status\",\n \"payload\",\n \"scheduled_at\",\n \"started_at\",\n \"finished_at\",\n \"error_message\",\n \"error_stack\",\n \"schedule_id\",\n \"created_at\",\n \"updated_at\"\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n runId,\n schedule.type,\n \"queued\",\n schedule.payload,\n schedule.next_run_at,\n null,\n null,\n null,\n null,\n schedule.id,\n now,\n now,\n ],\n );\n\n execute(\n ctx.storage,\n `UPDATE \"${JOB_SCHEDULES_TABLE}\"\n SET \"last_run_at\" = ?, \"next_run_at\" = ?, \"updated_at\" = ?\n WHERE \"id\" = ?`,\n [now, now + schedule.interval_ms, now, schedule.id],\n );\n\n insertedJobs += 1;\n }\n });\n\n return insertedJobs;\n}\n\nexport function getDueQueuedJobs(ctx: DurableObjectState, now: number, limit: number): JobRow[] {\n return execute<JobRow>(\n ctx.storage,\n `SELECT\n \"id\",\n \"type\",\n \"status\",\n \"payload\",\n \"scheduled_at\",\n \"started_at\",\n \"finished_at\",\n \"error_message\",\n \"error_stack\",\n \"schedule_id\",\n \"created_at\",\n \"updated_at\"\n FROM \"${JOBS_TABLE}\"\n WHERE \"status\" = 'queued' AND \"scheduled_at\" <= ?\n ORDER BY \"scheduled_at\" ASC, \"id\" ASC\n LIMIT ?`,\n [now, limit],\n );\n}\n\nexport function markJobRunning(ctx: DurableObjectState, jobId: string, startedAt: number): void {\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOBS_TABLE}\"\n SET \"status\" = 'running', \"started_at\" = ?, \"updated_at\" = ?\n WHERE \"id\" = ?`,\n [startedAt, startedAt, jobId],\n );\n });\n}\n\nexport function markJobCompleted(ctx: DurableObjectState, jobId: string, finishedAt: number): void {\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOBS_TABLE}\"\n SET\n \"status\" = 'completed',\n \"finished_at\" = ?,\n \"updated_at\" = ?,\n \"error_message\" = NULL,\n \"error_stack\" = NULL\n WHERE \"id\" = ?`,\n [finishedAt, finishedAt, jobId],\n );\n });\n}\n\nfunction toErrorDetails(error: unknown): { message: string; stack: string | null } {\n if (error instanceof Error) {\n return {\n message: error.message,\n stack: error.stack ?? null,\n };\n }\n\n return {\n message: String(error),\n stack: null,\n };\n}\n\nexport function markJobFailed(ctx: DurableObjectState, jobId: string, finishedAt: number, error: unknown): void {\n const details = toErrorDetails(error);\n\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOBS_TABLE}\"\n SET\n \"status\" = 'failed',\n \"finished_at\" = ?,\n \"updated_at\" = ?,\n \"error_message\" = ?,\n \"error_stack\" = ?\n WHERE \"id\" = ?`,\n [finishedAt, finishedAt, details.message, details.stack, jobId],\n );\n });\n}\n\nexport async function setNextAlarmFromDb(ctx: DurableObjectState): Promise<number | null> {\n const [jobRow] = execute<{ next_at: number | null }>(\n ctx.storage,\n `SELECT MIN(\"scheduled_at\") AS \"next_at\"\n FROM \"${JOBS_TABLE}\"\n WHERE \"status\" = 'queued'`,\n );\n const [scheduleRow] = execute<{ next_at: number | null }>(\n ctx.storage,\n `SELECT MIN(\"next_run_at\") AS \"next_at\"\n FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"status\" = 'active'`,\n );\n\n const nextJobAt = jobRow?.next_at ?? null;\n const nextScheduleAt = scheduleRow?.next_at ?? null;\n\n let nextAlarmAt: number | null = null;\n if (nextJobAt !== null && nextScheduleAt !== null) {\n nextAlarmAt = Math.min(nextJobAt, nextScheduleAt);\n } else if (nextJobAt !== null) {\n nextAlarmAt = nextJobAt;\n } else if (nextScheduleAt !== null) {\n nextAlarmAt = nextScheduleAt;\n }\n\n if (nextAlarmAt === null) {\n await ctx.storage.deleteAlarm();\n return null;\n }\n\n await ctx.storage.setAlarm(nextAlarmAt);\n return nextAlarmAt;\n}\n\nexport type { JobRow };\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { type InternalDefinedJob, jobDefinitionInternals } from \"./define-job\";\nimport { ensureJobsSchema } from \"./schema\";\nimport {\n cancelIntervalSchedule,\n getDueQueuedJobs,\n insertOneOffJob,\n markJobCompleted,\n markJobFailed,\n markJobRunning,\n materializeDueSchedules,\n setNextAlarmFromDb,\n toJobRunRecord,\n upsertIntervalSchedule,\n} from \"./storage\";\nimport type { AnyDefinedJob, JobRunResult, JobRuntime } from \"./types\";\n\ntype SetupJobsOptions<TContext extends Record<string, unknown>, TJobs extends readonly AnyDefinedJob[]> = {\n jobs: TJobs;\n ctx: DurableObjectState;\n context: TContext;\n maxJobsPerAlarm?: number;\n};\n\ntype InternalJob = InternalDefinedJob<string, StandardSchemaV1>;\n\nfunction getInternalJob(job: AnyDefinedJob): InternalJob {\n const internal = (job as InternalJob)[jobDefinitionInternals];\n if (!internal) {\n throw new Error(`Invalid job \"${job.type}\". Jobs must be created by defineJob(...).input(...).handler(...).`);\n }\n\n return job as InternalJob;\n}\n\nfunction validateMaxJobsPerAlarm(maxJobsPerAlarm: number): number {\n if (!Number.isFinite(maxJobsPerAlarm) || !Number.isInteger(maxJobsPerAlarm) || maxJobsPerAlarm < 1) {\n throw new Error(`Invalid \"maxJobsPerAlarm\". Expected a positive integer.`);\n }\n return maxJobsPerAlarm;\n}\n\nfunction requireRegisteredJob(jobsByType: Map<string, InternalJob>, job: AnyDefinedJob): InternalJob {\n const registered = jobsByType.get(job.type);\n if (!registered) {\n throw new Error(`Job type \"${job.type}\" is not registered. Pass it to setupJobs({ jobs: [...] }).`);\n }\n return registered;\n}\n\nfunction normalizeTimestamp(value: number, label: string): number {\n if (!Number.isFinite(value)) {\n throw new Error(`Invalid ${label}. Expected a finite timestamp in milliseconds.`);\n }\n return Math.floor(value);\n}\n\nfunction normalizeIntervalMs(everyMs: number): number {\n if (!Number.isFinite(everyMs) || !Number.isInteger(everyMs) || everyMs < 1) {\n throw new Error(`Invalid \"everyMs\". Expected a positive integer number of milliseconds.`);\n }\n return everyMs;\n}\n\nasync function parseJobInput<TSchema extends StandardSchemaV1>(\n schema: TSchema,\n input: unknown,\n): Promise<StandardSchemaV1.InferOutput<TSchema>> {\n const result = await schema[\"~standard\"].validate(input);\n if (result.issues) {\n const firstMessage = result.issues[0]?.message;\n throw new Error(\n firstMessage ? `Invalid \"input\". ${firstMessage}` : `Invalid \"input\". Payload does not match schema.`,\n );\n }\n\n return result.value;\n}\n\nasync function validatePersistedInput<TSchema extends StandardSchemaV1>(\n schema: TSchema,\n input: StandardSchemaV1.InferOutput<TSchema>,\n): Promise<void> {\n let serialized: string;\n try {\n serialized = JSON.stringify(input);\n } catch (error) {\n throw new Error(`Invalid \"input\". Job payload must be JSON-serializable before persistence: ${String(error)}`);\n }\n\n if (serialized === undefined) {\n throw new Error(`Invalid \"input\". Job payload must serialize to JSON.`);\n }\n\n const roundTripped: unknown = JSON.parse(serialized);\n const result = await schema[\"~standard\"].validate(roundTripped);\n if (result.issues) {\n throw new Error(`Invalid \"input\". Job payload must remain valid after JSON serialization for persisted jobs.`);\n }\n}\n\nfunction validateDedupeKey(dedupeKey: string): void {\n if (!dedupeKey || dedupeKey.trim().length === 0) {\n throw new Error(`Invalid \"dedupeKey\". Expected a non-empty string.`);\n }\n}\n\nexport async function setupJobs<TContext extends Record<string, unknown>, TJobs extends readonly AnyDefinedJob[]>(\n options: SetupJobsOptions<TContext, TJobs>,\n): Promise<JobRuntime> {\n const maxJobsPerAlarm = validateMaxJobsPerAlarm(options.maxJobsPerAlarm ?? 50);\n const jobsByType = new Map<string, InternalJob>();\n\n for (const job of options.jobs) {\n const internalJob = getInternalJob(job);\n if (jobsByType.has(job.type)) {\n throw new Error(`Duplicate job type \"${job.type}\" during setupJobs.`);\n }\n jobsByType.set(job.type, internalJob);\n }\n\n ensureJobsSchema(options.ctx);\n await setNextAlarmFromDb(options.ctx);\n\n const onAlarm = async (): Promise<JobRunResult> => {\n const now = Date.now();\n materializeDueSchedules(options.ctx, now);\n\n let processedJobs = 0;\n\n while (processedJobs < maxJobsPerAlarm) {\n const remaining = maxJobsPerAlarm - processedJobs;\n const dueJobs = getDueQueuedJobs(options.ctx, Date.now(), remaining);\n\n if (dueJobs.length === 0) {\n break;\n }\n\n for (const jobRow of dueJobs) {\n if (processedJobs >= maxJobsPerAlarm) {\n break;\n }\n\n const internalJob = jobsByType.get(jobRow.type);\n const startedAt = Date.now();\n markJobRunning(options.ctx, jobRow.id, startedAt);\n\n try {\n if (!internalJob) {\n throw new Error(`No registered handler for job type \"${jobRow.type}\".`);\n }\n\n const queuedRecord = toJobRunRecord(jobRow);\n const parsed = await internalJob[jobDefinitionInternals].schema[\"~standard\"].validate(queuedRecord.payload);\n if (parsed.issues) {\n throw new Error(`Invalid persisted payload for job type \"${jobRow.type}\".`);\n }\n\n const input = parsed.value;\n const runningRecord = {\n ...queuedRecord,\n status: \"running\" as const,\n payload: input,\n startedAt,\n updatedAt: startedAt,\n };\n\n await internalJob[jobDefinitionInternals].handler({\n input,\n context: options.context,\n job: runningRecord,\n });\n\n markJobCompleted(options.ctx, jobRow.id, Date.now());\n } catch (error) {\n markJobFailed(options.ctx, jobRow.id, Date.now(), error);\n }\n\n processedJobs += 1;\n }\n }\n\n const nextAlarmAt = await setNextAlarmFromDb(options.ctx);\n return {\n processedJobs,\n nextAlarmAt,\n };\n };\n\n return {\n onAlarm,\n setNextAlarm: async () => setNextAlarmFromDb(options.ctx),\n\n schedule: (async (job, scheduleOptions) => {\n const registered = requireRegisteredJob(jobsByType, job);\n const schema = registered[jobDefinitionInternals].schema;\n const at = normalizeTimestamp(scheduleOptions.at, `\"at\"`);\n const input = await parseJobInput(schema, scheduleOptions.input);\n await validatePersistedInput(schema, input);\n\n const record = insertOneOffJob({\n ctx: options.ctx,\n type: job.type,\n input,\n at,\n });\n\n await setNextAlarmFromDb(options.ctx);\n return record;\n }) as JobRuntime[\"schedule\"],\n\n scheduleInterval: (async (job, scheduleOptions) => {\n const registered = requireRegisteredJob(jobsByType, job);\n const schema = registered[jobDefinitionInternals].schema;\n validateDedupeKey(scheduleOptions.dedupeKey);\n const everyMs = normalizeIntervalMs(scheduleOptions.everyMs);\n const startAt = normalizeTimestamp(scheduleOptions.startAt ?? Date.now() + everyMs, `\"startAt\"`);\n const input = await parseJobInput(schema, scheduleOptions.input);\n await validatePersistedInput(schema, input);\n\n const record = upsertIntervalSchedule({\n ctx: options.ctx,\n type: job.type,\n dedupeKey: scheduleOptions.dedupeKey,\n input,\n everyMs,\n startAt,\n });\n\n await setNextAlarmFromDb(options.ctx);\n return record;\n }) as JobRuntime[\"scheduleInterval\"],\n\n cancelInterval: (async (job, cancelOptions) => {\n requireRegisteredJob(jobsByType, job);\n validateDedupeKey(cancelOptions.dedupeKey);\n\n const cancelled = cancelIntervalSchedule({\n ctx: options.ctx,\n type: job.type,\n dedupeKey: cancelOptions.dedupeKey,\n });\n\n await setNextAlarmFromDb(options.ctx);\n return cancelled;\n }) as JobRuntime[\"cancelInterval\"],\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/define-job.ts","../src/schema.ts","../src/storage.ts","../src/runtime.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { CreateDefineJobBuilder, DefinedJob, DefineJobBuilder, DefineJobInputBuilder, JobHandler } from \"./types\";\n\nexport const jobDefinitionInternals = Symbol.for(\"cloudflare-do-jobs/definition\");\n\ntype JobDefinitionInternals<TType extends string, TSchema extends StandardSchemaV1> = {\n schema: TSchema;\n handler: JobHandler<StandardSchemaV1.InferOutput<TSchema>, Record<string, unknown>, TType>;\n};\n\nexport type InternalDefinedJob<TType extends string, TSchema extends StandardSchemaV1> = DefinedJob<\n TType,\n TSchema,\n Record<string, unknown>\n> & {\n [jobDefinitionInternals]: JobDefinitionInternals<TType, TSchema>;\n};\n\nexport function createDefineJob<\n TContext extends Record<string, unknown> = Record<string, unknown>,\n>(): CreateDefineJobBuilder<TContext> {\n return function defineJob<TType extends string>(options: { type: TType }): DefineJobBuilder<TType, TContext> {\n return {\n input: <TSchema extends StandardSchemaV1>(schema: TSchema): DefineJobInputBuilder<TType, TSchema, TContext> => ({\n handler: (handler: JobHandler<StandardSchemaV1.InferOutput<TSchema>, TContext, TType>) => {\n const job: InternalDefinedJob<TType, TSchema> = {\n type: options.type,\n [jobDefinitionInternals]: {\n schema,\n handler: handler as JobHandler<StandardSchemaV1.InferOutput<TSchema>, Record<string, unknown>, TType>,\n },\n };\n return job as DefinedJob<TType, TSchema, TContext>;\n },\n }),\n };\n };\n}\n","const JOBS_SCHEMA_VERSION_KEY = \"jobs-schema-version\";\n\nexport const JOBS_TABLE = \"__jobs\";\nexport const JOB_SCHEDULES_TABLE = \"__job_schedules\";\n\ntype JobsSchemaMigration = {\n version: number;\n up: (storage: DurableObjectStorage) => void;\n};\n\nconst jobsSchemaMigrations: JobsSchemaMigration[] = [\n {\n version: 0,\n up: (storage) => {\n storage.sql.exec(`CREATE TABLE IF NOT EXISTS \"${JOBS_TABLE}\" (\n \"id\" TEXT NOT NULL PRIMARY KEY,\n \"type\" TEXT NOT NULL,\n \"status\" TEXT NOT NULL,\n \"payload\" TEXT NOT NULL,\n \"scheduled_at\" INTEGER NOT NULL,\n \"started_at\" INTEGER,\n \"finished_at\" INTEGER,\n \"error_message\" TEXT,\n \"error_stack\" TEXT,\n \"schedule_id\" TEXT,\n \"created_at\" INTEGER NOT NULL,\n \"updated_at\" INTEGER NOT NULL\n )`);\n\n storage.sql.exec(`CREATE INDEX IF NOT EXISTS \"idx_jobs_due\" ON \"${JOBS_TABLE}\" (\"status\", \"scheduled_at\", \"id\")`);\n\n storage.sql.exec(`CREATE TABLE IF NOT EXISTS \"${JOB_SCHEDULES_TABLE}\" (\n \"id\" TEXT NOT NULL PRIMARY KEY,\n \"type\" TEXT NOT NULL,\n \"dedupe_key\" TEXT NOT NULL,\n \"payload\" TEXT NOT NULL,\n \"interval_ms\" INTEGER NOT NULL,\n \"next_run_at\" INTEGER NOT NULL,\n \"status\" TEXT NOT NULL,\n \"created_at\" INTEGER NOT NULL,\n \"updated_at\" INTEGER NOT NULL,\n \"last_run_at\" INTEGER\n )`);\n\n storage.sql.exec(\n `CREATE UNIQUE INDEX IF NOT EXISTS \"idx_job_schedules_type_key\" ON \"${JOB_SCHEDULES_TABLE}\" (\"type\", \"dedupe_key\")`,\n );\n storage.sql.exec(\n `CREATE INDEX IF NOT EXISTS \"idx_job_schedules_due\" ON \"${JOB_SCHEDULES_TABLE}\" (\"status\", \"next_run_at\", \"id\")`,\n );\n },\n },\n];\n\nexport function ensureJobsSchema(ctx: DurableObjectState): void {\n const currentVersion = ctx.storage.kv.get<number>(JOBS_SCHEMA_VERSION_KEY) ?? -1;\n\n for (const migration of jobsSchemaMigrations) {\n if (migration.version <= currentVersion) continue;\n\n ctx.storage.transactionSync(() => {\n migration.up(ctx.storage);\n ctx.storage.kv.put(JOBS_SCHEMA_VERSION_KEY, migration.version);\n });\n }\n}\n","import { JOB_SCHEDULES_TABLE, JOBS_TABLE } from \"./schema\";\nimport type { IntervalScheduleRecord, JobRunRecord } from \"./types\";\n\ntype JobRow = {\n id: string;\n type: string;\n status: \"queued\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n payload: string;\n scheduled_at: number;\n started_at: number | null;\n finished_at: number | null;\n error_message: string | null;\n error_stack: string | null;\n schedule_id: string | null;\n created_at: number;\n updated_at: number;\n};\n\ntype ScheduleRow = {\n id: string;\n type: string;\n dedupe_key: string;\n payload: string;\n interval_ms: number;\n next_run_at: number;\n status: \"active\" | \"cancelled\";\n created_at: number;\n updated_at: number;\n last_run_at: number | null;\n};\n\nfunction execute<TResult = unknown>(\n storage: DurableObjectStorage,\n sql: string,\n parameters: readonly unknown[] = [],\n): TResult[] {\n return storage.sql.exec(sql, ...parameters).toArray() as TResult[];\n}\n\nfunction parsePayload(payload: string): unknown {\n return JSON.parse(payload);\n}\n\nexport function toJobRunRecord<TType extends string = string, TInput = unknown>(\n row: JobRow,\n): JobRunRecord<TType, TInput> {\n return {\n id: row.id,\n type: row.type as TType,\n status: row.status,\n payload: parsePayload(row.payload) as TInput,\n scheduledAt: row.scheduled_at,\n startedAt: row.started_at,\n finishedAt: row.finished_at,\n errorMessage: row.error_message,\n errorStack: row.error_stack,\n scheduleId: row.schedule_id,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n };\n}\n\nfunction toIntervalScheduleRecord<TType extends string = string, TInput = unknown>(\n row: ScheduleRow,\n): IntervalScheduleRecord<TType, TInput> {\n return {\n id: row.id,\n type: row.type as TType,\n dedupeKey: row.dedupe_key,\n payload: parsePayload(row.payload) as TInput,\n intervalMs: row.interval_ms,\n nextRunAt: row.next_run_at,\n status: row.status,\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n lastRunAt: row.last_run_at,\n };\n}\n\nexport function insertOneOffJob<TInput>({\n ctx,\n type,\n input,\n at,\n}: {\n ctx: DurableObjectState;\n type: string;\n input: TInput;\n at: number;\n}): JobRunRecord<string, TInput> {\n const now = Date.now();\n const row: JobRow = {\n id: crypto.randomUUID(),\n type,\n status: \"queued\",\n payload: JSON.stringify(input),\n scheduled_at: at,\n started_at: null,\n finished_at: null,\n error_message: null,\n error_stack: null,\n schedule_id: null,\n created_at: now,\n updated_at: now,\n };\n\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `INSERT INTO \"${JOBS_TABLE}\" (\n \"id\",\n \"type\",\n \"status\",\n \"payload\",\n \"scheduled_at\",\n \"started_at\",\n \"finished_at\",\n \"error_message\",\n \"error_stack\",\n \"schedule_id\",\n \"created_at\",\n \"updated_at\"\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n row.id,\n row.type,\n row.status,\n row.payload,\n row.scheduled_at,\n row.started_at,\n row.finished_at,\n row.error_message,\n row.error_stack,\n row.schedule_id,\n row.created_at,\n row.updated_at,\n ],\n );\n });\n\n return toJobRunRecord<string, TInput>(row);\n}\n\nexport function upsertIntervalSchedule<TInput>({\n ctx,\n type,\n dedupeKey,\n input,\n everyMs,\n startAt,\n}: {\n ctx: DurableObjectState;\n type: string;\n dedupeKey: string;\n input: TInput;\n everyMs: number;\n /**\n * Explicit first-run timestamp. When omitted the schedule first runs one\n * interval from now. Only honored when (re)creating or restarting a\n * schedule — see the `next_run_at` logic below.\n */\n startAt?: number;\n}): IntervalScheduleRecord<string, TInput> {\n const now = Date.now();\n const scheduleId = crypto.randomUUID();\n const payload = JSON.stringify(input);\n const startAtProvided = startAt !== undefined;\n // The value used both for a fresh INSERT and (via `excluded`) whenever the\n // conflict branch decides the timer should be (re)started.\n const nextRunAt = startAtProvided ? startAt : now + everyMs;\n\n ctx.storage.transactionSync(() => {\n // `scheduleInterval` is an upsert keyed by (type, dedupe_key), so it is\n // routinely re-called every time the Durable Object boots (constructors /\n // `onStart` run on every wake from hibernation). It must therefore be\n // idempotent w.r.t. `next_run_at`: re-registering an unchanged, still-active\n // schedule must NOT push the next run forward, otherwise a DO that wakes\n // more often than the interval would defer the job forever.\n //\n // The timer is (re)started only when the caller explicitly asks for it\n // (`startAt` provided), when the cadence changes (`interval_ms` differs), or\n // when reviving a previously cancelled schedule. Otherwise the existing\n // `next_run_at` is preserved.\n execute(\n ctx.storage,\n `INSERT INTO \"${JOB_SCHEDULES_TABLE}\" (\n \"id\",\n \"type\",\n \"dedupe_key\",\n \"payload\",\n \"interval_ms\",\n \"next_run_at\",\n \"status\",\n \"created_at\",\n \"updated_at\",\n \"last_run_at\"\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (\"type\", \"dedupe_key\") DO UPDATE SET\n \"payload\" = excluded.\"payload\",\n \"interval_ms\" = excluded.\"interval_ms\",\n \"next_run_at\" = CASE\n WHEN ? = 1 THEN excluded.\"next_run_at\"\n WHEN \"${JOB_SCHEDULES_TABLE}\".\"status\" <> 'active' THEN excluded.\"next_run_at\"\n WHEN \"${JOB_SCHEDULES_TABLE}\".\"interval_ms\" <> excluded.\"interval_ms\" THEN excluded.\"next_run_at\"\n ELSE \"${JOB_SCHEDULES_TABLE}\".\"next_run_at\"\n END,\n \"status\" = excluded.\"status\",\n \"updated_at\" = excluded.\"updated_at\"`,\n [scheduleId, type, dedupeKey, payload, everyMs, nextRunAt, \"active\", now, now, null, startAtProvided ? 1 : 0],\n );\n });\n\n const [row] = execute<ScheduleRow>(\n ctx.storage,\n `SELECT\n \"id\",\n \"type\",\n \"dedupe_key\",\n \"payload\",\n \"interval_ms\",\n \"next_run_at\",\n \"status\",\n \"created_at\",\n \"updated_at\",\n \"last_run_at\"\n FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"type\" = ? AND \"dedupe_key\" = ?\n LIMIT 1`,\n [type, dedupeKey],\n );\n\n if (!row) {\n throw new Error(`Failed to create schedule for job type \"${type}\"`);\n }\n\n return toIntervalScheduleRecord<string, TInput>(row);\n}\n\nexport function cancelIntervalSchedule({\n ctx,\n type,\n dedupeKey,\n}: {\n ctx: DurableObjectState;\n type: string;\n dedupeKey: string;\n}): boolean {\n const [existing] = execute<Pick<ScheduleRow, \"id\">>(\n ctx.storage,\n `SELECT \"id\" FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"type\" = ? AND \"dedupe_key\" = ? AND \"status\" = 'active'\n LIMIT 1`,\n [type, dedupeKey],\n );\n\n if (!existing) {\n return false;\n }\n\n const now = Date.now();\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOB_SCHEDULES_TABLE}\"\n SET \"status\" = 'cancelled', \"updated_at\" = ?\n WHERE \"id\" = ?`,\n [now, existing.id],\n );\n });\n\n return true;\n}\n\nexport function materializeDueSchedules(ctx: DurableObjectState, now: number): number {\n let insertedJobs = 0;\n\n ctx.storage.transactionSync(() => {\n const dueSchedules = execute<ScheduleRow>(\n ctx.storage,\n `SELECT\n \"id\",\n \"type\",\n \"dedupe_key\",\n \"payload\",\n \"interval_ms\",\n \"next_run_at\",\n \"status\",\n \"created_at\",\n \"updated_at\",\n \"last_run_at\"\n FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"status\" = 'active' AND \"next_run_at\" <= ?\n ORDER BY \"next_run_at\" ASC, \"id\" ASC`,\n [now],\n );\n\n for (const schedule of dueSchedules) {\n const runId = crypto.randomUUID();\n execute(\n ctx.storage,\n `INSERT INTO \"${JOBS_TABLE}\" (\n \"id\",\n \"type\",\n \"status\",\n \"payload\",\n \"scheduled_at\",\n \"started_at\",\n \"finished_at\",\n \"error_message\",\n \"error_stack\",\n \"schedule_id\",\n \"created_at\",\n \"updated_at\"\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n runId,\n schedule.type,\n \"queued\",\n schedule.payload,\n schedule.next_run_at,\n null,\n null,\n null,\n null,\n schedule.id,\n now,\n now,\n ],\n );\n\n execute(\n ctx.storage,\n `UPDATE \"${JOB_SCHEDULES_TABLE}\"\n SET \"last_run_at\" = ?, \"next_run_at\" = ?, \"updated_at\" = ?\n WHERE \"id\" = ?`,\n [now, now + schedule.interval_ms, now, schedule.id],\n );\n\n insertedJobs += 1;\n }\n });\n\n return insertedJobs;\n}\n\nexport function getDueQueuedJobs(ctx: DurableObjectState, now: number, limit: number): JobRow[] {\n return execute<JobRow>(\n ctx.storage,\n `SELECT\n \"id\",\n \"type\",\n \"status\",\n \"payload\",\n \"scheduled_at\",\n \"started_at\",\n \"finished_at\",\n \"error_message\",\n \"error_stack\",\n \"schedule_id\",\n \"created_at\",\n \"updated_at\"\n FROM \"${JOBS_TABLE}\"\n WHERE \"status\" = 'queued' AND \"scheduled_at\" <= ?\n ORDER BY \"scheduled_at\" ASC, \"id\" ASC\n LIMIT ?`,\n [now, limit],\n );\n}\n\nexport function markJobRunning(ctx: DurableObjectState, jobId: string, startedAt: number): void {\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOBS_TABLE}\"\n SET \"status\" = 'running', \"started_at\" = ?, \"updated_at\" = ?\n WHERE \"id\" = ?`,\n [startedAt, startedAt, jobId],\n );\n });\n}\n\nexport function markJobCompleted(ctx: DurableObjectState, jobId: string, finishedAt: number): void {\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOBS_TABLE}\"\n SET\n \"status\" = 'completed',\n \"finished_at\" = ?,\n \"updated_at\" = ?,\n \"error_message\" = NULL,\n \"error_stack\" = NULL\n WHERE \"id\" = ?`,\n [finishedAt, finishedAt, jobId],\n );\n });\n}\n\nfunction toErrorDetails(error: unknown): { message: string; stack: string | null } {\n if (error instanceof Error) {\n return {\n message: error.message,\n stack: error.stack ?? null,\n };\n }\n\n return {\n message: String(error),\n stack: null,\n };\n}\n\nexport function markJobFailed(ctx: DurableObjectState, jobId: string, finishedAt: number, error: unknown): void {\n const details = toErrorDetails(error);\n\n ctx.storage.transactionSync(() => {\n execute(\n ctx.storage,\n `UPDATE \"${JOBS_TABLE}\"\n SET\n \"status\" = 'failed',\n \"finished_at\" = ?,\n \"updated_at\" = ?,\n \"error_message\" = ?,\n \"error_stack\" = ?\n WHERE \"id\" = ?`,\n [finishedAt, finishedAt, details.message, details.stack, jobId],\n );\n });\n}\n\nexport async function setNextAlarmFromDb(ctx: DurableObjectState): Promise<number | null> {\n const [jobRow] = execute<{ next_at: number | null }>(\n ctx.storage,\n `SELECT MIN(\"scheduled_at\") AS \"next_at\"\n FROM \"${JOBS_TABLE}\"\n WHERE \"status\" = 'queued'`,\n );\n const [scheduleRow] = execute<{ next_at: number | null }>(\n ctx.storage,\n `SELECT MIN(\"next_run_at\") AS \"next_at\"\n FROM \"${JOB_SCHEDULES_TABLE}\"\n WHERE \"status\" = 'active'`,\n );\n\n const nextJobAt = jobRow?.next_at ?? null;\n const nextScheduleAt = scheduleRow?.next_at ?? null;\n\n let nextAlarmAt: number | null = null;\n if (nextJobAt !== null && nextScheduleAt !== null) {\n nextAlarmAt = Math.min(nextJobAt, nextScheduleAt);\n } else if (nextJobAt !== null) {\n nextAlarmAt = nextJobAt;\n } else if (nextScheduleAt !== null) {\n nextAlarmAt = nextScheduleAt;\n }\n\n if (nextAlarmAt === null) {\n await ctx.storage.deleteAlarm();\n return null;\n }\n\n await ctx.storage.setAlarm(nextAlarmAt);\n return nextAlarmAt;\n}\n\nexport type { JobRow };\n","import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { type InternalDefinedJob, jobDefinitionInternals } from \"./define-job\";\nimport { ensureJobsSchema } from \"./schema\";\nimport {\n cancelIntervalSchedule,\n getDueQueuedJobs,\n insertOneOffJob,\n markJobCompleted,\n markJobFailed,\n markJobRunning,\n materializeDueSchedules,\n setNextAlarmFromDb,\n toJobRunRecord,\n upsertIntervalSchedule,\n} from \"./storage\";\nimport type { AnyDefinedJob, JobRunResult, JobRuntime } from \"./types\";\n\ntype SetupJobsOptions<TContext extends Record<string, unknown>, TJobs extends readonly AnyDefinedJob[]> = {\n jobs: TJobs;\n ctx: DurableObjectState;\n context: TContext;\n maxJobsPerAlarm?: number;\n};\n\ntype InternalJob = InternalDefinedJob<string, StandardSchemaV1>;\n\nfunction getInternalJob(job: AnyDefinedJob): InternalJob {\n const internal = (job as InternalJob)[jobDefinitionInternals];\n if (!internal) {\n throw new Error(`Invalid job \"${job.type}\". Jobs must be created by defineJob(...).input(...).handler(...).`);\n }\n\n return job as InternalJob;\n}\n\nfunction validateMaxJobsPerAlarm(maxJobsPerAlarm: number): number {\n if (!Number.isFinite(maxJobsPerAlarm) || !Number.isInteger(maxJobsPerAlarm) || maxJobsPerAlarm < 1) {\n throw new Error(`Invalid \"maxJobsPerAlarm\". Expected a positive integer.`);\n }\n return maxJobsPerAlarm;\n}\n\nfunction requireRegisteredJob(jobsByType: Map<string, InternalJob>, job: AnyDefinedJob): InternalJob {\n const registered = jobsByType.get(job.type);\n if (!registered) {\n throw new Error(`Job type \"${job.type}\" is not registered. Pass it to setupJobs({ jobs: [...] }).`);\n }\n return registered;\n}\n\nfunction normalizeTimestamp(value: number, label: string): number {\n if (!Number.isFinite(value)) {\n throw new Error(`Invalid ${label}. Expected a finite timestamp in milliseconds.`);\n }\n return Math.floor(value);\n}\n\nfunction normalizeIntervalMs(everyMs: number): number {\n if (!Number.isFinite(everyMs) || !Number.isInteger(everyMs) || everyMs < 1) {\n throw new Error(`Invalid \"everyMs\". Expected a positive integer number of milliseconds.`);\n }\n return everyMs;\n}\n\nasync function parseJobInput<TSchema extends StandardSchemaV1>(\n schema: TSchema,\n input: unknown,\n): Promise<StandardSchemaV1.InferOutput<TSchema>> {\n const result = await schema[\"~standard\"].validate(input);\n if (result.issues) {\n const firstMessage = result.issues[0]?.message;\n throw new Error(\n firstMessage ? `Invalid \"input\". ${firstMessage}` : `Invalid \"input\". Payload does not match schema.`,\n );\n }\n\n return result.value;\n}\n\nasync function validatePersistedInput<TSchema extends StandardSchemaV1>(\n schema: TSchema,\n input: StandardSchemaV1.InferOutput<TSchema>,\n): Promise<void> {\n let serialized: string;\n try {\n serialized = JSON.stringify(input);\n } catch (error) {\n throw new Error(`Invalid \"input\". Job payload must be JSON-serializable before persistence: ${String(error)}`);\n }\n\n if (serialized === undefined) {\n throw new Error(`Invalid \"input\". Job payload must serialize to JSON.`);\n }\n\n const roundTripped: unknown = JSON.parse(serialized);\n const result = await schema[\"~standard\"].validate(roundTripped);\n if (result.issues) {\n throw new Error(`Invalid \"input\". Job payload must remain valid after JSON serialization for persisted jobs.`);\n }\n}\n\nfunction validateDedupeKey(dedupeKey: string): void {\n if (!dedupeKey || dedupeKey.trim().length === 0) {\n throw new Error(`Invalid \"dedupeKey\". Expected a non-empty string.`);\n }\n}\n\nexport async function setupJobs<TContext extends Record<string, unknown>, TJobs extends readonly AnyDefinedJob[]>(\n options: SetupJobsOptions<TContext, TJobs>,\n): Promise<JobRuntime> {\n const maxJobsPerAlarm = validateMaxJobsPerAlarm(options.maxJobsPerAlarm ?? 50);\n const jobsByType = new Map<string, InternalJob>();\n\n for (const job of options.jobs) {\n const internalJob = getInternalJob(job);\n if (jobsByType.has(job.type)) {\n throw new Error(`Duplicate job type \"${job.type}\" during setupJobs.`);\n }\n jobsByType.set(job.type, internalJob);\n }\n\n ensureJobsSchema(options.ctx);\n await setNextAlarmFromDb(options.ctx);\n\n const onAlarm = async (): Promise<JobRunResult> => {\n const now = Date.now();\n materializeDueSchedules(options.ctx, now);\n\n let processedJobs = 0;\n\n while (processedJobs < maxJobsPerAlarm) {\n const remaining = maxJobsPerAlarm - processedJobs;\n const dueJobs = getDueQueuedJobs(options.ctx, Date.now(), remaining);\n\n if (dueJobs.length === 0) {\n break;\n }\n\n for (const jobRow of dueJobs) {\n if (processedJobs >= maxJobsPerAlarm) {\n break;\n }\n\n const internalJob = jobsByType.get(jobRow.type);\n const startedAt = Date.now();\n markJobRunning(options.ctx, jobRow.id, startedAt);\n\n try {\n if (!internalJob) {\n throw new Error(`No registered handler for job type \"${jobRow.type}\".`);\n }\n\n const queuedRecord = toJobRunRecord(jobRow);\n const parsed = await internalJob[jobDefinitionInternals].schema[\"~standard\"].validate(queuedRecord.payload);\n if (parsed.issues) {\n throw new Error(`Invalid persisted payload for job type \"${jobRow.type}\".`);\n }\n\n const input = parsed.value;\n const runningRecord = {\n ...queuedRecord,\n status: \"running\" as const,\n payload: input,\n startedAt,\n updatedAt: startedAt,\n };\n\n await internalJob[jobDefinitionInternals].handler({\n input,\n context: options.context,\n job: runningRecord,\n });\n\n markJobCompleted(options.ctx, jobRow.id, Date.now());\n } catch (error) {\n markJobFailed(options.ctx, jobRow.id, Date.now(), error);\n }\n\n processedJobs += 1;\n }\n }\n\n const nextAlarmAt = await setNextAlarmFromDb(options.ctx);\n return {\n processedJobs,\n nextAlarmAt,\n };\n };\n\n return {\n onAlarm,\n setNextAlarm: async () => setNextAlarmFromDb(options.ctx),\n\n schedule: (async (job, scheduleOptions) => {\n const registered = requireRegisteredJob(jobsByType, job);\n const schema = registered[jobDefinitionInternals].schema;\n const at = normalizeTimestamp(scheduleOptions.at, `\"at\"`);\n const input = await parseJobInput(schema, scheduleOptions.input);\n await validatePersistedInput(schema, input);\n\n const record = insertOneOffJob({\n ctx: options.ctx,\n type: job.type,\n input,\n at,\n });\n\n await setNextAlarmFromDb(options.ctx);\n return record;\n }) as JobRuntime[\"schedule\"],\n\n scheduleInterval: (async (job, scheduleOptions) => {\n const registered = requireRegisteredJob(jobsByType, job);\n const schema = registered[jobDefinitionInternals].schema;\n validateDedupeKey(scheduleOptions.dedupeKey);\n const everyMs = normalizeIntervalMs(scheduleOptions.everyMs);\n // Keep `startAt` undefined when the caller omits it so the upsert can stay\n // idempotent on re-registration instead of rewinding `next_run_at`.\n const startAt =\n scheduleOptions.startAt === undefined ? undefined : normalizeTimestamp(scheduleOptions.startAt, `\"startAt\"`);\n const input = await parseJobInput(schema, scheduleOptions.input);\n await validatePersistedInput(schema, input);\n\n const record = upsertIntervalSchedule({\n ctx: options.ctx,\n type: job.type,\n dedupeKey: scheduleOptions.dedupeKey,\n input,\n everyMs,\n startAt,\n });\n\n await setNextAlarmFromDb(options.ctx);\n return record;\n }) as JobRuntime[\"scheduleInterval\"],\n\n cancelInterval: (async (job, cancelOptions) => {\n requireRegisteredJob(jobsByType, job);\n validateDedupeKey(cancelOptions.dedupeKey);\n\n const cancelled = cancelIntervalSchedule({\n ctx: options.ctx,\n type: job.type,\n dedupeKey: cancelOptions.dedupeKey,\n });\n\n await setNextAlarmFromDb(options.ctx);\n return cancelled;\n }) as JobRuntime[\"cancelInterval\"],\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "do-jobs",
3
- "version": "0.0.2",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {