@chidchanun/bcp 0.2.7 → 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,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "versionTarget": "0.2.7",
4
+ "versionTarget": "0.2.8",
5
5
  "releaseState": "unreleased",
6
6
  "sections": [
7
7
  {
@@ -57,12 +57,13 @@
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, 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" },
66
67
  { "route": "/docs/caching", "source": "caching.md", "title": "Caching" },
67
68
  { "route": "/docs/security", "source": "security.md", "title": "Security" },
68
69
  { "route": "/docs/production-hardening", "source": "production-hardening.md", "title": "Production Hardening" }
@@ -108,7 +109,8 @@
108
109
  }
109
110
  ],
110
111
  "releases": [
111
- { "route": "/releases/0.2.7", "source": "releases/0.2.7.md", "version": "0.2.7", "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" },
112
114
  { "route": "/releases/0.2.6", "source": "releases/0.2.6.md", "version": "0.2.6" },
113
115
  { "route": "/releases/0.2.5", "source": "releases/0.2.5.md", "version": "0.2.5" },
114
116
  { "route": "/releases/0.2.4", "source": "releases/0.2.4.md", "version": "0.2.4" },
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "framework": "bcp",
4
- "version": "0.2.7",
4
+ "version": "0.2.8",
5
5
  "releaseState": "unreleased",
6
- "baseline": "observability-platform-v2",
6
+ "baseline": "background-jobs-platform",
7
7
  "runtime": {
8
8
  "node": ">=24.11.0",
9
9
  "react": "19",
@@ -19,6 +19,7 @@
19
19
  "bcp/error",
20
20
  "bcp/database",
21
21
  "bcp/auth",
22
+ "bcp/jobs",
22
23
  "bcp/observability",
23
24
  "bcp/server",
24
25
  "bcp/server-only",
@@ -65,6 +66,13 @@
65
66
  "prometheusMetrics": true,
66
67
  "requestMetricsMiddleware": true,
67
68
  "healthChecks": true,
69
+ "backgroundJobsPlatform": true,
70
+ "jobQueueAdapterContract": true,
71
+ "inMemoryJobQueue": true,
72
+ "delayedJobs": true,
73
+ "jobRetries": true,
74
+ "jobWorkerConcurrency": true,
75
+ "jobCancellation": true,
68
76
  "databaseMigrations": true,
69
77
  "databaseAdapterContract": true,
70
78
  "databasePostgresql": true,
@@ -104,7 +112,7 @@
104
112
  "s3-compatible"
105
113
  ],
106
114
  "compatibility": {
107
- "previousBaseline": "0.2.6",
115
+ "previousBaseline": "0.2.7",
108
116
  "intentionalBreakingChangesFromPreviousBaseline": false,
109
117
  "migrationGuide": "migration-0.2.md"
110
118
  },
@@ -121,7 +129,8 @@
121
129
  "authSessionStore": "auth-session-store.md",
122
130
  "authorizationSecurity": "authorization-security.md",
123
131
  "observability": "observability.md",
132
+ "backgroundJobs": "background-jobs.md",
124
133
  "migrationGuide": "migration-0.2.md",
125
- "releaseNotes": "releases/0.2.7.md"
134
+ "releaseNotes": "releases/0.2.8.md"
126
135
  }
127
136
  }
@@ -0,0 +1,150 @@
1
+ # BCP Framework 0.2.8
2
+
3
+ **Milestone:** Background Jobs Platform
4
+
5
+ `0.2.8` adds a framework-native server-side background-job queue contract with a process-local reference adapter, delayed execution, retries/backoff, worker concurrency and cancellation.
6
+
7
+ > Release state: unreleased until the final RC sequence, tag and npm publication complete.
8
+
9
+ ## New public entrypoint
10
+
11
+ ```ts
12
+ import {
13
+ createJobQueue,
14
+ createMemoryJobQueueAdapter,
15
+ } from "bcp/jobs";
16
+ ```
17
+
18
+ `bcp/jobs` is server-only and is protected by both package browser exports and the BCP client-boundary validator.
19
+
20
+ ## Queue adapter contract
21
+
22
+ Applications and ecosystem packages can implement `JobQueueAdapter` for durable/shared infrastructure.
23
+
24
+ The contract covers:
25
+
26
+ ```text
27
+ enqueue
28
+ reserve
29
+ complete
30
+ fail
31
+ cancel
32
+ get
33
+ list
34
+ close (optional)
35
+ ```
36
+
37
+ `reserve()` is the adapter's atomic claim boundary for multi-worker processing.
38
+
39
+ ## In-memory reference adapter
40
+
41
+ `createMemoryJobQueueAdapter()` provides a dependency-free process-local adapter for development, tests and prototypes.
42
+
43
+ The memory adapter is intentionally not durable and cannot coordinate multiple Node.js processes or containers.
44
+
45
+ ## Delayed jobs
46
+
47
+ ```ts
48
+ await jobs.enqueue(
49
+ "report.generate",
50
+ payload,
51
+ {
52
+ delayMs: 60_000,
53
+ }
54
+ );
55
+ ```
56
+
57
+ Jobs are eligible for reservation only after `availableAt`.
58
+
59
+ ## Retry and backoff
60
+
61
+ Queues default to three maximum attempts and capped exponential retry delay.
62
+
63
+ Applications can configure a fixed delay or callback:
64
+
65
+ ```ts
66
+ createJobQueue({
67
+ defaultMaxAttempts: 5,
68
+ retryDelayMs: attempt =>
69
+ attempt * 5_000,
70
+ });
71
+ ```
72
+
73
+ Per-job `maxAttempts` overrides the queue default.
74
+
75
+ ## Workers
76
+
77
+ ```ts
78
+ const worker =
79
+ jobs.startWorker({
80
+ concurrency: 4,
81
+ pollIntervalMs: 250,
82
+ });
83
+ ```
84
+
85
+ Workers run concurrent processing loops and can be stopped gracefully.
86
+
87
+ `jobs.close()` stops all workers started by that queue and closes the adapter when supported.
88
+
89
+ ## Cancellation
90
+
91
+ ```ts
92
+ await jobs.cancel(jobId);
93
+ ```
94
+
95
+ Cancelled records remain terminal; later worker completion/failure writes do not overwrite cancellation in the memory adapter.
96
+
97
+ ## Manual processing
98
+
99
+ `processNext()` remains available for deterministic tests and custom supervisors.
100
+
101
+ ```ts
102
+ const processed =
103
+ await jobs.processNext();
104
+ ```
105
+
106
+ ## Package and platform contracts
107
+
108
+ `0.2.8` adds:
109
+
110
+ - `bcp/jobs` to the framework package export map,
111
+ - TypeScript path mapping for repository development,
112
+ - server-only client-boundary enforcement,
113
+ - Background Jobs capability metadata in `platform-manifest.json`,
114
+ - API-manifest ownership and docs-web navigation,
115
+ - prepared-package smoke coverage.
116
+
117
+ ## Compatibility
118
+
119
+ `0.2.8` does not intentionally remove or rename existing `0.2.7` public APIs.
120
+
121
+ Existing applications do not need to adopt background jobs.
122
+
123
+ The built-in queue is opt-in and no additional runtime dependency is installed.
124
+
125
+ ## Delivery semantics
126
+
127
+ Durable adapters should be designed for at-least-once processing. Job handlers should be idempotent when duplicate side effects are unsafe.
128
+
129
+ `0.2.8` does not promise exactly-once execution.
130
+
131
+ ## Not included
132
+
133
+ This milestone does not ship built-in adapters for Redis, PostgreSQL, SQS, RabbitMQ, Kafka, cron scheduling or workflow orchestration.
134
+
135
+ Those providers/features can be layered behind the new queue contract in later versions.
136
+
137
+ ## Validation
138
+
139
+ Before tagging/publishing `0.2.8` run:
140
+
141
+ ```bash
142
+ npm run typecheck
143
+ npm run test:unit
144
+ npm run test:integration
145
+ npm run test:e2e
146
+ npm run test:package
147
+ npm run rc:check
148
+ ```
149
+
150
+ The final release tag must point to the exact commit that passed the complete RC sequence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -62,6 +62,11 @@
62
62
  "browser": "./packages/client/src/server-only.browser.mjs",
63
63
  "default": "./packages/client/src/auth.ts"
64
64
  },
65
+ "./jobs": {
66
+ "types": "./packages/client/src/jobs.ts",
67
+ "browser": "./packages/client/src/server-only.browser.mjs",
68
+ "default": "./packages/client/src/jobs.ts"
69
+ },
65
70
  "./observability": {
66
71
  "types": "./packages/client/src/observability.ts",
67
72
  "browser": "./packages/client/src/server-only.browser.mjs",
@@ -28,6 +28,7 @@ const SERVER_ONLY_IMPORTS =
28
28
  "bcp/server-only",
29
29
  "bcp/database",
30
30
  "bcp/auth",
31
+ "bcp/jobs",
31
32
  "bcp/observability",
32
33
  ]);
33
34
 
@@ -0,0 +1,16 @@
1
+ export {
2
+ createJobQueue,
3
+ createMemoryJobQueueAdapter,
4
+ type BackgroundJobQueue,
5
+ type EnqueueJobOptions,
6
+ type JobHandler,
7
+ type JobHandlerContext,
8
+ type JobQueueAdapter,
9
+ type JobQueueOptions,
10
+ type JobRecord,
11
+ type JobRetryDelay,
12
+ type JobState,
13
+ type JobWorker,
14
+ type MemoryJobQueueAdapter,
15
+ type StartJobWorkerOptions,
16
+ } from "../../server/src/jobs.js";