nuxt-cf-jobs 0.0.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.
Files changed (34) hide show
  1. package/README.md +331 -0
  2. package/dist/module.d.mts +73 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +380 -0
  5. package/dist/runtime/server/app.d.ts +80 -0
  6. package/dist/runtime/server/app.js +81 -0
  7. package/dist/runtime/server/d1.d.ts +119 -0
  8. package/dist/runtime/server/d1.js +259 -0
  9. package/dist/runtime/server/dev.d.ts +39 -0
  10. package/dist/runtime/server/dev.js +93 -0
  11. package/dist/runtime/server/dispatch.d.ts +25 -0
  12. package/dist/runtime/server/dispatch.js +66 -0
  13. package/dist/runtime/server/index.d.ts +12 -0
  14. package/dist/runtime/server/index.js +12 -0
  15. package/dist/runtime/server/outbox.d.ts +220 -0
  16. package/dist/runtime/server/outbox.js +246 -0
  17. package/dist/runtime/server/payload.d.ts +3 -0
  18. package/dist/runtime/server/payload.js +3 -0
  19. package/dist/runtime/server/plugins/dev-queues.d.ts +2 -0
  20. package/dist/runtime/server/plugins/dev-queues.js +25 -0
  21. package/dist/runtime/server/policy.d.ts +10 -0
  22. package/dist/runtime/server/policy.js +49 -0
  23. package/dist/runtime/server/queue.d.ts +211 -0
  24. package/dist/runtime/server/queue.js +495 -0
  25. package/dist/runtime/server/registry.d.ts +79 -0
  26. package/dist/runtime/server/registry.js +82 -0
  27. package/dist/runtime/server/schema.d.ts +965 -0
  28. package/dist/runtime/server/schema.js +80 -0
  29. package/dist/runtime/server/testing.d.ts +34 -0
  30. package/dist/runtime/server/testing.js +61 -0
  31. package/dist/runtime/server/types.d.ts +123 -0
  32. package/dist/runtime/server/types.js +0 -0
  33. package/dist/types.d.mts +3 -0
  34. package/package.json +109 -0
package/README.md ADDED
@@ -0,0 +1,331 @@
1
+ # nuxt-cf-jobs
2
+
3
+ Typed Cloudflare Queue jobs for Nuxt.
4
+
5
+ `nuxt-cf-jobs` scans your Nuxt server job files, generates a typed registry, and gives you small runtime helpers for:
6
+
7
+ - building typed queue payloads
8
+ - sending jobs to Cloudflare Queue bindings
9
+ - consuming Cloudflare queue batches through Nitro hooks
10
+ - persisting durable jobs in D1
11
+ - testing queues without Cloudflare
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add nuxt-cf-jobs
17
+ ```
18
+
19
+ Add the module:
20
+
21
+ ```ts
22
+ // nuxt.config.ts
23
+ export default defineNuxtConfig({
24
+ modules: ['nuxt-cf-jobs'],
25
+ cfJobs: {
26
+ queues: {
27
+ default: 'QUEUE_DEFAULT',
28
+ analytics: {
29
+ binding: 'QUEUE_ANALYTICS',
30
+ queueName: 'analytics-production',
31
+ jobType: 'analytics',
32
+ },
33
+ },
34
+ jobsDir: 'server/jobs',
35
+ },
36
+ })
37
+ ```
38
+
39
+ `queues` maps your logical queue names to Cloudflare environment binding names. The string form uses the logical name as the Cloudflare queue name. The object form lets the Cloudflare queue name differ from the logical name.
40
+
41
+ ## Define Jobs
42
+
43
+ Create default-exported jobs under `server/jobs`:
44
+
45
+ ```ts
46
+ // server/jobs/sync/table.ts
47
+ import { defineJob } from '#cf-jobs/server'
48
+
49
+ export default defineJob({
50
+ name: 'sync/table',
51
+ queue: 'default',
52
+ async handle(payload: {
53
+ siteId: string
54
+ userId: number
55
+ table: string
56
+ priority?: 'low' | 'normal'
57
+ }, ctx) {
58
+ ctx.log.info('syncing table', payload.table)
59
+ },
60
+ })
61
+ ```
62
+
63
+ Job names are derived from file paths for the generated registry, so this file is available as `sync/table`. Duplicate derived names fail during template generation.
64
+
65
+ The module ignores private/test files by default:
66
+
67
+ ```ts
68
+ jobsIgnore: ['**/_*.ts', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts']
69
+ ```
70
+
71
+ ## Use The Typed Registry
72
+
73
+ The generated registry is available as `#cf-jobs/app` by default.
74
+
75
+ ```ts
76
+ import {
77
+ buildJobPayload,
78
+ getJobDefinition,
79
+ getQueue,
80
+ prepareJob,
81
+ type JobName,
82
+ type JobPayload,
83
+ } from '#cf-jobs/app'
84
+
85
+ const name: JobName = 'sync/table'
86
+
87
+ const payload = {
88
+ siteId: 'site_1',
89
+ userId: 123,
90
+ table: 'pages',
91
+ priority: 'low',
92
+ } satisfies JobPayload<'sync/table'>
93
+
94
+ const message = buildJobPayload(name, payload)
95
+ const definition = getJobDefinition(name)
96
+ ```
97
+
98
+ Unknown job names and invalid payload shapes are rejected by TypeScript when the job payload type can be inferred from the job definition.
99
+
100
+ ## Send Jobs
101
+
102
+ Use `getQueue(event, jobDefinition)` inside server routes, event handlers, Nitro plugins, or other server code where you have an `H3Event` or Cloudflare env-like source.
103
+
104
+ ```ts
105
+ // server/api/sync.post.ts
106
+ import { getJobDefinition, getQueue } from '#cf-jobs/app'
107
+
108
+ export default defineEventHandler(async (event) => {
109
+ const job = getJobDefinition('sync/table')
110
+ if (!job)
111
+ throw createError({ statusCode: 500, statusMessage: 'Job not registered' })
112
+
113
+ const queue = getQueue(event, job)
114
+ const queued = await queue.send({
115
+ siteId: 'site_1',
116
+ userId: 123,
117
+ table: 'pages',
118
+ })
119
+
120
+ return { queued }
121
+ })
122
+ ```
123
+
124
+ `queue.send()` returns `false` when the configured Cloudflare binding is unavailable. That lets development or unsupported runtimes fail explicitly without throwing.
125
+
126
+ In Nuxt dev, the module installs a dev-only Nitro plugin that creates in-memory queue bindings from your `cfJobs.queues` config and forwards messages to the `cloudflare:queue` hook.
127
+
128
+ ## Consume Queue Batches
129
+
130
+ Register a queue consumer from a Nitro plugin:
131
+
132
+ ```ts
133
+ // server/plugins/cf-jobs.ts
134
+ import { registerQueueConsumer } from '#cf-jobs/app'
135
+
136
+ export default defineNitroPlugin((nitroApp) => {
137
+ registerQueueConsumer(nitroApp, {
138
+ createContext({ env, job, message, control }) {
139
+ return {
140
+ env,
141
+ db: null,
142
+ log: console,
143
+ jobId: job.id,
144
+ batchId: job.batchId,
145
+ attempt: message.attempts,
146
+ async release(delaySeconds: number) {
147
+ control.handled = true
148
+ control.action = 'released'
149
+ control.delaySeconds = delaySeconds
150
+ message.retry({ delaySeconds })
151
+ },
152
+ async fail(error: string) {
153
+ control.handled = true
154
+ control.action = 'failed'
155
+ control.error = error
156
+ message.ack()
157
+ },
158
+ }
159
+ },
160
+ })
161
+ })
162
+ ```
163
+
164
+ The generated `registerQueueConsumer()` wires the generated registry and `runtimeConfig.cfJobs.queues` for you. You provide only the application-specific context.
165
+
166
+ Useful options:
167
+
168
+ ```ts
169
+ registerQueueConsumer(nitroApp, {
170
+ createContext,
171
+ getJobId: ({ payload }) => String(payload.jobId),
172
+ getSiteId: payload => typeof payload.siteId === 'string' ? payload.siteId : null,
173
+ getUserId: payload => typeof payload.userId === 'number' ? payload.userId : null,
174
+ retryDelaySeconds: ({ error, job }) => 30,
175
+ onInvalidPayload: input => console.warn(input.error),
176
+ onDispatchError: input => console.error(input.error),
177
+ })
178
+ ```
179
+
180
+ If you do not provide `getJobId`, the consumer uses `payload.jobId` when present, otherwise a stable serialized payload ID.
181
+
182
+ ## Durable D1 Jobs
183
+
184
+ For jobs that should survive process restarts or queue delivery issues, persist a durable job record first, then enqueue a lightweight queue message.
185
+
186
+ ```ts
187
+ import {
188
+ createD1DurableJobRepository,
189
+ createQueuePublisher,
190
+ enqueueDurableJob,
191
+ } from 'nuxt-cf-jobs/server'
192
+ import { prepareJob } from '#cf-jobs/app'
193
+
194
+ export default defineEventHandler(async (event) => {
195
+ const env = event.context.cloudflare?.env as {
196
+ DB: D1Database
197
+ QUEUE_DEFAULT: Queue
198
+ }
199
+
200
+ const repository = createD1DurableJobRepository(env.DB)
201
+ await repository.migrate()
202
+
203
+ const publisher = createQueuePublisher(env, queue =>
204
+ queue === 'default' ? 'QUEUE_DEFAULT' : undefined,
205
+ )
206
+
207
+ const record = await prepareJob({
208
+ name: 'sync/table',
209
+ payload: {
210
+ siteId: 'site_1',
211
+ userId: 123,
212
+ table: 'pages',
213
+ },
214
+ })
215
+
216
+ return await enqueueDurableJob(repository, publisher, record)
217
+ })
218
+ ```
219
+
220
+ Consume durable queue messages with `runDurableJobMessage()` and the D1 repository:
221
+
222
+ ```ts
223
+ import { runDurableJobMessage } from 'nuxt-cf-jobs/durable'
224
+ import { createD1DurableJobRepository } from 'nuxt-cf-jobs/d1'
225
+ import { jobRegistry } from '#cf-jobs/app'
226
+
227
+ export default defineNitroPlugin((nitroApp) => {
228
+ nitroApp.hooks.hook('cloudflare:queue', async ({ batch, env }) => {
229
+ const repository = createD1DurableJobRepository(env.DB)
230
+
231
+ for (const message of batch.messages) {
232
+ await runDurableJobMessage({
233
+ message,
234
+ lifecycle: repository,
235
+ registry: jobRegistry,
236
+ toDispatchableJob: repository.toDispatchableJob,
237
+ createJobContext({ job, storedJob, control }) {
238
+ return {
239
+ env,
240
+ db: env.DB,
241
+ log: console,
242
+ jobId: job.id,
243
+ batchId: job.batchId,
244
+ attempt: job.attempts,
245
+ async release(delaySeconds: number) {
246
+ control.handled = true
247
+ control.action = 'released'
248
+ control.delaySeconds = delaySeconds
249
+ await repository.releaseJob(storedJob, { delaySeconds })
250
+ message.retry({ delaySeconds })
251
+ },
252
+ async fail(error: string) {
253
+ control.handled = true
254
+ control.action = 'failed'
255
+ control.error = error
256
+ await repository.failJob(storedJob, error)
257
+ },
258
+ }
259
+ },
260
+ })
261
+ }
262
+ })
263
+ })
264
+ ```
265
+
266
+ `createD1DurableJobRepository()` exposes:
267
+
268
+ - `migrate()`
269
+ - `insertJob()`
270
+ - `claimJob()`
271
+ - `completeJob()`
272
+ - `failJob()`
273
+ - `releaseJob()`
274
+ - `findDispatchableJobs()`
275
+ - `findStaleReservedJobs()`
276
+ - `releaseStaleReservedJobs()`
277
+ - `toDispatchableJob()`
278
+
279
+ ## Runtime Validation
280
+
281
+ The generated registry validates jobs at startup. It fails loudly for:
282
+
283
+ - invalid job definitions
284
+ - duplicate job names
285
+ - missing or invalid queue names
286
+
287
+ Queue binding checks are available from `#cf-jobs/app`:
288
+
289
+ ```ts
290
+ import { assertQueueBindings, validateQueueBindings } from '#cf-jobs/app'
291
+
292
+ const issues = validateQueueBindings()
293
+ assertQueueBindings()
294
+ ```
295
+
296
+ ## Public Imports
297
+
298
+ Use the narrow subpaths when you can:
299
+
300
+ ```ts
301
+ import { defineJob } from 'nuxt-cf-jobs/server'
302
+ import { runDurableJobMessage } from 'nuxt-cf-jobs/durable'
303
+ import { createD1DurableJobRepository } from 'nuxt-cf-jobs/d1'
304
+ import { createFakeQueue } from 'nuxt-cf-jobs/testing'
305
+ ```
306
+
307
+ Available package subpaths:
308
+
309
+ - `nuxt-cf-jobs` - Nuxt module
310
+ - `nuxt-cf-jobs/server` - full server runtime barrel
311
+ - `nuxt-cf-jobs/d1` - D1 durable repository adapter
312
+ - `nuxt-cf-jobs/durable` - durable outbox helpers
313
+ - `nuxt-cf-jobs/queue` - queue binding and consumer helpers
314
+ - `nuxt-cf-jobs/schema` - Drizzle schema
315
+ - `nuxt-cf-jobs/testing` - fake queue helpers
316
+
317
+ Inside a Nuxt app, prefer the generated aliases:
318
+
319
+ - `#cf-jobs/server` for server runtime helpers
320
+ - `#cf-jobs/app` for your generated typed job registry
321
+
322
+ ## Tests
323
+
324
+ ```bash
325
+ pnpm test
326
+ pnpm typecheck
327
+ pnpm build
328
+ pnpm test:e2e
329
+ ```
330
+
331
+ `pnpm test:e2e` starts Wrangler fixtures and requires the local Cloudflare/Wrangler toolchain to be available.
@@ -0,0 +1,73 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface QueueBindingOptions {
4
+ binding: string;
5
+ queueName?: string;
6
+ jobType?: string;
7
+ maxBatchSize?: number;
8
+ maxBatchTimeout?: number;
9
+ maxConcurrency?: number;
10
+ maxRetries?: number;
11
+ retryDelay?: number;
12
+ /** Cloudflare queue name of the DLQ (mirrors wrangler `dead_letter_queue`). */
13
+ deadLetterQueue?: string;
14
+ /** Binding name of the DLQ producer so the module can forward exhausted messages. */
15
+ deadLetterQueueBinding?: string;
16
+ }
17
+ interface ModuleOptions {
18
+ /**
19
+ * Logical queue name -> Cloudflare env binding name.
20
+ *
21
+ * Example:
22
+ * {
23
+ * "lh-scans": "QUEUE_LH_SCANS",
24
+ * "sync-critical": { binding: "SYNC_CRITICAL", jobType: "sync" }
25
+ * }
26
+ */
27
+ queues: Record<string, string | QueueBindingOptions>;
28
+ /**
29
+ * Logical queue name used when a job omits `queue` on its `defineJob` call.
30
+ * Must be a key of `queues`.
31
+ */
32
+ defaultQueue?: string;
33
+ /**
34
+ * Directories scanned at build/dev time for default-exported job definitions.
35
+ * Relative paths are resolved from the Nuxt root directory.
36
+ */
37
+ jobsDir?: string | string[];
38
+ /**
39
+ * Glob pattern used inside each jobsDir.
40
+ */
41
+ jobsPattern?: string | string[];
42
+ /**
43
+ * Extra glob ignore patterns for jobsDir scanning.
44
+ */
45
+ jobsIgnore?: string[];
46
+ /**
47
+ * Alias for the generated typed registry module.
48
+ */
49
+ registryAlias?: string;
50
+ /**
51
+ * Cross-check the user's wrangler config against `queues` at build time
52
+ * and emit `.nuxt/cf-jobs/wrangler.suggested.toml`. Defaults to `true`.
53
+ */
54
+ validateWrangler?: boolean;
55
+ /**
56
+ * Override the wrangler config path (relative to rootDir).
57
+ * Defaults to scanning for `wrangler.jsonc`, `wrangler.json`, `wrangler.toml`.
58
+ */
59
+ wranglerPath?: string;
60
+ }
61
+
62
+ declare module '@nuxt/schema' {
63
+ interface RuntimeConfig {
64
+ cfJobs: {
65
+ queues: ModuleOptions['queues'];
66
+ defaultQueue?: string;
67
+ };
68
+ }
69
+ }
70
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
71
+
72
+ export { _default as default };
73
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-cf-jobs",
3
+ "configKey": "cfJobs",
4
+ "version": "0.0.2",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "3.6.1"
8
+ }
9
+ }