nuxt-cf-jobs 0.0.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.
Files changed (34) hide show
  1. package/README.md +331 -0
  2. package/dist/module.d.mts +73 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +380 -0
  5. package/dist/runtime/server/app.d.ts +80 -0
  6. package/dist/runtime/server/app.js +81 -0
  7. package/dist/runtime/server/d1.d.ts +119 -0
  8. package/dist/runtime/server/d1.js +259 -0
  9. package/dist/runtime/server/dev.d.ts +39 -0
  10. package/dist/runtime/server/dev.js +93 -0
  11. package/dist/runtime/server/dispatch.d.ts +25 -0
  12. package/dist/runtime/server/dispatch.js +66 -0
  13. package/dist/runtime/server/index.d.ts +12 -0
  14. package/dist/runtime/server/index.js +12 -0
  15. package/dist/runtime/server/outbox.d.ts +220 -0
  16. package/dist/runtime/server/outbox.js +246 -0
  17. package/dist/runtime/server/payload.d.ts +3 -0
  18. package/dist/runtime/server/payload.js +3 -0
  19. package/dist/runtime/server/plugins/dev-queues.d.ts +2 -0
  20. package/dist/runtime/server/plugins/dev-queues.js +25 -0
  21. package/dist/runtime/server/policy.d.ts +10 -0
  22. package/dist/runtime/server/policy.js +49 -0
  23. package/dist/runtime/server/queue.d.ts +211 -0
  24. package/dist/runtime/server/queue.js +495 -0
  25. package/dist/runtime/server/registry.d.ts +79 -0
  26. package/dist/runtime/server/registry.js +82 -0
  27. package/dist/runtime/server/schema.d.ts +965 -0
  28. package/dist/runtime/server/schema.js +80 -0
  29. package/dist/runtime/server/testing.d.ts +34 -0
  30. package/dist/runtime/server/testing.js +61 -0
  31. package/dist/runtime/server/types.d.ts +123 -0
  32. package/dist/runtime/server/types.js +0 -0
  33. package/dist/types.d.mts +3 -0
  34. package/package.json +109 -0
@@ -0,0 +1,220 @@
1
+ import type { DispatchableJob, DispatchResult, JobContext, JobControlResult, JobDefinition, JobHandler, QueueMessage } from './types.js';
2
+ import type { SendBackpressureOptions } from './queue.js';
3
+ import type { QueueSendOptions } from './types.js';
4
+ import type { AnyJobDefinition, JobNameOf, JobPayloadByName, JobQueueByName } from './registry.js';
5
+ export interface DurableJobRoute<Queue extends string = string> {
6
+ queue: Queue;
7
+ jobType: string;
8
+ }
9
+ export interface DurableJobRecord<Queue extends string = string> {
10
+ id: string;
11
+ queue: Queue;
12
+ jobType: string;
13
+ batchId?: string;
14
+ userId?: number;
15
+ siteId?: string;
16
+ partnerId?: string;
17
+ traceId: string;
18
+ uniqueKey?: string;
19
+ payload: string;
20
+ attempts: number;
21
+ maxAttempts: number;
22
+ availableAt: number;
23
+ createdAt: number;
24
+ }
25
+ export interface QueueJobMessage<Queue extends string = string> {
26
+ jobId: string;
27
+ queue: Queue;
28
+ }
29
+ export type DurableJobContinuationStage = 'then' | 'catch' | 'finally';
30
+ export interface DurableJobContinuation<Name extends string = string, Payload extends object = Record<string, unknown>, Queue extends string = string> {
31
+ name: Name;
32
+ payload: Payload;
33
+ queue?: Queue;
34
+ delaySeconds?: number;
35
+ }
36
+ export type DurableJobContinuations<Name extends string = string, Payload extends object = Record<string, unknown>, Queue extends string = string> = Partial<Record<DurableJobContinuationStage, Array<DurableJobContinuation<Name, Payload, Queue>>>>;
37
+ export interface DurableJobRepository<Queue extends string = string, Record extends DurableJobRecord<Queue> = DurableJobRecord<Queue>> {
38
+ insertJob: (record: Record) => Promise<boolean>;
39
+ /**
40
+ * Optional batched insert. When implemented, callers can persist many records in chunks
41
+ * (chunked at the D1 100-statement limit by D1-backed implementations).
42
+ */
43
+ insertJobs?: (records: readonly Record[], opts?: {
44
+ batchSize?: number;
45
+ }) => Promise<{
46
+ inserted: Record[];
47
+ chunks: Array<{
48
+ ok: boolean;
49
+ ids: string[];
50
+ changes: number;
51
+ error?: unknown;
52
+ }>;
53
+ }>;
54
+ }
55
+ export interface RecordDurableJobFailureInput<Queue extends string = string> {
56
+ id?: string;
57
+ queue: Queue | string;
58
+ jobType: string;
59
+ batchId?: string | null;
60
+ userId?: number | null;
61
+ siteId?: string | null;
62
+ partnerId?: string | null;
63
+ traceId?: string | null;
64
+ uniqueKey?: string | null;
65
+ payload: string;
66
+ exception: string;
67
+ attempts: number;
68
+ maxAttempts?: number;
69
+ }
70
+ /**
71
+ * Persists a failure record without claiming or deleting a `jobs` row.
72
+ * Used by the DLQ helper to log exhausted messages whose original job rows may not exist
73
+ * (e.g. lightweight queue path) or whose lifecycle has already been finalized.
74
+ */
75
+ export interface DurableJobFailureRepository {
76
+ recordFailure: (input: RecordDurableJobFailureInput) => Promise<void>;
77
+ }
78
+ export interface DurableJobRegistryLike<Env = unknown, Db = unknown, Logger = unknown> {
79
+ getHandler?: (name: string) => JobHandler<unknown, Env, Db, Logger> | undefined;
80
+ getJobDefinition?: (name: string) => JobDefinition<string, unknown, string, Env, Db, Logger> | undefined;
81
+ getJobRoute?: (name: string) => DurableJobRoute<string> | undefined;
82
+ }
83
+ export interface TypedDurableJobRegistryLike<Jobs extends readonly AnyJobDefinition[]> extends DurableJobRegistryLike {
84
+ jobs: Jobs;
85
+ }
86
+ export type DurableJobClaimMiss = 'already-resolved' | 'in-flight' | 'not-found';
87
+ export type DurableJobClaimResult<Job> = {
88
+ status: 'claimed';
89
+ job: Job;
90
+ } | {
91
+ status: DurableJobClaimMiss;
92
+ };
93
+ export interface ReleaseDurableJobOptions {
94
+ delaySeconds?: number;
95
+ availableAt?: number;
96
+ error?: string;
97
+ }
98
+ export interface DurableJobLifecycle<Job, CompleteResult = void, FailOptions = unknown, ReleaseResult = void> {
99
+ claimJob: (id: string) => Promise<Job | null>;
100
+ resolveClaimMiss?: (id: string) => Promise<DurableJobClaimMiss>;
101
+ completeJob: (job: Job, result?: unknown) => Promise<CompleteResult>;
102
+ failJob: (job: Job, error: string, opts?: FailOptions) => Promise<void>;
103
+ releaseJob?: (job: Job, opts?: ReleaseDurableJobOptions) => Promise<ReleaseResult>;
104
+ }
105
+ export interface QueuePublisher<Queue extends string = string, Message extends QueueJobMessage<Queue> = QueueJobMessage<Queue>> {
106
+ send: (queue: Queue, message: Message, opts?: QueueSendOptions & SendBackpressureOptions) => Promise<boolean>;
107
+ sendBatch: (queue: Queue, messages: Message[], opts?: QueueSendOptions & SendBackpressureOptions) => Promise<boolean>;
108
+ }
109
+ export interface DurableJobRecoveryQuery {
110
+ now?: number;
111
+ limit?: number;
112
+ }
113
+ export interface DurableJobStaleRecoveryQuery extends DurableJobRecoveryQuery {
114
+ staleBefore: number;
115
+ availableAt?: number;
116
+ error?: string;
117
+ }
118
+ export interface DurableJobRecoveryRepository<Queue extends string = string, Record extends Pick<DurableJobRecord<Queue>, 'id' | 'queue'> = Pick<DurableJobRecord<Queue>, 'id' | 'queue'>> {
119
+ findDispatchableJobs?: (query?: DurableJobRecoveryQuery) => Promise<Record[]>;
120
+ findStaleReservedJobs?: (query: DurableJobStaleRecoveryQuery) => Promise<Record[]>;
121
+ releaseStaleReservedJobs?: (query: DurableJobStaleRecoveryQuery) => Promise<number>;
122
+ }
123
+ export interface PrepareDurableJobOptions<Name extends string, Payload extends object, Queue extends string> {
124
+ name: Name;
125
+ payload: Payload;
126
+ route?: DurableJobRoute<Queue>;
127
+ registry?: DurableJobRegistryLike;
128
+ definition?: Pick<JobDefinition<Name, Payload, Queue, unknown, unknown, unknown>, 'name' | 'queue' | 'jobType' | 'input' | 'tries' | 'maxAttempts' | 'unique' | 'uniqueId'>;
129
+ id?: string;
130
+ batchId?: string;
131
+ userId?: number;
132
+ siteId?: string;
133
+ partnerId?: string;
134
+ delaySeconds?: number;
135
+ now?: number;
136
+ traceId?: string;
137
+ defaultMaxAttempts?: number;
138
+ continuations?: DurableJobContinuations<string, Record<string, unknown>, Queue>;
139
+ }
140
+ export declare function prepareDurableJob<const Name extends string, Payload extends object, Queue extends string>(opts: PrepareDurableJobOptions<Name, Payload, Queue>): Promise<DurableJobRecord<Queue>>;
141
+ export declare function prepareRegisteredDurableJob<const Jobs extends readonly AnyJobDefinition[], const Name extends JobNameOf<Jobs>>(registry: TypedDurableJobRegistryLike<Jobs>, opts: PrepareRegisteredDurableJobOptions<Jobs, Name>): Promise<DurableJobRecord<JobQueueByName<Jobs, Name>>>;
142
+ export type PrepareRegisteredDurableJobOptions<Jobs extends readonly AnyJobDefinition[], Name extends JobNameOf<Jobs>> = Omit<PrepareDurableJobOptions<Name, JobPayloadByName<Jobs, Name> & object, JobQueueByName<Jobs, Name>>, 'registry' | 'definition' | 'route' | 'name' | 'payload'> & {
143
+ name: Name;
144
+ payload: JobPayloadByName<Jobs, Name> & object;
145
+ route?: DurableJobRoute<JobQueueByName<Jobs, Name>>;
146
+ };
147
+ export declare function normalizeDurableJobContinuations<Name extends string = string, Payload extends object = Record<string, unknown>, Queue extends string = string>(continuations?: DurableJobContinuations<Name, Payload, Queue>): DurableJobContinuations<Name, Payload, Queue> | undefined;
148
+ export declare function validateDurableJobContinuations(registry: DurableJobRegistryLike | undefined, continuations: DurableJobContinuations<string, Record<string, unknown>, string> | undefined): void;
149
+ export declare function getDurableJobContinuations<Name extends string = string, Payload extends object = Record<string, unknown>, Queue extends string = string>(payload: unknown): DurableJobContinuations<Name, Payload, Queue> | undefined;
150
+ export declare function getDurableJobContinuationsForStage<Name extends string = string, Payload extends object = Record<string, unknown>, Queue extends string = string>(payload: unknown, stage: DurableJobContinuationStage): Array<DurableJobContinuation<Name, Payload, Queue>>;
151
+ export declare function serializeDurableJobContinuation<Name extends string, Payload extends object, Queue extends string = string>(continuation: DurableJobContinuation<Name, Payload, Queue>): string;
152
+ export declare function parseDurableJobContinuation<Name extends string = string, Payload extends object = Record<string, unknown>, Queue extends string = string>(value: string): DurableJobContinuation<Name, Payload, Queue>;
153
+ export declare function dispatchDurableJobContinuations<Name extends string, Payload extends object, Queue extends string = string>(continuations: Array<DurableJobContinuation<Name, Payload, Queue>>, dispatch: (continuation: DurableJobContinuation<Name, Payload, Queue>) => Promise<void>): Promise<void>;
154
+ export declare function toQueueJobMessage<Queue extends string>(record: Pick<DurableJobRecord<Queue>, 'id' | 'queue'>): QueueJobMessage<Queue>;
155
+ export declare function groupQueueJobMessagesByQueue<Queue extends string>(records: Array<Pick<DurableJobRecord<Queue>, 'id' | 'queue'>>): Map<Queue, Array<QueueJobMessage<Queue>>>;
156
+ export declare function createQueuePublisher<Env extends Record<string, unknown>, Queue extends string, Message extends QueueJobMessage<Queue> = QueueJobMessage<Queue>>(env: Env, resolveBinding: (queue: Queue) => string | undefined, opts?: {
157
+ onMissingBinding?: (queue: Queue, count: number) => void | Promise<void>;
158
+ }): QueuePublisher<Queue, Message>;
159
+ export declare function claimDurableJob<Job>(lifecycle: Pick<DurableJobLifecycle<Job>, 'claimJob' | 'resolveClaimMiss'>, id: string): Promise<DurableJobClaimResult<Job>>;
160
+ export declare function completeDurableJob<Job, CompleteResult>(lifecycle: Pick<DurableJobLifecycle<Job, CompleteResult>, 'completeJob'>, job: Job, result?: unknown): Promise<CompleteResult>;
161
+ export declare function failDurableJob<Job, FailOptions>(lifecycle: Pick<DurableJobLifecycle<Job, unknown, FailOptions>, 'failJob'>, job: Job, error: string, opts?: FailOptions): Promise<void>;
162
+ export declare function releaseDurableJob<Job, ReleaseResult>(lifecycle: Pick<DurableJobLifecycle<Job, unknown, unknown, ReleaseResult>, 'releaseJob'>, job: Job, opts?: ReleaseDurableJobOptions): Promise<ReleaseResult | undefined>;
163
+ export declare function findDispatchableDurableJobs<Queue extends string, Record extends Pick<DurableJobRecord<Queue>, 'id' | 'queue'>>(repository: Pick<DurableJobRecoveryRepository<Queue, Record>, 'findDispatchableJobs'>, query?: DurableJobRecoveryQuery): Promise<Record[]>;
164
+ export declare function releaseStaleReservedDurableJobs<Queue extends string, Record extends Pick<DurableJobRecord<Queue>, 'id' | 'queue'>>(repository: Pick<DurableJobRecoveryRepository<Queue, Record>, 'releaseStaleReservedJobs'>, query: DurableJobStaleRecoveryQuery): Promise<number>;
165
+ export declare function enqueueDurableJob<Queue extends string, Record extends DurableJobRecord<Queue>>(repository: Pick<DurableJobRepository<Queue, Record>, 'insertJob'>, publisher: Pick<QueuePublisher<Queue>, 'send'>, record: Record, opts?: {
166
+ delaySeconds?: number;
167
+ }): Promise<{
168
+ inserted: boolean;
169
+ dispatched: boolean;
170
+ error?: unknown;
171
+ }>;
172
+ export interface SweepDurableJobsResult<Queue extends string> {
173
+ swept: number;
174
+ dispatched: Array<{
175
+ queue: Queue;
176
+ dispatched: boolean;
177
+ error?: unknown;
178
+ }>;
179
+ }
180
+ export declare function sweepDispatchableDurableJobs<Queue extends string>(repository: Pick<DurableJobRecoveryRepository<Queue, Pick<DurableJobRecord<Queue>, 'id' | 'queue'>>, 'findDispatchableJobs'>, publisher: Pick<QueuePublisher<Queue>, 'sendBatch'>, query?: DurableJobRecoveryQuery): Promise<SweepDurableJobsResult<Queue>>;
181
+ export declare function dispatchDurableJobBatch<Queue extends string>(publisher: Pick<QueuePublisher<Queue>, 'sendBatch'>, records: Array<Pick<DurableJobRecord<Queue>, 'id' | 'queue'>>, opts?: {
182
+ delaySeconds?: number;
183
+ }): Promise<Array<{
184
+ queue: Queue;
185
+ dispatched: boolean;
186
+ error?: unknown;
187
+ }>>;
188
+ export type DurableJobMessageStatus = 'invalid-message' | DurableJobClaimMiss | 'dispatch-failed' | 'released' | 'failed' | 'completed';
189
+ export interface RunDurableJobMessageOptions<StoredJob, Job extends DispatchableJob, Message extends QueueJobMessage = QueueJobMessage, Env = unknown, Db = unknown, Logger = unknown, CompleteResult = unknown, FailOptions = unknown> {
190
+ message: Pick<QueueMessage<Message>, 'body' | 'ack' | 'retry'>;
191
+ lifecycle: Pick<DurableJobLifecycle<StoredJob, CompleteResult, FailOptions>, 'claimJob' | 'resolveClaimMiss' | 'completeJob' | 'failJob' | 'releaseJob'>;
192
+ registry: {
193
+ getHandler: (name: string) => JobHandler<unknown, Env, Db, Logger> | undefined;
194
+ getJobDefinition?: (name: string) => JobDefinition<string, unknown, string, Env, Db, Logger> | undefined;
195
+ };
196
+ toDispatchableJob: (job: StoredJob) => Job;
197
+ createJobContext: (input: {
198
+ job: Job;
199
+ storedJob: StoredJob;
200
+ taskName: string;
201
+ payload: Record<string, unknown>;
202
+ control: JobControlResult;
203
+ }) => JobContext<Env, Db, Logger> | Promise<JobContext<Env, Db, Logger>>;
204
+ getJobId?: (message: Message) => string | undefined;
205
+ retryDelaySeconds?: number | ((input: {
206
+ error: unknown;
207
+ job: StoredJob;
208
+ }) => number);
209
+ failDispatchFailure?: boolean;
210
+ completeResult?: (input: {
211
+ job: StoredJob;
212
+ dispatch: DispatchResult;
213
+ }) => unknown | Promise<unknown>;
214
+ }
215
+ export interface RunDurableJobMessageResult {
216
+ status: DurableJobMessageStatus;
217
+ dispatch?: DispatchResult;
218
+ error?: unknown;
219
+ }
220
+ export declare function runDurableJobMessage<StoredJob, Job extends DispatchableJob, Message extends QueueJobMessage = QueueJobMessage, Env = unknown, Db = unknown, Logger = unknown, CompleteResult = unknown, FailOptions = unknown>(opts: RunDurableJobMessageOptions<StoredJob, Job, Message, Env, Db, Logger, CompleteResult, FailOptions>): Promise<RunDurableJobMessageResult>;
@@ -0,0 +1,246 @@
1
+ import { buildJobPayload } from "./payload.js";
2
+ import { createJobTraceId, createJobUniqueKey, resolveJobMaxAttempts } from "./policy.js";
3
+ import { dispatchRegisteredJob } from "./dispatch.js";
4
+ import { CF_QUEUE_MAX_MESSAGE_BYTES, sendBatchChunked, withSendBackpressure } from "./queue.js";
5
+ import { parseJobInput } from "./registry.js";
6
+ function byteLength(value) {
7
+ return typeof Buffer !== "undefined" ? Buffer.byteLength(value, "utf8") : new TextEncoder().encode(value).byteLength;
8
+ }
9
+ export async function prepareDurableJob(opts) {
10
+ const now = opts.now ?? Math.floor(Date.now() / 1e3);
11
+ const definition = opts.definition ?? opts.registry?.getJobDefinition?.(opts.name);
12
+ const route = resolveDurableJobRoute(opts.name, opts.route, definition, opts.registry);
13
+ const parsedPayload = parseJobInput(definition, opts.payload);
14
+ if (!parsedPayload.success)
15
+ throw new Error(`Invalid payload for task: ${opts.name}`);
16
+ const continuations = normalizeDurableJobContinuations(opts.continuations);
17
+ validateDurableJobContinuations(opts.registry, continuations);
18
+ const uniqueKey = definition?.unique ? await createJobUniqueKey(opts.name, parsedPayload.data, definition.uniqueId) : void 0;
19
+ const payload = buildJobPayload(opts.name, parsedPayload.data);
20
+ const serialized = JSON.stringify(continuations ? { ...payload, _continuations: continuations } : payload);
21
+ if (byteLength(serialized) > CF_QUEUE_MAX_MESSAGE_BYTES)
22
+ throw new Error(`Job payload exceeds Cloudflare Queue limit of ${CF_QUEUE_MAX_MESSAGE_BYTES} bytes for task: ${opts.name}`);
23
+ return {
24
+ id: opts.id ?? crypto.randomUUID(),
25
+ queue: route.queue,
26
+ jobType: route.jobType,
27
+ batchId: opts.batchId,
28
+ userId: opts.userId,
29
+ siteId: opts.siteId,
30
+ partnerId: opts.partnerId,
31
+ traceId: opts.traceId ?? createJobTraceId(),
32
+ uniqueKey,
33
+ payload: serialized,
34
+ attempts: 0,
35
+ maxAttempts: resolveJobMaxAttempts(definition) ?? opts.defaultMaxAttempts ?? 3,
36
+ availableAt: now + (opts.delaySeconds ?? 0),
37
+ createdAt: now
38
+ };
39
+ }
40
+ export async function prepareRegisteredDurableJob(registry, opts) {
41
+ return prepareDurableJob({
42
+ ...opts,
43
+ registry
44
+ });
45
+ }
46
+ function resolveDurableJobRoute(name, route, definition, registry) {
47
+ if (route)
48
+ return route;
49
+ const registeredRoute = registry?.getJobRoute?.(name);
50
+ if (registeredRoute)
51
+ return registeredRoute;
52
+ if (definition)
53
+ return { queue: definition.queue, jobType: definition.jobType ?? definition.name };
54
+ throw new Error(`No route for task: ${name}`);
55
+ }
56
+ export function normalizeDurableJobContinuations(continuations) {
57
+ if (!continuations)
58
+ return void 0;
59
+ const normalized = {};
60
+ for (const stage of ["then", "catch", "finally"]) {
61
+ const entries = continuations[stage]?.filter(Boolean);
62
+ if (entries?.length)
63
+ normalized[stage] = entries;
64
+ }
65
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
66
+ }
67
+ export function validateDurableJobContinuations(registry, continuations) {
68
+ if (!registry || !continuations)
69
+ return;
70
+ for (const stage of ["then", "catch", "finally"]) {
71
+ for (const continuation of continuations[stage] ?? []) {
72
+ const definition = registry.getJobDefinition?.(continuation.name);
73
+ if (!definition)
74
+ throw new Error(`No handler for continuation task: ${continuation.name}`);
75
+ const parsed = parseJobInput(definition, continuation.payload);
76
+ if (!parsed.success)
77
+ throw new Error(`Invalid payload for continuation task: ${continuation.name}`);
78
+ if (continuation.queue && definition.queue !== continuation.queue)
79
+ throw new Error(`Continuation task "${continuation.name}" is registered on queue "${definition.queue}", not "${continuation.queue}"`);
80
+ }
81
+ }
82
+ }
83
+ export function getDurableJobContinuations(payload) {
84
+ if (!payload || typeof payload !== "object")
85
+ return void 0;
86
+ return normalizeDurableJobContinuations(
87
+ payload._continuations
88
+ );
89
+ }
90
+ export function getDurableJobContinuationsForStage(payload, stage) {
91
+ return getDurableJobContinuations(payload)?.[stage] ?? [];
92
+ }
93
+ export function serializeDurableJobContinuation(continuation) {
94
+ return JSON.stringify(continuation);
95
+ }
96
+ export function parseDurableJobContinuation(value) {
97
+ const parsed = JSON.parse(value);
98
+ if (!parsed || typeof parsed.name !== "string" || !parsed.payload || typeof parsed.payload !== "object")
99
+ throw new Error("Invalid durable job continuation");
100
+ return parsed;
101
+ }
102
+ export async function dispatchDurableJobContinuations(continuations, dispatch) {
103
+ await Promise.all(continuations.map((continuation) => dispatch(continuation)));
104
+ }
105
+ export function toQueueJobMessage(record) {
106
+ return { jobId: record.id, queue: record.queue };
107
+ }
108
+ export function groupQueueJobMessagesByQueue(records) {
109
+ const groups = /* @__PURE__ */ new Map();
110
+ for (const record of records) {
111
+ const messages = groups.get(record.queue) ?? [];
112
+ messages.push(toQueueJobMessage(record));
113
+ groups.set(record.queue, messages);
114
+ }
115
+ return groups;
116
+ }
117
+ export function createQueuePublisher(env, resolveBinding, opts = {}) {
118
+ function getBinding(queue) {
119
+ const binding = resolveBinding(queue);
120
+ if (!binding)
121
+ return void 0;
122
+ const cfQueue = env[binding];
123
+ return cfQueue && typeof cfQueue.send === "function" ? cfQueue : void 0;
124
+ }
125
+ return {
126
+ async send(queue, message, sendOpts) {
127
+ const cfQueue = getBinding(queue);
128
+ if (!cfQueue) {
129
+ await opts.onMissingBinding?.(queue, 1);
130
+ return false;
131
+ }
132
+ await withSendBackpressure(() => cfQueue.send(message, sendOpts), sendOpts);
133
+ return true;
134
+ },
135
+ async sendBatch(queue, messages, sendOpts) {
136
+ if (messages.length === 0)
137
+ return true;
138
+ const cfQueue = getBinding(queue);
139
+ if (!cfQueue) {
140
+ await opts.onMissingBinding?.(queue, messages.length);
141
+ return false;
142
+ }
143
+ await sendBatchChunked(cfQueue, messages, sendOpts);
144
+ return true;
145
+ }
146
+ };
147
+ }
148
+ export async function claimDurableJob(lifecycle, id) {
149
+ const job = await lifecycle.claimJob(id);
150
+ if (job)
151
+ return { status: "claimed", job };
152
+ return {
153
+ status: lifecycle.resolveClaimMiss ? await lifecycle.resolveClaimMiss(id) : "not-found"
154
+ };
155
+ }
156
+ export async function completeDurableJob(lifecycle, job, result) {
157
+ return await lifecycle.completeJob(job, result);
158
+ }
159
+ export async function failDurableJob(lifecycle, job, error, opts) {
160
+ await lifecycle.failJob(job, error, opts);
161
+ }
162
+ export async function releaseDurableJob(lifecycle, job, opts) {
163
+ return await lifecycle.releaseJob?.(job, opts);
164
+ }
165
+ export async function findDispatchableDurableJobs(repository, query) {
166
+ return await repository.findDispatchableJobs?.(query) ?? [];
167
+ }
168
+ export async function releaseStaleReservedDurableJobs(repository, query) {
169
+ return await repository.releaseStaleReservedJobs?.(query) ?? 0;
170
+ }
171
+ export async function enqueueDurableJob(repository, publisher, record, opts) {
172
+ const inserted = await repository.insertJob(record);
173
+ if (!inserted)
174
+ return { inserted: false, dispatched: false };
175
+ const dispatched = await publisher.send(record.queue, toQueueJobMessage(record), opts).catch((error) => ({ error }));
176
+ if (typeof dispatched === "object" && dispatched && "error" in dispatched)
177
+ return { inserted: true, dispatched: false, error: dispatched.error };
178
+ return { inserted: true, dispatched };
179
+ }
180
+ export async function sweepDispatchableDurableJobs(repository, publisher, query) {
181
+ const records = await findDispatchableDurableJobs(repository, query);
182
+ if (records.length === 0)
183
+ return { swept: 0, dispatched: [] };
184
+ const dispatched = await dispatchDurableJobBatch(publisher, records);
185
+ return { swept: records.length, dispatched };
186
+ }
187
+ export async function dispatchDurableJobBatch(publisher, records, opts) {
188
+ const groups = groupQueueJobMessagesByQueue(records);
189
+ return await Promise.all(
190
+ [...groups].map(async ([queue, messages]) => {
191
+ try {
192
+ return {
193
+ queue,
194
+ dispatched: await publisher.sendBatch(queue, messages, opts)
195
+ };
196
+ } catch (error) {
197
+ return { queue, dispatched: false, error };
198
+ }
199
+ })
200
+ );
201
+ }
202
+ export async function runDurableJobMessage(opts) {
203
+ const jobId = opts.getJobId?.(opts.message.body) ?? opts.message.body.jobId;
204
+ if (!jobId) {
205
+ opts.message.ack();
206
+ return { status: "invalid-message" };
207
+ }
208
+ const claimed = await claimDurableJob(opts.lifecycle, jobId);
209
+ if (claimed.status !== "claimed") {
210
+ if (claimed.status === "in-flight")
211
+ opts.message.retry({ delaySeconds: 60 });
212
+ else
213
+ opts.message.ack();
214
+ return { status: claimed.status };
215
+ }
216
+ const storedJob = claimed.job;
217
+ const job = opts.toDispatchableJob(storedJob);
218
+ try {
219
+ const dispatch = await dispatchRegisteredJob({
220
+ registry: opts.registry,
221
+ job,
222
+ createContext: (input) => opts.createJobContext({ ...input, storedJob })
223
+ });
224
+ if (!dispatch.success) {
225
+ if (opts.failDispatchFailure !== false)
226
+ await failDurableJob(opts.lifecycle, storedJob, dispatch.error ?? "Job dispatch failed");
227
+ opts.message.ack();
228
+ return { status: "dispatch-failed", dispatch };
229
+ }
230
+ if (dispatch.control?.handled) {
231
+ opts.message.ack();
232
+ return { status: dispatch.control.action === "failed" ? "failed" : "released", dispatch };
233
+ }
234
+ await completeDurableJob(opts.lifecycle, storedJob, await opts.completeResult?.({ job: storedJob, dispatch }));
235
+ opts.message.ack();
236
+ return { status: "completed", dispatch };
237
+ } catch (error) {
238
+ const delaySeconds = typeof opts.retryDelaySeconds === "function" ? opts.retryDelaySeconds({ error, job: storedJob }) : opts.retryDelaySeconds ?? 0;
239
+ await releaseDurableJob(opts.lifecycle, storedJob, {
240
+ delaySeconds,
241
+ error: error instanceof Error ? error.message : String(error)
242
+ });
243
+ opts.message.retry({ delaySeconds });
244
+ return { status: "released", error };
245
+ }
246
+ }
@@ -0,0 +1,3 @@
1
+ export declare function buildJobPayload<const Name extends string, Payload extends object>(name: Name, payload: Payload): {
2
+ _task: Name;
3
+ } & Payload;
@@ -0,0 +1,3 @@
1
+ export function buildJobPayload(name, payload) {
2
+ return { _task: name, ...payload };
3
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -0,0 +1,25 @@
1
+ import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime";
2
+ import { createDevQueueRuntime } from "../dev.js";
3
+ export default defineNitroPlugin((nitroApp) => {
4
+ const config = useRuntimeConfig().cfJobs;
5
+ const queues = config?.queues ?? {};
6
+ if (!Object.keys(queues).length)
7
+ return;
8
+ const runtime = createDevQueueRuntime({
9
+ queues,
10
+ onBatch: async (payload) => {
11
+ await nitroApp.hooks.callHook("cloudflare:queue", payload);
12
+ },
13
+ onError(error) {
14
+ console.error("[nuxt-cf-jobs] dev queue error:", error);
15
+ }
16
+ });
17
+ nitroApp.hooks.hook("request", (event) => {
18
+ const existing = event.context.cloudflare?.env;
19
+ event.context.cloudflare = {
20
+ ...event.context.cloudflare ?? {},
21
+ env: existing ? { ...runtime.env, ...existing } : runtime.env
22
+ };
23
+ });
24
+ nitroApp.hooks.hook("close", () => runtime.dispose());
25
+ });
@@ -0,0 +1,10 @@
1
+ import type { JobBackoff, JobDefinition } from './types.js';
2
+ export declare function clampDelay(seconds: number | undefined): number | undefined;
3
+ export declare function resolveJobMaxAttempts(definition: Pick<JobDefinition<string, unknown, string, unknown, unknown, unknown>, 'tries' | 'maxAttempts'> | undefined): number | undefined;
4
+ export declare function resolveJobBackoff(backoff: JobBackoff | undefined, attempt: number): number | undefined;
5
+ export declare function resolveJobRetryDelay(definition: Pick<JobDefinition<string, unknown, string, unknown, unknown, unknown>, 'backoff'> | undefined, attempt: number, opts?: {
6
+ baseSeconds?: number;
7
+ maxSeconds?: number;
8
+ }): number;
9
+ export declare function createJobTraceId(prefix?: string): string;
10
+ export declare function createJobUniqueKey(name: string, payload: unknown, uniqueId?: (payload: never) => string): Promise<string>;
@@ -0,0 +1,49 @@
1
+ const CF_QUEUE_MAX_DELAY_SECONDS = 43200;
2
+ export function clampDelay(seconds) {
3
+ if (seconds === void 0)
4
+ return void 0;
5
+ if (seconds < 0)
6
+ return 0;
7
+ return Math.min(seconds, CF_QUEUE_MAX_DELAY_SECONDS);
8
+ }
9
+ export function resolveJobMaxAttempts(definition) {
10
+ return definition?.tries ?? definition?.maxAttempts;
11
+ }
12
+ export function resolveJobBackoff(backoff, attempt) {
13
+ if (typeof backoff === "function")
14
+ return clampDelay(backoff(attempt));
15
+ if (Array.isArray(backoff))
16
+ return clampDelay(backoff[Math.min(Math.max(attempt - 1, 0), backoff.length - 1)]);
17
+ return clampDelay(backoff);
18
+ }
19
+ export function resolveJobRetryDelay(definition, attempt, opts = {}) {
20
+ const configured = resolveJobBackoff(definition?.backoff, attempt);
21
+ if (configured !== void 0)
22
+ return configured;
23
+ const base = opts.baseSeconds ?? 10;
24
+ const max = Math.min(opts.maxSeconds ?? 300, CF_QUEUE_MAX_DELAY_SECONDS);
25
+ return Math.min(base * 2 ** Math.max(0, attempt - 1), max);
26
+ }
27
+ export function createJobTraceId(prefix = "job") {
28
+ return `${prefix}_${crypto.randomUUID()}`;
29
+ }
30
+ export async function createJobUniqueKey(name, payload, uniqueId) {
31
+ const source = uniqueId ? `${name}:${uniqueId(payload)}` : `${name}:${stableStringify(payload)}`;
32
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(source));
33
+ return `job_unique_${toHex(digest)}`;
34
+ }
35
+ function toHex(buffer) {
36
+ return [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
37
+ }
38
+ function stableStringify(value) {
39
+ if (typeof value === "bigint")
40
+ return `"@bigint:${value.toString()}"`;
41
+ if (value instanceof Date)
42
+ return `"@date:${value.toISOString()}"`;
43
+ if (Array.isArray(value))
44
+ return `[${value.map(stableStringify).join(",")}]`;
45
+ if (value && typeof value === "object") {
46
+ return `{${Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => `${JSON.stringify(key)}:${stableStringify(nested)}`).join(",")}}`;
47
+ }
48
+ return JSON.stringify(value);
49
+ }