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 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" = 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":";AAGA,MAAa,yBAAyB,OAAO,IAAI,gCAAgC;AAejF,SAAgB,kBAEsB;AACpC,QAAO,SAAS,UAAgC,SAA6D;AAC3G,SAAO,EACL,QAA0C,YAAsE,EAC9G,UAAU,YAAgF;AAQxF,UAPgD;IAC9C,MAAM,QAAQ;KACb,yBAAyB;KACxB;KACS;KACV;IACF;KAGJ,GACF;;;;;;ACnCL,MAAM,0BAA0B;AAEhC,MAAa,aAAa;AAC1B,MAAa,sBAAsB;AAOnC,MAAM,uBAA8C,CAClD;CACE,SAAS;CACT,KAAK,YAAY;AACf,UAAQ,IAAI,KAAK,+BAA+B,WAAW;;;;;;;;;;;;;SAaxD;AAEH,UAAQ,IAAI,KAAK,iDAAiD,WAAW,oCAAoC;AAEjH,UAAQ,IAAI,KAAK,+BAA+B,oBAAoB;;;;;;;;;;;SAWjE;AAEH,UAAQ,IAAI,KACV,sEAAsE,oBAAoB,0BAC3F;AACD,UAAQ,IAAI,KACV,0DAA0D,oBAAoB,mCAC/E;;CAEJ,CACF;AAED,SAAgB,iBAAiB,KAA+B;CAC9D,MAAM,iBAAiB,IAAI,QAAQ,GAAG,IAAY,wBAAwB,IAAI;AAE9E,MAAK,MAAM,aAAa,sBAAsB;AAC5C,MAAI,UAAU,WAAW,eAAgB;AAEzC,MAAI,QAAQ,sBAAsB;AAChC,aAAU,GAAG,IAAI,QAAQ;AACzB,OAAI,QAAQ,GAAG,IAAI,yBAAyB,UAAU,QAAQ;IAC9D;;;;;;AChCN,SAAS,QACP,SACA,KACA,aAAiC,EAAE,EACxB;AACX,QAAO,QAAQ,IAAI,KAAK,KAAK,GAAG,WAAW,CAAC,SAAS;;AAGvD,SAAS,aAAa,SAA0B;AAC9C,QAAO,KAAK,MAAM,QAAQ;;AAG5B,SAAgB,eACd,KAC6B;AAC7B,QAAO;EACL,IAAI,IAAI;EACR,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ,SAAS,aAAa,IAAI,QAAQ;EAClC,aAAa,IAAI;EACjB,WAAW,IAAI;EACf,YAAY,IAAI;EAChB,cAAc,IAAI;EAClB,YAAY,IAAI;EAChB,YAAY,IAAI;EAChB,WAAW,IAAI;EACf,WAAW,IAAI;EAChB;;AAGH,SAAS,yBACP,KACuC;AACvC,QAAO;EACL,IAAI,IAAI;EACR,MAAM,IAAI;EACV,WAAW,IAAI;EACf,SAAS,aAAa,IAAI,QAAQ;EAClC,YAAY,IAAI;EAChB,WAAW,IAAI;EACf,QAAQ,IAAI;EACZ,WAAW,IAAI;EACf,WAAW,IAAI;EACf,WAAW,IAAI;EAChB;;AAGH,SAAgB,gBAAwB,EACtC,KACA,MACA,OACA,MAM+B;CAC/B,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,MAAc;EAClB,IAAI,OAAO,YAAY;EACvB;EACA,QAAQ;EACR,SAAS,KAAK,UAAU,MAAM;EAC9B,cAAc;EACd,YAAY;EACZ,aAAa;EACb,eAAe;EACf,aAAa;EACb,aAAa;EACb,YAAY;EACZ,YAAY;EACb;AAED,KAAI,QAAQ,sBAAsB;AAChC,UACE,IAAI,SACJ,gBAAgB,WAAW;;;;;;;;;;;;;oDAc3B;GACE,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACJ,IAAI;GACL,CACF;GACD;AAEF,QAAO,eAA+B,IAAI;;AAG5C,SAAgB,uBAA+B,EAC7C,KACA,MACA,WACA,OACA,SACA,WAayC;CACzC,MAAM,MAAM,KAAK,KAAK;CACtB,MAAM,aAAa,OAAO,YAAY;CACtC,MAAM,UAAU,KAAK,UAAU,MAAM;CACrC,MAAM,kBAAkB,YAAY;CAGpC,MAAM,YAAY,kBAAkB,UAAU,MAAM;AAEpD,KAAI,QAAQ,sBAAsB;AAYhC,UACE,IAAI,SACJ,gBAAgB,oBAAoB;;;;;;;;;;;;;;;;;kBAiBxB,oBAAoB;kBACpB,oBAAoB;kBACpB,oBAAoB;;;+CAIhC;GAAC;GAAY;GAAM;GAAW;GAAS;GAAS;GAAW;GAAU;GAAK;GAAK;GAAM,kBAAkB,IAAI;GAAE,CAC9G;GACD;CAEF,MAAM,CAAC,OAAO,QACZ,IAAI,SACJ;;;;;;;;;;;YAWQ,oBAAoB;;cAG5B,CAAC,MAAM,UAAU,CAClB;AAED,KAAI,CAAC,IACH,OAAM,IAAI,MAAM,2CAA2C,KAAK,GAAG;AAGrE,QAAO,yBAAyC,IAAI;;AAGtD,SAAgB,uBAAuB,EACrC,KACA,MACA,aAKU;CACV,MAAM,CAAC,YAAY,QACjB,IAAI,SACJ,qBAAqB,oBAAoB;;cAGzC,CAAC,MAAM,UAAU,CAClB;AAED,KAAI,CAAC,SACH,QAAO;CAGT,MAAM,MAAM,KAAK,KAAK;AACtB,KAAI,QAAQ,sBAAsB;AAChC,UACE,IAAI,SACJ,WAAW,oBAAoB;;uBAG/B,CAAC,KAAK,SAAS,GAAG,CACnB;GACD;AAEF,QAAO;;AAGT,SAAgB,wBAAwB,KAAyB,KAAqB;CACpF,IAAI,eAAe;AAEnB,KAAI,QAAQ,sBAAsB;EAChC,MAAM,eAAe,QACnB,IAAI,SACJ;;;;;;;;;;;cAWQ,oBAAoB;;6CAG5B,CAAC,IAAI,CACN;AAED,OAAK,MAAM,YAAY,cAAc;GACnC,MAAM,QAAQ,OAAO,YAAY;AACjC,WACE,IAAI,SACJ,gBAAgB,WAAW;;;;;;;;;;;;;wDAc3B;IACE;IACA,SAAS;IACT;IACA,SAAS;IACT,SAAS;IACT;IACA;IACA;IACA;IACA,SAAS;IACT;IACA;IACD,CACF;AAED,WACE,IAAI,SACJ,WAAW,oBAAoB;;yBAG/B;IAAC;IAAK,MAAM,SAAS;IAAa;IAAK,SAAS;IAAG,CACpD;AAED,mBAAgB;;GAElB;AAEF,QAAO;;AAGT,SAAgB,iBAAiB,KAAyB,KAAa,OAAyB;AAC9F,QAAO,QACL,IAAI,SACJ;;;;;;;;;;;;;YAaQ,WAAW;;;cAInB,CAAC,KAAK,MAAM,CACb;;AAGH,SAAgB,eAAe,KAAyB,OAAe,WAAyB;AAC9F,KAAI,QAAQ,sBAAsB;AAChC,UACE,IAAI,SACJ,WAAW,WAAW;;uBAGtB;GAAC;GAAW;GAAW;GAAM,CAC9B;GACD;;AAGJ,SAAgB,iBAAiB,KAAyB,OAAe,YAA0B;AACjG,KAAI,QAAQ,sBAAsB;AAChC,UACE,IAAI,SACJ,WAAW,WAAW;;;;;;;uBAQtB;GAAC;GAAY;GAAY;GAAM,CAChC;GACD;;AAGJ,SAAS,eAAe,OAA2D;AACjF,KAAI,iBAAiB,MACnB,QAAO;EACL,SAAS,MAAM;EACf,OAAO,MAAM,SAAS;EACvB;AAGH,QAAO;EACL,SAAS,OAAO,MAAM;EACtB,OAAO;EACR;;AAGH,SAAgB,cAAc,KAAyB,OAAe,YAAoB,OAAsB;CAC9G,MAAM,UAAU,eAAe,MAAM;AAErC,KAAI,QAAQ,sBAAsB;AAChC,UACE,IAAI,SACJ,WAAW,WAAW;;;;;;;uBAQtB;GAAC;GAAY;GAAY,QAAQ;GAAS,QAAQ;GAAO;GAAM,CAChE;GACD;;AAGJ,eAAsB,mBAAmB,KAAiD;CACxF,MAAM,CAAC,UAAU,QACf,IAAI,SACJ;YACQ,WAAW;+BAEpB;CACD,MAAM,CAAC,eAAe,QACpB,IAAI,SACJ;YACQ,oBAAoB;+BAE7B;CAED,MAAM,YAAY,QAAQ,WAAW;CACrC,MAAM,iBAAiB,aAAa,WAAW;CAE/C,IAAI,cAA6B;AACjC,KAAI,cAAc,QAAQ,mBAAmB,KAC3C,eAAc,KAAK,IAAI,WAAW,eAAe;UACxC,cAAc,KACvB,eAAc;UACL,mBAAmB,KAC5B,eAAc;AAGhB,KAAI,gBAAgB,MAAM;AACxB,QAAM,IAAI,QAAQ,aAAa;AAC/B,SAAO;;AAGT,OAAM,IAAI,QAAQ,SAAS,YAAY;AACvC,QAAO;;;;;ACrbT,SAAS,eAAe,KAAiC;AAEvD,KAAI,CADc,IAAoB,wBAEpC,OAAM,IAAI,MAAM,gBAAgB,IAAI,KAAK,oEAAoE;AAG/G,QAAO;;AAGT,SAAS,wBAAwB,iBAAiC;AAChE,KAAI,CAAC,OAAO,SAAS,gBAAgB,IAAI,CAAC,OAAO,UAAU,gBAAgB,IAAI,kBAAkB,EAC/F,OAAM,IAAI,MAAM,0DAA0D;AAE5E,QAAO;;AAGT,SAAS,qBAAqB,YAAsC,KAAiC;CACnG,MAAM,aAAa,WAAW,IAAI,IAAI,KAAK;AAC3C,KAAI,CAAC,WACH,OAAM,IAAI,MAAM,aAAa,IAAI,KAAK,6DAA6D;AAErG,QAAO;;AAGT,SAAS,mBAAmB,OAAe,OAAuB;AAChE,KAAI,CAAC,OAAO,SAAS,MAAM,CACzB,OAAM,IAAI,MAAM,WAAW,MAAM,gDAAgD;AAEnF,QAAO,KAAK,MAAM,MAAM;;AAG1B,SAAS,oBAAoB,SAAyB;AACpD,KAAI,CAAC,OAAO,SAAS,QAAQ,IAAI,CAAC,OAAO,UAAU,QAAQ,IAAI,UAAU,EACvE,OAAM,IAAI,MAAM,yEAAyE;AAE3F,QAAO;;AAGT,eAAe,cACb,QACA,OACgD;CAChD,MAAM,SAAS,MAAM,OAAO,aAAa,SAAS,MAAM;AACxD,KAAI,OAAO,QAAQ;EACjB,MAAM,eAAe,OAAO,OAAO,IAAI;AACvC,QAAM,IAAI,MACR,eAAe,oBAAoB,iBAAiB,kDACrD;;AAGH,QAAO,OAAO;;AAGhB,eAAe,uBACb,QACA,OACe;CACf,IAAI;AACJ,KAAI;AACF,eAAa,KAAK,UAAU,MAAM;UAC3B,OAAO;AACd,QAAM,IAAI,MAAM,8EAA8E,OAAO,MAAM,GAAG;;AAGhH,KAAI,eAAe,OACjB,OAAM,IAAI,MAAM,uDAAuD;CAGzE,MAAM,eAAwB,KAAK,MAAM,WAAW;AAEpD,MADe,MAAM,OAAO,aAAa,SAAS,aAAa,EACpD,OACT,OAAM,IAAI,MAAM,8FAA8F;;AAIlH,SAAS,kBAAkB,WAAyB;AAClD,KAAI,CAAC,aAAa,UAAU,MAAM,CAAC,WAAW,EAC5C,OAAM,IAAI,MAAM,oDAAoD;;AAIxE,eAAsB,UACpB,SACqB;CACrB,MAAM,kBAAkB,wBAAwB,QAAQ,mBAAmB,GAAG;CAC9E,MAAM,6BAAa,IAAI,KAA0B;AAEjD,MAAK,MAAM,OAAO,QAAQ,MAAM;EAC9B,MAAM,cAAc,eAAe,IAAI;AACvC,MAAI,WAAW,IAAI,IAAI,KAAK,CAC1B,OAAM,IAAI,MAAM,uBAAuB,IAAI,KAAK,qBAAqB;AAEvE,aAAW,IAAI,IAAI,MAAM,YAAY;;AAGvC,kBAAiB,QAAQ,IAAI;AAC7B,OAAM,mBAAmB,QAAQ,IAAI;CAErC,MAAM,UAAU,YAAmC;EACjD,MAAM,MAAM,KAAK,KAAK;AACtB,0BAAwB,QAAQ,KAAK,IAAI;EAEzC,IAAI,gBAAgB;AAEpB,SAAO,gBAAgB,iBAAiB;GACtC,MAAM,YAAY,kBAAkB;GACpC,MAAM,UAAU,iBAAiB,QAAQ,KAAK,KAAK,KAAK,EAAE,UAAU;AAEpE,OAAI,QAAQ,WAAW,EACrB;AAGF,QAAK,MAAM,UAAU,SAAS;AAC5B,QAAI,iBAAiB,gBACnB;IAGF,MAAM,cAAc,WAAW,IAAI,OAAO,KAAK;IAC/C,MAAM,YAAY,KAAK,KAAK;AAC5B,mBAAe,QAAQ,KAAK,OAAO,IAAI,UAAU;AAEjD,QAAI;AACF,SAAI,CAAC,YACH,OAAM,IAAI,MAAM,uCAAuC,OAAO,KAAK,IAAI;KAGzE,MAAM,eAAe,eAAe,OAAO;KAC3C,MAAM,SAAS,MAAM,YAAY,wBAAwB,OAAO,aAAa,SAAS,aAAa,QAAQ;AAC3G,SAAI,OAAO,OACT,OAAM,IAAI,MAAM,2CAA2C,OAAO,KAAK,IAAI;KAG7E,MAAM,QAAQ,OAAO;KACrB,MAAM,gBAAgB;MACpB,GAAG;MACH,QAAQ;MACR,SAAS;MACT;MACA,WAAW;MACZ;AAED,WAAM,YAAY,wBAAwB,QAAQ;MAChD;MACA,SAAS,QAAQ;MACjB,KAAK;MACN,CAAC;AAEF,sBAAiB,QAAQ,KAAK,OAAO,IAAI,KAAK,KAAK,CAAC;aAC7C,OAAO;AACd,mBAAc,QAAQ,KAAK,OAAO,IAAI,KAAK,KAAK,EAAE,MAAM;;AAG1D,qBAAiB;;;EAIrB,MAAM,cAAc,MAAM,mBAAmB,QAAQ,IAAI;AACzD,SAAO;GACL;GACA;GACD;;AAGH,QAAO;EACL;EACA,cAAc,YAAY,mBAAmB,QAAQ,IAAI;EAEzD,WAAW,OAAO,KAAK,oBAAoB;GAEzC,MAAM,SADa,qBAAqB,YAAY,IAAI,CAC9B,wBAAwB;GAClD,MAAM,KAAK,mBAAmB,gBAAgB,IAAI,OAAO;GACzD,MAAM,QAAQ,MAAM,cAAc,QAAQ,gBAAgB,MAAM;AAChE,SAAM,uBAAuB,QAAQ,MAAM;GAE3C,MAAM,SAAS,gBAAgB;IAC7B,KAAK,QAAQ;IACb,MAAM,IAAI;IACV;IACA;IACD,CAAC;AAEF,SAAM,mBAAmB,QAAQ,IAAI;AACrC,UAAO;;EAGT,mBAAmB,OAAO,KAAK,oBAAoB;GAEjD,MAAM,SADa,qBAAqB,YAAY,IAAI,CAC9B,wBAAwB;AAClD,qBAAkB,gBAAgB,UAAU;GAC5C,MAAM,UAAU,oBAAoB,gBAAgB,QAAQ;GAG5D,MAAM,UACJ,gBAAgB,YAAY,SAAY,SAAY,mBAAmB,gBAAgB,SAAS,YAAY;GAC9G,MAAM,QAAQ,MAAM,cAAc,QAAQ,gBAAgB,MAAM;AAChE,SAAM,uBAAuB,QAAQ,MAAM;GAE3C,MAAM,SAAS,uBAAuB;IACpC,KAAK,QAAQ;IACb,MAAM,IAAI;IACV,WAAW,gBAAgB;IAC3B;IACA;IACA;IACD,CAAC;AAEF,SAAM,mBAAmB,QAAQ,IAAI;AACrC,UAAO;;EAGT,iBAAiB,OAAO,KAAK,kBAAkB;AAC7C,wBAAqB,YAAY,IAAI;AACrC,qBAAkB,cAAc,UAAU;GAE1C,MAAM,YAAY,uBAAuB;IACvC,KAAK,QAAQ;IACb,MAAM,IAAI;IACV,WAAW,cAAc;IAC1B,CAAC;AAEF,SAAM,mBAAmB,QAAQ,IAAI;AACrC,UAAO;;EAEV"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "do-jobs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {