@chidchanun/bcp 0.2.9 → 0.2.11

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,148 @@
1
+ # 0.2.10 — Durable Jobs Platform
2
+
3
+ **State:** unreleased development target
4
+
5
+ BCP Framework `0.2.10` extends the Background Jobs and Job Scheduling platforms with durable worker lifecycle contracts, dead-letter handling, retention/statistics APIs and Redis-compatible reference adapters.
6
+
7
+ ## Highlights
8
+
9
+ ### Durable worker leases
10
+
11
+ Workers can reserve jobs with a visibility timeout and stable worker identity.
12
+
13
+ New job record metadata:
14
+
15
+ ```text
16
+ leaseOwner
17
+ leaseUntil
18
+ heartbeatAt
19
+ recoveredAt
20
+ ```
21
+
22
+ When an adapter supports `heartbeat()`, BCP workers renew the lease while a handler is running.
23
+
24
+ When an adapter supports `recoverStale()`, expired running jobs can be returned to the queue for at-least-once recovery.
25
+
26
+ ### Dead-letter queue
27
+
28
+ Retry-exhausted jobs remain terminal `failed` jobs and can also be indexed in the adapter DLQ.
29
+
30
+ New queue APIs:
31
+
32
+ ```ts
33
+ jobs.deadLetters()
34
+ jobs.requeueDeadLetter()
35
+ ```
36
+
37
+ The built-in memory adapter and Redis-compatible adapter implement both APIs.
38
+
39
+ ### Queue maintenance
40
+
41
+ New operational APIs:
42
+
43
+ ```ts
44
+ jobs.stats()
45
+ jobs.cleanup()
46
+ jobs.recoverStale()
47
+ ```
48
+
49
+ Statistics include queue state totals and DLQ depth.
50
+
51
+ Cleanup can remove old succeeded, failed and cancelled records after an application-defined retention period.
52
+
53
+ ### Redis-compatible queue adapter
54
+
55
+ New public API:
56
+
57
+ ```ts
58
+ createRedisJobQueueAdapter()
59
+ ```
60
+
61
+ The adapter consumes a minimal application-owned Redis command client exposing:
62
+
63
+ ```ts
64
+ sendCommand(command: string[]): Promise<unknown>
65
+ ```
66
+
67
+ BCP does not add a Redis client runtime dependency.
68
+
69
+ The Redis adapter uses atomic Lua operations for:
70
+
71
+ - duplicate-safe enqueue,
72
+ - job reservation,
73
+ - complete/fail/cancel transitions,
74
+ - heartbeat lease renewal,
75
+ - stale-job recovery,
76
+ - DLQ requeue,
77
+ - retention cleanup.
78
+
79
+ The default Redis namespace is `bcp:{jobs}` so all adapter keys share one Redis Cluster hash slot.
80
+
81
+ ### Redis-compatible scheduler store
82
+
83
+ New public API:
84
+
85
+ ```ts
86
+ createRedisJobScheduleStore()
87
+ ```
88
+
89
+ The scheduler store implements the `0.2.9` `JobScheduleStore` contract on shared Redis state and uses atomic schedule leasing for multi-instance schedulers.
90
+
91
+ ### Compiled npm runtime
92
+
93
+ The published `bcp/jobs` default runtime continues to use generated `jobs.mjs` rather than requiring consumers to execute framework TypeScript directly.
94
+
95
+ The Durable Jobs package smoke imports that compiled runtime and verifies the new public APIs.
96
+
97
+ ## Public API additions
98
+
99
+ `bcp/jobs` adds:
100
+
101
+ ```text
102
+ createRedisJobQueueAdapter
103
+ createRedisJobScheduleStore
104
+ RedisCommandClient
105
+ RedisJobQueueAdapter
106
+ RedisJobScheduleStore
107
+ RedisJobsAdapterOptions
108
+
109
+ CleanupJobsOptions
110
+ DeadLetterJobRecord
111
+ HeartbeatJobOptions
112
+ JobQueueStats
113
+ ProcessNextJobOptions
114
+ RecoverStaleJobsOptions
115
+ RequeueDeadLetterOptions
116
+ ReserveJobOptions
117
+ ```
118
+
119
+ Existing queue and scheduling APIs remain available.
120
+
121
+ ## Compatibility
122
+
123
+ The release is intended to be backward compatible with `0.2.9`.
124
+
125
+ Existing `JobQueueAdapter` implementations remain valid because durable capabilities are optional and the new reservation metadata is provided as an optional second parameter to `reserve()`.
126
+
127
+ Adapters that need production visibility-timeout semantics should implement the new durable methods.
128
+
129
+ ## Delivery semantics
130
+
131
+ Durable jobs use at-least-once processing semantics.
132
+
133
+ Applications should make side-effecting handlers idempotent when duplicate execution would be unsafe.
134
+
135
+ ## Validation target
136
+
137
+ Before tagging/publishing `0.2.10`, run:
138
+
139
+ ```bash
140
+ npm run typecheck
141
+ npm run test:unit
142
+ npm run test:integration
143
+ npm run test:e2e
144
+ npm run test:package
145
+ npm run rc:check
146
+ ```
147
+
148
+ The final release tag must point to the exact commit that passed the complete RC sequence.
@@ -0,0 +1,180 @@
1
+ # BCP Framework 0.2.11 — Workflow Orchestration
2
+
3
+ > Release state: unreleased development target until RC validation, tagging and npm publication complete.
4
+
5
+ `0.2.11` adds server-side workflow orchestration on top of the durable jobs foundation introduced in `0.2.8`–`0.2.10`.
6
+
7
+ ## Highlights
8
+
9
+ - new `bcp/workflow` server-only public entrypoint,
10
+ - sequential workflow steps,
11
+ - parallel step groups,
12
+ - per-step retry policies,
13
+ - persisted workflow delays,
14
+ - manual resume/retry/cancel controls,
15
+ - saga-style compensation in reverse completion order,
16
+ - `WorkflowStore` persistence contract,
17
+ - workflow run leases via atomic `claim()` / `release()`,
18
+ - optional `bcp/jobs` queue-backed execution,
19
+ - delayed workflow continuation through the job queue,
20
+ - compiled `workflow.mjs` runtime in the npm package,
21
+ - browser/client bundle boundary enforcement,
22
+ - unit and prepared-package smoke coverage.
23
+
24
+ ## Basic API
25
+
26
+ ```ts
27
+ import {
28
+ createWorkflow,
29
+ } from "bcp/workflow";
30
+
31
+ const onboarding =
32
+ createWorkflow<{
33
+ userId: number;
34
+ }>(
35
+ "user.onboarding",
36
+ workflow => {
37
+ workflow.step(
38
+ "profile",
39
+ createProfile
40
+ );
41
+
42
+ workflow.parallel(
43
+ "initialize",
44
+ parallel => {
45
+ parallel.step(
46
+ "preferences",
47
+ createPreferences
48
+ );
49
+ parallel.step(
50
+ "workspace",
51
+ createWorkspace
52
+ );
53
+ }
54
+ );
55
+
56
+ workflow.delay(
57
+ "cooldown",
58
+ 1_000
59
+ );
60
+ }
61
+ );
62
+ ```
63
+
64
+ ## Queue-backed workflows
65
+
66
+ Applications may provide an existing `BackgroundJobQueue`:
67
+
68
+ ```ts
69
+ const workflow =
70
+ createWorkflow(
71
+ "order.fulfillment",
72
+ defineWorkflow,
73
+ {
74
+ queue: jobs,
75
+ store:
76
+ workflowStore,
77
+ }
78
+ );
79
+ ```
80
+
81
+ `start()`, `retry()` and `resume()` then enqueue workflow execution rather than executing the run inline. Delay steps enqueue delayed continuation jobs.
82
+
83
+ The queue retains responsibility for delivery, worker concurrency, visibility timeout, heartbeat and stale-job recovery.
84
+
85
+ ## Persistence
86
+
87
+ `createMemoryWorkflowStore()` is included for development/tests.
88
+
89
+ Production multi-instance deployments should implement `WorkflowStore` with shared durable storage. `claim()` must atomically lease a workflow run so multiple workers cannot execute the same run concurrently.
90
+
91
+ ## Retry behavior
92
+
93
+ A step can define:
94
+
95
+ ```ts
96
+ {
97
+ maxAttempts: 3,
98
+ retryDelayMs:
99
+ attempt =>
100
+ attempt * 1_000,
101
+ }
102
+ ```
103
+
104
+ A run that remains failed after its step retry policy is exhausted can later be retried with:
105
+
106
+ ```ts
107
+ await workflow.retry(
108
+ runId
109
+ );
110
+ ```
111
+
112
+ Already-successful preceding steps remain complete.
113
+
114
+ ## Compensation
115
+
116
+ Steps can define compensation handlers:
117
+
118
+ ```ts
119
+ workflow.step(
120
+ "reserve-stock",
121
+ reserveStock,
122
+ {
123
+ compensate:
124
+ releaseStock,
125
+ }
126
+ );
127
+ ```
128
+
129
+ Then:
130
+
131
+ ```ts
132
+ await workflow.compensate(
133
+ runId
134
+ );
135
+ ```
136
+
137
+ Successful compensatable steps execute in reverse completion order.
138
+
139
+ ## Delays
140
+
141
+ Delay steps persist `waiting` state and `waitUntil` rather than holding a long-running process timer.
142
+
143
+ Without a queue, application code calls `resume()` after the due time. With a queue, BCP schedules a delayed workflow continuation job.
144
+
145
+ ## Compatibility
146
+
147
+ `0.2.11` is additive relative to `0.2.10`.
148
+
149
+ Existing `bcp/jobs`, scheduler, Redis durable adapters and all prior public entrypoints remain supported.
150
+
151
+ No intentional breaking changes are introduced from the `0.2.10` baseline.
152
+
153
+ ## Validation
154
+
155
+ Before release:
156
+
157
+ ```bash
158
+ npm run typecheck
159
+ npm run test:unit
160
+ npm run test:integration
161
+ npm run test:e2e
162
+ npm run test:package
163
+ npm run rc:check
164
+ ```
165
+
166
+ Workflow validation covers:
167
+
168
+ - sequential step execution,
169
+ - transient step retry,
170
+ - parallel groups,
171
+ - persisted delay/resume semantics,
172
+ - manual failed-run retry,
173
+ - reverse compensation order,
174
+ - queue-backed execution,
175
+ - workflow store lease behavior,
176
+ - browser bundle rejection,
177
+ - compiled `workflow.mjs` package execution,
178
+ - API/platform/docs manifest parity.
179
+
180
+ The final release tag must point to the exact commit that passed the complete RC sequence.
@@ -0,0 +1,374 @@
1
+ # Workflow Orchestration
2
+
3
+ BCP `0.2.11` adds a server-only workflow orchestration layer through `bcp/workflow`.
4
+
5
+ The workflow runtime is designed for application processes that need to coordinate multiple backend operations as one observable run instead of manually chaining unrelated jobs.
6
+
7
+ ## Public entrypoint
8
+
9
+ ```ts
10
+ import {
11
+ createMemoryWorkflowStore,
12
+ createWorkflow,
13
+ } from "bcp/workflow";
14
+ ```
15
+
16
+ `bcp/workflow` is server-only and must not be imported into page/client bundles.
17
+
18
+ ## Basic workflow
19
+
20
+ ```ts
21
+ import {
22
+ createWorkflow,
23
+ } from "bcp/workflow";
24
+
25
+ export const onboarding =
26
+ createWorkflow<{
27
+ userId: number;
28
+ }>(
29
+ "user.onboarding",
30
+ workflow => {
31
+ workflow.step(
32
+ "create-profile",
33
+ async ({ input }) => {
34
+ await createProfile(
35
+ input.userId
36
+ );
37
+ }
38
+ );
39
+
40
+ workflow.step(
41
+ "send-email",
42
+ async ({ input }) => {
43
+ await sendWelcomeEmail(
44
+ input.userId
45
+ );
46
+ }
47
+ );
48
+ }
49
+ );
50
+ ```
51
+
52
+ Start a run:
53
+
54
+ ```ts
55
+ const run =
56
+ await onboarding.start({
57
+ userId: 42,
58
+ });
59
+ ```
60
+
61
+ Without a background queue, `start()` executes immediately until the workflow either succeeds, fails, is cancelled or reaches a delay.
62
+
63
+ ## Step retries
64
+
65
+ Each step can define its own retry policy:
66
+
67
+ ```ts
68
+ workflow.step(
69
+ "charge-card",
70
+ chargeCard,
71
+ {
72
+ maxAttempts: 3,
73
+ retryDelayMs:
74
+ attempt =>
75
+ attempt * 1_000,
76
+ }
77
+ );
78
+ ```
79
+
80
+ The runtime persists the step attempt count and terminal error in the workflow run record.
81
+
82
+ Step retries happen inside the workflow execution attempt. Long-running production workflows should prefer idempotent handlers so a process restart or at-least-once queue delivery can safely repeat work.
83
+
84
+ ## Parallel steps
85
+
86
+ Use `parallel()` when independent operations can run together:
87
+
88
+ ```ts
89
+ workflow.parallel(
90
+ "initialize",
91
+ parallel => {
92
+ parallel.step(
93
+ "preferences",
94
+ createPreferences
95
+ );
96
+
97
+ parallel.step(
98
+ "workspace",
99
+ createDefaultWorkspace
100
+ );
101
+ }
102
+ );
103
+ ```
104
+
105
+ All child step records are persisted under the parent parallel step.
106
+
107
+ The parallel group succeeds only when every child succeeds.
108
+
109
+ ## Delays
110
+
111
+ A workflow delay is persisted instead of blocking a process with a long timer:
112
+
113
+ ```ts
114
+ workflow.delay(
115
+ "cooldown",
116
+ 5 * 60 * 1000
117
+ );
118
+ ```
119
+
120
+ The run transitions to:
121
+
122
+ ```text
123
+ waiting
124
+ ```
125
+
126
+ with a persisted `waitUntil` timestamp.
127
+
128
+ For an in-process workflow, resume after the timestamp:
129
+
130
+ ```ts
131
+ await workflow.resume(
132
+ run.id
133
+ );
134
+ ```
135
+
136
+ Calling `resume()` before `waitUntil` returns the current waiting run unchanged.
137
+
138
+ For administrative tooling, a forced resume is available:
139
+
140
+ ```ts
141
+ await workflow.resume(
142
+ run.id,
143
+ {
144
+ force: true,
145
+ }
146
+ );
147
+ ```
148
+
149
+ ## Queue-backed execution
150
+
151
+ A workflow can use the BCP background job queue as its execution layer:
152
+
153
+ ```ts
154
+ import {
155
+ createJobQueue,
156
+ } from "bcp/jobs";
157
+ import {
158
+ createWorkflow,
159
+ } from "bcp/workflow";
160
+
161
+ const jobs =
162
+ createJobQueue({
163
+ adapter:
164
+ redisQueueAdapter,
165
+ });
166
+
167
+ export const workflow =
168
+ createWorkflow(
169
+ "order.fulfillment",
170
+ defineOrderWorkflow,
171
+ {
172
+ queue: jobs,
173
+ store:
174
+ workflowStore,
175
+ }
176
+ );
177
+ ```
178
+
179
+ When a queue is configured:
180
+
181
+ - `start()` persists the run and enqueues execution,
182
+ - `retry()` enqueues another execution attempt,
183
+ - `resume()` enqueues continuation,
184
+ - workflow delays enqueue a delayed continuation job,
185
+ - worker concurrency, visibility leases, heartbeat and stale-job recovery remain owned by `bcp/jobs`.
186
+
187
+ This keeps job delivery and workflow state as separate contracts.
188
+
189
+ ## Workflow store
190
+
191
+ `WorkflowStore` is the persistence boundary:
192
+
193
+ ```ts
194
+ interface WorkflowStore {
195
+ create(run): Promise<void>;
196
+ update(run): Promise<void>;
197
+ get(id): Promise<WorkflowRunRecord | null>;
198
+ list(workflowName?): Promise<WorkflowRunRecord[]>;
199
+
200
+ claim(
201
+ id,
202
+ {
203
+ ownerId,
204
+ now,
205
+ leaseMs,
206
+ }
207
+ ): Promise<boolean>;
208
+
209
+ release(
210
+ id,
211
+ ownerId
212
+ ): Promise<void>;
213
+ }
214
+ ```
215
+
216
+ The built-in:
217
+
218
+ ```ts
219
+ createMemoryWorkflowStore()
220
+ ```
221
+
222
+ is intended for local development and deterministic tests.
223
+
224
+ Production multi-instance deployments should use a shared durable store whose `claim()` operation is atomic.
225
+
226
+ The claim is the workflow concurrency boundary. Only one owner should execute a run while its lease is active.
227
+
228
+ ## Run states
229
+
230
+ Workflow runs use:
231
+
232
+ ```text
233
+ pending
234
+ running
235
+ waiting
236
+ succeeded
237
+ failed
238
+ cancelled
239
+ compensating
240
+ compensated
241
+ ```
242
+
243
+ Step records use the corresponding lifecycle states where applicable.
244
+
245
+ ## Inspect runs
246
+
247
+ ```ts
248
+ const run =
249
+ await workflow.get(
250
+ runId
251
+ );
252
+
253
+ const runs =
254
+ await workflow.list();
255
+ ```
256
+
257
+ A run includes:
258
+
259
+ - workflow name,
260
+ - input payload,
261
+ - current state,
262
+ - timestamps,
263
+ - current step,
264
+ - step attempts/errors,
265
+ - parallel child state,
266
+ - `waitUntil`,
267
+ - completion order,
268
+ - current workflow lease metadata.
269
+
270
+ ## Manual retry
271
+
272
+ A failed run can retry its failed step without rerunning already successful preceding steps:
273
+
274
+ ```ts
275
+ await workflow.retry(
276
+ runId
277
+ );
278
+ ```
279
+
280
+ The failed step attempt counter is reset for the new manual retry cycle.
281
+
282
+ ## Compensation
283
+
284
+ A successful step can provide a compensation handler:
285
+
286
+ ```ts
287
+ workflow.step(
288
+ "reserve-stock",
289
+ reserveStock,
290
+ {
291
+ compensate:
292
+ releaseStock,
293
+ }
294
+ );
295
+ ```
296
+
297
+ If a later step fails, application code can request compensation:
298
+
299
+ ```ts
300
+ await workflow.compensate(
301
+ runId
302
+ );
303
+ ```
304
+
305
+ Compensation runs successful compensatable steps in reverse completion order.
306
+
307
+ This is a saga-style compensation mechanism. It does not provide database transaction rollback across external services.
308
+
309
+ Cancellation can also request compensation:
310
+
311
+ ```ts
312
+ await workflow.cancel(
313
+ runId,
314
+ {
315
+ compensate: true,
316
+ }
317
+ );
318
+ ```
319
+
320
+ ## Idempotency
321
+
322
+ Workflow state and background queues provide orchestration and delivery coordination, but external side effects should still be idempotent.
323
+
324
+ For example, payment and email handlers should use application-level idempotency keys where duplicate execution would be harmful.
325
+
326
+ A durable queue provides at-least-once delivery semantics, not exactly-once external side effects.
327
+
328
+ ## Recommended production topology
329
+
330
+ ```text
331
+ API / application
332
+ |
333
+ v
334
+ WorkflowStore (shared durable state)
335
+ |
336
+ v
337
+ BCP Workflow
338
+ |
339
+ v
340
+ Durable JobQueueAdapter
341
+ |
342
+ +---+---+
343
+ | |
344
+ Worker A Worker B
345
+ ```
346
+
347
+ The workflow store and queue do not have to use the same backend.
348
+
349
+ ## Lifecycle
350
+
351
+ ```ts
352
+ await workflow.close();
353
+ await jobs.close();
354
+ ```
355
+
356
+ `workflow.close()` unregisters its internal queue handler and closes the configured workflow store when the store exposes `close()`.
357
+
358
+ The application remains responsible for lifecycle of external database/Redis clients used by custom stores/adapters.
359
+
360
+ ## Current scope
361
+
362
+ `0.2.11` intentionally focuses on the orchestration core:
363
+
364
+ - sequential steps,
365
+ - parallel steps,
366
+ - step retries,
367
+ - persisted delays,
368
+ - manual retry/resume/cancel,
369
+ - compensation,
370
+ - workflow state persistence,
371
+ - run leases,
372
+ - optional `bcp/jobs` execution.
373
+
374
+ Transactional outbox/event delivery remains a separate future milestone so workflow orchestration does not pretend to make a database transaction and external queue publish atomic.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
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",
@@ -67,6 +67,11 @@
67
67
  "browser": "./packages/client/src/server-only.browser.mjs",
68
68
  "default": "./packages/client/src/jobs.mjs"
69
69
  },
70
+ "./workflow": {
71
+ "types": "./packages/client/src/workflow.ts",
72
+ "browser": "./packages/client/src/server-only.browser.mjs",
73
+ "default": "./packages/client/src/workflow.mjs"
74
+ },
70
75
  "./observability": {
71
76
  "types": "./packages/client/src/observability.ts",
72
77
  "browser": "./packages/client/src/server-only.browser.mjs",
@@ -29,6 +29,7 @@ const SERVER_ONLY_IMPORTS =
29
29
  "bcp/database",
30
30
  "bcp/auth",
31
31
  "bcp/jobs",
32
+ "bcp/workflow",
32
33
  "bcp/observability",
33
34
  ]);
34
35