@chidchanun/bcp 0.2.7 → 0.2.9
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 +106 -72
- package/docs/README.md +47 -139
- package/docs/api-manifest.json +13 -1
- package/docs/api-reference.md +55 -235
- package/docs/background-jobs.md +356 -0
- package/docs/docs-web-manifest.json +7 -3
- package/docs/job-scheduling.md +357 -0
- package/docs/platform-manifest.json +21 -4
- package/docs/releases/0.2.8.md +150 -0
- package/docs/releases/0.2.9.md +162 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/jobs.mjs +1040 -0
- package/packages/client/src/jobs.ts +33 -0
- package/packages/server/src/job-scheduler.ts +1144 -0
- package/packages/server/src/jobs.ts +822 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# Background Jobs Platform
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.2.8` adds a server-only background jobs contract through `bcp/jobs`.
|
|
4
|
+
|
|
5
|
+
The goal is to provide one framework API for enqueueing and processing background work while keeping storage/queue providers replaceable.
|
|
6
|
+
|
|
7
|
+
## Public entrypoint
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
createJobQueue,
|
|
12
|
+
createMemoryJobQueueAdapter,
|
|
13
|
+
} from "bcp/jobs";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`bcp/jobs` is server-only and must not be imported by pages or other client-bundled modules.
|
|
17
|
+
|
|
18
|
+
## Create a queue
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
createJobQueue,
|
|
23
|
+
} from "bcp/jobs";
|
|
24
|
+
|
|
25
|
+
export const jobs =
|
|
26
|
+
createJobQueue();
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Without an explicit adapter BCP uses the process-local in-memory adapter.
|
|
30
|
+
|
|
31
|
+
The memory adapter is intended for:
|
|
32
|
+
|
|
33
|
+
- development,
|
|
34
|
+
- unit/integration tests,
|
|
35
|
+
- single-process prototypes,
|
|
36
|
+
- applications where losing queued jobs during restart is acceptable.
|
|
37
|
+
|
|
38
|
+
For durable production processing, implement `JobQueueAdapter` against shared infrastructure such as a database, Redis-backed queue, managed queue service, or another durable store.
|
|
39
|
+
|
|
40
|
+
## Register handlers
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
jobs.register<{
|
|
44
|
+
userId: number;
|
|
45
|
+
}>(
|
|
46
|
+
"email.welcome",
|
|
47
|
+
async ({
|
|
48
|
+
payload,
|
|
49
|
+
job,
|
|
50
|
+
signal,
|
|
51
|
+
}) => {
|
|
52
|
+
console.log(
|
|
53
|
+
"Attempt",
|
|
54
|
+
job.attempts
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (signal.aborted) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await sendWelcomeEmail(
|
|
62
|
+
payload.userId
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A job name can have only one registered handler within a queue instance.
|
|
69
|
+
|
|
70
|
+
The unregister function returned by `register()` can remove the handler:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const unregister =
|
|
74
|
+
jobs.register(
|
|
75
|
+
"search.reindex",
|
|
76
|
+
reindexSearch
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
unregister();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Enqueue jobs
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const job =
|
|
86
|
+
await jobs.enqueue(
|
|
87
|
+
"email.welcome",
|
|
88
|
+
{
|
|
89
|
+
userId: 42,
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The returned record starts in `queued` state.
|
|
95
|
+
|
|
96
|
+
Job states are:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
queued
|
|
100
|
+
running
|
|
101
|
+
succeeded
|
|
102
|
+
failed
|
|
103
|
+
cancelled
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Delayed jobs
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
await jobs.enqueue(
|
|
110
|
+
"invoice.reminder",
|
|
111
|
+
{
|
|
112
|
+
invoiceId: 9001,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
delayMs:
|
|
116
|
+
15 * 60 * 1000,
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
A delayed job remains queued until `availableAt` is reached.
|
|
122
|
+
|
|
123
|
+
`delayMs` is a delay from enqueue time; `0` means the job is immediately eligible for reservation.
|
|
124
|
+
|
|
125
|
+
## Retries
|
|
126
|
+
|
|
127
|
+
The default maximum attempt count is `3`.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const jobs =
|
|
131
|
+
createJobQueue({
|
|
132
|
+
defaultMaxAttempts: 5,
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Override per job:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
await jobs.enqueue(
|
|
140
|
+
"payment.sync",
|
|
141
|
+
payload,
|
|
142
|
+
{
|
|
143
|
+
maxAttempts: 8,
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
When a handler throws, the job is queued again while attempts remain.
|
|
149
|
+
|
|
150
|
+
Default retry delay uses capped exponential backoff:
|
|
151
|
+
|
|
152
|
+
```text
|
|
153
|
+
attempt 1 -> 1 second
|
|
154
|
+
attempt 2 -> 2 seconds
|
|
155
|
+
attempt 3 -> 4 seconds
|
|
156
|
+
...
|
|
157
|
+
maximum -> 30 seconds
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Use a fixed delay:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
createJobQueue({
|
|
164
|
+
retryDelayMs: 5_000,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Or a custom backoff function:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
createJobQueue({
|
|
172
|
+
retryDelayMs: attempt =>
|
|
173
|
+
attempt * 10_000,
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The callback receives the attempt number that just failed.
|
|
178
|
+
|
|
179
|
+
## Manual processing
|
|
180
|
+
|
|
181
|
+
`processNext()` reserves and processes one eligible job:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const processed =
|
|
185
|
+
await jobs.processNext();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
It returns `false` when no eligible queued job is available.
|
|
189
|
+
|
|
190
|
+
This form is useful for deterministic tests and custom process supervisors.
|
|
191
|
+
|
|
192
|
+
## Start a worker
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
const worker =
|
|
196
|
+
jobs.startWorker({
|
|
197
|
+
concurrency: 4,
|
|
198
|
+
pollIntervalMs: 250,
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`concurrency` controls the number of independent processing loops in the current process.
|
|
203
|
+
|
|
204
|
+
Stop gracefully:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
await worker.stop();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Closing the queue stops all workers created by that queue and then closes the adapter when it exposes `close()`:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
await jobs.close();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Cancellation
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const cancelled =
|
|
220
|
+
await jobs.cancel(job.id);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Queued and running records can transition to `cancelled`. The in-memory adapter will not allow a later completion/failure write to replace a cancelled terminal state.
|
|
224
|
+
|
|
225
|
+
Handlers receive an `AbortSignal` for worker lifecycle shutdown. Application handlers should check the signal during long-running operations when graceful cancellation matters.
|
|
226
|
+
|
|
227
|
+
## Read queue state
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const job =
|
|
231
|
+
await jobs.get(jobId);
|
|
232
|
+
|
|
233
|
+
const allJobs =
|
|
234
|
+
await jobs.list();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
These APIs are intended primarily for diagnostics/admin tooling and testing. Durable adapters may choose their own indexing/storage strategy behind the contract.
|
|
238
|
+
|
|
239
|
+
## Adapter contract
|
|
240
|
+
|
|
241
|
+
Production adapters implement:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import type {
|
|
245
|
+
JobQueueAdapter,
|
|
246
|
+
} from "bcp/jobs";
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Required operations:
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
enqueue
|
|
253
|
+
reserve
|
|
254
|
+
complete
|
|
255
|
+
fail
|
|
256
|
+
cancel
|
|
257
|
+
get
|
|
258
|
+
list
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Optional:
|
|
262
|
+
|
|
263
|
+
```text
|
|
264
|
+
close
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`reserve(now)` must atomically claim one eligible queued job and increment its attempt count. This atomicity requirement is important for multi-worker or multi-instance adapters.
|
|
268
|
+
|
|
269
|
+
For durable adapters, use storage-level locking/transactions/leases appropriate to the provider. Do not implement `reserve()` as an unlocked read followed by an unrelated write.
|
|
270
|
+
|
|
271
|
+
## Unique job IDs
|
|
272
|
+
|
|
273
|
+
BCP generates UUID job IDs by default.
|
|
274
|
+
|
|
275
|
+
Applications can supply an explicit ID for idempotent enqueueing:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
await jobs.enqueue(
|
|
279
|
+
"statement.generate",
|
|
280
|
+
{
|
|
281
|
+
accountId: 12,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id:
|
|
285
|
+
"statement-12-2026-08",
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The memory adapter rejects duplicate IDs.
|
|
291
|
+
|
|
292
|
+
A durable adapter should enforce equivalent uniqueness at the storage layer.
|
|
293
|
+
|
|
294
|
+
## Observability
|
|
295
|
+
|
|
296
|
+
Background job infrastructure can be combined with `bcp/observability`.
|
|
297
|
+
|
|
298
|
+
Recommended metrics include:
|
|
299
|
+
|
|
300
|
+
```text
|
|
301
|
+
jobs_enqueued_total
|
|
302
|
+
jobs_completed_total
|
|
303
|
+
jobs_failed_total
|
|
304
|
+
jobs_retry_total
|
|
305
|
+
jobs_active
|
|
306
|
+
job_duration_seconds
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Avoid putting job IDs, user IDs, email addresses, URLs, or other unbounded values into metric labels.
|
|
310
|
+
|
|
311
|
+
Use a bounded job-name label when your application has a controlled handler catalog.
|
|
312
|
+
|
|
313
|
+
## Deployment model
|
|
314
|
+
|
|
315
|
+
The queue contract is intentionally independent from the web server lifecycle.
|
|
316
|
+
|
|
317
|
+
A small deployment may start a worker in the same Node.js process as the application. Higher reliability deployments should normally run workers as separate processes/containers using a shared durable adapter.
|
|
318
|
+
|
|
319
|
+
Example topology:
|
|
320
|
+
|
|
321
|
+
```text
|
|
322
|
+
BCP web application
|
|
323
|
+
|
|
|
324
|
+
| enqueue
|
|
325
|
+
v
|
|
326
|
+
shared durable queue
|
|
327
|
+
/ \
|
|
328
|
+
worker A worker B
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The built-in memory adapter cannot coordinate across processes or containers.
|
|
332
|
+
|
|
333
|
+
## Delivery semantics
|
|
334
|
+
|
|
335
|
+
BCP's adapter contract is designed for practical at-least-once processing semantics with durable adapters.
|
|
336
|
+
|
|
337
|
+
A worker may retry a job after an error. Application handlers should therefore be idempotent when side effects cannot safely run twice.
|
|
338
|
+
|
|
339
|
+
Examples:
|
|
340
|
+
|
|
341
|
+
- use a unique database key before sending a one-time notification,
|
|
342
|
+
- use idempotency keys with payment providers,
|
|
343
|
+
- write processing state transactionally where possible,
|
|
344
|
+
- avoid assuming one handler invocation means one side effect exactly once.
|
|
345
|
+
|
|
346
|
+
## Security
|
|
347
|
+
|
|
348
|
+
Job payloads are server-side application data. Do not expose `bcp/jobs` to client bundles.
|
|
349
|
+
|
|
350
|
+
Avoid storing secrets in payloads when the backing adapter may persist or log the full job record. Prefer stable identifiers and load sensitive data inside the handler from the authoritative store.
|
|
351
|
+
|
|
352
|
+
## 0.2.8 scope
|
|
353
|
+
|
|
354
|
+
`0.2.8` provides the queue/worker contract and process-local reference adapter.
|
|
355
|
+
|
|
356
|
+
It does not include a built-in Redis, PostgreSQL, SQS, RabbitMQ, Kafka, cron, or workflow-engine adapter. Those can be added behind `JobQueueAdapter` in later milestones without changing application-facing queue semantics.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"framework": "bcp",
|
|
4
|
-
"versionTarget": "0.2.
|
|
4
|
+
"versionTarget": "0.2.9",
|
|
5
5
|
"releaseState": "unreleased",
|
|
6
6
|
"sections": [
|
|
7
7
|
{
|
|
@@ -57,12 +57,14 @@
|
|
|
57
57
|
{
|
|
58
58
|
"id": "runtime",
|
|
59
59
|
"title": "Runtime & Infrastructure",
|
|
60
|
-
"description": "Middleware, observability, logging, caching, security and production hardening.",
|
|
60
|
+
"description": "Middleware, background jobs, scheduling, observability, logging, caching, security and production hardening.",
|
|
61
61
|
"pages": [
|
|
62
62
|
{ "route": "/docs/middleware", "source": "middleware.md", "title": "Middleware" },
|
|
63
63
|
{ "route": "/docs/hydration", "source": "hydration.md", "title": "Hydration" },
|
|
64
64
|
{ "route": "/docs/development-logging", "source": "development-logging.md", "title": "Logging" },
|
|
65
65
|
{ "route": "/docs/observability", "source": "observability.md", "title": "Observability Platform v2" },
|
|
66
|
+
{ "route": "/docs/background-jobs", "source": "background-jobs.md", "title": "Background Jobs Platform" },
|
|
67
|
+
{ "route": "/docs/job-scheduling", "source": "job-scheduling.md", "title": "Job Scheduling Platform" },
|
|
66
68
|
{ "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
|
|
67
69
|
{ "route": "/docs/security", "source": "security.md", "title": "Security" },
|
|
68
70
|
{ "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
|
|
@@ -108,7 +110,9 @@
|
|
|
108
110
|
}
|
|
109
111
|
],
|
|
110
112
|
"releases": [
|
|
111
|
-
{ "route": "/releases/0.2.
|
|
113
|
+
{ "route": "/releases/0.2.9", "source": "releases/0.2.9.md", "version": "0.2.9", "state": "unreleased" },
|
|
114
|
+
{ "route": "/releases/0.2.8", "source": "releases/0.2.8.md", "version": "0.2.8" },
|
|
115
|
+
{ "route": "/releases/0.2.7", "source": "releases/0.2.7.md", "version": "0.2.7" },
|
|
112
116
|
{ "route": "/releases/0.2.6", "source": "releases/0.2.6.md", "version": "0.2.6" },
|
|
113
117
|
{ "route": "/releases/0.2.5", "source": "releases/0.2.5.md", "version": "0.2.5" },
|
|
114
118
|
{ "route": "/releases/0.2.4", "source": "releases/0.2.4.md", "version": "0.2.4" },
|