@chidchanun/bcp 0.2.6 → 0.2.8

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.
@@ -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,6 +1,21 @@
1
1
  # Logging and observability
2
2
 
3
- BCP Framework 0.1.23 adds a structured server logger and upgrades development request timing output while keeping framework-internal browser/bootstrap traffic quiet by default.
3
+ BCP Framework 0.1.23 added the structured server logger and development request timing output. BCP Framework `0.2.7` complements that logging layer with process-local metrics and health/readiness primitives through `bcp/observability`.
4
+
5
+ Use these layers together:
6
+
7
+ ```text
8
+ bcp/server
9
+ -> structured event/request logs
10
+
11
+ bcp/observability
12
+ -> counters, gauges, histograms
13
+ -> Prometheus exposition
14
+ -> request metrics middleware
15
+ -> health/readiness checks
16
+ ```
17
+
18
+ See [Observability Platform v2](observability.md) for the `0.2.7` metrics and health APIs.
4
19
 
5
20
  ## Server logger
6
21
 
@@ -205,6 +220,34 @@ ssr.render
205
220
 
206
221
  They are therefore visible when `BCP_LOG_LEVEL=debug` and stay out of normal `info` output.
207
222
 
223
+ ## Metrics vs logs
224
+
225
+ Logs and metrics serve different purposes.
226
+
227
+ Use structured logs when you need per-event context:
228
+
229
+ ```text
230
+ request ID
231
+ operation
232
+ error stack
233
+ resource identity
234
+ diagnostic fields
235
+ ```
236
+
237
+ Use metrics when you need aggregate trends:
238
+
239
+ ```text
240
+ request count
241
+ status distribution
242
+ latency buckets
243
+ queue depth
244
+ worker count
245
+ ```
246
+
247
+ Avoid copying high-cardinality log fields such as request IDs, user IDs or raw paths into metric labels.
248
+
249
+ The `0.2.7` request metrics middleware follows this rule by using `method` and `status` labels by default.
250
+
208
251
  ## Errors
209
252
 
210
253
  Pass an `Error` as a structured field:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "versionTarget": "0.2.6",
4
+ "versionTarget": "0.2.8",
5
5
  "releaseState": "unreleased",
6
6
  "sections": [
7
7
  {
@@ -57,11 +57,13 @@
57
57
  {
58
58
  "id": "runtime",
59
59
  "title": "Runtime & Infrastructure",
60
- "description": "Middleware, hydration, logging, caching, security and production hardening.",
60
+ "description": "Middleware, background jobs, 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
- { "route": "/docs/development-logging", "source": "development-logging.md", "title": "Logging & Observability" },
64
+ { "route": "/docs/development-logging", "source": "development-logging.md", "title": "Logging" },
65
+ { "route": "/docs/observability", "source": "observability.md", "title": "Observability Platform v2" },
66
+ { "route": "/docs/background-jobs", "source": "background-jobs.md", "title": "Background Jobs Platform" },
65
67
  { "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
66
68
  { "route": "/docs/security", "source": "security.md", "title": "Security" },
67
69
  { "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
@@ -107,7 +109,9 @@
107
109
  }
108
110
  ],
109
111
  "releases": [
110
- { "route": "/releases/0.2.6", "source": "releases/0.2.6.md", "version": "0.2.6", "state": "unreleased" },
112
+ { "route": "/releases/0.2.8", "source": "releases/0.2.8.md", "version": "0.2.8", "state": "unreleased" },
113
+ { "route": "/releases/0.2.7", "source": "releases/0.2.7.md", "version": "0.2.7" },
114
+ { "route": "/releases/0.2.6", "source": "releases/0.2.6.md", "version": "0.2.6" },
111
115
  { "route": "/releases/0.2.5", "source": "releases/0.2.5.md", "version": "0.2.5" },
112
116
  { "route": "/releases/0.2.4", "source": "releases/0.2.4.md", "version": "0.2.4" },
113
117
  { "route": "/releases/0.2.3", "source": "releases/0.2.3.md", "version": "0.2.3" },