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.
- package/README.md +331 -0
- package/dist/module.d.mts +73 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +380 -0
- package/dist/runtime/server/app.d.ts +80 -0
- package/dist/runtime/server/app.js +81 -0
- package/dist/runtime/server/d1.d.ts +119 -0
- package/dist/runtime/server/d1.js +259 -0
- package/dist/runtime/server/dev.d.ts +39 -0
- package/dist/runtime/server/dev.js +93 -0
- package/dist/runtime/server/dispatch.d.ts +25 -0
- package/dist/runtime/server/dispatch.js +66 -0
- package/dist/runtime/server/index.d.ts +12 -0
- package/dist/runtime/server/index.js +12 -0
- package/dist/runtime/server/outbox.d.ts +220 -0
- package/dist/runtime/server/outbox.js +246 -0
- package/dist/runtime/server/payload.d.ts +3 -0
- package/dist/runtime/server/payload.js +3 -0
- package/dist/runtime/server/plugins/dev-queues.d.ts +2 -0
- package/dist/runtime/server/plugins/dev-queues.js +25 -0
- package/dist/runtime/server/policy.d.ts +10 -0
- package/dist/runtime/server/policy.js +49 -0
- package/dist/runtime/server/queue.d.ts +211 -0
- package/dist/runtime/server/queue.js +495 -0
- package/dist/runtime/server/registry.d.ts +79 -0
- package/dist/runtime/server/registry.js +82 -0
- package/dist/runtime/server/schema.d.ts +965 -0
- package/dist/runtime/server/schema.js +80 -0
- package/dist/runtime/server/testing.d.ts +34 -0
- package/dist/runtime/server/testing.js +61 -0
- package/dist/runtime/server/types.d.ts +123 -0
- package/dist/runtime/server/types.js +0 -0
- package/dist/types.d.mts +3 -0
- 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 };
|