nuxt-cf-jobs 0.6.0 → 0.6.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/dist/module.json
CHANGED
|
@@ -308,6 +308,47 @@ export interface RunDurableJobMessageOptions<StoredJob, Job extends Dispatchable
|
|
|
308
308
|
* Omit to keep retrying every throw (the transport/DLQ then decides terminal).
|
|
309
309
|
*/
|
|
310
310
|
maxAttemptsOf?: (job: StoredJob) => number | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* Per-job scope created right after claim: `wrapDispatch` wraps the handler run
|
|
313
|
+
* (e.g. AsyncLocalStorage telemetry), `onSettled` observes every terminal/
|
|
314
|
+
* released outcome (e.g. write a run-log row). Both close over the same scope so
|
|
315
|
+
* the wrapper's collected data is available to the observer.
|
|
316
|
+
*/
|
|
317
|
+
createJobScope?: (storedJob: StoredJob) => DurableJobScope<StoredJob>;
|
|
318
|
+
/**
|
|
319
|
+
* Decide whether a THROWN error is terminal (→ `failed_jobs`) vs released for
|
|
320
|
+
* retry. Overrides the default `attempts >= maxAttemptsOf` so callers can, e.g.,
|
|
321
|
+
* never fail on transient infra errors. `maxAttempts` is `maxAttemptsOf`'s value.
|
|
322
|
+
*/
|
|
323
|
+
isPermanentFailure?: (input: {
|
|
324
|
+
error: unknown;
|
|
325
|
+
storedJob: StoredJob;
|
|
326
|
+
attempts: number;
|
|
327
|
+
maxAttempts: number | undefined;
|
|
328
|
+
}) => boolean;
|
|
329
|
+
/**
|
|
330
|
+
* Dispatch the job payload's `then`/`catch`/`finally` continuations after a
|
|
331
|
+
* terminal outcome: `then`+`finally` on success, `catch`+`finally` on terminal
|
|
332
|
+
* failure, none on a release/retry.
|
|
333
|
+
*/
|
|
334
|
+
dispatchContinuations?: (input: {
|
|
335
|
+
storedJob: StoredJob;
|
|
336
|
+
stage: DurableJobContinuationStage;
|
|
337
|
+
}) => void | Promise<void>;
|
|
338
|
+
}
|
|
339
|
+
export interface DurableJobSettlement<StoredJob> {
|
|
340
|
+
storedJob: StoredJob;
|
|
341
|
+
status: RunDurableJobMessageResult['status'];
|
|
342
|
+
/** Wall-clock ms from claim to settle. */
|
|
343
|
+
durationMs: number;
|
|
344
|
+
/** True when the job reached a terminal failure (not a release/retry). */
|
|
345
|
+
permanent: boolean;
|
|
346
|
+
/** The error for failed/released/exhausted outcomes (raw, for the caller to classify). */
|
|
347
|
+
error?: unknown;
|
|
348
|
+
}
|
|
349
|
+
export interface DurableJobScope<StoredJob> {
|
|
350
|
+
wrapDispatch?: (run: () => Promise<DispatchResult>) => Promise<DispatchResult>;
|
|
351
|
+
onSettled?: (settlement: DurableJobSettlement<StoredJob>) => void | Promise<void>;
|
|
311
352
|
}
|
|
312
353
|
/**
|
|
313
354
|
* Outcome of {@link runDurableJobMessage}, discriminated on `status` so each
|
|
@@ -234,26 +234,41 @@ export async function runDurableJobMessage(opts) {
|
|
|
234
234
|
reportedStats[k] = (reportedStats[k] ?? 0) + s[k];
|
|
235
235
|
}
|
|
236
236
|
};
|
|
237
|
+
const scope = opts.createJobScope?.(storedJob);
|
|
238
|
+
const startedMs = Date.now();
|
|
239
|
+
const settle = async (status, permanent, error) => {
|
|
240
|
+
await scope?.onSettled?.({ storedJob, status, durationMs: Date.now() - startedMs, permanent, error });
|
|
241
|
+
};
|
|
242
|
+
const continuations = async (...stages) => {
|
|
243
|
+
for (const stage of stages)
|
|
244
|
+
await opts.dispatchContinuations?.({ storedJob, stage });
|
|
245
|
+
};
|
|
237
246
|
try {
|
|
238
|
-
const
|
|
247
|
+
const runOnce = () => dispatchRegisteredJob({
|
|
239
248
|
registry: opts.registry,
|
|
240
249
|
job,
|
|
241
250
|
createContext: async (input) => ({ ...await opts.createJobContext({ ...input, storedJob }), reportStats })
|
|
242
251
|
});
|
|
252
|
+
const dispatch = await (scope?.wrapDispatch ? scope.wrapDispatch(runOnce) : runOnce());
|
|
243
253
|
if (!dispatch.success) {
|
|
244
254
|
if (opts.failDispatchFailure !== false)
|
|
245
255
|
await failDurableJob(opts.lifecycle, storedJob, dispatch.error ? formatJobError(dispatch.error) : "Job dispatch failed");
|
|
256
|
+
await settle("dispatch-failed", true, dispatch.error);
|
|
257
|
+
await continuations("catch", "finally");
|
|
246
258
|
opts.message.ack();
|
|
247
259
|
return { status: "dispatch-failed", dispatch, error: dispatch.error };
|
|
248
260
|
}
|
|
249
261
|
if (dispatch.control?.handled) {
|
|
250
262
|
if (dispatch.control.action === "failed") {
|
|
251
263
|
await failDurableJob(opts.lifecycle, storedJob, dispatch.control.error ?? "Job failed via ctx.fail()");
|
|
264
|
+
await settle("failed", true, dispatch.control.error);
|
|
265
|
+
await continuations("catch", "finally");
|
|
252
266
|
opts.message.ack();
|
|
253
267
|
return { status: "failed", dispatch };
|
|
254
268
|
}
|
|
255
269
|
const delaySeconds = dispatch.control.delaySeconds ?? 0;
|
|
256
270
|
await releaseDurableJob(opts.lifecycle, storedJob, { delaySeconds, error: dispatch.control.error });
|
|
271
|
+
await settle("released", false, dispatch.control.error);
|
|
257
272
|
opts.message.retry({ delaySeconds });
|
|
258
273
|
return { status: "released", dispatch };
|
|
259
274
|
}
|
|
@@ -261,12 +276,17 @@ export async function runDurableJobMessage(opts) {
|
|
|
261
276
|
const hasStats = Object.keys(reportedStats).length > 0;
|
|
262
277
|
const completeWith = hasStats ? { ...result && typeof result === "object" ? result : {}, ...reportedStats } : result;
|
|
263
278
|
await completeDurableJob(opts.lifecycle, storedJob, completeWith);
|
|
279
|
+
await settle("completed", false);
|
|
280
|
+
await continuations("then", "finally");
|
|
264
281
|
opts.message.ack();
|
|
265
282
|
return { status: "completed", dispatch };
|
|
266
283
|
} catch (error) {
|
|
267
284
|
const maxAttempts = opts.maxAttemptsOf?.(storedJob);
|
|
268
|
-
|
|
285
|
+
const permanent = opts.isPermanentFailure ? opts.isPermanentFailure({ error, storedJob, attempts: job.attempts, maxAttempts }) : typeof maxAttempts === "number" && job.attempts >= maxAttempts;
|
|
286
|
+
if (permanent) {
|
|
269
287
|
await failDurableJob(opts.lifecycle, storedJob, describeCause(error));
|
|
288
|
+
await settle("exhausted", true, error);
|
|
289
|
+
await continuations("catch", "finally");
|
|
270
290
|
opts.message.ack();
|
|
271
291
|
return { status: "exhausted", error: jobErrors.handlerThrew(error) };
|
|
272
292
|
}
|
|
@@ -275,6 +295,7 @@ export async function runDurableJobMessage(opts) {
|
|
|
275
295
|
delaySeconds,
|
|
276
296
|
error: describeCause(error)
|
|
277
297
|
});
|
|
298
|
+
await settle("errored", false, error);
|
|
278
299
|
opts.message.retry({ delaySeconds });
|
|
279
300
|
return { status: "errored", error: jobErrors.handlerThrew(error) };
|
|
280
301
|
}
|
|
@@ -18,14 +18,16 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
18
18
|
console.error("[nuxt-cf-jobs] dev queue error:", error);
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
|
+
const taskEnvHost = globalThis;
|
|
22
|
+
taskEnvHost.__env__ = { ...runtime.env, ...taskEnvHost.__env__ ?? {} };
|
|
21
23
|
nitroApp.hooks.hook("request", (event) => {
|
|
22
24
|
const existing = event.context.cloudflare?.env;
|
|
23
25
|
event.context.cloudflare = {
|
|
24
26
|
...event.context.cloudflare ?? {},
|
|
25
27
|
env: existing ? { ...runtime.env, ...existing } : runtime.env
|
|
26
28
|
};
|
|
29
|
+
if (existing)
|
|
30
|
+
taskEnvHost.__env__ = { ...taskEnvHost.__env__, ...existing, ...runtime.env };
|
|
27
31
|
});
|
|
28
|
-
const taskEnvHost = globalThis;
|
|
29
|
-
taskEnvHost.__env__ = { ...runtime.env, ...taskEnvHost.__env__ ?? {} };
|
|
30
32
|
nitroApp.hooks.hook("close", () => runtime.dispose());
|
|
31
33
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BatchProgress, DurableBatchStore, SettleBatchMemberOptions, SettleBatchMemberResult } from './batch.js';
|
|
2
|
-
import type { D1DatabaseLike, D1DurableJobRepository } from './d1.js';
|
|
2
|
+
import type { D1DatabaseLike, D1DurableJobRecord, D1DurableJobRepository } from './d1.js';
|
|
3
3
|
import type { JobMetricsSink } from './metrics.js';
|
|
4
|
-
import type { CreateJobBatchOptions, CreateJobBatchResult, DurableJobRecord, EnqueueDurableJobResult, PruneDurableJobsOptions, PruneDurableJobsResult, QueuePublisher, RunDurableJobMessageOptions, RunDurableJobMessageResult } from './outbox.js';
|
|
4
|
+
import type { CreateJobBatchOptions, CreateJobBatchResult, DurableJobContinuationStage, DurableJobRecord, DurableJobScope, EnqueueDurableJobResult, PruneDurableJobsOptions, PruneDurableJobsResult, QueuePublisher, RunDurableJobMessageOptions, RunDurableJobMessageResult } from './outbox.js';
|
|
5
5
|
import type { DispatchableJob, JobDefinition, JobHandler, QueueBatch, QueueMessage } from './types.js';
|
|
6
6
|
export interface RunDurableJobBatchMessageOptions<StoredJob, Job extends DispatchableJob, Message extends {
|
|
7
7
|
jobId: string;
|
|
@@ -70,6 +70,25 @@ export interface ConsumeQueueBatchOptions<Queue extends string, Env, Db, Logger>
|
|
|
70
70
|
jobId?: string;
|
|
71
71
|
error?: string;
|
|
72
72
|
}) => void;
|
|
73
|
+
createJobScope?: (storedJob: D1DurableJobRecord<Queue>) => DurableJobScope<D1DurableJobRecord<Queue>>;
|
|
74
|
+
isPermanentFailure?: (input: {
|
|
75
|
+
error: unknown;
|
|
76
|
+
storedJob: D1DurableJobRecord<Queue>;
|
|
77
|
+
attempts: number;
|
|
78
|
+
maxAttempts: number | undefined;
|
|
79
|
+
}) => boolean;
|
|
80
|
+
dispatchContinuations?: (input: {
|
|
81
|
+
storedJob: D1DurableJobRecord<Queue>;
|
|
82
|
+
stage: DurableJobContinuationStage;
|
|
83
|
+
}) => void | Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Soft batch CPU budget (ms). Before each message, if the batch has run longer
|
|
86
|
+
* than this, the remaining messages are retried instead of processed — so a
|
|
87
|
+
* heavy batch can't blow the Worker CPU limit mid-message.
|
|
88
|
+
*/
|
|
89
|
+
maxBatchCpuMs?: number;
|
|
90
|
+
/** Delay (s) for messages deferred by the CPU guard. Default 5. */
|
|
91
|
+
cpuGuardRetryDelaySeconds?: number;
|
|
73
92
|
}
|
|
74
93
|
/**
|
|
75
94
|
* Process one CF queue batch end-to-end — the loop both apps hand-roll:
|
|
@@ -109,6 +128,8 @@ export interface CreateDurableJobsRuntimeOptions<Queue extends string = string,
|
|
|
109
128
|
metricsSink?: JobMetricsSink;
|
|
110
129
|
/** Notified after each batch settle. */
|
|
111
130
|
onBatchProgress?: (progress: BatchProgress) => void | Promise<void>;
|
|
131
|
+
/** Override how a batch's onFinish continuation runs (default: durable enqueue). */
|
|
132
|
+
dispatchOnFinish?: SettleBatchMemberOptions['dispatchOnFinish'];
|
|
112
133
|
/** Per-throw retry backoff for the consumer. */
|
|
113
134
|
retryDelaySeconds?: RunDurableJobMessageOptions<unknown, DispatchableJob>['retryDelaySeconds'];
|
|
114
135
|
onMissingBinding?: (queue: Queue, count: number) => void | Promise<void>;
|
|
@@ -128,6 +149,24 @@ export interface CreateDurableJobsRuntimeOptions<Queue extends string = string,
|
|
|
128
149
|
jobId?: string;
|
|
129
150
|
error?: string;
|
|
130
151
|
}) => void;
|
|
152
|
+
/** Per-job scope: wrap dispatch (e.g. telemetry) + observe each settlement. */
|
|
153
|
+
createJobScope?: (storedJob: D1DurableJobRecord<Queue>) => DurableJobScope<D1DurableJobRecord<Queue>>;
|
|
154
|
+
/** Decide whether a thrown error is terminal (default: attempts >= max). */
|
|
155
|
+
isPermanentFailure?: (input: {
|
|
156
|
+
error: unknown;
|
|
157
|
+
storedJob: D1DurableJobRecord<Queue>;
|
|
158
|
+
attempts: number;
|
|
159
|
+
maxAttempts: number | undefined;
|
|
160
|
+
}) => boolean;
|
|
161
|
+
/** Dispatch then/catch/finally payload continuations after a terminal outcome. */
|
|
162
|
+
dispatchContinuations?: (input: {
|
|
163
|
+
storedJob: D1DurableJobRecord<Queue>;
|
|
164
|
+
stage: DurableJobContinuationStage;
|
|
165
|
+
}) => void | Promise<void>;
|
|
166
|
+
/** Soft per-batch CPU budget (ms); remaining messages are deferred once exceeded. */
|
|
167
|
+
maxBatchCpuMs?: number;
|
|
168
|
+
/** Delay (s) for CPU-guard-deferred messages. Default 5. */
|
|
169
|
+
cpuGuardRetryDelaySeconds?: number;
|
|
131
170
|
}
|
|
132
171
|
export interface DurableJobsRuntime<Queue extends string = string> {
|
|
133
172
|
repository: D1DurableJobRepository<Queue>;
|
|
@@ -93,7 +93,13 @@ export async function consumeQueueBatch(opts) {
|
|
|
93
93
|
}
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
|
+
const batchStartedMs = Date.now();
|
|
97
|
+
const cpuBudget = opts.maxBatchCpuMs;
|
|
96
98
|
for (const message of opts.batch.messages) {
|
|
99
|
+
if (cpuBudget != null && Date.now() - batchStartedMs > cpuBudget) {
|
|
100
|
+
message.retry({ delaySeconds: opts.cpuGuardRetryDelaySeconds ?? 5 });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
97
103
|
const body = message.body ?? {};
|
|
98
104
|
const jobId = typeof body.jobId === "string" ? body.jobId : void 0;
|
|
99
105
|
if (jobId) {
|
|
@@ -107,6 +113,9 @@ export async function consumeQueueBatch(opts) {
|
|
|
107
113
|
retryDelaySeconds: opts.retryDelaySeconds,
|
|
108
114
|
// Honour the stored job's attempt cap (Laravel worker model).
|
|
109
115
|
maxAttemptsOf: (stored) => stored.max_attempts,
|
|
116
|
+
createJobScope: opts.createJobScope,
|
|
117
|
+
isPermanentFailure: opts.isPermanentFailure,
|
|
118
|
+
dispatchContinuations: opts.dispatchContinuations,
|
|
110
119
|
dispatchOnFinish: opts.dispatchOnFinish,
|
|
111
120
|
onBatchProgress: opts.onBatchProgress
|
|
112
121
|
});
|
|
@@ -150,7 +159,7 @@ export function createDurableJobsRuntime(opts) {
|
|
|
150
159
|
opts.resolveQueueBinding,
|
|
151
160
|
{ onMissingBinding: opts.onMissingBinding }
|
|
152
161
|
);
|
|
153
|
-
const dispatchOnFinish = async ({ continuation, batch }) => {
|
|
162
|
+
const dispatchOnFinish = opts.dispatchOnFinish ?? (async ({ continuation, batch }) => {
|
|
154
163
|
const c = continuation;
|
|
155
164
|
try {
|
|
156
165
|
const record = await prepareDurableJob({ name: c.name, payload: c.payload, registry: opts.registry });
|
|
@@ -158,7 +167,7 @@ export function createDurableJobsRuntime(opts) {
|
|
|
158
167
|
} catch (error) {
|
|
159
168
|
opts.onLog?.({ stage: "onfinish-failed", taskName: c.name, jobId: batch.id, error: error instanceof Error ? error.message : String(error) });
|
|
160
169
|
}
|
|
161
|
-
};
|
|
170
|
+
});
|
|
162
171
|
const isDuplicate = makeIsDuplicate(opts.dedup);
|
|
163
172
|
return {
|
|
164
173
|
repository,
|
|
@@ -180,6 +189,9 @@ export function createDurableJobsRuntime(opts) {
|
|
|
180
189
|
createJobContext: opts.createJobContext,
|
|
181
190
|
retryDelaySeconds: opts.retryDelaySeconds,
|
|
182
191
|
maxAttemptsOf: (stored) => stored.max_attempts,
|
|
192
|
+
createJobScope: opts.createJobScope,
|
|
193
|
+
isPermanentFailure: opts.isPermanentFailure,
|
|
194
|
+
dispatchContinuations: opts.dispatchContinuations,
|
|
183
195
|
dispatchOnFinish,
|
|
184
196
|
onBatchProgress: opts.onBatchProgress
|
|
185
197
|
}),
|
|
@@ -194,7 +206,12 @@ export function createDurableJobsRuntime(opts) {
|
|
|
194
206
|
retryDelaySeconds: opts.retryDelaySeconds,
|
|
195
207
|
isDlqQueue: opts.isDlqQueue,
|
|
196
208
|
isDuplicate,
|
|
197
|
-
onLog: opts.onLog
|
|
209
|
+
onLog: opts.onLog,
|
|
210
|
+
createJobScope: opts.createJobScope,
|
|
211
|
+
isPermanentFailure: opts.isPermanentFailure,
|
|
212
|
+
dispatchContinuations: opts.dispatchContinuations,
|
|
213
|
+
maxBatchCpuMs: opts.maxBatchCpuMs,
|
|
214
|
+
cpuGuardRetryDelaySeconds: opts.cpuGuardRetryDelaySeconds
|
|
198
215
|
}),
|
|
199
216
|
prune: (pruneOpts) => pruneDurableJobs(repository, pruneOpts)
|
|
200
217
|
};
|