nuxt-cf-jobs 0.6.3 โ 0.7.0
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 +27 -1
- package/dist/cli/index.mjs +92 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +8 -2
- package/dist/runtime/server/dev-worker.d.ts +53 -0
- package/dist/runtime/server/dev-worker.js +76 -0
- package/dist/runtime/server/handlers/dev-work.d.ts +10 -0
- package/dist/runtime/server/handlers/dev-work.js +54 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Typed Cloudflare Queue jobs for Nuxt, with Laravel-style ergonomics.
|
|
|
25
25
|
- ๐๏ธ **Durable D1 jobs**: persist a record before enqueue so work survives restarts and delivery gaps, with retry, release, and DLQ.
|
|
26
26
|
- โฐ **Scheduled tasks**: co-locate a cron with its handler; `nitro.tasks`, `scheduledTasks`, and Cloudflare `triggers.crons` are derived from it.
|
|
27
27
|
- ๐งช **Laravel-style testing**: run handlers inline, fake the queue, drain the outbox, or drive the whole `queue:work` loop on a virtual clock.
|
|
28
|
-
- ๐ ๏ธ **`cf-jobs` CLI**: `artisan queue:*`-style status, retry, flush, and migrate against local or remote D1
|
|
28
|
+
- ๐ ๏ธ **`cf-jobs` CLI**: `artisan queue:*`-style status, retry, flush, and migrate against local or remote D1, plus a `work` dev worker that runs durable jobs out-of-band so WebSockets stream live progress in `nuxt dev`.
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
@@ -331,6 +331,32 @@ Every command accepts `--config <wrangler path>`, `--db <binding>`, `--remote`,
|
|
|
331
331
|
|
|
332
332
|
`cf-jobs` shells out to `wrangler`, resolving the binary from `node_modules/.bin` and falling back to `wrangler` on `PATH` (override with `CF_JOBS_WRANGLER_BIN`).
|
|
333
333
|
|
|
334
|
+
### Dev worker (`cf-jobs work`)
|
|
335
|
+
|
|
336
|
+
In production the queue consumer is a separate Worker invocation, so a running job is naturally decoupled from the request that enqueued it. Under `nuxt dev` everything shares one process, so a durable job dispatched on a request can run to completion inside that same request, before a client has a chance to observe it. That hides the asynchronous behaviour you actually want to test, most painfully a WebSocket streaming live job progress: the job finishes before the socket is even connected.
|
|
337
|
+
|
|
338
|
+
`cf-jobs work` restores that decoupling. It is a long-running dev worker that drains durable jobs **out-of-band**, on its own clock, by driving the running dev server:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# in one terminal
|
|
342
|
+
pnpm nuxt dev
|
|
343
|
+
|
|
344
|
+
# in another: poll the dev server, run whatever durable jobs are ready
|
|
345
|
+
pnpm cf-jobs work
|
|
346
|
+
pnpm cf-jobs work --queue sync-critical # only one logical queue
|
|
347
|
+
pnpm cf-jobs work --once # drain everything ready now, then exit (handy in scripts/CI)
|
|
348
|
+
pnpm cf-jobs work --interval 1000 # idle poll interval in ms (backs off to 5s); default 500
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
So a client connects to your WebSocket, enqueues a job (the request returns immediately, job persisted in D1), then `cf-jobs work` picks it up a tick later and runs it. Because the worker drives a dev-only endpoint (`POST /__cf-jobs/work`) that fires your app's registered `cloudflare:queue` consumer **in the dev process**, the job runs with the app's real context and an in-memory WebSocket broadcast reaches the connected client, exactly as it would in production via a Durable Object.
|
|
352
|
+
|
|
353
|
+
Concurrency matches your queue config: each logical queue drains in batches of its wrangler `max_batch_size`, with up to `max_concurrency` batches in flight (default: serial, batch of 10). A queue declaring `{ maxConcurrency: 4, maxBatchSize: 10 }` drains 10-at-a-time, 4 batches concurrent, like its production consumer.
|
|
354
|
+
|
|
355
|
+
Two things to know:
|
|
356
|
+
|
|
357
|
+
- This is a **`nuxt dev` companion, not a production worker.** It assumes one process, so the request that runs the job shares memory (and thus WebSocket maps) with the rest of the dev server. Under multi-isolate setups (e.g. `wrangler dev`) the broadcast can land in a different isolate. The `/__cf-jobs/work` endpoint is registered only in dev and is an unauthenticated job executor, so it is never built into a deployment.
|
|
358
|
+
- Unlike the D1-querying commands above, `work` talks HTTP to the running dev server. It takes `--url` (default `http://localhost:3000`), `--db <binding>` to disambiguate when several D1 bindings exist, and `--json` to emit one machine-readable line per active tick.
|
|
359
|
+
|
|
334
360
|
## Runtime Validation
|
|
335
361
|
|
|
336
362
|
The generated registry validates jobs at startup and fails loudly for invalid definitions, duplicate names, and missing or invalid queue names. Queue binding checks are available from `#cf-jobs/app`:
|
package/dist/cli/index.mjs
CHANGED
|
@@ -698,13 +698,104 @@ const tasks = defineCommand({
|
|
|
698
698
|
});
|
|
699
699
|
}
|
|
700
700
|
});
|
|
701
|
+
const TRAILING_SLASH_RE = /\/+$/;
|
|
702
|
+
function sleep(ms) {
|
|
703
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
704
|
+
}
|
|
705
|
+
const work = defineCommand({
|
|
706
|
+
meta: {
|
|
707
|
+
name: "work",
|
|
708
|
+
description: "Dev worker: drive a running `nuxt dev` server to drain durable jobs out-of-band so WebSockets observe live progress (nuxt dev only \u2014 NOT a production worker)"
|
|
709
|
+
},
|
|
710
|
+
args: {
|
|
711
|
+
url: { type: "string", description: "Dev server base URL", default: "http://localhost:3000" },
|
|
712
|
+
interval: { type: "string", description: "Poll interval (ms) when idle; backs off up to 5s", default: "500" },
|
|
713
|
+
queue: { type: "string", description: "Only drain this logical queue" },
|
|
714
|
+
limit: { type: "string", description: "Max jobs claimed per tick", default: "100" },
|
|
715
|
+
db: { type: "string", description: "D1 binding name (default: auto-detect the only binding)" },
|
|
716
|
+
once: { type: "boolean", description: "Drain everything ready now, then exit", default: false },
|
|
717
|
+
json: { type: "boolean", description: "Emit one JSON line per active tick", default: false }
|
|
718
|
+
},
|
|
719
|
+
async run({ args }) {
|
|
720
|
+
const base = args.url.replace(TRAILING_SLASH_RE, "");
|
|
721
|
+
const params = new URLSearchParams({ limit: String(Number(args.limit) || 100) });
|
|
722
|
+
if (args.queue)
|
|
723
|
+
params.set("queue", args.queue);
|
|
724
|
+
if (args.db)
|
|
725
|
+
params.set("db", args.db);
|
|
726
|
+
const endpoint = `${base}/__cf-jobs/work?${params}`;
|
|
727
|
+
const baseInterval = Math.max(50, Number(args.interval) || 500);
|
|
728
|
+
const maxInterval = Math.max(baseInterval, 5e3);
|
|
729
|
+
let stopped = false;
|
|
730
|
+
const stop = () => {
|
|
731
|
+
stopped = true;
|
|
732
|
+
};
|
|
733
|
+
process$1.on("SIGINT", stop);
|
|
734
|
+
process$1.on("SIGTERM", stop);
|
|
735
|
+
if (!args.once)
|
|
736
|
+
process$1.stderr.write(`${color.dim(`cf-jobs work \u2192 ${endpoint} (Ctrl-C to stop)`)}
|
|
737
|
+
`);
|
|
738
|
+
let warnedAmbiguous = false;
|
|
739
|
+
let idle = baseInterval;
|
|
740
|
+
for (; ; ) {
|
|
741
|
+
if (stopped)
|
|
742
|
+
break;
|
|
743
|
+
const tick = await fetch(endpoint, { method: "POST" }).then((r) => r.json()).catch((error) => {
|
|
744
|
+
process$1.stderr.write(`${color.yellow("\u2026")} dev server unreachable at ${base} (${error instanceof Error ? error.message : String(error)})
|
|
745
|
+
`);
|
|
746
|
+
return null;
|
|
747
|
+
});
|
|
748
|
+
if (!tick || tick.error) {
|
|
749
|
+
if (tick?.error)
|
|
750
|
+
process$1.stderr.write(`${color.red("\u2716")} ${tick.error === "no-d1-binding" ? "no D1 binding found on the dev server env" : tick.error}
|
|
751
|
+
`);
|
|
752
|
+
if (args.once) {
|
|
753
|
+
process$1.exitCode = 1;
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
await sleep(maxInterval);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (tick.ambiguousBindings && !warnedAmbiguous) {
|
|
760
|
+
warnedAmbiguous = true;
|
|
761
|
+
process$1.stderr.write(`${color.yellow("!")} multiple D1 bindings (${tick.ambiguousBindings.join(", ")}); using the first. Pass --db to choose.
|
|
762
|
+
`);
|
|
763
|
+
}
|
|
764
|
+
if (tick.processed > 0) {
|
|
765
|
+
idle = baseInterval;
|
|
766
|
+
if (args.json) {
|
|
767
|
+
process$1.stdout.write(`${JSON.stringify(tick)}
|
|
768
|
+
`);
|
|
769
|
+
} else {
|
|
770
|
+
const detail = tick.byQueue && Object.keys(tick.byQueue).length ? ` ${color.dim(`(${Object.entries(tick.byQueue).map(([q, n]) => `${q}:${n}`).join(", ")})`)}` : "";
|
|
771
|
+
const waiting = tick.remaining > 0 ? color.dim(` \u2014 ${tick.remaining} waiting`) : "";
|
|
772
|
+
process$1.stdout.write(`${color.green("\u2713")} ran ${tick.processed}${detail}${waiting}
|
|
773
|
+
`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (args.once) {
|
|
777
|
+
if (tick.processed === 0 && tick.remaining === 0)
|
|
778
|
+
break;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (tick.processed === 0) {
|
|
782
|
+
await sleep(idle);
|
|
783
|
+
idle = Math.min(maxInterval, idle * 2);
|
|
784
|
+
} else {
|
|
785
|
+
await sleep(baseInterval);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
process$1.off("SIGINT", stop);
|
|
789
|
+
process$1.off("SIGTERM", stop);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
701
792
|
const main = defineCommand({
|
|
702
793
|
meta: {
|
|
703
794
|
name: "cf-jobs",
|
|
704
795
|
description: "Inspect and manage nuxt-cf-jobs durable jobs in Cloudflare D1"
|
|
705
796
|
},
|
|
706
797
|
args: sharedArgs,
|
|
707
|
-
subCommands: { status, jobs, failed, retry, forget, flush, clear, prune, migrate, schedule, tasks },
|
|
798
|
+
subCommands: { status, jobs, failed, retry, forget, flush, clear, prune, migrate, schedule, tasks, work },
|
|
708
799
|
async run({ args, rawArgs }) {
|
|
709
800
|
if (rawArgs.length === 0 || rawArgs.every((a) => a.startsWith("-")))
|
|
710
801
|
await runStatus(args);
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve, relative, sep } from 'node:path';
|
|
4
|
-
import { defineNuxtModule, createResolver, addServerImports, addTemplate, addTypeTemplate, updateTemplates, useLogger, addServerPlugin, resolveFiles } from '@nuxt/kit';
|
|
4
|
+
import { defineNuxtModule, createResolver, addServerImports, addTemplate, addTypeTemplate, updateTemplates, useLogger, addServerPlugin, addServerHandler, resolveFiles } from '@nuxt/kit';
|
|
5
5
|
import { parseModule } from 'magicast';
|
|
6
6
|
import { cfJobsAppExportNames } from '../dist/runtime/server/app.js';
|
|
7
7
|
import { c as collectTasks, f as findDuplicateTaskNames, b as buildCronUnion, a as buildScheduledTasks, g as renderSuggestedCronsToml, e as findWranglerConfig, p as parseWranglerConfig, d as crossCheckCrons, r as reconcileQueues } from './shared/nuxt-cf-jobs.BkJA3gwQ.mjs';
|
|
@@ -158,8 +158,14 @@ const module$1 = defineNuxtModule({
|
|
|
158
158
|
queues: nuxt.options.runtimeConfig.cfJobs?.queues ?? options.queues,
|
|
159
159
|
defaultQueue: nuxt.options.runtimeConfig.cfJobs?.defaultQueue ?? options.defaultQueue
|
|
160
160
|
};
|
|
161
|
-
if (nuxt.options.dev)
|
|
161
|
+
if (nuxt.options.dev) {
|
|
162
162
|
addServerPlugin(resolver.resolve("./runtime/server/plugins/dev-queues"));
|
|
163
|
+
addServerHandler({
|
|
164
|
+
route: "/__cf-jobs/work",
|
|
165
|
+
method: "post",
|
|
166
|
+
handler: resolver.resolve("./runtime/server/handlers/dev-work")
|
|
167
|
+
});
|
|
168
|
+
}
|
|
163
169
|
if (options.validateWrangler !== false)
|
|
164
170
|
runWranglerCrossCheck(options, nuxt.options.rootDir, resolve(nuxt.options.buildDir, "cf-jobs"), nuxt.options.nitro);
|
|
165
171
|
await wireScheduledTasks(options, nuxt, resolve(nuxt.options.buildDir, "cf-jobs"));
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { D1DatabaseLike } from './d1.js';
|
|
2
|
+
import type { QueueJobMessage } from './outbox.js';
|
|
3
|
+
export interface DevWorkerQueueConfig {
|
|
4
|
+
/** Wrangler `max_concurrency`: how many batches of this queue drain at once. */
|
|
5
|
+
maxConcurrency: number;
|
|
6
|
+
/** Wrangler `max_batch_size`: messages handed to one consumer invocation. */
|
|
7
|
+
maxBatchSize: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DevWorkerDeps<Queue extends string = string> {
|
|
10
|
+
/** Ready (unreserved, available, not terminal) durable jobs, oldest first, up to `limit`. */
|
|
11
|
+
findDispatchable: (limit: number) => Promise<ReadonlyArray<{
|
|
12
|
+
id: string;
|
|
13
|
+
queue: Queue;
|
|
14
|
+
}>>;
|
|
15
|
+
/**
|
|
16
|
+
* Drive one consumer invocation in-process โ fires the `cloudflare:queue` hook
|
|
17
|
+
* and AWAITS it, so the handler runs (and broadcasts) before the tick returns.
|
|
18
|
+
*/
|
|
19
|
+
dispatchBatch: (queue: Queue, messages: ReadonlyArray<QueueJobMessage<Queue>>) => Promise<void>;
|
|
20
|
+
/** Per-queue sizing โ "match the queue" off the wrangler consumer config. */
|
|
21
|
+
queueConfig: (queue: Queue) => DevWorkerQueueConfig;
|
|
22
|
+
}
|
|
23
|
+
export interface DevWorkerTickResult {
|
|
24
|
+
/** Jobs handed to a consumer this tick (โ processed; the consumer ran them inline). */
|
|
25
|
+
processed: number;
|
|
26
|
+
/** Per-logical-queue counts. */
|
|
27
|
+
byQueue: Record<string, number>;
|
|
28
|
+
/** Ready jobs still waiting after this tick (lets the CLI decide to keep going). */
|
|
29
|
+
remaining: number;
|
|
30
|
+
}
|
|
31
|
+
export interface DevWorkerTickOptions {
|
|
32
|
+
/** Max jobs claimed per tick. */
|
|
33
|
+
limit: number;
|
|
34
|
+
/** Restrict the tick to a single logical queue. */
|
|
35
|
+
queue?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Derive per-queue sizing from a `cfJobs.queues` entry (string binding or options object). */
|
|
38
|
+
export declare function resolveQueueWorkerConfig(entry: string | {
|
|
39
|
+
maxConcurrency?: number;
|
|
40
|
+
maxBatchSize?: number;
|
|
41
|
+
} | undefined): DevWorkerQueueConfig;
|
|
42
|
+
export declare function chunk<T>(items: readonly T[], size: number): T[][];
|
|
43
|
+
/** Run `tasks` with at most `concurrency` in flight (a simple slot pool). */
|
|
44
|
+
export declare function runWithConcurrency<T>(tasks: ReadonlyArray<() => Promise<T>>, concurrency: number): Promise<T[]>;
|
|
45
|
+
export declare function runDevWorkerTick<Queue extends string = string>(deps: DevWorkerDeps<Queue>, opts: DevWorkerTickOptions): Promise<DevWorkerTickResult>;
|
|
46
|
+
export interface D1BindingMatch {
|
|
47
|
+
binding: string;
|
|
48
|
+
db: D1DatabaseLike;
|
|
49
|
+
/** Set when more than one D1 binding exists and the first was picked. */
|
|
50
|
+
ambiguous?: string[];
|
|
51
|
+
}
|
|
52
|
+
/** Resolve the durable-jobs D1 database off the runtime env (auto-detect, or by name). */
|
|
53
|
+
export declare function findD1Binding(env: Record<string, unknown>, preferred?: string): D1BindingMatch | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const DEFAULT_MAX_BATCH_SIZE = 10;
|
|
2
|
+
const DEFAULT_MAX_CONCURRENCY = 1;
|
|
3
|
+
export function resolveQueueWorkerConfig(entry) {
|
|
4
|
+
if (!entry || typeof entry === "string")
|
|
5
|
+
return { maxConcurrency: DEFAULT_MAX_CONCURRENCY, maxBatchSize: DEFAULT_MAX_BATCH_SIZE };
|
|
6
|
+
return {
|
|
7
|
+
maxConcurrency: clampPositive(entry.maxConcurrency, DEFAULT_MAX_CONCURRENCY),
|
|
8
|
+
maxBatchSize: clampPositive(entry.maxBatchSize, DEFAULT_MAX_BATCH_SIZE)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function clampPositive(value, fallback) {
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback;
|
|
13
|
+
}
|
|
14
|
+
export function chunk(items, size) {
|
|
15
|
+
const step = Math.max(1, size);
|
|
16
|
+
const out = [];
|
|
17
|
+
for (let i = 0; i < items.length; i += step)
|
|
18
|
+
out.push(items.slice(i, i + step));
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
export async function runWithConcurrency(tasks, concurrency) {
|
|
22
|
+
const limit = Math.max(1, concurrency);
|
|
23
|
+
const results = Array.from({ length: tasks.length });
|
|
24
|
+
let cursor = 0;
|
|
25
|
+
async function worker() {
|
|
26
|
+
for (; ; ) {
|
|
27
|
+
const index = cursor++;
|
|
28
|
+
if (index >= tasks.length)
|
|
29
|
+
return;
|
|
30
|
+
results[index] = await tasks[index]();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
await Promise.all(Array.from({ length: Math.min(limit, tasks.length) }, worker));
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
export async function runDevWorkerTick(deps, opts) {
|
|
37
|
+
const ready = await deps.findDispatchable(opts.limit);
|
|
38
|
+
const filtered = opts.queue ? ready.filter((r) => r.queue === opts.queue) : ready;
|
|
39
|
+
const byQueue = {};
|
|
40
|
+
if (filtered.length === 0)
|
|
41
|
+
return { processed: 0, byQueue, remaining: 0 };
|
|
42
|
+
const groups = /* @__PURE__ */ new Map();
|
|
43
|
+
for (const record of filtered) {
|
|
44
|
+
const messages = groups.get(record.queue) ?? [];
|
|
45
|
+
messages.push({ jobId: record.id, queue: record.queue });
|
|
46
|
+
groups.set(record.queue, messages);
|
|
47
|
+
}
|
|
48
|
+
let processed = 0;
|
|
49
|
+
await Promise.all([...groups].map(async ([queue, messages]) => {
|
|
50
|
+
const { maxConcurrency, maxBatchSize } = deps.queueConfig(queue);
|
|
51
|
+
const batches = chunk(messages, maxBatchSize);
|
|
52
|
+
await runWithConcurrency(
|
|
53
|
+
batches.map((batch) => () => deps.dispatchBatch(queue, batch)),
|
|
54
|
+
maxConcurrency
|
|
55
|
+
);
|
|
56
|
+
byQueue[queue] = messages.length;
|
|
57
|
+
processed += messages.length;
|
|
58
|
+
}));
|
|
59
|
+
const after = await deps.findDispatchable(opts.limit);
|
|
60
|
+
const remaining = opts.queue ? after.filter((r) => r.queue === opts.queue).length : after.length;
|
|
61
|
+
return { processed, byQueue, remaining };
|
|
62
|
+
}
|
|
63
|
+
export function findD1Binding(env, preferred) {
|
|
64
|
+
if (preferred) {
|
|
65
|
+
const db = env[preferred];
|
|
66
|
+
return isD1Database(db) ? { binding: preferred, db } : void 0;
|
|
67
|
+
}
|
|
68
|
+
const matches = Object.entries(env).filter((entry) => isD1Database(entry[1]));
|
|
69
|
+
const first = matches[0];
|
|
70
|
+
if (!first)
|
|
71
|
+
return void 0;
|
|
72
|
+
return matches.length > 1 ? { binding: first[0], db: first[1], ambiguous: matches.map(([name]) => name) } : { binding: first[0], db: first[1] };
|
|
73
|
+
}
|
|
74
|
+
function isD1Database(value) {
|
|
75
|
+
return !!value && typeof value === "object" && typeof value.prepare === "function" && typeof value.exec === "function";
|
|
76
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only worker endpoint. Registered ONLY when `nuxt.options.dev` (see
|
|
3
|
+
* module.ts) and guarded again here โ it is an unauthenticated job executor by
|
|
4
|
+
* design and must never reach a deployment. Driven by `cf-jobs work`, it finds
|
|
5
|
+
* ready durable jobs in D1 and runs them through the app's `cloudflare:queue`
|
|
6
|
+
* consumer IN THIS dev process, so an already-connected WebSocket sees live
|
|
7
|
+
* progress. See `dev-worker.ts` for the rationale.
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: any;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { defineEventHandler, getQuery, useNitroApp, useRuntimeConfig } from "nitropack/runtime";
|
|
2
|
+
import { createD1DurableJobRepository } from "../d1.js";
|
|
3
|
+
import { findD1Binding, resolveQueueWorkerConfig, runDevWorkerTick } from "../dev-worker.js";
|
|
4
|
+
import { findDispatchableDurableJobs } from "../outbox.js";
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
if (!import.meta.dev)
|
|
7
|
+
return { processed: 0, byQueue: {}, remaining: 0, error: "dev-only" };
|
|
8
|
+
const query = getQuery(event);
|
|
9
|
+
const limit = clampInt(query.limit, 100, 1, 1e3);
|
|
10
|
+
const onlyQueue = pickString(query.queue);
|
|
11
|
+
const preferredDb = pickString(query.db);
|
|
12
|
+
const env = resolveEnv(event);
|
|
13
|
+
const d1 = findD1Binding(env, preferredDb);
|
|
14
|
+
if (!d1)
|
|
15
|
+
return { processed: 0, byQueue: {}, remaining: 0, error: "no-d1-binding" };
|
|
16
|
+
const repo = createD1DurableJobRepository(d1.db);
|
|
17
|
+
const nitroApp = useNitroApp();
|
|
18
|
+
const queues = useRuntimeConfig().cfJobs?.queues ?? {};
|
|
19
|
+
const result = await runDevWorkerTick({
|
|
20
|
+
findDispatchable: (max) => findDispatchableDurableJobs(repo, { limit: max }),
|
|
21
|
+
queueConfig: (queue) => resolveQueueWorkerConfig(queues[queue]),
|
|
22
|
+
async dispatchBatch(queue, messages) {
|
|
23
|
+
await nitroApp.hooks.callHook("cloudflare:queue", {
|
|
24
|
+
batch: {
|
|
25
|
+
queue,
|
|
26
|
+
messages: messages.map((body) => ({ id: body.jobId, body, attempts: 1, ack() {
|
|
27
|
+
}, retry() {
|
|
28
|
+
} })),
|
|
29
|
+
ackAll() {
|
|
30
|
+
},
|
|
31
|
+
retryAll() {
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
env
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}, { limit, queue: onlyQueue });
|
|
38
|
+
return d1.ambiguous ? { ...result, d1Binding: d1.binding, ambiguousBindings: d1.ambiguous } : result;
|
|
39
|
+
});
|
|
40
|
+
function resolveEnv(event) {
|
|
41
|
+
const fromEvent = event.context?.cloudflare?.env;
|
|
42
|
+
const fromGlobal = globalThis.__env__;
|
|
43
|
+
return { ...fromGlobal ?? {}, ...fromEvent ?? {} };
|
|
44
|
+
}
|
|
45
|
+
function pickString(value) {
|
|
46
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
47
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
48
|
+
}
|
|
49
|
+
function clampInt(value, fallback, min, max) {
|
|
50
|
+
const n = Number(Array.isArray(value) ? value[0] : value);
|
|
51
|
+
if (!Number.isFinite(n))
|
|
52
|
+
return fallback;
|
|
53
|
+
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
54
|
+
}
|