nuxt-cf-jobs 0.0.2 → 0.1.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/dist/module.d.mts CHANGED
@@ -69,5 +69,8 @@ declare module '@nuxt/schema' {
69
69
  }
70
70
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
71
71
 
72
- export { _default as default };
72
+ declare function generateRegistryTemplate(options: ModuleOptions, rootDir: string, templateDir: string): Promise<string>;
73
+ declare function generateRegistryTypesTemplate(options: ModuleOptions, rootDir: string, templateDir: string): Promise<string>;
74
+
75
+ export { _default as default, generateRegistryTemplate, generateRegistryTypesTemplate };
73
76
  export type { ModuleOptions };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-cf-jobs",
3
3
  "configKey": "cfJobs",
4
- "version": "0.0.2",
4
+ "version": "0.1.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -174,6 +174,11 @@ const module$1 = defineNuxtModule({
174
174
  write: true,
175
175
  getContents: async () => generateRegistryTemplate(options, nuxt.options.rootDir, resolve(nuxt.options.buildDir, "cf-jobs"))
176
176
  });
177
+ addTemplate({
178
+ filename: "cf-jobs/registry.d.ts",
179
+ write: true,
180
+ getContents: async () => generateRegistryTypesTemplate(options, nuxt.options.rootDir, resolve(nuxt.options.buildDir, "cf-jobs"))
181
+ });
177
182
  nuxt.options.alias[options.registryAlias ?? "#cf-jobs/app"] = registryTemplate.dst;
178
183
  nuxt.hooks.hook("builder:watch", (async (_event, path) => {
179
184
  if (!isWatchedJobPath(path, options, nuxt.options.rootDir))
@@ -283,38 +288,21 @@ async function generateRegistryTemplate(options, rootDir, templateDir) {
283
288
  const files = await resolveJobFiles(options, rootDir);
284
289
  assertUniqueGeneratedJobNames(files, options, rootDir);
285
290
  const imports = files.map((file, index) => {
286
- return `import job${index} from ${JSON.stringify(toImportPath(templateDir, file))}`;
287
- });
288
- const loaders = files.map((file) => {
289
- return ` ${JSON.stringify(toJobName(file, options, rootDir))}: () => import(${JSON.stringify(toImportPath(templateDir, file))}).then(m => m.default),`;
291
+ return `import job${index} from ${JSON.stringify(toImportPath(templateDir, file).replace(/\.ts$/, ""))}`;
290
292
  });
291
293
  const jobItems = files.map((_, index) => `job${index}`).join(", ");
292
294
  return [
293
295
  "/* This file is generated by nuxt-cf-jobs. Do not edit directly. */",
294
- `import { createCfJobsApp } from '#cf-jobs/server'`,
295
- `import { useRuntimeConfig } from '#imports'`,
296
- `import type { JobDefinitionsByNameOfLoaders, JobMessageByName, JobMessageByQueue, JobNameOf, JobPayloadByName, JobPayloadOf, JobQueueByName, QueueNameOf } from '#cf-jobs/server'`,
297
- `export type { QueueConsumerOptions } from '#cf-jobs/server'`,
296
+ `// @ts-expect-error - nitropack/runtime is resolved at build time inside Nuxt`,
297
+ `import { useRuntimeConfig } from 'nitropack/runtime'`,
298
+ `import { createCfJobsApp } from 'nuxt-cf-jobs/server'`,
298
299
  ...imports,
299
300
  "",
300
- "export const jobLoaders = {",
301
- ...loaders,
302
- "} as const",
303
301
  `export const jobs = [${jobItems}] as const`,
304
- `export const app = createCfJobsApp(jobs, useRuntimeConfig as never, { defaultQueue: ${options.defaultQueue ? JSON.stringify(options.defaultQueue) : "undefined"} })`,
305
- "export const jobRegistry = app.jobRegistry",
306
- "",
307
- "export type Jobs = typeof jobs",
308
- "export type JobsByName = JobDefinitionsByNameOfLoaders<typeof jobLoaders>",
309
- "export type JobName = keyof JobsByName & JobNameOf<Jobs>",
310
- "export type JobDefinitionOf<Name extends JobName> = JobsByName[Name]",
311
- "export type QueueName = QueueNameOf<Jobs>",
312
- "export type JobPayload<Name extends JobName> = JobPayloadOf<JobDefinitionOf<Name>>",
313
- "export type JobQueue<Name extends JobName> = JobQueueByName<Jobs, Name>",
314
- "export type JobMessage<Name extends JobName> = JobMessageByName<Jobs, Name>",
315
- "export type QueueMessage<Queue extends QueueName> = JobMessageByQueue<Jobs, Queue>",
302
+ `export const app = createCfJobsApp(jobs, { useRuntimeConfig, defaultQueue: ${options.defaultQueue ? JSON.stringify(options.defaultQueue) : "undefined"} })`,
316
303
  "",
317
304
  "export const {",
305
+ " jobRegistry,",
318
306
  " getHandler,",
319
307
  " getJobDefinition,",
320
308
  " getJobQueue,",
@@ -330,6 +318,47 @@ async function generateRegistryTemplate(options, rootDir, templateDir) {
330
318
  ""
331
319
  ].join("\n");
332
320
  }
321
+ async function generateRegistryTypesTemplate(options, rootDir, templateDir) {
322
+ const files = await resolveJobFiles(options, rootDir);
323
+ assertUniqueGeneratedJobNames(files, options, rootDir);
324
+ const jobTypeLines = files.map((file) => {
325
+ return ` typeof import(${JSON.stringify(toImportPath(templateDir, file).replace(/\.ts$/, ""))})['default'],`;
326
+ });
327
+ return [
328
+ "/* This file is generated by nuxt-cf-jobs. Do not edit directly. */",
329
+ `import type { JobMessageByName, JobMessageByQueue, JobNameOf, JobPayloadByName, JobPayloadOf, JobQueueByName, QueueNameOf, QueueConsumerOptions } from 'nuxt-cf-jobs/server'`,
330
+ `export type { QueueConsumerOptions }`,
331
+ "",
332
+ "export declare const jobs: readonly [",
333
+ ...jobTypeLines,
334
+ "]",
335
+ "export declare const app: ReturnType<typeof import('nuxt-cf-jobs/server').createCfJobsApp<typeof jobs>>",
336
+ "",
337
+ "export type Jobs = typeof jobs",
338
+ "export type JobsByName = { readonly [Job in Jobs[number] as Job['name']]: Job }",
339
+ "export type JobName = keyof JobsByName & JobNameOf<Jobs>",
340
+ "export type JobDefinitionOf<Name extends JobName> = JobsByName[Name]",
341
+ "export type QueueName = QueueNameOf<Jobs>",
342
+ "export type JobPayload<Name extends JobName> = JobPayloadOf<JobDefinitionOf<Name>>",
343
+ "export type JobQueue<Name extends JobName> = JobQueueByName<Jobs, Name>",
344
+ "export type JobMessage<Name extends JobName> = JobMessageByName<Jobs, Name>",
345
+ "export type QueueMessage<Queue extends QueueName> = JobMessageByQueue<Jobs, Queue>",
346
+ "",
347
+ "export declare const jobRegistry: typeof app.jobRegistry",
348
+ "export declare const getHandler: typeof app.getHandler",
349
+ "export declare const getJobDefinition: typeof app.getJobDefinition",
350
+ "export declare const getJobQueue: typeof app.getJobQueue",
351
+ "export declare const getJobRoute: typeof app.getJobRoute",
352
+ "export declare const validateRegistry: typeof app.validateRegistry",
353
+ "export declare const validateQueueBindings: typeof app.validateQueueBindings",
354
+ "export declare const assertQueueBindings: typeof app.assertQueueBindings",
355
+ "export declare const getQueue: typeof app.getQueue",
356
+ "export declare const buildJobPayload: typeof app.buildJobPayload",
357
+ "export declare const prepareJob: typeof app.prepareJob",
358
+ "export declare const registerQueueConsumer: typeof app.registerQueueConsumer",
359
+ ""
360
+ ].join("\n");
361
+ }
333
362
  async function resolveJobFiles(options, rootDir) {
334
363
  const dirs = toArray(options.jobsDir ?? "server/jobs");
335
364
  const pattern = options.jobsPattern ?? "**/*.ts";
@@ -365,7 +394,7 @@ function toArray(value) {
365
394
  return Array.isArray(value) ? value : [value];
366
395
  }
367
396
  function toImportPath(fromDir, file) {
368
- const path = relative(fromDir, file).replace(/\\/g, "/").replace(/\.[cm]?tsx?$/, "");
397
+ const path = relative(fromDir, file).replace(/\\/g, "/");
369
398
  return path.startsWith(".") ? path : `./${path}`;
370
399
  }
371
400
  function toJobName(file, options, rootDir) {
@@ -377,4 +406,4 @@ function toJobName(file, options, rootDir) {
377
406
  return path.replace(/\/index$/, "");
378
407
  }
379
408
 
380
- export { module$1 as default };
409
+ export { module$1 as default, generateRegistryTemplate, generateRegistryTypesTemplate };
@@ -1,4 +1,4 @@
1
- import type { AnyJobDefinition, JobNameOf, JobPayloadByName, JobQueueByName } from './registry.js';
1
+ import type { AnyJobDefinition, JobNameOf, JobPayloadByName } from './registry.js';
2
2
  import type { PrepareRegisteredDurableJobOptions } from './outbox.js';
3
3
  import type { QueueSource, RegisterRegisteredQueueConsumerOptions } from './queue.js';
4
4
  import type { QueueBindingsConfig } from './types.js';
@@ -9,11 +9,24 @@ export interface CfJobsRuntimeConfig {
9
9
  };
10
10
  }
11
11
  export type CfJobsQueueConsumerOptions<Env extends Record<string, unknown>, Db, Logger> = Omit<RegisterRegisteredQueueConsumerOptions<Env, Db, Logger>, 'registry' | 'queues'>;
12
+ export type UseRuntimeConfigFn = (event?: unknown) => CfJobsRuntimeConfig;
12
13
  export interface CreateCfJobsAppOptions {
14
+ /** Bundled nitro's `useRuntimeConfig`. Required — tests can pass a stub. */
15
+ useRuntimeConfig: UseRuntimeConfigFn;
13
16
  /** Fallback queue applied to jobs whose `defineJob` omits `queue`. */
14
17
  defaultQueue?: string;
15
18
  }
16
- export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefinition[]>(jobs: Jobs, useRuntimeConfig: (event?: unknown) => CfJobsRuntimeConfig, appOpts?: CreateCfJobsAppOptions): {
19
+ /**
20
+ * Builds the registry + helpers around a statically-known array of jobs. The
21
+ * generated `#cf-jobs/app` template imports each job source file directly and
22
+ * passes the resulting array in — rollup resolves nuxt `#aliases` and
23
+ * extensionless paths inside the bundle.
24
+ *
25
+ * `useRuntimeConfig` is injected so this module never imports `nitropack/runtime`
26
+ * itself; that keeps `app.ts` usable from unit tests / non-nitro consumers and
27
+ * avoids `#nitro-internal-virtual/*` pulling into anything that isn't bundled.
28
+ */
29
+ export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefinition[]>(jobs: Jobs, { useRuntimeConfig, defaultQueue }: CreateCfJobsAppOptions): {
17
30
  jobs: Jobs;
18
31
  jobRegistry: {
19
32
  jobs: Jobs;
@@ -26,7 +39,7 @@ export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefini
26
39
  (name: string): AnyJobDefinition | undefined;
27
40
  };
28
41
  getJobQueue: {
29
- <Name extends JobNameOf<Jobs>>(name: Name): JobQueueByName<Jobs, Name> | undefined;
42
+ <Name extends JobNameOf<Jobs>>(name: Name): import("./registry.js").JobQueueByName<Jobs, Name> | undefined;
30
43
  (name: string): string | undefined;
31
44
  };
32
45
  buildPayload<Name extends JobNameOf<Jobs>>(name: Name, payload: JobPayloadByName<Jobs, Name>): {
@@ -49,7 +62,7 @@ export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefini
49
62
  (name: string): AnyJobDefinition | undefined;
50
63
  };
51
64
  getJobQueue: {
52
- <Name extends JobNameOf<Jobs>>(name: Name): JobQueueByName<Jobs, Name> | undefined;
65
+ <Name extends JobNameOf<Jobs>>(name: Name): import("./registry.js").JobQueueByName<Jobs, Name> | undefined;
53
66
  (name: string): string | undefined;
54
67
  };
55
68
  getJobRoute: (name: string) => {
@@ -69,7 +82,7 @@ export declare function createCfJobsApp<const Jobs extends readonly AnyJobDefini
69
82
  buildJobPayload: <Name extends JobNameOf<Jobs>>(name: Name, payload: JobPayloadByName<Jobs, Name>) => {
70
83
  _task: Name;
71
84
  } & JobPayloadByName<Jobs, Name>;
72
- prepareJob: <Name extends JobNameOf<Jobs>>(opts: PrepareRegisteredDurableJobOptions<Jobs, Name>) => Promise<import("./outbox.js").DurableJobRecord<JobQueueByName<Jobs, Name>>>;
85
+ prepareJob: <Name extends JobNameOf<Jobs>>(opts: PrepareRegisteredDurableJobOptions<Jobs, Name>) => Promise<import("./outbox.js").DurableJobRecord<import("./registry.js").JobQueueByName<Jobs, Name>>>;
73
86
  registerQueueConsumer: <Env extends Record<string, unknown>, Db, Logger>(nitroApp: {
74
87
  hooks: {
75
88
  hook: (name: any, handler: any) => void;
@@ -3,16 +3,21 @@ import { prepareRegisteredDurableJob } from "./outbox.js";
3
3
  import {
4
4
  assertJobQueueBindings,
5
5
  createJobQueue,
6
- registerRegisteredQueueConsumer,
6
+ processRegisteredQueueBatch,
7
7
  resolveNitroTaskEnv,
8
8
  validateJobQueueBindings,
9
9
  validateQueueBindingShape,
10
10
  validateQueueConsumerConfig
11
11
  } from "./queue.js";
12
12
  import { validateJobDefinitions } from "./registry.js";
13
- export function createCfJobsApp(jobs, useRuntimeConfig, appOpts = {}) {
14
- const effectiveJobs = appOpts.defaultQueue ? jobs.map((job) => job.queue ? job : { ...job, queue: appOpts.defaultQueue }) : jobs;
15
- const jobRegistry = defineJobRegistry(effectiveJobs);
13
+ export function createCfJobsApp(jobs, { useRuntimeConfig, defaultQueue }) {
14
+ const materialized = defaultQueue ? jobs.map((j) => j.queue ? j : { ...j, queue: defaultQueue }) : jobs.slice();
15
+ const jobRegistry = defineJobRegistry(materialized);
16
+ const jobIssues = validateJobDefinitions(materialized);
17
+ if (jobIssues.length > 0) {
18
+ console.warn(`[nuxt-cf-jobs] job definition warnings:
19
+ ${jobIssues.map((i) => ` - [job:${i.name}] ${i.reason}`).join("\n")}`);
20
+ }
16
21
  function getQueue(sourceOrJob, maybeJob) {
17
22
  const isJobOnly = maybeJob === void 0 && isJobDefinition(sourceOrJob);
18
23
  const job = isJobOnly ? sourceOrJob : maybeJob;
@@ -30,38 +35,38 @@ export function createCfJobsApp(jobs, useRuntimeConfig, appOpts = {}) {
30
35
  function prepareJob(opts) {
31
36
  return prepareRegisteredDurableJob(jobRegistry, opts);
32
37
  }
33
- let startupLogged = false;
34
- function logStartupWarnings(queues) {
35
- if (startupLogged)
36
- return;
37
- startupLogged = true;
38
+ function logQueueWarnings(queues) {
38
39
  const issues = [];
39
- for (const issue of validateJobDefinitions(effectiveJobs))
40
- issues.push(`[job:${issue.name}] ${issue.reason}`);
41
40
  for (const issue of validateQueueBindingShape(queues))
42
41
  issues.push(`[queue:${issue.queue}] ${issue.reason}: ${issue.detail}`);
43
- for (const issue of validateJobQueueBindings(queues, effectiveJobs))
42
+ for (const issue of validateJobQueueBindings(queues, materialized))
44
43
  issues.push(`[job:${issue.jobName}] missing binding for queue "${issue.queue}"`);
45
- for (const issue of validateQueueConsumerConfig(queues, effectiveJobs))
44
+ for (const issue of validateQueueConsumerConfig(queues, materialized))
46
45
  issues.push(`[job:${issue.jobName ?? "?"}@${issue.queue}] ${issue.reason}: ${issue.detail}`);
47
46
  if (issues.length === 0)
48
47
  return;
49
- console.warn(`[nuxt-cf-jobs] configuration warnings:
48
+ console.warn(`[nuxt-cf-jobs] queue config warnings:
50
49
  ${issues.map((i) => ` - ${i}`).join("\n")}`);
51
50
  }
52
51
  function registerQueueConsumer(nitroApp, opts) {
53
- const queues = useRuntimeConfig().cfJobs.queues;
54
- logStartupWarnings(queues);
55
- return registerRegisteredQueueConsumer(nitroApp, {
52
+ const ready = {
56
53
  ...opts,
57
54
  registry: jobRegistry,
58
55
  queues: () => useRuntimeConfig().cfJobs.queues
56
+ };
57
+ let warned = false;
58
+ nitroApp.hooks.hook("cloudflare:queue", async (payload) => {
59
+ if (!warned) {
60
+ warned = true;
61
+ logQueueWarnings(useRuntimeConfig().cfJobs.queues);
62
+ }
63
+ await processRegisteredQueueBatch(payload, ready);
59
64
  });
60
65
  }
61
- const validateQueueBindings = (queues = useRuntimeConfig().cfJobs.queues) => validateJobQueueBindings(queues, effectiveJobs);
62
- const assertQueueBindings = (queues = useRuntimeConfig().cfJobs.queues) => assertJobQueueBindings(queues, effectiveJobs);
66
+ const validateQueueBindings = (queues = useRuntimeConfig().cfJobs.queues) => validateJobQueueBindings(queues, materialized);
67
+ const assertQueueBindings = (queues = useRuntimeConfig().cfJobs.queues) => assertJobQueueBindings(queues, materialized);
63
68
  return {
64
- jobs: effectiveJobs,
69
+ jobs: materialized,
65
70
  jobRegistry,
66
71
  getHandler: jobRegistry.getHandler,
67
72
  getJobDefinition: jobRegistry.getJobDefinition,
@@ -1,12 +1,6 @@
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
- export type JobDefinitionLoader<Job extends AnyJobDefinition = AnyJobDefinition> = () => Promise<Job>;
5
- export type JobDefinitionLoaderMap = Record<string, JobDefinitionLoader>;
6
- export type JobDefinitionOfLoader<Loader extends JobDefinitionLoader> = Awaited<ReturnType<Loader>>;
7
- export type JobDefinitionsByNameOfLoaders<Loaders extends JobDefinitionLoaderMap> = {
8
- readonly [Name in keyof Loaders]: Loaders[Name] extends JobDefinitionLoader<infer Job> ? Job : never;
9
- };
10
4
  export type JobPayloadOf<Job extends AnyJobDefinition> = Job extends JobDefinition<string, infer Payload, string, any, any, any> ? Payload extends object ? Payload : never : never;
11
5
  export type JobMessageOf<Job extends AnyJobDefinition> = Job extends JobDefinition<infer Name, any, string, any, any, any> ? {
12
6
  _task: Name;
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- export { default } from './module.mjs'
1
+ export { default, type generateRegistryTemplate, type generateRegistryTypesTemplate } from './module.mjs'
2
2
 
3
3
  export { type ModuleOptions } from './module.mjs'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-cf-jobs",
3
3
  "type": "module",
4
- "version": "0.0.2",
4
+ "version": "0.1.0",
5
5
  "description": "Nuxt module for typed Cloudflare queue jobs.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",