@chidchanun/bcp 0.2.10 → 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,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.10",
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