do-jobs 0.1.1 → 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 +2 -1
- package/dist/index.mjs +12 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Initializes runtime state and returns a `JobRuntime`.
|
|
|
108
108
|
- `onAlarm()`: run due jobs and schedule the next alarm.
|
|
109
109
|
- `setNextAlarm()`: force recalculation of next alarm time.
|
|
110
110
|
- `schedule(job, { input, at })`: queue a one-off run.
|
|
111
|
-
- `scheduleInterval(job, { input, dedupeKey, everyMs, startAt? })`: create/update an interval schedule.
|
|
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
112
|
- `cancelInterval(job, { dedupeKey })`: cancel a previously scheduled interval.
|
|
113
113
|
|
|
114
114
|
### Behavior Notes
|
|
@@ -116,5 +116,6 @@ Initializes runtime state and returns a `JobRuntime`.
|
|
|
116
116
|
- Inputs are validated before enqueueing and again before handler execution.
|
|
117
117
|
- Persisted payloads must remain valid after JSON serialization.
|
|
118
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.
|
|
119
120
|
- Failed handlers are marked as `failed` and store error message/stack.
|
|
120
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" =
|
|
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
|
-
|
|
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 =
|
|
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({
|
package/dist/index.mjs.map
CHANGED
|
@@ -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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"}
|