@boopkit/cap-background-jobs 0.2.6 → 0.3.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 +186 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @boopkit/cap-background-jobs
|
|
2
|
+
|
|
3
|
+
**Part of [Boop](https://www.npmjs.com/package/@boopkit/cli)** — a bootstrap platform for AI-first builders. `boop init` scaffolds a running Node/TypeScript web app from composable **capabilities**, each one bundling the code, dependencies, middleware wiring, and agent skills needed for a feature. You pick the capabilities; `boop add` handles the rest.
|
|
4
|
+
|
|
5
|
+
`cap-background-jobs` adds async job processing via RabbitMQ (CloudAMQP). You get a publish/listen API, a dedicated worker process, a scheduler CLI for cron-triggered jobs, and automatic retry/dead-letter topology per queue. Use it for anything that shouldn't block an HTTP response: image processing, outbound email, nightly reconciles, third-party webhooks.
|
|
6
|
+
|
|
7
|
+
## What You Get
|
|
8
|
+
|
|
9
|
+
### Scaffold files
|
|
10
|
+
|
|
11
|
+
Copied into your project on install:
|
|
12
|
+
|
|
13
|
+
- `src/worker.ts` — worker process entry point. Boots services, registers consumers, handles graceful shutdown
|
|
14
|
+
- `src/scheduler.ts` — CLI for publishing scheduled triggers, filtered by `--interval` and `--job`
|
|
15
|
+
- `src/controllers/example-job.controller.ts` — copyable template for message consumers (auto-ACK pattern)
|
|
16
|
+
- `tests/controllers/example-job.controller.test.ts` — example consumer tests
|
|
17
|
+
- `reference/background-jobs.ref.md` — infrastructure topology, ACK modes, Heroku setup
|
|
18
|
+
|
|
19
|
+
### Scripts added to `package.json`
|
|
20
|
+
|
|
21
|
+
| Script | Command | Purpose |
|
|
22
|
+
|--------|---------|---------|
|
|
23
|
+
| `worker` | `tsx src/worker.ts` | Start the background job worker process |
|
|
24
|
+
| `scheduler` | `tsx src/scheduler.ts` | Publish trigger messages to job queues (`run-jobs`, `publish`, `list` subcommands) |
|
|
25
|
+
|
|
26
|
+
### Skills installed
|
|
27
|
+
|
|
28
|
+
- `design.background-jobs.skill.md` — teaches agents how to add a consumer, choose between auto- and manual-ACK, add scheduled jobs, and handle retries.
|
|
29
|
+
|
|
30
|
+
### Post-install
|
|
31
|
+
|
|
32
|
+
1. Provision CloudAMQP (or any RabbitMQ instance) and add `CLOUDAMQP_URL` to `.env`
|
|
33
|
+
2. Set `QUEUE_PREFIX=dev/<yourname>` to isolate your queues from other developers sharing the instance
|
|
34
|
+
3. Add `worker: npm run worker` to your `Procfile`
|
|
35
|
+
4. Configure your external scheduler (e.g. Heroku Scheduler) to run `npm run scheduler -- run-jobs --interval <tag>` on the desired cadence
|
|
36
|
+
|
|
37
|
+
`boop status` reminds you of these steps after install.
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
**Other capabilities:**
|
|
42
|
+
|
|
43
|
+
- `cap-services` — the message queue is registered as `services.message-queue` via the service map
|
|
44
|
+
|
|
45
|
+
**npm dependencies added to your project:**
|
|
46
|
+
|
|
47
|
+
- `amqplib` — RabbitMQ client
|
|
48
|
+
- `minimist` — scheduler CLI argument parsing
|
|
49
|
+
|
|
50
|
+
**External services:**
|
|
51
|
+
|
|
52
|
+
- **RabbitMQ** — CloudAMQP is the easiest path on Heroku (`heroku addons:create cloudamqp`, which sets `CLOUDAMQP_URL` automatically). Any RabbitMQ broker works — local Docker, self-hosted, or AWS MQ — point `CLOUDAMQP_URL` at it.
|
|
53
|
+
|
|
54
|
+
**Environment variables:**
|
|
55
|
+
|
|
56
|
+
| Variable | Required | Description |
|
|
57
|
+
|----------|----------|-------------|
|
|
58
|
+
| `CLOUDAMQP_URL` | Yes (at boot) | RabbitMQ connection URL (`amqp://...` or `amqps://...`) |
|
|
59
|
+
| `QUEUE_PREFIX` | No | Queue name prefix for environment isolation (e.g. `dev/mike`). Leave empty in production |
|
|
60
|
+
| `MAX_MESSAGE_RETRIES` | No | Max retry attempts before dead-lettering (default: `5`) |
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
Three moving parts — web publishes, scheduler publishes on a cron, worker consumes. All share one `MessageQueueService` instance registered in the service map.
|
|
65
|
+
|
|
66
|
+
### Publish a job
|
|
67
|
+
|
|
68
|
+
From a route handler, service, or anywhere in the web process:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { services } from '@services';
|
|
72
|
+
|
|
73
|
+
app.post('/upload', async (req, res) => {
|
|
74
|
+
const upload = await services.uploads.create(req.body);
|
|
75
|
+
|
|
76
|
+
// Enqueue async processing — returns immediately
|
|
77
|
+
await services.messageQueue.publish('process-upload', {
|
|
78
|
+
task: 'process-upload',
|
|
79
|
+
data: { uploadId: upload.id, userId: req.user.id },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
res.status(202).json({ id: upload.id });
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The publish connection is lazy — it only opens when you actually publish. Your web dynos stay cheap.
|
|
87
|
+
|
|
88
|
+
### Consume a job
|
|
89
|
+
|
|
90
|
+
**1.** Copy `src/controllers/example-job.controller.ts` to `src/controllers/process-upload.controller.ts` and replace the handler body:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
export const QUEUE_NAME = 'process-upload';
|
|
94
|
+
|
|
95
|
+
export async function onMessage(msg: unknown): Promise<boolean> {
|
|
96
|
+
const payload = validatePayload(msg);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await services.uploads.process(payload.data.uploadId);
|
|
100
|
+
return true; // ACK — success
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('[process-upload] failed:', err);
|
|
103
|
+
return false; // retry with exponential backoff
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**2.** Register the consumer in `src/worker.ts`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import * as processUploadController from './controllers/process-upload.controller';
|
|
112
|
+
|
|
113
|
+
const consumers = [
|
|
114
|
+
{ queue: exampleJobController.QUEUE_NAME, handler: exampleJobController.onMessage },
|
|
115
|
+
{ queue: processUploadController.QUEUE_NAME, handler: processUploadController.onMessage },
|
|
116
|
+
];
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**3.** Run the worker locally:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm run worker
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
On first listen, the service asserts the queue topology: main queue, `.retry` queue (TTL-backed redelivery), and `.failed` dead-letter queue. Failed handlers retry with exponential backoff (4s → 60s, capped) up to `MAX_MESSAGE_RETRIES`, then land in the DLQ for inspection.
|
|
126
|
+
|
|
127
|
+
### Error classification
|
|
128
|
+
|
|
129
|
+
Return value from the handler decides routing:
|
|
130
|
+
|
|
131
|
+
| Return | Result |
|
|
132
|
+
|--------|--------|
|
|
133
|
+
| `return true` | ACK — success, or permanent failure that retrying won't fix |
|
|
134
|
+
| `return false` | Retry with backoff |
|
|
135
|
+
| `throw` | Retry, then DLQ after max retries |
|
|
136
|
+
|
|
137
|
+
### Scheduled jobs
|
|
138
|
+
|
|
139
|
+
Add an entry to the `jobs` array in `src/scheduler.ts`:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const jobs: JobConfig[] = [
|
|
143
|
+
{ name: 'nightly-sync', queue: 'sync-accounts', interval: '60-minutes',
|
|
144
|
+
payload: { task: 'nightly-sync' } },
|
|
145
|
+
];
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Then configure your external scheduler:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Run all jobs tagged 60-minutes
|
|
152
|
+
npm run scheduler -- run-jobs --interval 60-minutes
|
|
153
|
+
|
|
154
|
+
# Run a specific job on demand
|
|
155
|
+
npm run scheduler -- run-jobs --job nightly-sync
|
|
156
|
+
|
|
157
|
+
# Publish an ad-hoc message
|
|
158
|
+
npm run scheduler -- publish --queue sync-accounts --task manual-run
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Manual ACK
|
|
162
|
+
|
|
163
|
+
For batch commits or transaction coordination, opt into manual ACK:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
await services.messageQueue.listen('batch-job', async (msg, ack, nack) => {
|
|
167
|
+
try {
|
|
168
|
+
await processBatch(msg);
|
|
169
|
+
ack();
|
|
170
|
+
} catch {
|
|
171
|
+
nack();
|
|
172
|
+
}
|
|
173
|
+
}, { manualAck: true });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
First call to `ack()` or `nack()` wins — subsequent calls are silent no-ops.
|
|
177
|
+
|
|
178
|
+
See `reference/background-jobs.ref.md` in your scaffolded project for the full topology diagram, connection lifecycle, and Heroku deployment guide.
|
|
179
|
+
|
|
180
|
+
## Added by
|
|
181
|
+
|
|
182
|
+
Not included by default. Add to an existing project with:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
boop add background-jobs
|
|
186
|
+
```
|