nuxt-cf-jobs 0.3.0 → 0.4.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 +37 -0
- package/dist/module.d.mts +8 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +146 -32
- package/dist/runtime/server/app.d.ts +26 -25
- package/dist/runtime/server/app.js +5 -1
- package/dist/runtime/server/dispatch.d.ts +13 -1
- package/dist/runtime/server/dispatch.js +2 -2
- package/dist/runtime/server/outbox.d.ts +2 -1
- package/dist/runtime/server/registry.d.ts +29 -2
- package/dist/runtime/server/registry.js +51 -15
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -275,6 +275,43 @@ export default defineNitroPlugin((nitroApp) => {
|
|
|
275
275
|
- `releaseStaleReservedJobs()`
|
|
276
276
|
- `toDispatchableJob()`
|
|
277
277
|
|
|
278
|
+
## Scheduled Tasks (cron)
|
|
279
|
+
|
|
280
|
+
Queue jobs handle per-request deferred work; **scheduled tasks** handle cron work. `defineScheduledTask` co-locates the cron schedule with its handler, and the module derives `nitro.tasks`, `nitro.scheduledTasks`, and the Cloudflare `triggers.crons` from it — so there is no central list to keep in sync (and no way for the three to silently drift apart).
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
// server/tasks/cleanup.ts
|
|
284
|
+
export default defineScheduledTask({
|
|
285
|
+
name: 'db:cleanup', // nitro task name (also the runTask id)
|
|
286
|
+
cron: '0 3 * * *', // or cron: ['0 3 * * *', '0 */6 * * *']
|
|
287
|
+
description: 'Nightly cleanup',
|
|
288
|
+
run() {
|
|
289
|
+
// ...same shape as nitro defineTask's run
|
|
290
|
+
return { result: 'ok' }
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Enable scanning via `cfJobs.tasksDir`:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
export default defineNuxtConfig({
|
|
299
|
+
cfJobs: {
|
|
300
|
+
// `true` → auto-discover `server/tasks` in the app AND every extended layer
|
|
301
|
+
// (nuxt.options._layers), so a new layer with cron work needs no host config.
|
|
302
|
+
tasksDir: true,
|
|
303
|
+
// …or be explicit: tasksDir: ['server/tasks', '../some-layer/server/tasks']
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Notes:
|
|
309
|
+
|
|
310
|
+
- `name` and `cron` must be **string literals** — the module reads them statically at build time (without executing the file, which typically imports a DB/server utils that won't load outside nitro). Computed values are skipped with a warning.
|
|
311
|
+
- Plain nitro `defineTask` files in the same dirs are still registered (so they're runnable via `runTask`), just not scheduled.
|
|
312
|
+
- `nitro.scheduledTasks` is populated only outside dev by default (so crons don't fire locally); override with `cfJobs.scheduledTasks: true | false`. The deploy-only `triggers.crons` is always written.
|
|
313
|
+
- Opt-in: nothing is scanned or registered unless `tasksDir` is set.
|
|
314
|
+
|
|
278
315
|
## CLI
|
|
279
316
|
|
|
280
317
|
The package ships a `cf-jobs` binary, an `artisan queue:*`-style tool for inspecting and managing the durable D1 job tables. It queries D1 through `wrangler d1 execute`, so it works against both the local miniflare database (default) and production (`--remote`), auto-detecting the D1 binding from your wrangler config.
|
package/dist/module.d.mts
CHANGED
|
@@ -106,6 +106,14 @@ declare module '@nuxt/schema' {
|
|
|
106
106
|
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
107
107
|
|
|
108
108
|
declare function generateRegistryTemplate(options: ModuleOptions, rootDir: string, templateDir: string): Promise<string>;
|
|
109
|
+
/**
|
|
110
|
+
* Emits a `declare module '#cf-jobs/app'` augmentation. The data-only runtime
|
|
111
|
+
* `registry.ts` carries no hand-written types; this augmentation MERGES the
|
|
112
|
+
* type-only aliases into that module so consumers can `import type { JobName,
|
|
113
|
+
* JobPayload } from '#cf-jobs/app'`. Types are derived from each job's *full*
|
|
114
|
+
* default-export type via `typeof import(...)` — purely type-level, so no
|
|
115
|
+
* handler module is loaded to compute them.
|
|
116
|
+
*/
|
|
109
117
|
declare function generateRegistryTypesTemplate(options: ModuleOptions, rootDir: string, templateDir: string): Promise<string>;
|
|
110
118
|
|
|
111
119
|
export { _default as default, generateRegistryTemplate, generateRegistryTypesTemplate };
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,9 +1,108 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import { resolve, relative, sep } from 'node:path';
|
|
3
|
-
import { defineNuxtModule, createResolver, addServerImports, addTemplate, updateTemplates, useLogger, addServerPlugin, resolveFiles } from '@nuxt/kit';
|
|
4
|
+
import { defineNuxtModule, createResolver, addServerImports, addTemplate, addTypeTemplate, updateTemplates, useLogger, addServerPlugin, resolveFiles } from '@nuxt/kit';
|
|
5
|
+
import { parseModule } from 'magicast';
|
|
4
6
|
import { cfJobsAppExportNames } from '../dist/runtime/server/app.js';
|
|
5
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.C2yTYlMg.mjs';
|
|
6
|
-
|
|
8
|
+
|
|
9
|
+
function isNode(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
11
|
+
}
|
|
12
|
+
function emptyMeta() {
|
|
13
|
+
return { hasInput: false, hasUniqueId: false };
|
|
14
|
+
}
|
|
15
|
+
function findDefineJobCall(node) {
|
|
16
|
+
if (Array.isArray(node)) {
|
|
17
|
+
for (const child of node) {
|
|
18
|
+
const found = findDefineJobCall(child);
|
|
19
|
+
if (found)
|
|
20
|
+
return found;
|
|
21
|
+
}
|
|
22
|
+
return void 0;
|
|
23
|
+
}
|
|
24
|
+
if (!isNode(node))
|
|
25
|
+
return void 0;
|
|
26
|
+
if (node.type === "CallExpression") {
|
|
27
|
+
const callee = node.callee;
|
|
28
|
+
if (isNode(callee) && callee.type === "Identifier" && callee.name === "defineJob")
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
for (const key in node) {
|
|
32
|
+
if (key === "type")
|
|
33
|
+
continue;
|
|
34
|
+
const found = findDefineJobCall(node[key]);
|
|
35
|
+
if (found)
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
function propKeyName(prop) {
|
|
41
|
+
const key = prop.key;
|
|
42
|
+
if (!isNode(key))
|
|
43
|
+
return void 0;
|
|
44
|
+
if (key.type === "Identifier" && typeof key.name === "string")
|
|
45
|
+
return key.name;
|
|
46
|
+
if (key.type === "StringLiteral" && typeof key.value === "string")
|
|
47
|
+
return key.value;
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
function extractJobMeta(code) {
|
|
51
|
+
const meta = emptyMeta();
|
|
52
|
+
let ast;
|
|
53
|
+
try {
|
|
54
|
+
ast = parseModule(code).$ast;
|
|
55
|
+
} catch {
|
|
56
|
+
return meta;
|
|
57
|
+
}
|
|
58
|
+
const call = findDefineJobCall(ast);
|
|
59
|
+
if (!call)
|
|
60
|
+
return meta;
|
|
61
|
+
const args = call.arguments;
|
|
62
|
+
const arg = Array.isArray(args) ? args[0] : void 0;
|
|
63
|
+
if (!isNode(arg) || arg.type !== "ObjectExpression")
|
|
64
|
+
return meta;
|
|
65
|
+
const properties = Array.isArray(arg.properties) ? arg.properties : [];
|
|
66
|
+
for (const prop of properties) {
|
|
67
|
+
if (!isNode(prop) || prop.type !== "ObjectProperty")
|
|
68
|
+
continue;
|
|
69
|
+
if (prop.computed === true)
|
|
70
|
+
continue;
|
|
71
|
+
const name = propKeyName(prop);
|
|
72
|
+
if (!name)
|
|
73
|
+
continue;
|
|
74
|
+
const value = prop.value;
|
|
75
|
+
switch (name) {
|
|
76
|
+
case "queue":
|
|
77
|
+
if (isNode(value) && value.type === "StringLiteral" && typeof value.value === "string")
|
|
78
|
+
meta.queue = value.value;
|
|
79
|
+
break;
|
|
80
|
+
case "jobType":
|
|
81
|
+
if (isNode(value) && value.type === "StringLiteral" && typeof value.value === "string")
|
|
82
|
+
meta.jobType = value.value;
|
|
83
|
+
break;
|
|
84
|
+
case "maxAttempts":
|
|
85
|
+
if (isNode(value) && value.type === "NumericLiteral" && typeof value.value === "number")
|
|
86
|
+
meta.maxAttempts = value.value;
|
|
87
|
+
break;
|
|
88
|
+
case "tries":
|
|
89
|
+
if (isNode(value) && value.type === "NumericLiteral" && typeof value.value === "number")
|
|
90
|
+
meta.tries = value.value;
|
|
91
|
+
break;
|
|
92
|
+
case "unique":
|
|
93
|
+
if (isNode(value) && value.type === "BooleanLiteral" && typeof value.value === "boolean")
|
|
94
|
+
meta.unique = value.value;
|
|
95
|
+
break;
|
|
96
|
+
case "input":
|
|
97
|
+
meta.hasInput = true;
|
|
98
|
+
break;
|
|
99
|
+
case "uniqueId":
|
|
100
|
+
meta.hasUniqueId = true;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return meta;
|
|
105
|
+
}
|
|
7
106
|
|
|
8
107
|
const module$1 = defineNuxtModule({
|
|
9
108
|
meta: {
|
|
@@ -33,16 +132,15 @@ const module$1 = defineNuxtModule({
|
|
|
33
132
|
write: true,
|
|
34
133
|
getContents: async () => generateRegistryTemplate(options, nuxt.options.rootDir, resolve(nuxt.options.buildDir, "cf-jobs"))
|
|
35
134
|
});
|
|
36
|
-
addTemplate({
|
|
37
|
-
filename: "cf-jobs/registry.d.ts",
|
|
38
|
-
write: true,
|
|
39
|
-
getContents: async () => generateRegistryTypesTemplate(options, nuxt.options.rootDir, resolve(nuxt.options.buildDir, "cf-jobs"))
|
|
40
|
-
});
|
|
41
135
|
nuxt.options.alias[options.registryAlias ?? "#cf-jobs/app"] = registryTemplate.dst;
|
|
136
|
+
const typesTemplate = addTypeTemplate({
|
|
137
|
+
filename: "cf-jobs/registry-augmentation.d.ts",
|
|
138
|
+
getContents: async () => generateRegistryTypesTemplate(options, nuxt.options.rootDir, resolve(nuxt.options.buildDir, "cf-jobs"))
|
|
139
|
+
}, { nuxt: true, nitro: true });
|
|
42
140
|
nuxt.hooks.hook("builder:watch", (async (_event, path) => {
|
|
43
141
|
if (!isWatchedJobPath(path, options, nuxt.options.rootDir))
|
|
44
142
|
return;
|
|
45
|
-
await updateTemplates({ filter: (template) => template.dst === registryTemplate.dst });
|
|
143
|
+
await updateTemplates({ filter: (template) => template.dst === registryTemplate.dst || template.dst === typesTemplate.dst });
|
|
46
144
|
}));
|
|
47
145
|
if (options.defaultQueue && !options.queues[options.defaultQueue])
|
|
48
146
|
useLogger("nuxt-cf-jobs").warn(`cfJobs.defaultQueue="${options.defaultQueue}" is not a key of cfJobs.queues`);
|
|
@@ -151,17 +249,36 @@ See ${resolve(templateDir, "wrangler.suggested.toml")} for the expected blocks.`
|
|
|
151
249
|
async function generateRegistryTemplate(options, rootDir, templateDir) {
|
|
152
250
|
const files = await resolveJobFiles(options, rootDir);
|
|
153
251
|
assertUniqueGeneratedJobNames(files, options, rootDir);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
252
|
+
const entryLines = await Promise.all(files.map(async (file) => {
|
|
253
|
+
const name = toJobName(file, options, rootDir);
|
|
254
|
+
const importPath = toImportPath(templateDir, file).replace(/\.ts$/, "");
|
|
255
|
+
const meta = extractJobMeta(await readFile(file, "utf8").catch(() => ""));
|
|
256
|
+
const fields = [`name: ${JSON.stringify(name)}`];
|
|
257
|
+
if (meta.queue !== void 0)
|
|
258
|
+
fields.push(`queue: ${JSON.stringify(meta.queue)}`);
|
|
259
|
+
if (meta.jobType !== void 0)
|
|
260
|
+
fields.push(`jobType: ${JSON.stringify(meta.jobType)}`);
|
|
261
|
+
if (meta.maxAttempts !== void 0)
|
|
262
|
+
fields.push(`maxAttempts: ${meta.maxAttempts}`);
|
|
263
|
+
if (meta.tries !== void 0)
|
|
264
|
+
fields.push(`tries: ${meta.tries}`);
|
|
265
|
+
if (meta.unique !== void 0)
|
|
266
|
+
fields.push(`unique: ${meta.unique}`);
|
|
267
|
+
if (meta.hasInput)
|
|
268
|
+
fields.push(`hasInput: true`);
|
|
269
|
+
if (meta.hasUniqueId)
|
|
270
|
+
fields.push(`hasUniqueId: true`);
|
|
271
|
+
fields.push(`load: () => import(${JSON.stringify(importPath)}).then(m => m.default)`);
|
|
272
|
+
return ` { ${fields.join(", ")} },`;
|
|
273
|
+
}));
|
|
158
274
|
return [
|
|
159
275
|
"/* This file is generated by nuxt-cf-jobs. Do not edit directly. */",
|
|
160
276
|
`import { useRuntimeConfig } from 'nitropack/runtime'`,
|
|
161
277
|
`import { createGeneratedCfJobsApp } from 'nuxt-cf-jobs/server'`,
|
|
162
|
-
...imports,
|
|
163
278
|
"",
|
|
164
|
-
|
|
279
|
+
"export const jobs = [",
|
|
280
|
+
...entryLines,
|
|
281
|
+
"]",
|
|
165
282
|
`export const app = createGeneratedCfJobsApp(jobs, useRuntimeConfig, ${options.defaultQueue ? JSON.stringify(options.defaultQueue) : "undefined"})`,
|
|
166
283
|
"",
|
|
167
284
|
"export const {",
|
|
@@ -174,29 +291,26 @@ async function generateRegistryTypesTemplate(options, rootDir, templateDir) {
|
|
|
174
291
|
const files = await resolveJobFiles(options, rootDir);
|
|
175
292
|
assertUniqueGeneratedJobNames(files, options, rootDir);
|
|
176
293
|
const jobTypeLines = files.map((file) => {
|
|
177
|
-
return `
|
|
294
|
+
return ` typeof import(${JSON.stringify(toImportPath(templateDir, file).replace(/\.ts$/, ""))})['default'],`;
|
|
178
295
|
});
|
|
179
296
|
return [
|
|
180
297
|
"/* This file is generated by nuxt-cf-jobs. Do not edit directly. */",
|
|
181
|
-
`import type { JobMessageByName, JobMessageByQueue, JobNameOf,
|
|
182
|
-
`export type { QueueConsumerOptions }`,
|
|
298
|
+
`import type { JobMessageByName, JobMessageByQueue, JobNameOf, JobPayloadOf, JobQueueByName, QueueNameOf } from 'nuxt-cf-jobs/server'`,
|
|
183
299
|
"",
|
|
184
|
-
|
|
300
|
+
`declare module '#cf-jobs/app' {`,
|
|
301
|
+
` export type { QueueConsumerOptions } from 'nuxt-cf-jobs/server'`,
|
|
302
|
+
" type Jobs = readonly [",
|
|
185
303
|
...jobTypeLines,
|
|
186
|
-
"]",
|
|
187
|
-
"
|
|
188
|
-
"",
|
|
189
|
-
"export type
|
|
190
|
-
"export type
|
|
191
|
-
"export type JobName =
|
|
192
|
-
"export type
|
|
193
|
-
"export type
|
|
194
|
-
"export type
|
|
195
|
-
"
|
|
196
|
-
"export type JobMessage<Name extends JobName> = JobMessageByName<Jobs, Name>",
|
|
197
|
-
"export type QueueMessage<Queue extends QueueName> = JobMessageByQueue<Jobs, Queue>",
|
|
198
|
-
"",
|
|
199
|
-
...cfJobsAppExportNames.map((name) => `export declare const ${name}: typeof app.${name}`),
|
|
304
|
+
" ]",
|
|
305
|
+
" type JobsByName = { readonly [Job in Jobs[number] as Job['name']]: Job }",
|
|
306
|
+
" export type JobName = keyof JobsByName & JobNameOf<Jobs>",
|
|
307
|
+
" export type JobDefinitionOf<Name extends JobName> = JobsByName[Name]",
|
|
308
|
+
" export type QueueName = QueueNameOf<Jobs>",
|
|
309
|
+
" export type JobPayload<Name extends JobName> = JobPayloadOf<JobDefinitionOf<Name>>",
|
|
310
|
+
" export type JobQueue<Name extends JobName> = JobQueueByName<Jobs, Name>",
|
|
311
|
+
" export type JobMessage<Name extends JobName> = JobMessageByName<Jobs, Name>",
|
|
312
|
+
" export type QueueMessage<Queue extends QueueName> = JobMessageByQueue<Jobs, Queue>",
|
|
313
|
+
"}",
|
|
200
314
|
""
|
|
201
315
|
].join("\n");
|
|
202
316
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PrepareRegisteredDurableJobOptions } from './outbox.js';
|
|
2
2
|
import type { QueueSource, RegisterRegisteredQueueConsumerOptions } from './queue.js';
|
|
3
|
-
import type { AnyJobDefinition, JobNameOf, JobPayloadByName } from './registry.js';
|
|
3
|
+
import type { AnyJobDefinition, JobNameOf, JobPayloadByName, LazyJobEntry } from './registry.js';
|
|
4
4
|
import type { QueueBindingsConfig } from './types.js';
|
|
5
5
|
import { createJobQueue } from './queue.js';
|
|
6
6
|
export interface CfJobsRuntimeConfig {
|
|
@@ -30,8 +30,8 @@ export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefini
|
|
|
30
30
|
jobs: Jobs;
|
|
31
31
|
jobRegistry: {
|
|
32
32
|
jobs: Jobs;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
getHandler: (name: string) => import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | Promise<import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | undefined> | undefined;
|
|
34
|
+
loadJobDefinition: (name: string) => Promise<AnyJobDefinition | undefined>;
|
|
35
35
|
getJobDefinition: {
|
|
36
36
|
<Name extends JobNameOf<Jobs>>(name: Name): Extract<Jobs[number], {
|
|
37
37
|
name: Name;
|
|
@@ -54,7 +54,8 @@ export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefini
|
|
|
54
54
|
extra: string[];
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
|
-
getHandler: (name: string) => import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | undefined;
|
|
57
|
+
getHandler: (name: string) => import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | Promise<import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | undefined> | undefined;
|
|
58
|
+
loadJobDefinition: (name: string) => Promise<AnyJobDefinition | undefined>;
|
|
58
59
|
getJobDefinition: {
|
|
59
60
|
<Name extends JobNameOf<Jobs>>(name: Name): Extract<Jobs[number], {
|
|
60
61
|
name: Name;
|
|
@@ -95,25 +96,25 @@ export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefini
|
|
|
95
96
|
* `any` param + cast) to the strictly-typed injectable. Keeping this here means
|
|
96
97
|
* the template emits only dynamic data, and the cast is type-checked in source.
|
|
97
98
|
*/
|
|
98
|
-
export declare function createGeneratedCfJobsApp<const Jobs extends readonly
|
|
99
|
-
jobs:
|
|
99
|
+
export declare function createGeneratedCfJobsApp<const Jobs extends readonly LazyJobEntry[]>(jobs: Jobs, useRuntimeConfig: (...args: any[]) => any, defaultQueue?: string): {
|
|
100
|
+
jobs: readonly AnyJobDefinition[];
|
|
100
101
|
jobRegistry: {
|
|
101
|
-
jobs:
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
jobs: readonly AnyJobDefinition[];
|
|
103
|
+
getHandler: (name: string) => import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | Promise<import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | undefined> | undefined;
|
|
104
|
+
loadJobDefinition: (name: string) => Promise<AnyJobDefinition | undefined>;
|
|
104
105
|
getJobDefinition: {
|
|
105
|
-
<Name extends
|
|
106
|
+
<Name extends string>(name: Name): Extract<AnyJobDefinition, {
|
|
106
107
|
name: Name;
|
|
107
108
|
}> | undefined;
|
|
108
109
|
(name: string): AnyJobDefinition | undefined;
|
|
109
110
|
};
|
|
110
111
|
getJobQueue: {
|
|
111
|
-
<Name extends
|
|
112
|
+
<Name extends string>(name: Name): import("./registry.js").JobQueueByName<readonly AnyJobDefinition[], Name> | undefined;
|
|
112
113
|
(name: string): string | undefined;
|
|
113
114
|
};
|
|
114
|
-
buildPayload<Name extends
|
|
115
|
+
buildPayload<Name extends string>(name: Name, payload: JobPayloadByName<readonly AnyJobDefinition[], Name>): {
|
|
115
116
|
_task: Name;
|
|
116
|
-
} & JobPayloadByName<
|
|
117
|
+
} & JobPayloadByName<readonly AnyJobDefinition[], Name>;
|
|
117
118
|
getJobRoute(name: string): {
|
|
118
119
|
queue: string;
|
|
119
120
|
jobType: string;
|
|
@@ -123,15 +124,16 @@ export declare function createGeneratedCfJobsApp<const Jobs extends readonly Any
|
|
|
123
124
|
extra: string[];
|
|
124
125
|
};
|
|
125
126
|
};
|
|
126
|
-
getHandler: (name: string) => import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | undefined;
|
|
127
|
+
getHandler: (name: string) => import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | Promise<import("./types.js").JobHandler<unknown, unknown, unknown, unknown> | undefined> | undefined;
|
|
128
|
+
loadJobDefinition: (name: string) => Promise<AnyJobDefinition | undefined>;
|
|
127
129
|
getJobDefinition: {
|
|
128
|
-
<Name extends
|
|
130
|
+
<Name extends string>(name: Name): Extract<AnyJobDefinition, {
|
|
129
131
|
name: Name;
|
|
130
132
|
}> | undefined;
|
|
131
133
|
(name: string): AnyJobDefinition | undefined;
|
|
132
134
|
};
|
|
133
135
|
getJobQueue: {
|
|
134
|
-
<Name extends
|
|
136
|
+
<Name extends string>(name: Name): import("./registry.js").JobQueueByName<readonly AnyJobDefinition[], Name> | undefined;
|
|
135
137
|
(name: string): string | undefined;
|
|
136
138
|
};
|
|
137
139
|
getJobRoute: (name: string) => {
|
|
@@ -145,13 +147,13 @@ export declare function createGeneratedCfJobsApp<const Jobs extends readonly Any
|
|
|
145
147
|
validateQueueBindings: (queues?: QueueBindingsConfig) => import("./queue.js").QueueBindingValidationIssue[];
|
|
146
148
|
assertQueueBindings: (queues?: QueueBindingsConfig) => void;
|
|
147
149
|
getQueue: {
|
|
148
|
-
<const Job extends
|
|
149
|
-
<const Job extends
|
|
150
|
+
<const Job extends AnyJobDefinition>(job: Job): import("./queue.js").JobQueuePublisher<Job>;
|
|
151
|
+
<const Job extends AnyJobDefinition>(source: QueueSource | undefined, job: Job): import("./queue.js").JobQueuePublisher<Job>;
|
|
150
152
|
};
|
|
151
|
-
buildJobPayload: <Name extends
|
|
153
|
+
buildJobPayload: <Name extends string>(name: Name, payload: JobPayloadByName<readonly AnyJobDefinition[], Name>) => {
|
|
152
154
|
_task: Name;
|
|
153
|
-
} & JobPayloadByName<
|
|
154
|
-
prepareJob: <Name extends
|
|
155
|
+
} & JobPayloadByName<readonly AnyJobDefinition[], Name>;
|
|
156
|
+
prepareJob: <Name extends string>(opts: PrepareRegisteredDurableJobOptions<readonly AnyJobDefinition[], Name>) => Promise<import("./outbox.js").DurableJobRecord<import("./registry.js").JobQueueByName<readonly AnyJobDefinition[], Name>>>;
|
|
155
157
|
registerQueueConsumer: <Env extends Record<string, unknown>, Db, Logger>(nitroApp: {
|
|
156
158
|
hooks: {
|
|
157
159
|
hook: (name: any, handler: any) => void;
|
|
@@ -161,12 +163,11 @@ export declare function createGeneratedCfJobsApp<const Jobs extends readonly Any
|
|
|
161
163
|
export type CfJobsApp<Jobs extends readonly AnyJobDefinition[]> = ReturnType<typeof createCfJobsApp<Jobs>>;
|
|
162
164
|
/**
|
|
163
165
|
* Authoritative list of `createCfJobsApp` members re-exported by the generated
|
|
164
|
-
* `#cf-jobs/app` module.
|
|
165
|
-
*
|
|
166
|
-
* `.d.ts` declarations, and the app's return shape can't drift apart.
|
|
166
|
+
* `#cf-jobs/app` module. `generateRegistryTemplate` maps over this so the runtime
|
|
167
|
+
* destructure and the app's return shape can't drift apart.
|
|
167
168
|
*
|
|
168
169
|
* `jobs` is exported separately by the template (as a `const` tuple), so it is
|
|
169
170
|
* intentionally absent here.
|
|
170
171
|
*/
|
|
171
|
-
export declare const cfJobsAppExportNames: readonly ["jobRegistry", "getHandler", "getJobDefinition", "getJobQueue", "getJobRoute", "validateRegistry", "validateQueueBindings", "assertQueueBindings", "getQueue", "buildJobPayload", "prepareJob", "registerQueueConsumer"];
|
|
172
|
+
export declare const cfJobsAppExportNames: readonly ["jobRegistry", "getHandler", "loadJobDefinition", "getJobDefinition", "getJobQueue", "getJobRoute", "validateRegistry", "validateQueueBindings", "assertQueueBindings", "getQueue", "buildJobPayload", "prepareJob", "registerQueueConsumer"];
|
|
172
173
|
export type QueueConsumerOptions<Env extends Record<string, unknown>, Db, Logger> = CfJobsQueueConsumerOptions<Env, Db, Logger>;
|
|
@@ -69,6 +69,7 @@ ${issues.map((i) => ` - ${i}`).join("\n")}`);
|
|
|
69
69
|
jobs: materialized,
|
|
70
70
|
jobRegistry,
|
|
71
71
|
getHandler: jobRegistry.getHandler,
|
|
72
|
+
loadJobDefinition: jobRegistry.loadJobDefinition,
|
|
72
73
|
getJobDefinition: jobRegistry.getJobDefinition,
|
|
73
74
|
getJobQueue: jobRegistry.getJobQueue,
|
|
74
75
|
getJobRoute: jobRegistry.getJobRoute,
|
|
@@ -87,6 +88,7 @@ export function createGeneratedCfJobsApp(jobs, useRuntimeConfig, defaultQueue) {
|
|
|
87
88
|
export const cfJobsAppExportNames = [
|
|
88
89
|
"jobRegistry",
|
|
89
90
|
"getHandler",
|
|
91
|
+
"loadJobDefinition",
|
|
90
92
|
"getJobDefinition",
|
|
91
93
|
"getJobQueue",
|
|
92
94
|
"getJobRoute",
|
|
@@ -99,5 +101,7 @@ export const cfJobsAppExportNames = [
|
|
|
99
101
|
"registerQueueConsumer"
|
|
100
102
|
];
|
|
101
103
|
function isJobDefinition(value) {
|
|
102
|
-
|
|
104
|
+
if (!value || typeof value !== "object" || typeof value.name !== "string")
|
|
105
|
+
return false;
|
|
106
|
+
return typeof value.handle === "function" || typeof value.load === "function";
|
|
103
107
|
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import type { DispatchableJob, DispatchResult, JobContext, JobControlResult, JobDefinition, JobHandler, JobMiddleware } from './types.js';
|
|
2
2
|
export interface JobRegistryLike<Env, Db, Logger> {
|
|
3
|
-
|
|
3
|
+
/** May resolve asynchronously for lazily-loaded jobs; callers must await. */
|
|
4
|
+
getHandler: (name: string) => JobHandler<unknown, Env, Db, Logger> | undefined | Promise<JobHandler<unknown, Env, Db, Logger> | undefined>;
|
|
5
|
+
/**
|
|
6
|
+
* Returns the definition for routing/validation. For lazy entries this is
|
|
7
|
+
* static routing metadata typed as a full definition (its `handle`/`input` are
|
|
8
|
+
* absent at runtime); dispatch prefers `loadJobDefinition` for the real module.
|
|
9
|
+
*/
|
|
4
10
|
getJobDefinition?: (name: string) => JobDefinition<string, unknown, string, Env, Db, Logger> | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Loads the full definition (handler, `input`, `middleware`, `failed`) for a
|
|
13
|
+
* lazy entry. Preferred at dispatch so payload validation + failure hooks see
|
|
14
|
+
* the real module. Falls back to `getJobDefinition`/`getHandler` when absent.
|
|
15
|
+
*/
|
|
16
|
+
loadJobDefinition?: (name: string) => Promise<JobDefinition<string, unknown, string, Env, Db, Logger> | undefined>;
|
|
5
17
|
}
|
|
6
18
|
export interface DispatchContextInput<Job extends DispatchableJob> {
|
|
7
19
|
job: Job;
|
|
@@ -5,8 +5,8 @@ export async function dispatchRegisteredJob(opts) {
|
|
|
5
5
|
if (typeof taskName !== "string" || taskName.length === 0) {
|
|
6
6
|
return { success: false, error: "No _task in payload", handlerNotFound: true };
|
|
7
7
|
}
|
|
8
|
-
const definition = opts.registry.getJobDefinition?.(taskName);
|
|
9
|
-
const handler = definition?.handle ?? opts.registry.getHandler(taskName);
|
|
8
|
+
const definition = opts.registry.loadJobDefinition ? await opts.registry.loadJobDefinition(taskName) : opts.registry.getJobDefinition?.(taskName);
|
|
9
|
+
const handler = definition?.handle ?? await opts.registry.getHandler(taskName);
|
|
10
10
|
if (!handler) {
|
|
11
11
|
return { success: false, error: `No handler for task: ${taskName}`, handlerNotFound: true };
|
|
12
12
|
}
|
|
@@ -75,7 +75,8 @@ export interface DurableJobFailureRepository {
|
|
|
75
75
|
recordFailure: (input: RecordDurableJobFailureInput) => Promise<void>;
|
|
76
76
|
}
|
|
77
77
|
export interface DurableJobRegistryLike<Env = unknown, Db = unknown, Logger = unknown> {
|
|
78
|
-
|
|
78
|
+
/** May resolve asynchronously for lazily-loaded jobs. Unused on the producer path. */
|
|
79
|
+
getHandler?: (name: string) => JobHandler<unknown, Env, Db, Logger> | undefined | Promise<JobHandler<unknown, Env, Db, Logger> | undefined>;
|
|
79
80
|
getJobDefinition?: (name: string) => JobDefinition<string, unknown, string, Env, Db, Logger> | undefined;
|
|
80
81
|
getJobRoute?: (name: string) => DurableJobRoute<string> | undefined;
|
|
81
82
|
}
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import type { JobBackoff, JobDefinition, JobFailedHandler, JobHandler, JobMiddleware, JobPayloadSchema } from './types.js';
|
|
2
2
|
export type JobPayloadMap = Record<string, unknown>;
|
|
3
3
|
export type AnyJobDefinition = JobDefinition<string, any, string, any, any, any>;
|
|
4
|
+
/**
|
|
5
|
+
* A build-time registry entry that defers loading the handler module. The
|
|
6
|
+
* static routing fields are AST-extracted from the job's `defineJob({...})`
|
|
7
|
+
* call so the producer/consumer can route, validate queues and resolve attempts
|
|
8
|
+
* WITHOUT importing (and evaluating) the handler — `load()` pulls the full
|
|
9
|
+
* definition only when a handler/`input`/`failed` is actually needed (dispatch,
|
|
10
|
+
* or a producer of a job that declares `input`/`unique`). This is what keeps a
|
|
11
|
+
* worker from evaluating all job modules to run one job.
|
|
12
|
+
*/
|
|
13
|
+
export interface LazyJobEntry<Name extends string = string, Queue extends string = string> {
|
|
14
|
+
name: Name;
|
|
15
|
+
queue?: Queue;
|
|
16
|
+
jobType?: string;
|
|
17
|
+
maxAttempts?: number;
|
|
18
|
+
tries?: number;
|
|
19
|
+
unique?: boolean;
|
|
20
|
+
/** Whether the source `defineJob` declares an `input` schema (AST flag). */
|
|
21
|
+
hasInput?: boolean;
|
|
22
|
+
/** Whether the source `defineJob` declares a `uniqueId` fn (AST flag). */
|
|
23
|
+
hasUniqueId?: boolean;
|
|
24
|
+
load: () => Promise<AnyJobDefinition>;
|
|
25
|
+
}
|
|
26
|
+
/** Either an eagerly-constructed definition or a lazily-loaded entry. */
|
|
27
|
+
export type RegistryEntry = AnyJobDefinition | LazyJobEntry;
|
|
28
|
+
export declare function isLazyJobEntry(entry: RegistryEntry): entry is LazyJobEntry;
|
|
29
|
+
/** Static routing metadata shared by eager defs and lazy entries. */
|
|
30
|
+
export type JobStaticDefinition = Pick<AnyJobDefinition, 'name' | 'queue' | 'jobType' | 'maxAttempts' | 'tries' | 'unique'>;
|
|
4
31
|
export type JobPayloadOf<Job extends AnyJobDefinition> = Job extends JobDefinition<string, infer Payload, string, any, any, any> ? Payload extends object ? Payload : never : never;
|
|
5
32
|
export type JobMessageOf<Job extends AnyJobDefinition> = Job extends JobDefinition<infer Name, any, string, any, any, any> ? {
|
|
6
33
|
_task: Name;
|
|
@@ -49,8 +76,8 @@ export declare function validateJobDefinitions(jobs: readonly unknown[]): JobDef
|
|
|
49
76
|
export declare function assertJobDefinitions(jobs: readonly unknown[]): void;
|
|
50
77
|
export declare function defineJobRegistry<const Jobs extends readonly AnyJobDefinition[]>(jobs: Jobs): {
|
|
51
78
|
jobs: Jobs;
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
getHandler: (name: string) => JobHandler<unknown, unknown, unknown, unknown> | Promise<JobHandler<unknown, unknown, unknown, unknown> | undefined> | undefined;
|
|
80
|
+
loadJobDefinition: (name: string) => Promise<AnyJobDefinition | undefined>;
|
|
54
81
|
getJobDefinition: {
|
|
55
82
|
<Name extends JobNameOf<Jobs>>(name: Name): JobDefinitionByName<Jobs, Name> | undefined;
|
|
56
83
|
(name: string): AnyJobDefinition | undefined;
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { buildJobPayload } from "./payload.js";
|
|
2
|
+
export function isLazyJobEntry(entry) {
|
|
3
|
+
return typeof entry.load === "function" && typeof entry.handle !== "function";
|
|
4
|
+
}
|
|
5
|
+
function toStaticDefinition(entry) {
|
|
6
|
+
return {
|
|
7
|
+
name: entry.name,
|
|
8
|
+
queue: entry.queue,
|
|
9
|
+
jobType: entry.jobType,
|
|
10
|
+
maxAttempts: entry.maxAttempts,
|
|
11
|
+
tries: entry.tries,
|
|
12
|
+
unique: entry.unique
|
|
13
|
+
};
|
|
14
|
+
}
|
|
2
15
|
export function defineJob(opts) {
|
|
3
16
|
return opts;
|
|
4
17
|
}
|
|
@@ -23,7 +36,7 @@ export function validateJobDefinitions(jobs) {
|
|
|
23
36
|
}
|
|
24
37
|
const definition = job;
|
|
25
38
|
const name = typeof definition.name === "string" && definition.name.length > 0 ? definition.name : "<unknown>";
|
|
26
|
-
if (typeof definition.name !== "string" || definition.name.length === 0 || typeof definition.handle !== "function") {
|
|
39
|
+
if (typeof definition.name !== "string" || definition.name.length === 0 || typeof definition.handle !== "function" && typeof definition.load !== "function") {
|
|
27
40
|
issues.push({ name, reason: "invalid-definition" });
|
|
28
41
|
}
|
|
29
42
|
if (typeof definition.queue !== "string" || definition.queue.length === 0)
|
|
@@ -43,35 +56,58 @@ export function assertJobDefinitions(jobs) {
|
|
|
43
56
|
}
|
|
44
57
|
export function defineJobRegistry(jobs) {
|
|
45
58
|
assertJobDefinitions(jobs);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
);
|
|
59
|
+
const entries = jobs;
|
|
60
|
+
const byName = new Map(entries.map((job) => [job.name, job]));
|
|
61
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
62
|
+
function loadJobDefinition(name) {
|
|
63
|
+
const entry = byName.get(name);
|
|
64
|
+
if (!entry)
|
|
65
|
+
return Promise.resolve(void 0);
|
|
66
|
+
if (!isLazyJobEntry(entry))
|
|
67
|
+
return Promise.resolve(entry);
|
|
68
|
+
let pending = loaded.get(name);
|
|
69
|
+
if (!pending) {
|
|
70
|
+
pending = entry.load();
|
|
71
|
+
loaded.set(name, pending);
|
|
72
|
+
}
|
|
73
|
+
return pending;
|
|
74
|
+
}
|
|
49
75
|
function getJobDefinition(name) {
|
|
50
|
-
|
|
76
|
+
const entry = byName.get(name);
|
|
77
|
+
if (!entry)
|
|
78
|
+
return void 0;
|
|
79
|
+
return isLazyJobEntry(entry) ? toStaticDefinition(entry) : entry;
|
|
51
80
|
}
|
|
52
81
|
function getJobQueue(name) {
|
|
53
|
-
return
|
|
82
|
+
return byName.get(name)?.queue;
|
|
83
|
+
}
|
|
84
|
+
function getHandler(name) {
|
|
85
|
+
const entry = byName.get(name);
|
|
86
|
+
if (!entry)
|
|
87
|
+
return void 0;
|
|
88
|
+
if (!isLazyJobEntry(entry))
|
|
89
|
+
return entry.handle;
|
|
90
|
+
return loadJobDefinition(name).then((def) => def?.handle);
|
|
54
91
|
}
|
|
55
92
|
return {
|
|
56
93
|
jobs,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return handlers.get(name);
|
|
60
|
-
},
|
|
94
|
+
getHandler,
|
|
95
|
+
loadJobDefinition,
|
|
61
96
|
getJobDefinition,
|
|
62
97
|
getJobQueue,
|
|
63
98
|
buildPayload(name, payload) {
|
|
64
|
-
const
|
|
65
|
-
if (!
|
|
99
|
+
const entry = byName.get(name);
|
|
100
|
+
if (!entry)
|
|
66
101
|
throw new Error(`Unknown task: ${name}`);
|
|
102
|
+
const definition = isLazyJobEntry(entry) ? toStaticDefinition(entry) : entry;
|
|
67
103
|
return buildJobMessage(definition, payload);
|
|
68
104
|
},
|
|
69
105
|
getJobRoute(name) {
|
|
70
|
-
const
|
|
71
|
-
return
|
|
106
|
+
const entry = byName.get(name);
|
|
107
|
+
return entry ? { queue: entry.queue, jobType: entry.jobType ?? entry.name } : void 0;
|
|
72
108
|
},
|
|
73
109
|
validate(expectedTasks) {
|
|
74
|
-
const registered = new Set(
|
|
110
|
+
const registered = new Set(byName.keys());
|
|
75
111
|
const expected = new Set(expectedTasks);
|
|
76
112
|
return {
|
|
77
113
|
missing: expectedTasks.filter((task) => !registered.has(task)),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-cf-jobs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Nuxt module for typed Cloudflare queue jobs.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -61,8 +61,9 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@nuxt/kit": "^4.4.6",
|
|
63
63
|
"@nuxt/schema": "^4.4.6",
|
|
64
|
-
"citty": "^0.
|
|
65
|
-
"drizzle-orm": "1.0.0-rc.3"
|
|
64
|
+
"citty": "^0.2.2",
|
|
65
|
+
"drizzle-orm": "1.0.0-rc.3",
|
|
66
|
+
"magicast": "^0.5.3"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@antfu/eslint-config": "latest",
|