nuxt-cf-jobs 0.12.0 → 0.12.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 +1 -1
- package/dist/runtime/server/d1.js +2 -2
- package/dist/runtime/server/errors.d.ts +24 -0
- package/dist/runtime/server/errors.js +27 -0
- package/dist/runtime/server/outbox.js +2 -2
- package/dist/runtime/server/runtime.d.ts +8 -1
- package/dist/runtime/server/runtime.js +3 -3
- package/package.json +5 -5
package/dist/module.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DurableJobOwnershipError } from "./errors.js";
|
|
1
|
+
import { DurableJobOwnershipError, headlineOf } from "./errors.js";
|
|
2
2
|
export const d1DurableJobMigrationSql = [
|
|
3
3
|
"CREATE TABLE IF NOT EXISTS job_batches (id text PRIMARY KEY, name text, parent_batch_id text, total_jobs integer NOT NULL DEFAULT 0, pending_jobs integer NOT NULL DEFAULT 0, failed_jobs integer NOT NULL DEFAULT 0, on_finish text, handler text, allow_failures integer DEFAULT 0, site_id text, user_id integer, created_at integer NOT NULL DEFAULT (unixepoch()), updated_at integer NOT NULL DEFAULT (unixepoch()), finished_at integer)",
|
|
4
4
|
"CREATE TABLE IF NOT EXISTS jobs (id text PRIMARY KEY, queue text NOT NULL, job_type text NOT NULL, batch_id text REFERENCES job_batches(id), user_id integer, site_id text, partner_id text, trace_id text, unique_key text, payload text NOT NULL, attempts integer DEFAULT 0, max_attempts integer DEFAULT 3, reserved_at integer, available_at integer NOT NULL, created_at integer NOT NULL DEFAULT (unixepoch()), completed_at integer, failed_at integer, last_error text, retry_reasons text, rows_fetched integer, rows_inserted integer, d1_rows_read integer, d1_rows_written integer, duration_ms integer)",
|
|
@@ -180,7 +180,7 @@ export function createD1DurableJobRepository(db, opts = {}) {
|
|
|
180
180
|
AND failed_at IS NULL
|
|
181
181
|
`).bind(job.id, job.reserved_at, job.attempts).run();
|
|
182
182
|
assertOwnedMutation(deleted, job.id);
|
|
183
|
-
fireHook(() => opts.onJobFailed?.({ job, error }));
|
|
183
|
+
fireHook(() => opts.onJobFailed?.({ job, error: headlineOf(error) }));
|
|
184
184
|
},
|
|
185
185
|
async releaseJob(job, releaseOpts) {
|
|
186
186
|
const result = await db.prepare(`
|
|
@@ -72,6 +72,30 @@ export type JobErrorTag = JobError['_tag'];
|
|
|
72
72
|
export declare function isJobError(value: unknown): value is JobError;
|
|
73
73
|
/** Renders any defect into a string for `message` fields and string-only sinks. */
|
|
74
74
|
export declare function describeCause(cause: unknown): string;
|
|
75
|
+
/** Upper bound on a rendered stack, so one defect can't blow up a `failed_jobs` row. */
|
|
76
|
+
export declare const MAX_DESCRIBED_STACK_CHARS = 4000;
|
|
77
|
+
/**
|
|
78
|
+
* The first line of a rendered defect — `"TypeError: <message>"` for anything that
|
|
79
|
+
* came through {@link describeCauseWithStack}, and the string itself for a plain
|
|
80
|
+
* single-line message. Telemetry sinks (Sentry issue titles, realtime payloads)
|
|
81
|
+
* want this, not the whole stack.
|
|
82
|
+
*/
|
|
83
|
+
export declare function headlineOf(rendered: string): string;
|
|
84
|
+
/**
|
|
85
|
+
* Renders a defect for DIAGNOSTICS — the stack, plus the `cause` chain beneath it.
|
|
86
|
+
*
|
|
87
|
+
* `describeCause` deliberately collapses an `Error` to its `.message`, which is the
|
|
88
|
+
* right rendering for the short `message` fields of a `JobError`. But it is also what
|
|
89
|
+
* used to land in `failed_jobs.exception`, so a terminal failure was persisted as a
|
|
90
|
+
* bare sentence with no stack and no cause — undiagnosable without reproducing it
|
|
91
|
+
* (this is how a `TypeError: Cannot assign to read only property 'name'` sat in the
|
|
92
|
+
* crawl queue for 103 occurrences with nothing to point at). Use this at the
|
|
93
|
+
* persistence / observability boundary; keep `describeCause` for `message`.
|
|
94
|
+
*
|
|
95
|
+
* `error.stack` already begins with `"<name>: <message>"`, so it is used whole when
|
|
96
|
+
* present and synthesised otherwise. Cycles and runaway chains are bounded.
|
|
97
|
+
*/
|
|
98
|
+
export declare function describeCauseWithStack(cause: unknown, maxChars?: number): string;
|
|
75
99
|
export declare const jobErrors: {
|
|
76
100
|
readonly noTask: () => NoTaskError;
|
|
77
101
|
readonly handlerNotFound: (task: string) => HandlerNotFoundError;
|
|
@@ -16,6 +16,33 @@ export function describeCause(cause) {
|
|
|
16
16
|
return cause.message;
|
|
17
17
|
return typeof cause === "string" ? cause : String(cause);
|
|
18
18
|
}
|
|
19
|
+
export const MAX_DESCRIBED_STACK_CHARS = 4e3;
|
|
20
|
+
export function headlineOf(rendered) {
|
|
21
|
+
const newline = rendered.indexOf("\n");
|
|
22
|
+
return newline === -1 ? rendered : rendered.slice(0, newline);
|
|
23
|
+
}
|
|
24
|
+
export function describeCauseWithStack(cause, maxChars = MAX_DESCRIBED_STACK_CHARS) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
const seen = /* @__PURE__ */ new Set();
|
|
27
|
+
let current = cause;
|
|
28
|
+
while (current !== void 0 && current !== null && parts.length < 5) {
|
|
29
|
+
if (typeof current === "object") {
|
|
30
|
+
if (seen.has(current))
|
|
31
|
+
break;
|
|
32
|
+
seen.add(current);
|
|
33
|
+
}
|
|
34
|
+
if (current instanceof Error) {
|
|
35
|
+
parts.push(current.stack || `${current.name}: ${current.message}`);
|
|
36
|
+
current = current.cause;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
parts.push(describeCause(current));
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
const rendered = parts.join("\nCaused by: ") || describeCause(cause);
|
|
43
|
+
return rendered.length > maxChars ? `${rendered.slice(0, maxChars)}
|
|
44
|
+
\u2026 (truncated)` : rendered;
|
|
45
|
+
}
|
|
19
46
|
export const jobErrors = {
|
|
20
47
|
noTask() {
|
|
21
48
|
return { _tag: "no-task", message: "No _task in payload" };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dispatchRegisteredJob } from "./dispatch.js";
|
|
2
|
-
import { describeCause, formatJobError, isDurableJobOwnershipError, jobErrors, jobErrorToException } from "./errors.js";
|
|
2
|
+
import { describeCause, describeCauseWithStack, formatJobError, isDurableJobOwnershipError, jobErrors, jobErrorToException } from "./errors.js";
|
|
3
3
|
import { buildJobPayload } from "./payload.js";
|
|
4
4
|
import { createJobTraceId, createJobUniqueKey, resolveJobMaxAttempts } from "./policy.js";
|
|
5
5
|
import { CF_QUEUE_MAX_MESSAGE_BYTES, sendBatchChunked, withSendBackpressure } from "./queue.js";
|
|
@@ -338,7 +338,7 @@ export async function runDurableJobMessage(opts) {
|
|
|
338
338
|
const permanent = opts.isPermanentFailure ? opts.isPermanentFailure({ error, storedJob, attempts: job.attempts, maxAttempts }) : typeof maxAttempts === "number" && job.attempts >= maxAttempts;
|
|
339
339
|
if (permanent) {
|
|
340
340
|
try {
|
|
341
|
-
await failDurableJob(opts.lifecycle, storedJob,
|
|
341
|
+
await failDurableJob(opts.lifecycle, storedJob, describeCauseWithStack(error));
|
|
342
342
|
} catch (failError) {
|
|
343
343
|
const obsolete = obsoleteDelivery(failError);
|
|
344
344
|
if (obsolete)
|
|
@@ -43,11 +43,16 @@ export interface RunLightweightMessageOptions<Env = unknown, Db = unknown, Logge
|
|
|
43
43
|
isDuplicate?: (id: string | undefined) => boolean;
|
|
44
44
|
/** Mark an id as terminally handled. Retried/released messages must not be marked. */
|
|
45
45
|
markDuplicate?: (id: string | undefined) => void;
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Diagnostic sink for dropped/invalid messages (no throw). `stack` is present
|
|
48
|
+
* when the entry came from a thrown defect, so a consumer can report the real
|
|
49
|
+
* stack rather than reconstructing `new Error(error)`.
|
|
50
|
+
*/
|
|
47
51
|
onLog?: (event: {
|
|
48
52
|
stage: string;
|
|
49
53
|
taskName?: string;
|
|
50
54
|
error?: string;
|
|
55
|
+
stack?: string;
|
|
51
56
|
}) => void;
|
|
52
57
|
}
|
|
53
58
|
export type RunLightweightMessageResult = {
|
|
@@ -78,6 +83,7 @@ export interface ConsumeQueueBatchOptions<Queue extends string, Env, Db, Logger>
|
|
|
78
83
|
taskName?: string;
|
|
79
84
|
jobId?: string;
|
|
80
85
|
error?: string;
|
|
86
|
+
stack?: string;
|
|
81
87
|
}) => void;
|
|
82
88
|
createJobScope?: (storedJob: D1DurableJobRecord<Queue>) => DurableJobScope<D1DurableJobRecord<Queue>>;
|
|
83
89
|
isPermanentFailure?: (input: {
|
|
@@ -170,6 +176,7 @@ export interface CreateDurableJobsRuntimeOptions<Queue extends string = string,
|
|
|
170
176
|
taskName?: string;
|
|
171
177
|
jobId?: string;
|
|
172
178
|
error?: string;
|
|
179
|
+
stack?: string;
|
|
173
180
|
}) => void;
|
|
174
181
|
/** Per-job scope: wrap dispatch (e.g. telemetry) + observe each settlement. */
|
|
175
182
|
createJobScope?: (storedJob: D1DurableJobRecord<Queue>) => DurableJobScope<D1DurableJobRecord<Queue>>;
|
|
@@ -2,7 +2,7 @@ import { createD1DurableBatchStore, createJobBatch, settleBatchMember } from "./
|
|
|
2
2
|
import { createCfJobsBroadcastBatchProgressHandler, createCfJobsBroadcastRepositoryHooks } from "./broadcast.js";
|
|
3
3
|
import { createD1DurableJobRepository } from "./d1.js";
|
|
4
4
|
import { dispatchRegisteredJob } from "./dispatch.js";
|
|
5
|
-
import { formatJobError } from "./errors.js";
|
|
5
|
+
import { describeCause, describeCauseWithStack, formatJobError } from "./errors.js";
|
|
6
6
|
import { createMessageDedup } from "./message-dedup.js";
|
|
7
7
|
import { metricsSinkToRepoHooks } from "./metrics.js";
|
|
8
8
|
import { createQueuePublisher, enqueueDurableJob, prepareDurableJob, pruneDurableJobs, runDurableJobMessage } from "./outbox.js";
|
|
@@ -93,7 +93,7 @@ export async function runLightweightMessage(opts) {
|
|
|
93
93
|
return { status: "completed" };
|
|
94
94
|
} catch (error) {
|
|
95
95
|
const delaySeconds = resolveJobRetryDelay(definition, message.attempts);
|
|
96
|
-
opts.onLog?.({ stage: "unexpected", taskName, error: error
|
|
96
|
+
opts.onLog?.({ stage: "unexpected", taskName, error: describeCause(error), stack: describeCauseWithStack(error) });
|
|
97
97
|
message.retry({ delaySeconds });
|
|
98
98
|
return { status: "errored" };
|
|
99
99
|
}
|
|
@@ -246,7 +246,7 @@ export function createDurableJobsRuntime(opts) {
|
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
248
|
} catch (error) {
|
|
249
|
-
opts.onLog?.({ stage: "onfinish-failed", taskName: c.name, jobId: batch.id, error: error
|
|
249
|
+
opts.onLog?.({ stage: "onfinish-failed", taskName: c.name, jobId: batch.id, error: describeCause(error), stack: describeCauseWithStack(error) });
|
|
250
250
|
}
|
|
251
251
|
});
|
|
252
252
|
const dedup = opts.dedup ? createMessageDedup(opts.dedup) : void 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-cf-jobs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.12.
|
|
4
|
+
"version": "0.12.2",
|
|
5
5
|
"description": "Nuxt module for typed Cloudflare queue jobs.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -86,20 +86,20 @@
|
|
|
86
86
|
"@nuxt/module-builder": "^1.0.2",
|
|
87
87
|
"@nuxt/test-utils": "^4.0.3",
|
|
88
88
|
"@types/node": "^26.1.0",
|
|
89
|
-
"@vitest/coverage-v8": "4.1.
|
|
89
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
90
90
|
"eslint": "^10.6.0",
|
|
91
91
|
"eslint-plugin-harlanzw": "latest",
|
|
92
92
|
"eslint-plugin-vue": "latest",
|
|
93
|
-
"h3": "2.0.1-rc.
|
|
93
|
+
"h3": "2.0.1-rc.23",
|
|
94
94
|
"happy-dom": "^20.10.6",
|
|
95
95
|
"nitropack": "^2.13.4",
|
|
96
96
|
"nuxt": "^4.4.8",
|
|
97
97
|
"typescript": "^6.0.3",
|
|
98
98
|
"unbuild": "^3.6.1",
|
|
99
|
-
"vitest": "^4.1.
|
|
99
|
+
"vitest": "^4.1.10",
|
|
100
100
|
"vue": "^3.5.39",
|
|
101
101
|
"vue-tsc": "^3.3.6",
|
|
102
|
-
"wrangler": "^4.
|
|
102
|
+
"wrangler": "^4.107.0"
|
|
103
103
|
},
|
|
104
104
|
"scripts": {
|
|
105
105
|
"build": "nuxt-module-build build && nuxt-module-build prepare",
|