@wooksjs/event-wf 0.6.6 → 0.7.0
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 +7 -65
- package/dist/index.cjs +119 -59
- package/dist/index.d.ts +74 -27
- package/dist/index.mjs +102 -55
- package/package.json +6 -6
- package/skills/wooksjs-event-wf/SKILL.md +7 -7
- package/skills/wooksjs-event-wf/core.md +133 -67
- package/skills/wooksjs-event-wf/event-core.md +251 -372
- package/skills/wooksjs-event-wf/workflows.md +39 -48
package/dist/index.mjs
CHANGED
|
@@ -1,38 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EventContext, current, defineEventKind, key, run, slot, useLogger, useRouteParams } from "@wooksjs/event-core";
|
|
2
2
|
import { StepRetriableError, Workflow, createStep } from "@prostojs/wf";
|
|
3
3
|
import { WooksAdapterBase } from "wooks";
|
|
4
4
|
|
|
5
|
-
//#region packages/event-wf/src/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
options
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
/** Creates an async event context for resuming a paused workflow. */
|
|
18
|
-
function resumeWfContext(data, options) {
|
|
19
|
-
return createAsyncEventContext({
|
|
20
|
-
event: {
|
|
21
|
-
...data,
|
|
22
|
-
type: "WF"
|
|
23
|
-
},
|
|
24
|
-
resume: true,
|
|
25
|
-
options
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Wrapper on top of useEventContext that provides
|
|
30
|
-
* proper context types for WF event
|
|
31
|
-
* @returns set of hooks { getCtx, restoreCtx, clearCtx, hookStore, getStore, setStore }
|
|
32
|
-
*/
|
|
33
|
-
function useWFContext() {
|
|
34
|
-
return useAsyncEventContext("WF");
|
|
35
|
-
}
|
|
5
|
+
//#region packages/event-wf/src/wf-kind.ts
|
|
6
|
+
const wfKind = defineEventKind("WF", {
|
|
7
|
+
schemaId: slot(),
|
|
8
|
+
stepId: slot(),
|
|
9
|
+
inputContext: slot(),
|
|
10
|
+
indexes: slot(),
|
|
11
|
+
input: slot()
|
|
12
|
+
});
|
|
13
|
+
const resumeKey = key("wf.resume");
|
|
36
14
|
|
|
37
15
|
//#endregion
|
|
38
16
|
//#region packages/event-wf/src/composables/wf-state.ts
|
|
@@ -46,18 +24,58 @@ function useWFContext() {
|
|
|
46
24
|
* ```
|
|
47
25
|
*/
|
|
48
26
|
function useWfState() {
|
|
49
|
-
const
|
|
50
|
-
const event = store("event");
|
|
27
|
+
const c = current();
|
|
51
28
|
return {
|
|
52
|
-
ctx: () =>
|
|
53
|
-
input: () =>
|
|
54
|
-
schemaId:
|
|
55
|
-
stepId: () =>
|
|
56
|
-
indexes: () =>
|
|
57
|
-
resume:
|
|
29
|
+
ctx: () => c.get(wfKind.keys.inputContext),
|
|
30
|
+
input: () => c.get(wfKind.keys.input),
|
|
31
|
+
schemaId: c.get(wfKind.keys.schemaId),
|
|
32
|
+
stepId: () => c.get(wfKind.keys.stepId),
|
|
33
|
+
indexes: () => c.get(wfKind.keys.indexes),
|
|
34
|
+
resume: c.get(resumeKey)
|
|
58
35
|
};
|
|
59
36
|
}
|
|
60
37
|
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region packages/event-wf/src/event-wf.ts
|
|
40
|
+
const wfSeeds = (data) => ({
|
|
41
|
+
schemaId: data.schemaId,
|
|
42
|
+
stepId: data.stepId,
|
|
43
|
+
inputContext: data.inputContext,
|
|
44
|
+
indexes: data.indexes,
|
|
45
|
+
input: data.input
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Creates a new event context for a fresh workflow execution.
|
|
49
|
+
* When `parentCtx` is provided, the workflow creates a child context
|
|
50
|
+
* linked to the parent, so step handlers can transparently access
|
|
51
|
+
* composables from the parent scope (e.g. HTTP) via the parent chain.
|
|
52
|
+
*/
|
|
53
|
+
function createWfContext(data, options, parentCtx) {
|
|
54
|
+
const ctx = new EventContext(parentCtx ? {
|
|
55
|
+
...options,
|
|
56
|
+
parent: parentCtx
|
|
57
|
+
} : options);
|
|
58
|
+
return (fn) => run(ctx, () => {
|
|
59
|
+
ctx.set(resumeKey, false);
|
|
60
|
+
return ctx.seed(wfKind, wfSeeds(data), fn);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates an event context for resuming a paused workflow.
|
|
65
|
+
* When `parentCtx` is provided, the workflow creates a child context
|
|
66
|
+
* linked to the parent.
|
|
67
|
+
*/
|
|
68
|
+
function resumeWfContext(data, options, parentCtx) {
|
|
69
|
+
const ctx = new EventContext(parentCtx ? {
|
|
70
|
+
...options,
|
|
71
|
+
parent: parentCtx
|
|
72
|
+
} : options);
|
|
73
|
+
return (fn) => run(ctx, () => {
|
|
74
|
+
ctx.set(resumeKey, true);
|
|
75
|
+
return ctx.seed(wfKind, wfSeeds(data), fn);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
//#endregion
|
|
62
80
|
//#region packages/event-wf/src/workflow.ts
|
|
63
81
|
/** Workflow engine that resolves steps via Wooks router lookup. */
|
|
@@ -69,10 +87,10 @@ var WooksWorkflow = class extends Workflow {
|
|
|
69
87
|
resolveStep(stepId) {
|
|
70
88
|
const stepIdNorm = `/${stepId}`.replace(/\/{2,}/gu, "/");
|
|
71
89
|
try {
|
|
72
|
-
const
|
|
90
|
+
const ctx = current();
|
|
73
91
|
const found = this.wooks.lookup("WF_STEP", stepIdNorm);
|
|
74
92
|
if (found.handlers?.length) {
|
|
75
|
-
|
|
93
|
+
ctx.set(wfKind.keys.stepId, stepIdNorm);
|
|
76
94
|
return found.handlers[0]();
|
|
77
95
|
}
|
|
78
96
|
} catch {
|
|
@@ -122,15 +140,40 @@ var WooksWf = class extends WooksAdapterBase {
|
|
|
122
140
|
id
|
|
123
141
|
}));
|
|
124
142
|
}
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Starts a new workflow execution from the beginning.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* // Simple
|
|
149
|
+
* await app.start('my-flow', { result: 0 })
|
|
150
|
+
*
|
|
151
|
+
* // With options
|
|
152
|
+
* await app.start('my-flow', { result: 0 }, { input: 5, eventContext: current() })
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
start(schemaId, inputContext, opts) {
|
|
156
|
+
const parentCtx = opts?.eventContext;
|
|
157
|
+
return this._start(schemaId, inputContext, void 0, opts, parentCtx);
|
|
128
158
|
}
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Resumes a previously paused workflow from saved state.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* // With user input
|
|
165
|
+
* await app.resume(output.state, { input: userInput })
|
|
166
|
+
*
|
|
167
|
+
* // Simple retry (no input)
|
|
168
|
+
* await app.resume(output.state)
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
resume(state, opts) {
|
|
172
|
+
const parentCtx = opts?.eventContext;
|
|
173
|
+
return this._start(state.schemaId, state.context, state.indexes, opts, parentCtx);
|
|
132
174
|
}
|
|
133
|
-
async _start(schemaId, inputContext, indexes,
|
|
175
|
+
async _start(schemaId, inputContext, indexes, opts, parentCtx) {
|
|
176
|
+
const { input, spy, cleanup } = opts ?? {};
|
|
134
177
|
const resume = !!indexes?.length;
|
|
135
178
|
const runInContext = (resume ? resumeWfContext : createWfContext)({
|
|
136
179
|
inputContext,
|
|
@@ -138,7 +181,7 @@ var WooksWf = class extends WooksAdapterBase {
|
|
|
138
181
|
stepId: null,
|
|
139
182
|
indexes,
|
|
140
183
|
input
|
|
141
|
-
}, this.
|
|
184
|
+
}, this.getEventContextOptions(), parentCtx);
|
|
142
185
|
return runInContext(async () => {
|
|
143
186
|
const { handlers: foundHandlers } = this.wooks.lookup("WF_FLOW", `/${schemaId}`.replace(/^\/+/u, "/"));
|
|
144
187
|
const handlers = foundHandlers || this.opts?.onNotFound && [this.opts.onNotFound] || null;
|
|
@@ -149,8 +192,8 @@ var WooksWf = class extends WooksAdapterBase {
|
|
|
149
192
|
if (spy) spy(...args);
|
|
150
193
|
if (firstStep && args[0] === "step") {
|
|
151
194
|
firstStep = false;
|
|
152
|
-
const
|
|
153
|
-
|
|
195
|
+
const ctx = current();
|
|
196
|
+
ctx.set(wfKind.keys.input, void 0);
|
|
154
197
|
}
|
|
155
198
|
};
|
|
156
199
|
try {
|
|
@@ -174,7 +217,11 @@ var WooksWf = class extends WooksAdapterBase {
|
|
|
174
217
|
throw error;
|
|
175
218
|
}
|
|
176
219
|
clean();
|
|
177
|
-
if (result.resume) result.resume = (_input) => this.resume(result.state,
|
|
220
|
+
if (result.resume) result.resume = (_input) => this.resume(result.state, {
|
|
221
|
+
input: _input,
|
|
222
|
+
spy,
|
|
223
|
+
cleanup
|
|
224
|
+
});
|
|
178
225
|
return result;
|
|
179
226
|
}
|
|
180
227
|
clean();
|
|
@@ -205,7 +252,7 @@ var WooksWf = class extends WooksAdapterBase {
|
|
|
205
252
|
* ```ts
|
|
206
253
|
* const app = createWfApp()
|
|
207
254
|
* app.step('process', { handler: (ctx) => ctx })
|
|
208
|
-
* app.flow('my-flow', [
|
|
255
|
+
* app.flow('my-flow', ['process'])
|
|
209
256
|
* await app.start('my-flow', { data: 'hello' })
|
|
210
257
|
* ```
|
|
211
258
|
*/
|
|
@@ -214,4 +261,4 @@ function createWfApp(opts, wooks) {
|
|
|
214
261
|
}
|
|
215
262
|
|
|
216
263
|
//#endregion
|
|
217
|
-
export { StepRetriableError, WooksWf, createWfApp, createWfContext, resumeWfContext,
|
|
264
|
+
export { StepRetriableError, WooksWf, createWfApp, createWfContext, resumeKey, resumeWfContext, useLogger, useRouteParams, useWfState, wfKind, wfShortcuts };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-wf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "@wooksjs/event-wf",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"directory": "packages/event-wf"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
26
|
-
"
|
|
26
|
+
"wooksjs-event-wf-skill": "scripts/setup-skills.js"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"typescript": "^5.9.3",
|
|
49
49
|
"vitest": "^3.2.4",
|
|
50
|
-
"@wooksjs/event-core": "^0.
|
|
51
|
-
"wooks": "^0.
|
|
50
|
+
"@wooksjs/event-core": "^0.7.0",
|
|
51
|
+
"wooks": "^0.7.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@prostojs/logger": "^0.4.3",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
55
|
+
"wooks": "^0.7.0",
|
|
56
|
+
"@wooksjs/event-core": "^0.7.0"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "rolldown -c ../../rolldown.config.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wooksjs-event-wf
|
|
3
|
-
description: Wooks Workflow framework — composable, step-based workflow engine for Node.js. Load when building workflow/process automation with wooks; defining workflow steps and flows; using workflow composables (useWfState, useRouteParams); working with @wooksjs/event-core context
|
|
3
|
+
description: Wooks Workflow framework — composable, step-based workflow engine for Node.js. Load when building workflow/process automation with wooks; defining workflow steps and flows; using workflow composables (useWfState, useRouteParams, useLogger); working with @wooksjs/event-core context (key, cached, defineWook, defineEventKind); creating conditional and looping flows; resuming paused workflows; handling user input requirements; using string-based step handlers; attaching workflow spies; working with StepRetriableError; creating custom event context composables for workflows; sharing parent event context (eventContext) for HTTP integration; accessing HTTP composables from workflow steps.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# @wooksjs/event-wf
|
|
@@ -11,11 +11,11 @@ A composable workflow framework for Node.js built on async context (AsyncLocalSt
|
|
|
11
11
|
|
|
12
12
|
Read the domain file that matches the task. Do not load all files — only what you need.
|
|
13
13
|
|
|
14
|
-
| Domain
|
|
15
|
-
|
|
16
|
-
| Event context (core machinery) | [event-core.md](event-core.md) | Understanding
|
|
17
|
-
| Workflow app setup
|
|
18
|
-
| Steps & flows
|
|
14
|
+
| Domain | File | Load when... |
|
|
15
|
+
| ------------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| Event context (core machinery) | [event-core.md](event-core.md) | Understanding `EventContext`, `key()`/`cached()`/`defineWook()`/`defineEventKind()`/`slot()`, creating custom composables, lazy evaluation and caching, building your own `use*()` functions |
|
|
17
|
+
| Workflow app setup | [core.md](core.md) | Creating a workflow app, `createWfApp`, starting/resuming workflows, error handling, spies, testing, logging, sharing parent event context (`eventContext`), HTTP integration |
|
|
18
|
+
| Steps & flows | [workflows.md](workflows.md) | Defining steps (`app.step`), defining flows (`app.flow`), workflow schemas, conditions, loops, user input, parametric steps, `useWfState`, `StepRetriableError`, string-based handlers |
|
|
19
19
|
|
|
20
20
|
## Quick reference
|
|
21
21
|
|
|
@@ -39,4 +39,4 @@ const output = await app.start('calculate', { result: 0 })
|
|
|
39
39
|
console.log(output.state.context) // { result: 10 }
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Key exports: `createWfApp()`, `useWfState()`, `
|
|
42
|
+
Key exports: `createWfApp()`, `useWfState()`, `useRouteParams()`, `useLogger()`, `StepRetriableError`.
|
|
@@ -9,6 +9,7 @@ For the underlying event context store API (`init`, `get`, `set`, `hook`, etc.)
|
|
|
9
9
|
`@wooksjs/event-wf` is the workflow adapter for Wooks. It wraps the `@prostojs/wf` workflow engine, adding composable context management via `AsyncLocalStorage`. Each workflow execution gets its own isolated context store, and step handlers can call composable functions (`useWfState()`, `useRouteParams()`, etc.) from anywhere.
|
|
10
10
|
|
|
11
11
|
Key principles:
|
|
12
|
+
|
|
12
13
|
1. **Steps are route handlers** — Steps are registered with IDs that are resolved via the Wooks router, supporting parametric step IDs (`:param`), wildcards, and regex constraints.
|
|
13
14
|
2. **Flows are schemas** — Flows define the execution order of steps, with conditions, loops, and branching.
|
|
14
15
|
3. **Pause and resume** — Workflows can pause for user input and resume from saved state.
|
|
@@ -28,7 +29,9 @@ import { createWfApp } from '@wooksjs/event-wf'
|
|
|
28
29
|
const app = createWfApp<{ result: number }>()
|
|
29
30
|
|
|
30
31
|
app.step('increment', {
|
|
31
|
-
handler: (ctx) => {
|
|
32
|
+
handler: (ctx) => {
|
|
33
|
+
ctx.result++
|
|
34
|
+
},
|
|
32
35
|
})
|
|
33
36
|
|
|
34
37
|
app.flow('my-flow', [{ id: 'increment' }])
|
|
@@ -43,11 +46,11 @@ Options:
|
|
|
43
46
|
|
|
44
47
|
```ts
|
|
45
48
|
interface TWooksWfOptions {
|
|
46
|
-
onError?: (e: Error) => void
|
|
47
|
-
onNotFound?: TWooksHandler
|
|
49
|
+
onError?: (e: Error) => void // custom error handler
|
|
50
|
+
onNotFound?: TWooksHandler // handler when flow not found
|
|
48
51
|
onUnknownFlow?: (schemaId: string, raiseError: () => void) => unknown
|
|
49
|
-
logger?: TConsoleBase
|
|
50
|
-
eventOptions?:
|
|
52
|
+
logger?: TConsoleBase // custom logger
|
|
53
|
+
eventOptions?: EventContextOptions // event context options (logger, parent)
|
|
51
54
|
router?: {
|
|
52
55
|
ignoreTrailingSlash?: boolean
|
|
53
56
|
ignoreCase?: boolean
|
|
@@ -58,37 +61,51 @@ interface TWooksWfOptions {
|
|
|
58
61
|
|
|
59
62
|
## Starting a Workflow
|
|
60
63
|
|
|
61
|
-
### `app.start(schemaId, inputContext,
|
|
64
|
+
### `app.start(schemaId, inputContext, opts?)`
|
|
62
65
|
|
|
63
66
|
Starts a new workflow execution from the beginning:
|
|
64
67
|
|
|
65
68
|
```ts
|
|
66
69
|
const output = await app.start('my-flow', { result: 0 })
|
|
70
|
+
|
|
71
|
+
// With options
|
|
72
|
+
const output = await app.start(
|
|
73
|
+
'my-flow',
|
|
74
|
+
{ result: 0 },
|
|
75
|
+
{
|
|
76
|
+
input: 5,
|
|
77
|
+
eventContext: current(),
|
|
78
|
+
},
|
|
79
|
+
)
|
|
67
80
|
```
|
|
68
81
|
|
|
69
82
|
**Parameters:**
|
|
83
|
+
|
|
70
84
|
- `schemaId` — The flow ID registered with `app.flow()`
|
|
71
85
|
- `inputContext` — The initial context object (`T`)
|
|
72
|
-
- `
|
|
73
|
-
- `
|
|
74
|
-
- `
|
|
86
|
+
- `opts` — Optional `TWfRunOptions` object:
|
|
87
|
+
- `input` — Input for the first step (consumed after execution)
|
|
88
|
+
- `spy` — Spy function to observe step execution
|
|
89
|
+
- `cleanup` — Cleanup function called when execution ends
|
|
90
|
+
- `eventContext` — Parent `EventContext` to link to. Pass `current()` from within an active event scope (e.g. HTTP handler). The workflow creates a child context with `parent: current()`, so step handlers can access parent composables transparently via parent chain traversal.
|
|
75
91
|
|
|
76
92
|
**Return value (`TFlowOutput<T, I, IR>`):**
|
|
77
93
|
|
|
78
94
|
```ts
|
|
79
95
|
interface TFlowOutput<T, I, IR> {
|
|
80
|
-
finished: boolean
|
|
96
|
+
finished: boolean // true if workflow completed
|
|
81
97
|
state: {
|
|
82
|
-
schemaId: string
|
|
83
|
-
indexes: number[]
|
|
84
|
-
context: T
|
|
98
|
+
schemaId: string // flow ID
|
|
99
|
+
indexes: number[] // position in schema (for resume)
|
|
100
|
+
context: T // final context state
|
|
85
101
|
}
|
|
86
|
-
inputRequired?: {
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
inputRequired?: {
|
|
103
|
+
// present if paused for input
|
|
104
|
+
type: string // expected input type
|
|
105
|
+
schemaId: string // step requiring input
|
|
89
106
|
}
|
|
90
|
-
stepResult?: IR
|
|
91
|
-
resume?: (input?: I) => Promise<TFlowOutput<T, I, IR>>
|
|
107
|
+
stepResult?: IR // last step's return value
|
|
108
|
+
resume?: (input?: I) => Promise<TFlowOutput<T, I, IR>> // resume function
|
|
92
109
|
}
|
|
93
110
|
```
|
|
94
111
|
|
|
@@ -107,15 +124,20 @@ if (output.finished) {
|
|
|
107
124
|
|
|
108
125
|
## Resuming a Workflow
|
|
109
126
|
|
|
110
|
-
### `app.resume(state,
|
|
127
|
+
### `app.resume(state, opts?)`
|
|
111
128
|
|
|
112
129
|
Resumes a previously paused workflow from saved state:
|
|
113
130
|
|
|
114
131
|
```ts
|
|
115
132
|
// Resume with user-provided input
|
|
116
|
-
const resumed = await app.resume(output.state, userInput)
|
|
133
|
+
const resumed = await app.resume(output.state, { input: userInput })
|
|
134
|
+
|
|
135
|
+
// Simple retry (no input)
|
|
136
|
+
const retried = await app.resume(output.state)
|
|
117
137
|
```
|
|
118
138
|
|
|
139
|
+
The `opts` parameter accepts the same `TWfRunOptions` as `start()` — including `eventContext` to link to the active event context via a parent chain.
|
|
140
|
+
|
|
119
141
|
### Using the `resume()` function on output
|
|
120
142
|
|
|
121
143
|
The output object includes a convenience `resume()` method:
|
|
@@ -144,10 +166,7 @@ app.step('welcome', {
|
|
|
144
166
|
handler: (ctx) => console.log(`Welcome, ${ctx.username}!`),
|
|
145
167
|
})
|
|
146
168
|
|
|
147
|
-
app.flow('login', [
|
|
148
|
-
{ id: 'get-credentials' },
|
|
149
|
-
{ id: 'welcome' },
|
|
150
|
-
])
|
|
169
|
+
app.flow('login', [{ id: 'get-credentials' }, { id: 'welcome' }])
|
|
151
170
|
|
|
152
171
|
// Start — pauses at get-credentials because input is required
|
|
153
172
|
const output = await app.start('login', {})
|
|
@@ -159,7 +178,7 @@ const savedState = JSON.stringify(output.state)
|
|
|
159
178
|
|
|
160
179
|
// Later, resume with user input
|
|
161
180
|
const state = JSON.parse(savedState)
|
|
162
|
-
const final = await app.resume(state, { username: 'alice', password: 'secret' })
|
|
181
|
+
const final = await app.resume(state, { input: { username: 'alice', password: 'secret' } })
|
|
163
182
|
// final.finished === true
|
|
164
183
|
```
|
|
165
184
|
|
|
@@ -174,52 +193,50 @@ app.start(schemaId, inputContext)
|
|
|
174
193
|
→ router matches flow ID → handler runs
|
|
175
194
|
→ workflow engine executes steps sequentially
|
|
176
195
|
→ each step can call useWfState(), useRouteParams(), etc.
|
|
177
|
-
→ composables call
|
|
178
|
-
→ reads/writes the
|
|
196
|
+
→ composables call current() from @wooksjs/event-core
|
|
197
|
+
→ reads/writes the event context via key/cached accessors
|
|
179
198
|
```
|
|
180
199
|
|
|
181
200
|
### The WF Context Store
|
|
182
201
|
|
|
183
202
|
```ts
|
|
184
203
|
interface TWFContextStore {
|
|
185
|
-
resume: boolean
|
|
204
|
+
resume: boolean // true if this is a resumed execution
|
|
186
205
|
}
|
|
187
206
|
|
|
188
207
|
interface TWFEventData {
|
|
189
|
-
schemaId: string
|
|
190
|
-
stepId: string | null
|
|
191
|
-
inputContext: unknown
|
|
192
|
-
indexes?: number[]
|
|
193
|
-
input?: unknown
|
|
208
|
+
schemaId: string // flow ID being executed
|
|
209
|
+
stepId: string | null // current step ID (set during step execution)
|
|
210
|
+
inputContext: unknown // the workflow context object (T)
|
|
211
|
+
indexes?: number[] // position for resume
|
|
212
|
+
input?: unknown // input for current step
|
|
194
213
|
type: 'WF'
|
|
195
214
|
}
|
|
196
215
|
```
|
|
197
216
|
|
|
198
|
-
###
|
|
217
|
+
### Custom Composables for Workflows
|
|
199
218
|
|
|
200
|
-
|
|
201
|
-
import { useWFContext } from '@wooksjs/event-wf'
|
|
219
|
+
Use `defineWook` and `key()` from `@wooksjs/event-core` to create custom composables that store data in the event context:
|
|
202
220
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
startTime?: number
|
|
206
|
-
stepCount?: number
|
|
207
|
-
}
|
|
208
|
-
}
|
|
221
|
+
```ts
|
|
222
|
+
import { defineWook, key } from '@wooksjs/event-core'
|
|
209
223
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const { init, get, set } = store('metrics')
|
|
224
|
+
const startTimeKey = key<number>('wf.metrics.startTime')
|
|
225
|
+
const stepCountKey = key<number>('wf.metrics.stepCount')
|
|
213
226
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
227
|
+
export const useWorkflowMetrics = defineWook((ctx) => {
|
|
228
|
+
ctx.set(startTimeKey, Date.now())
|
|
229
|
+
ctx.set(stepCountKey, 0)
|
|
217
230
|
|
|
218
|
-
return {
|
|
219
|
-
|
|
231
|
+
return {
|
|
232
|
+
incrementSteps: () => ctx.set(stepCountKey, ctx.get(stepCountKey) + 1),
|
|
233
|
+
getElapsed: () => Date.now() - ctx.get(startTimeKey),
|
|
234
|
+
getStepCount: () => ctx.get(stepCountKey),
|
|
235
|
+
}
|
|
236
|
+
})
|
|
220
237
|
```
|
|
221
238
|
|
|
222
|
-
For the full context store API and composable patterns, see
|
|
239
|
+
For the full context store API and composable patterns, see the `@wooksjs/event-core` skill.
|
|
223
240
|
|
|
224
241
|
## Workflow Spies
|
|
225
242
|
|
|
@@ -241,14 +258,21 @@ app.detachSpy(spy)
|
|
|
241
258
|
### Per-execution spy
|
|
242
259
|
|
|
243
260
|
```ts
|
|
244
|
-
const output = await app.start(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
const output = await app.start(
|
|
262
|
+
'my-flow',
|
|
263
|
+
{ result: 0 },
|
|
264
|
+
{
|
|
265
|
+
spy: (event, ...args) => {
|
|
266
|
+
if (event === 'step') {
|
|
267
|
+
console.log('Step executed:', args)
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
)
|
|
249
272
|
```
|
|
250
273
|
|
|
251
274
|
The spy function receives:
|
|
275
|
+
|
|
252
276
|
- `event` — Event type (e.g., `'step'`)
|
|
253
277
|
- Additional arguments vary by event type
|
|
254
278
|
|
|
@@ -299,6 +323,49 @@ app.step('validate', {
|
|
|
299
323
|
})
|
|
300
324
|
```
|
|
301
325
|
|
|
326
|
+
## Sharing the Parent Event Context
|
|
327
|
+
|
|
328
|
+
By default, `start()` and `resume()` create an isolated event context — step handlers cannot access composables from the calling scope (e.g., HTTP composables). When `eventContext` is passed, the workflow creates a **child context** with a parent link (`parent: current()`) instead of sharing the parent context directly. The child context seeds its own WF slots locally, and slot lookups that are not found in the child automatically traverse the parent chain. This means both WF composables and parent composables (e.g., HTTP) work transparently inside step handlers.
|
|
329
|
+
|
|
330
|
+
### Use case: accessing HTTP auth data in workflow steps
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { current } from '@wooksjs/event-core'
|
|
334
|
+
import { createHttpApp, useRequest } from '@wooksjs/event-http'
|
|
335
|
+
import { createWfApp, useWfState } from '@wooksjs/event-wf'
|
|
336
|
+
|
|
337
|
+
const wf = createWfApp<{ userId: string; role: string }>()
|
|
338
|
+
|
|
339
|
+
wf.step('check-permissions', {
|
|
340
|
+
handler: () => {
|
|
341
|
+
const { ctx } = useWfState()
|
|
342
|
+
// useRequest() works because the child context traverses the parent chain
|
|
343
|
+
const { headers } = useRequest()
|
|
344
|
+
const user = decodeToken(headers.authorization)
|
|
345
|
+
ctx<{ userId: string; role: string }>().userId = user.id
|
|
346
|
+
ctx<{ userId: string; role: string }>().role = user.role
|
|
347
|
+
},
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
wf.flow('secure-action', ['check-permissions', 'do-work'])
|
|
351
|
+
|
|
352
|
+
const http = createHttpApp()
|
|
353
|
+
|
|
354
|
+
http.post('/actions/run', async () => {
|
|
355
|
+
const output = await wf.start(
|
|
356
|
+
'secure-action',
|
|
357
|
+
{ userId: '', role: '' },
|
|
358
|
+
{ eventContext: current() },
|
|
359
|
+
)
|
|
360
|
+
return output.state.context
|
|
361
|
+
})
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### When to inherit vs isolate
|
|
365
|
+
|
|
366
|
+
- **Inherit** (`eventContext: current()`) when the workflow runs entirely within a single HTTP request and steps need parent composables (auth, headers, cached user data). The child context links to the parent via a parent chain, keeping WF-specific slots isolated while providing transparent access to parent slots.
|
|
367
|
+
- **Isolate** (default) when the workflow may pause and resume across different requests, or when it should be testable without a parent context.
|
|
368
|
+
|
|
302
369
|
## Sharing Router Between Adapters
|
|
303
370
|
|
|
304
371
|
Multiple adapters can share the same Wooks router:
|
|
@@ -309,7 +376,7 @@ import { createWfApp } from '@wooksjs/event-wf'
|
|
|
309
376
|
|
|
310
377
|
const wooks = new Wooks()
|
|
311
378
|
const app1 = createWfApp({}, wooks)
|
|
312
|
-
const app2 = createWfApp({}, wooks)
|
|
379
|
+
const app2 = createWfApp({}, wooks) // shares the same routes
|
|
313
380
|
```
|
|
314
381
|
|
|
315
382
|
Or share with another adapter (e.g., HTTP):
|
|
@@ -319,7 +386,7 @@ import { createHttpApp } from '@wooksjs/event-http'
|
|
|
319
386
|
import { createWfApp } from '@wooksjs/event-wf'
|
|
320
387
|
|
|
321
388
|
const httpApp = createHttpApp()
|
|
322
|
-
const wfApp = createWfApp({}, httpApp)
|
|
389
|
+
const wfApp = createWfApp({}, httpApp) // shares httpApp's router
|
|
323
390
|
```
|
|
324
391
|
|
|
325
392
|
## Testing
|
|
@@ -332,13 +399,12 @@ import { createWfApp } from '@wooksjs/event-wf'
|
|
|
332
399
|
const app = createWfApp<{ count: number }>()
|
|
333
400
|
|
|
334
401
|
app.step('increment', {
|
|
335
|
-
handler: (ctx) => {
|
|
402
|
+
handler: (ctx) => {
|
|
403
|
+
ctx.count++
|
|
404
|
+
},
|
|
336
405
|
})
|
|
337
406
|
|
|
338
|
-
app.flow('test-flow', [
|
|
339
|
-
{ id: 'increment' },
|
|
340
|
-
{ id: 'increment' },
|
|
341
|
-
])
|
|
407
|
+
app.flow('test-flow', [{ id: 'increment' }, { id: 'increment' }])
|
|
342
408
|
|
|
343
409
|
// Test:
|
|
344
410
|
const output = await app.start('test-flow', { count: 0 })
|
|
@@ -359,7 +425,7 @@ app.flow('resume-flow', [{ id: 'needs-input' }])
|
|
|
359
425
|
const output = await app.start('resume-flow', { count: 0 })
|
|
360
426
|
expect(output.finished).toBe(false)
|
|
361
427
|
|
|
362
|
-
const final = await app.resume(output.state, 42)
|
|
428
|
+
const final = await app.resume(output.state, { input: 42 })
|
|
363
429
|
expect(final.state.context.count).toBe(42)
|
|
364
430
|
expect(final.finished).toBe(true)
|
|
365
431
|
```
|
|
@@ -369,12 +435,12 @@ expect(final.finished).toBe(true)
|
|
|
369
435
|
Inside a step handler, use the event-scoped logger:
|
|
370
436
|
|
|
371
437
|
```ts
|
|
372
|
-
import {
|
|
438
|
+
import { useLogger } from '@wooksjs/event-core'
|
|
373
439
|
|
|
374
440
|
app.step('process', {
|
|
375
441
|
handler: (ctx) => {
|
|
376
|
-
const logger =
|
|
377
|
-
logger.
|
|
442
|
+
const logger = useLogger()
|
|
443
|
+
logger.info('Processing...')
|
|
378
444
|
ctx.processed = true
|
|
379
445
|
},
|
|
380
446
|
})
|