@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.
- package/README.md +196 -129
- package/docs/README.md +49 -43
- package/docs/api-manifest.json +15 -2
- package/docs/api-reference.md +113 -8
- package/docs/docs-web-manifest.json +7 -3
- package/docs/durable-jobs.md +359 -0
- package/docs/platform-manifest.json +26 -4
- package/docs/releases/0.2.10.md +148 -0
- package/docs/releases/0.2.11.md +180 -0
- package/docs/workflow-orchestration.md +374 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/jobs.mjs +1310 -61
- package/packages/client/src/jobs.ts +17 -0
- package/packages/client/src/workflow.mjs +601 -0
- package/packages/client/src/workflow.ts +23 -0
- package/packages/server/src/jobs-redis.ts +1226 -0
- package/packages/server/src/jobs.ts +768 -128
- package/packages/server/src/workflow.ts +887 -0
|
@@ -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.
|
|
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",
|