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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-cf-jobs",
3
3
  "configKey": "cfJobs",
4
- "version": "0.12.0",
4
+ "version": "0.12.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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, describeCause(error));
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
- /** Diagnostic sink for dropped/invalid messages (no throw). */
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 instanceof Error ? error.message : String(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 instanceof Error ? error.message : String(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.0",
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.9",
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.22",
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.9",
99
+ "vitest": "^4.1.10",
100
100
  "vue": "^3.5.39",
101
101
  "vue-tsc": "^3.3.6",
102
- "wrangler": "^4.106.0"
102
+ "wrangler": "^4.107.0"
103
103
  },
104
104
  "scripts": {
105
105
  "build": "nuxt-module-build build && nuxt-module-build prepare",