@wooksjs/event-wf 0.7.15 → 0.7.16
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/dist/index.cjs +108 -22
- package/dist/index.d.ts +95 -22
- package/dist/index.mjs +107 -23
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -57,8 +57,6 @@ function resumeWfContext(options, seeds, fn) {
|
|
|
57
57
|
//#region packages/event-wf/src/outlets/outlet-context.ts
|
|
58
58
|
/** Registered outlet handlers, keyed by name */
|
|
59
59
|
const outletsRegistryKey = (0, _wooksjs_event_core.key)("wf.outlets.registry");
|
|
60
|
-
/** Active state strategy for current request */
|
|
61
|
-
const stateStrategyKey = (0, _wooksjs_event_core.key)("wf.outlets.stateStrategy");
|
|
62
60
|
/** Finished response set by workflow steps */
|
|
63
61
|
const wfFinishedKey = (0, _wooksjs_event_core.key)("wf.outlets.finished");
|
|
64
62
|
|
|
@@ -72,7 +70,6 @@ const wfFinishedKey = (0, _wooksjs_event_core.key)("wf.outlets.finished");
|
|
|
72
70
|
* where steps need to inspect or modify outlet state directly.
|
|
73
71
|
*/
|
|
74
72
|
const useWfOutlet = (0, _wooksjs_event_core.defineWook)((ctx) => ({
|
|
75
|
-
getStateStrategy: () => ctx.get(stateStrategyKey),
|
|
76
73
|
getOutlets: () => ctx.get(outletsRegistryKey),
|
|
77
74
|
getOutlet: (name) => ctx.get(outletsRegistryKey)?.get(name) ?? null
|
|
78
75
|
}));
|
|
@@ -96,8 +93,81 @@ const useWfFinished = (0, _wooksjs_event_core.defineWook)((ctx) => ({
|
|
|
96
93
|
get: () => ctx.has(wfFinishedKey) ? ctx.get(wfFinishedKey) : void 0
|
|
97
94
|
}));
|
|
98
95
|
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region packages/event-wf/src/strategy-context.ts
|
|
98
|
+
/** Name of the strategy currently active for the running workflow. */
|
|
99
|
+
const stateStrategyNameKey = (0, _wooksjs_event_core.key)("wf.strategyName");
|
|
100
|
+
/** Strategy names must match this regex (validated at swap call + config). */
|
|
101
|
+
const STRATEGY_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region packages/event-wf/src/outlets/use-wf-strategy.ts
|
|
105
|
+
/**
|
|
106
|
+
* Composable for inspecting / swapping the active state strategy name from
|
|
107
|
+
* within a workflow step. The new name applies to the NEXT pause — it travels
|
|
108
|
+
* back to the outlet trigger via `output.inputRequired.stateStrategy`, and
|
|
109
|
+
* persists in the issued token's prefix.
|
|
110
|
+
*
|
|
111
|
+
* The composable only validates the name FORMAT (regex). Existence in the
|
|
112
|
+
* trigger's strategy registry is validated at pause time by the trigger
|
|
113
|
+
* itself — `swapStrategy('typo')` here will not throw; the trigger will
|
|
114
|
+
* throw on pause when it can't find 'typo' in its registry.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* // Step that escalates from in-memory state to a durable KV store before
|
|
119
|
+
* // pausing for a long-running approval:
|
|
120
|
+
* app.step('await-approval', {
|
|
121
|
+
* handler: () => {
|
|
122
|
+
* swapStrategy('kv')
|
|
123
|
+
* return outletHttp({ fields: ['decision'] })
|
|
124
|
+
* },
|
|
125
|
+
* })
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
const useWfStrategy = (0, _wooksjs_event_core.defineWook)((ctx) => ({
|
|
129
|
+
current: () => ctx.get(stateStrategyNameKey),
|
|
130
|
+
swap: (name) => {
|
|
131
|
+
if (!STRATEGY_NAME_RE.test(name)) throw new Error(`swapStrategy: invalid name '${name}' — must match ${STRATEGY_NAME_RE}`);
|
|
132
|
+
ctx.set(stateStrategyNameKey, name);
|
|
133
|
+
}
|
|
134
|
+
}));
|
|
135
|
+
/**
|
|
136
|
+
* Sugar — calls `useWfStrategy().swap(name)`. Returns `undefined` so a step
|
|
137
|
+
* can write `return swapStrategy('kv')` when no outlet pause follows.
|
|
138
|
+
*/
|
|
139
|
+
function swapStrategy(name) {
|
|
140
|
+
useWfStrategy().swap(name);
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
//#endregion
|
|
100
144
|
//#region packages/event-wf/src/outlets/trigger.ts
|
|
145
|
+
function wrapToken(name, raw) {
|
|
146
|
+
return `${name}.${raw}`;
|
|
147
|
+
}
|
|
148
|
+
function unwrapToken(token) {
|
|
149
|
+
const i = token.indexOf(".");
|
|
150
|
+
if (i <= 0 || i === token.length - 1) return null;
|
|
151
|
+
return {
|
|
152
|
+
name: token.slice(0, i),
|
|
153
|
+
raw: token.slice(i + 1)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function normalizeStateConfig(state) {
|
|
157
|
+
if (typeof state === "object" && state !== null && "strategies" in state) {
|
|
158
|
+
const registry = state.strategies;
|
|
159
|
+
for (const name of Object.keys(registry)) if (!STRATEGY_NAME_RE.test(name)) throw new Error(`Invalid strategy name '${name}': must match /^[A-Za-z0-9_-]+$/`);
|
|
160
|
+
const def = state.default;
|
|
161
|
+
return {
|
|
162
|
+
registry,
|
|
163
|
+
resolveDefaultName: typeof def === "function" ? def : (_wfid) => def
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
registry: { default: state },
|
|
168
|
+
resolveDefaultName: () => "default"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
101
171
|
/**
|
|
102
172
|
* Handle an HTTP request that starts or resumes a workflow.
|
|
103
173
|
*
|
|
@@ -129,6 +199,7 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
129
199
|
const registry = new Map(config.outlets.map((o) => [o.name, o]));
|
|
130
200
|
ctx.set(outletsRegistryKey, registry);
|
|
131
201
|
ctx.set(wfFinishedKey, void 0);
|
|
202
|
+
const { registry: strategyRegistry, resolveDefaultName } = normalizeStateConfig(config.state);
|
|
132
203
|
const { parseBody } = (0, _wooksjs_http_body.useBody)();
|
|
133
204
|
const { params } = (0, _wooksjs_event_http.useUrlParams)();
|
|
134
205
|
const { getCookie } = (0, _wooksjs_event_http.useCookies)();
|
|
@@ -144,27 +215,31 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
144
215
|
}
|
|
145
216
|
const wfid = body?.[wfidName] ?? queryParams.get(wfidName) ?? void 0;
|
|
146
217
|
const input = body?.input;
|
|
147
|
-
const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
|
|
148
218
|
let output;
|
|
149
|
-
let
|
|
219
|
+
let incomingName;
|
|
220
|
+
let incomingRaw;
|
|
150
221
|
if (token) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
222
|
+
const unwrapped = unwrapToken(token);
|
|
223
|
+
if (!unwrapped) {
|
|
224
|
+
response.setStatus(410);
|
|
225
|
+
return { error: "Invalid workflow state token" };
|
|
226
|
+
}
|
|
227
|
+
const strategy = Object.prototype.hasOwnProperty.call(strategyRegistry, unwrapped.name) ? strategyRegistry[unwrapped.name] : void 0;
|
|
228
|
+
if (!strategy) {
|
|
229
|
+
response.setStatus(410);
|
|
230
|
+
return { error: "Invalid workflow state token" };
|
|
231
|
+
}
|
|
232
|
+
incomingName = unwrapped.name;
|
|
233
|
+
incomingRaw = unwrapped.raw;
|
|
234
|
+
const state = await strategy.consume(unwrapped.raw);
|
|
154
235
|
if (!state) {
|
|
155
236
|
response.setStatus(410);
|
|
156
237
|
return { error: "Invalid or expired workflow state" };
|
|
157
238
|
}
|
|
158
|
-
if (state.schemaId !== (wfid ?? "")) {
|
|
159
|
-
const realStrategy = resolveStrategy(state.schemaId);
|
|
160
|
-
if (realStrategy !== strategy) {
|
|
161
|
-
ctx.set(stateStrategyKey, realStrategy);
|
|
162
|
-
strategyReResolved = true;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
239
|
output = await deps.resume(state, {
|
|
166
240
|
input,
|
|
167
|
-
eventContext: ctx
|
|
241
|
+
eventContext: ctx,
|
|
242
|
+
strategy: { name: incomingName }
|
|
168
243
|
});
|
|
169
244
|
} else if (wfid) {
|
|
170
245
|
if (config.allow?.length && !config.allow.includes(wfid)) {
|
|
@@ -175,12 +250,13 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
175
250
|
response.setStatus(403);
|
|
176
251
|
return { error: `Workflow '${wfid}' is blocked` };
|
|
177
252
|
}
|
|
178
|
-
const
|
|
179
|
-
|
|
253
|
+
const defaultName = resolveDefaultName(wfid);
|
|
254
|
+
if (!(Object.prototype.hasOwnProperty.call(strategyRegistry, defaultName) ? strategyRegistry[defaultName] : void 0)) throw new Error(`Default strategy '${defaultName}' not found in registry. Known: ${Object.keys(strategyRegistry).join(", ")}`);
|
|
180
255
|
const initialContext = config.initialContext ? config.initialContext(body, wfid) : {};
|
|
181
256
|
output = await deps.start(wfid, initialContext, {
|
|
182
257
|
input,
|
|
183
|
-
eventContext: ctx
|
|
258
|
+
eventContext: ctx,
|
|
259
|
+
strategy: { name: defaultName }
|
|
184
260
|
});
|
|
185
261
|
} else {
|
|
186
262
|
response.setStatus(400);
|
|
@@ -211,13 +287,16 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
211
287
|
response.setStatus(500);
|
|
212
288
|
return { error: `Unknown outlet: '${outletReq.outlet}'` };
|
|
213
289
|
}
|
|
214
|
-
const
|
|
290
|
+
const finalName = outletReq.stateStrategy;
|
|
291
|
+
if (finalName === void 0) throw new Error("Workflow paused without `stateStrategy` on inputRequired — the WF adapter must augment the output with the active strategy name.");
|
|
292
|
+
const finalStrategy = Object.prototype.hasOwnProperty.call(strategyRegistry, finalName) ? strategyRegistry[finalName] : void 0;
|
|
293
|
+
if (!finalStrategy) throw new Error(`Workflow paused with unknown strategy '${finalName}' — step swapped to a name not in the trigger's registry. Known: ${Object.keys(strategyRegistry).join(", ")}`);
|
|
215
294
|
const stateWithMeta = {
|
|
216
295
|
...output.state,
|
|
217
296
|
meta: { outlet: outletReq.outlet }
|
|
218
297
|
};
|
|
219
|
-
const reuseHandle =
|
|
220
|
-
const newToken = await
|
|
298
|
+
const reuseHandle = incomingName !== void 0 && incomingName === finalName && incomingRaw !== void 0 ? { handle: incomingRaw } : void 0;
|
|
299
|
+
const newToken = wrapToken(finalName, await finalStrategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0, reuseHandle));
|
|
221
300
|
const outOfBand = outletHandler.tokenDelivery === "out-of-band";
|
|
222
301
|
if (tokenWrite === "cookie" && !outOfBand) response.setCookie(tokenName, newToken, {
|
|
223
302
|
httpOnly: true,
|
|
@@ -427,6 +506,7 @@ var WooksWf = class extends wooks.WooksAdapterBase {
|
|
|
427
506
|
indexes,
|
|
428
507
|
input
|
|
429
508
|
}, async () => {
|
|
509
|
+
if (opts?.strategy?.name !== void 0) (0, _wooksjs_event_core.current)().set(stateStrategyNameKey, opts.strategy.name);
|
|
430
510
|
const { handlers: foundHandlers } = this.wooks.lookup("WF_FLOW", `/${schemaId}`.replace(/^\/+/u, "/"));
|
|
431
511
|
const handlers = foundHandlers || this.opts?.onNotFound && [this.opts.onNotFound] || null;
|
|
432
512
|
if (handlers && handlers.length > 0) {
|
|
@@ -460,6 +540,10 @@ var WooksWf = class extends wooks.WooksAdapterBase {
|
|
|
460
540
|
throw error;
|
|
461
541
|
}
|
|
462
542
|
clean();
|
|
543
|
+
if (result.inputRequired) {
|
|
544
|
+
const finalName = (0, _wooksjs_event_core.current)().get(stateStrategyNameKey);
|
|
545
|
+
if (finalName !== void 0) result.inputRequired.stateStrategy = finalName;
|
|
546
|
+
}
|
|
463
547
|
if (result.resume) result.resume = (_input) => this.resume(result.state, {
|
|
464
548
|
input: _input,
|
|
465
549
|
spy,
|
|
@@ -555,6 +639,7 @@ Object.defineProperty(exports, 'outletHttp', {
|
|
|
555
639
|
});
|
|
556
640
|
exports.resumeKey = resumeKey;
|
|
557
641
|
exports.resumeWfContext = resumeWfContext;
|
|
642
|
+
exports.swapStrategy = swapStrategy;
|
|
558
643
|
Object.defineProperty(exports, 'useLogger', {
|
|
559
644
|
enumerable: true,
|
|
560
645
|
get: function () {
|
|
@@ -570,5 +655,6 @@ Object.defineProperty(exports, 'useRouteParams', {
|
|
|
570
655
|
exports.useWfFinished = useWfFinished;
|
|
571
656
|
exports.useWfOutlet = useWfOutlet;
|
|
572
657
|
exports.useWfState = useWfState;
|
|
658
|
+
exports.useWfStrategy = useWfStrategy;
|
|
573
659
|
exports.wfKind = wfKind;
|
|
574
660
|
exports.wfShortcuts = wfShortcuts;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as _wooksjs_event_core from '@wooksjs/event-core';
|
|
|
2
2
|
import { EventContextOptions, EventKindSeeds, EventContext } from '@wooksjs/event-core';
|
|
3
3
|
export { EventContext, EventContextOptions, useLogger, useRouteParams } from '@wooksjs/event-core';
|
|
4
4
|
import * as _prostojs_wf_outlets from '@prostojs/wf/outlets';
|
|
5
|
-
import { WfStateStrategy, WfOutlet
|
|
5
|
+
import { WfOutletRequest, WfStateStrategy, WfOutlet } from '@prostojs/wf/outlets';
|
|
6
6
|
export { EncapsulatedStateStrategy, HandleStateStrategy, WfOutlet, WfOutletRequest, WfOutletResult, WfState, WfStateStore, WfStateStoreMemory, WfStateStrategy, outlet, outletEmail, outletHttp } from '@prostojs/wf/outlets';
|
|
7
7
|
import { TFlowOutput, Workflow, Step, TWorkflowSpy, TStepHandler, TWorkflowSchema } from '@prostojs/wf';
|
|
8
8
|
export { StepRetriableError, TStepHandler, TWorkflowSchema } from '@prostojs/wf';
|
|
@@ -50,7 +50,6 @@ declare function resumeWfContext<R>(options: EventContextOptions, seeds: EventKi
|
|
|
50
50
|
* where steps need to inspect or modify outlet state directly.
|
|
51
51
|
*/
|
|
52
52
|
declare const useWfOutlet: _wooksjs_event_core.WookComposable<{
|
|
53
|
-
getStateStrategy: () => _prostojs_wf_outlets.WfStateStrategy;
|
|
54
53
|
getOutlets: () => Map<string, _prostojs_wf_outlets.WfOutlet>;
|
|
55
54
|
getOutlet: (name: string) => _prostojs_wf_outlets.WfOutlet | null;
|
|
56
55
|
}>;
|
|
@@ -85,6 +84,58 @@ declare const useWfFinished: _wooksjs_event_core.WookComposable<{
|
|
|
85
84
|
get: () => WfFinishedResponse | undefined;
|
|
86
85
|
}>;
|
|
87
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Composable for inspecting / swapping the active state strategy name from
|
|
89
|
+
* within a workflow step. The new name applies to the NEXT pause — it travels
|
|
90
|
+
* back to the outlet trigger via `output.inputRequired.stateStrategy`, and
|
|
91
|
+
* persists in the issued token's prefix.
|
|
92
|
+
*
|
|
93
|
+
* The composable only validates the name FORMAT (regex). Existence in the
|
|
94
|
+
* trigger's strategy registry is validated at pause time by the trigger
|
|
95
|
+
* itself — `swapStrategy('typo')` here will not throw; the trigger will
|
|
96
|
+
* throw on pause when it can't find 'typo' in its registry.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* // Step that escalates from in-memory state to a durable KV store before
|
|
101
|
+
* // pausing for a long-running approval:
|
|
102
|
+
* app.step('await-approval', {
|
|
103
|
+
* handler: () => {
|
|
104
|
+
* swapStrategy('kv')
|
|
105
|
+
* return outletHttp({ fields: ['decision'] })
|
|
106
|
+
* },
|
|
107
|
+
* })
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
declare const useWfStrategy: _wooksjs_event_core.WookComposable<{
|
|
111
|
+
/** Name of the strategy currently set for the next persist. */
|
|
112
|
+
current: () => string;
|
|
113
|
+
/**
|
|
114
|
+
* Swap the active strategy by name. Validates name format only; unknown
|
|
115
|
+
* names surface as a loud error at pause time from the trigger.
|
|
116
|
+
*/
|
|
117
|
+
swap: (name: string) => void;
|
|
118
|
+
}>;
|
|
119
|
+
/**
|
|
120
|
+
* Sugar — calls `useWfStrategy().swap(name)`. Returns `undefined` so a step
|
|
121
|
+
* can write `return swapStrategy('kv')` when no outlet pause follows.
|
|
122
|
+
*/
|
|
123
|
+
declare function swapStrategy(name: string): undefined;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `WfOutletRequest` extended with the strategy name that was active when the
|
|
127
|
+
* workflow paused. The WF adapter augments `output.inputRequired` with this
|
|
128
|
+
* field so callers can persist the next token under the post-swap strategy
|
|
129
|
+
* without depending on EventContext write-through.
|
|
130
|
+
*
|
|
131
|
+
* The field is named generically (`stateStrategy`) — outlet handlers that
|
|
132
|
+
* don't care about strategies ignore it; outlet triggers and offline
|
|
133
|
+
* resume drivers read it to look up the right strategy in their registry.
|
|
134
|
+
*/
|
|
135
|
+
type WfPauseRequest<P = unknown> = WfOutletRequest<P> & {
|
|
136
|
+
stateStrategy?: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
88
139
|
interface WfOutletTokenConfig {
|
|
89
140
|
/** Where to read state token from incoming request (default: `['body', 'query', 'cookie']`) */
|
|
90
141
|
read?: Array<'body' | 'query' | 'cookie'>;
|
|
@@ -99,26 +150,28 @@ interface WfOutletTriggerConfig {
|
|
|
99
150
|
/** Blacklist of workflow IDs. Checked after allow. */
|
|
100
151
|
block?: string[];
|
|
101
152
|
/**
|
|
102
|
-
* State persistence strategy.
|
|
103
|
-
* workflows, or a function that returns a strategy per workflow ID.
|
|
153
|
+
* State persistence strategy. Two forms:
|
|
104
154
|
*
|
|
105
|
-
* **
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
155
|
+
* - **Single-strategy shortcut**: pass a `WfStateStrategy` directly. The
|
|
156
|
+
* trigger registers it internally under the name `'default'`.
|
|
157
|
+
* - **Named map**: `{ strategies, default }` where `strategies` is a
|
|
158
|
+
* `Record<name, WfStateStrategy>` and `default` is either the name to use
|
|
159
|
+
* on workflow start or a function `(wfid) => name` that picks per
|
|
160
|
+
* workflow id. Steps may then call `swapStrategy(name)` to escalate the
|
|
161
|
+
* *next* outlet pause to a different strategy.
|
|
109
162
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* - OR every resume request must carry `wfid` so the correct strategy is
|
|
115
|
-
* always resolved.
|
|
163
|
+
* The active strategy name is embedded in the issued token as `<name>.<raw>`,
|
|
164
|
+
* so resume always picks the strategy that persisted the state. Each
|
|
165
|
+
* strategy can therefore have its own independent storage (no need for
|
|
166
|
+
* shared keyspaces between strategies).
|
|
116
167
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* token remains live in the real strategy.
|
|
168
|
+
* Strategy names must match `/^[A-Za-z0-9_-]+$/` (validated at trigger
|
|
169
|
+
* invocation).
|
|
120
170
|
*/
|
|
121
|
-
state: WfStateStrategy |
|
|
171
|
+
state: WfStateStrategy | {
|
|
172
|
+
strategies: Record<string, WfStateStrategy>;
|
|
173
|
+
default: string | ((wfid: string) => string);
|
|
174
|
+
};
|
|
122
175
|
/** Registered outlets */
|
|
123
176
|
outlets: WfOutlet[];
|
|
124
177
|
/** Token configuration (reading, writing, naming) */
|
|
@@ -148,7 +201,10 @@ interface WfOutletTriggerDeps {
|
|
|
148
201
|
start: (schemaId: string, context: unknown, opts?: {
|
|
149
202
|
input?: unknown;
|
|
150
203
|
eventContext?: unknown;
|
|
151
|
-
|
|
204
|
+
strategy?: {
|
|
205
|
+
name: string;
|
|
206
|
+
};
|
|
207
|
+
}) => Promise<TFlowOutput<unknown, unknown, WfPauseRequest>>;
|
|
152
208
|
/** Resume a workflow. Provided by WooksWf or MoostWf. */
|
|
153
209
|
resume: (state: {
|
|
154
210
|
schemaId: string;
|
|
@@ -157,7 +213,10 @@ interface WfOutletTriggerDeps {
|
|
|
157
213
|
}, opts?: {
|
|
158
214
|
input?: unknown;
|
|
159
215
|
eventContext?: unknown;
|
|
160
|
-
|
|
216
|
+
strategy?: {
|
|
217
|
+
name: string;
|
|
218
|
+
};
|
|
219
|
+
}) => Promise<TFlowOutput<unknown, unknown, WfPauseRequest>>;
|
|
161
220
|
}
|
|
162
221
|
|
|
163
222
|
/**
|
|
@@ -281,6 +340,20 @@ interface TWfRunOptions<I = unknown, T = unknown, IR = unknown> {
|
|
|
281
340
|
* Pass `current()` from within an active event scope (HTTP handler, etc.).
|
|
282
341
|
*/
|
|
283
342
|
eventContext?: EventContext;
|
|
343
|
+
/**
|
|
344
|
+
* Initial state strategy for the workflow run. Sets the strategy name on
|
|
345
|
+
* the WF event context so steps can inspect it via `useWfStrategy().current()`
|
|
346
|
+
* and swap it via `useWfStrategy().swap(name)`. The final post-swap name
|
|
347
|
+
* is reflected on `output.inputRequired.stateStrategy` (when paused) so
|
|
348
|
+
* callers can persist under the right keyspace without depending on
|
|
349
|
+
* EventContext write-through.
|
|
350
|
+
*
|
|
351
|
+
* The adapter only carries the name — strategy instances live in the
|
|
352
|
+
* caller (HTTP trigger, offline driver, etc.).
|
|
353
|
+
*/
|
|
354
|
+
strategy?: {
|
|
355
|
+
name: string;
|
|
356
|
+
};
|
|
284
357
|
}
|
|
285
358
|
/** Wooks adapter for defining and executing workflow schemas with step-based routing. */
|
|
286
359
|
declare class WooksWf<T = any, IR = any> extends WooksAdapterBase {
|
|
@@ -349,5 +422,5 @@ declare class WooksWf<T = any, IR = any> extends WooksAdapterBase {
|
|
|
349
422
|
*/
|
|
350
423
|
declare function createWfApp<T>(opts?: TWooksWfOptions, wooks?: Wooks | WooksAdapterBase): WooksWf<T, any>;
|
|
351
424
|
|
|
352
|
-
export { WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, createWfContext, handleWfOutletRequest, resumeKey, resumeWfContext, useWfFinished, useWfOutlet, useWfState, wfKind, wfShortcuts };
|
|
353
|
-
export type { TWFEventInput, TWfRunOptions, TWooksWfOptions, WfFinishedResponse, WfOutletTokenConfig, WfOutletTriggerConfig, WfOutletTriggerDeps };
|
|
425
|
+
export { WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, createWfContext, handleWfOutletRequest, resumeKey, resumeWfContext, swapStrategy, useWfFinished, useWfOutlet, useWfState, useWfStrategy, wfKind, wfShortcuts };
|
|
426
|
+
export type { TWFEventInput, TWfRunOptions, TWooksWfOptions, WfFinishedResponse, WfOutletTokenConfig, WfOutletTriggerConfig, WfOutletTriggerDeps, WfPauseRequest };
|
package/dist/index.mjs
CHANGED
|
@@ -56,8 +56,6 @@ function resumeWfContext(options, seeds, fn) {
|
|
|
56
56
|
//#region packages/event-wf/src/outlets/outlet-context.ts
|
|
57
57
|
/** Registered outlet handlers, keyed by name */
|
|
58
58
|
const outletsRegistryKey = key("wf.outlets.registry");
|
|
59
|
-
/** Active state strategy for current request */
|
|
60
|
-
const stateStrategyKey = key("wf.outlets.stateStrategy");
|
|
61
59
|
/** Finished response set by workflow steps */
|
|
62
60
|
const wfFinishedKey = key("wf.outlets.finished");
|
|
63
61
|
|
|
@@ -71,7 +69,6 @@ const wfFinishedKey = key("wf.outlets.finished");
|
|
|
71
69
|
* where steps need to inspect or modify outlet state directly.
|
|
72
70
|
*/
|
|
73
71
|
const useWfOutlet = defineWook((ctx) => ({
|
|
74
|
-
getStateStrategy: () => ctx.get(stateStrategyKey),
|
|
75
72
|
getOutlets: () => ctx.get(outletsRegistryKey),
|
|
76
73
|
getOutlet: (name) => ctx.get(outletsRegistryKey)?.get(name) ?? null
|
|
77
74
|
}));
|
|
@@ -95,8 +92,81 @@ const useWfFinished = defineWook((ctx) => ({
|
|
|
95
92
|
get: () => ctx.has(wfFinishedKey) ? ctx.get(wfFinishedKey) : void 0
|
|
96
93
|
}));
|
|
97
94
|
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region packages/event-wf/src/strategy-context.ts
|
|
97
|
+
/** Name of the strategy currently active for the running workflow. */
|
|
98
|
+
const stateStrategyNameKey = key("wf.strategyName");
|
|
99
|
+
/** Strategy names must match this regex (validated at swap call + config). */
|
|
100
|
+
const STRATEGY_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region packages/event-wf/src/outlets/use-wf-strategy.ts
|
|
104
|
+
/**
|
|
105
|
+
* Composable for inspecting / swapping the active state strategy name from
|
|
106
|
+
* within a workflow step. The new name applies to the NEXT pause — it travels
|
|
107
|
+
* back to the outlet trigger via `output.inputRequired.stateStrategy`, and
|
|
108
|
+
* persists in the issued token's prefix.
|
|
109
|
+
*
|
|
110
|
+
* The composable only validates the name FORMAT (regex). Existence in the
|
|
111
|
+
* trigger's strategy registry is validated at pause time by the trigger
|
|
112
|
+
* itself — `swapStrategy('typo')` here will not throw; the trigger will
|
|
113
|
+
* throw on pause when it can't find 'typo' in its registry.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* // Step that escalates from in-memory state to a durable KV store before
|
|
118
|
+
* // pausing for a long-running approval:
|
|
119
|
+
* app.step('await-approval', {
|
|
120
|
+
* handler: () => {
|
|
121
|
+
* swapStrategy('kv')
|
|
122
|
+
* return outletHttp({ fields: ['decision'] })
|
|
123
|
+
* },
|
|
124
|
+
* })
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
const useWfStrategy = defineWook((ctx) => ({
|
|
128
|
+
current: () => ctx.get(stateStrategyNameKey),
|
|
129
|
+
swap: (name) => {
|
|
130
|
+
if (!STRATEGY_NAME_RE.test(name)) throw new Error(`swapStrategy: invalid name '${name}' — must match ${STRATEGY_NAME_RE}`);
|
|
131
|
+
ctx.set(stateStrategyNameKey, name);
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
/**
|
|
135
|
+
* Sugar — calls `useWfStrategy().swap(name)`. Returns `undefined` so a step
|
|
136
|
+
* can write `return swapStrategy('kv')` when no outlet pause follows.
|
|
137
|
+
*/
|
|
138
|
+
function swapStrategy(name) {
|
|
139
|
+
useWfStrategy().swap(name);
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
//#endregion
|
|
99
143
|
//#region packages/event-wf/src/outlets/trigger.ts
|
|
144
|
+
function wrapToken(name, raw) {
|
|
145
|
+
return `${name}.${raw}`;
|
|
146
|
+
}
|
|
147
|
+
function unwrapToken(token) {
|
|
148
|
+
const i = token.indexOf(".");
|
|
149
|
+
if (i <= 0 || i === token.length - 1) return null;
|
|
150
|
+
return {
|
|
151
|
+
name: token.slice(0, i),
|
|
152
|
+
raw: token.slice(i + 1)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function normalizeStateConfig(state) {
|
|
156
|
+
if (typeof state === "object" && state !== null && "strategies" in state) {
|
|
157
|
+
const registry = state.strategies;
|
|
158
|
+
for (const name of Object.keys(registry)) if (!STRATEGY_NAME_RE.test(name)) throw new Error(`Invalid strategy name '${name}': must match /^[A-Za-z0-9_-]+$/`);
|
|
159
|
+
const def = state.default;
|
|
160
|
+
return {
|
|
161
|
+
registry,
|
|
162
|
+
resolveDefaultName: typeof def === "function" ? def : (_wfid) => def
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
registry: { default: state },
|
|
167
|
+
resolveDefaultName: () => "default"
|
|
168
|
+
};
|
|
169
|
+
}
|
|
100
170
|
/**
|
|
101
171
|
* Handle an HTTP request that starts or resumes a workflow.
|
|
102
172
|
*
|
|
@@ -128,6 +198,7 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
128
198
|
const registry = new Map(config.outlets.map((o) => [o.name, o]));
|
|
129
199
|
ctx.set(outletsRegistryKey, registry);
|
|
130
200
|
ctx.set(wfFinishedKey, void 0);
|
|
201
|
+
const { registry: strategyRegistry, resolveDefaultName } = normalizeStateConfig(config.state);
|
|
131
202
|
const { parseBody } = useBody();
|
|
132
203
|
const { params } = useUrlParams();
|
|
133
204
|
const { getCookie } = useCookies();
|
|
@@ -143,27 +214,31 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
143
214
|
}
|
|
144
215
|
const wfid = body?.[wfidName] ?? queryParams.get(wfidName) ?? void 0;
|
|
145
216
|
const input = body?.input;
|
|
146
|
-
const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
|
|
147
217
|
let output;
|
|
148
|
-
let
|
|
218
|
+
let incomingName;
|
|
219
|
+
let incomingRaw;
|
|
149
220
|
if (token) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
221
|
+
const unwrapped = unwrapToken(token);
|
|
222
|
+
if (!unwrapped) {
|
|
223
|
+
response.setStatus(410);
|
|
224
|
+
return { error: "Invalid workflow state token" };
|
|
225
|
+
}
|
|
226
|
+
const strategy = Object.prototype.hasOwnProperty.call(strategyRegistry, unwrapped.name) ? strategyRegistry[unwrapped.name] : void 0;
|
|
227
|
+
if (!strategy) {
|
|
228
|
+
response.setStatus(410);
|
|
229
|
+
return { error: "Invalid workflow state token" };
|
|
230
|
+
}
|
|
231
|
+
incomingName = unwrapped.name;
|
|
232
|
+
incomingRaw = unwrapped.raw;
|
|
233
|
+
const state = await strategy.consume(unwrapped.raw);
|
|
153
234
|
if (!state) {
|
|
154
235
|
response.setStatus(410);
|
|
155
236
|
return { error: "Invalid or expired workflow state" };
|
|
156
237
|
}
|
|
157
|
-
if (state.schemaId !== (wfid ?? "")) {
|
|
158
|
-
const realStrategy = resolveStrategy(state.schemaId);
|
|
159
|
-
if (realStrategy !== strategy) {
|
|
160
|
-
ctx.set(stateStrategyKey, realStrategy);
|
|
161
|
-
strategyReResolved = true;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
238
|
output = await deps.resume(state, {
|
|
165
239
|
input,
|
|
166
|
-
eventContext: ctx
|
|
240
|
+
eventContext: ctx,
|
|
241
|
+
strategy: { name: incomingName }
|
|
167
242
|
});
|
|
168
243
|
} else if (wfid) {
|
|
169
244
|
if (config.allow?.length && !config.allow.includes(wfid)) {
|
|
@@ -174,12 +249,13 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
174
249
|
response.setStatus(403);
|
|
175
250
|
return { error: `Workflow '${wfid}' is blocked` };
|
|
176
251
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
252
|
+
const defaultName = resolveDefaultName(wfid);
|
|
253
|
+
if (!(Object.prototype.hasOwnProperty.call(strategyRegistry, defaultName) ? strategyRegistry[defaultName] : void 0)) throw new Error(`Default strategy '${defaultName}' not found in registry. Known: ${Object.keys(strategyRegistry).join(", ")}`);
|
|
179
254
|
const initialContext = config.initialContext ? config.initialContext(body, wfid) : {};
|
|
180
255
|
output = await deps.start(wfid, initialContext, {
|
|
181
256
|
input,
|
|
182
|
-
eventContext: ctx
|
|
257
|
+
eventContext: ctx,
|
|
258
|
+
strategy: { name: defaultName }
|
|
183
259
|
});
|
|
184
260
|
} else {
|
|
185
261
|
response.setStatus(400);
|
|
@@ -210,13 +286,16 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
210
286
|
response.setStatus(500);
|
|
211
287
|
return { error: `Unknown outlet: '${outletReq.outlet}'` };
|
|
212
288
|
}
|
|
213
|
-
const
|
|
289
|
+
const finalName = outletReq.stateStrategy;
|
|
290
|
+
if (finalName === void 0) throw new Error("Workflow paused without `stateStrategy` on inputRequired — the WF adapter must augment the output with the active strategy name.");
|
|
291
|
+
const finalStrategy = Object.prototype.hasOwnProperty.call(strategyRegistry, finalName) ? strategyRegistry[finalName] : void 0;
|
|
292
|
+
if (!finalStrategy) throw new Error(`Workflow paused with unknown strategy '${finalName}' — step swapped to a name not in the trigger's registry. Known: ${Object.keys(strategyRegistry).join(", ")}`);
|
|
214
293
|
const stateWithMeta = {
|
|
215
294
|
...output.state,
|
|
216
295
|
meta: { outlet: outletReq.outlet }
|
|
217
296
|
};
|
|
218
|
-
const reuseHandle =
|
|
219
|
-
const newToken = await
|
|
297
|
+
const reuseHandle = incomingName !== void 0 && incomingName === finalName && incomingRaw !== void 0 ? { handle: incomingRaw } : void 0;
|
|
298
|
+
const newToken = wrapToken(finalName, await finalStrategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0, reuseHandle));
|
|
220
299
|
const outOfBand = outletHandler.tokenDelivery === "out-of-band";
|
|
221
300
|
if (tokenWrite === "cookie" && !outOfBand) response.setCookie(tokenName, newToken, {
|
|
222
301
|
httpOnly: true,
|
|
@@ -426,6 +505,7 @@ var WooksWf = class extends WooksAdapterBase {
|
|
|
426
505
|
indexes,
|
|
427
506
|
input
|
|
428
507
|
}, async () => {
|
|
508
|
+
if (opts?.strategy?.name !== void 0) current().set(stateStrategyNameKey, opts.strategy.name);
|
|
429
509
|
const { handlers: foundHandlers } = this.wooks.lookup("WF_FLOW", `/${schemaId}`.replace(/^\/+/u, "/"));
|
|
430
510
|
const handlers = foundHandlers || this.opts?.onNotFound && [this.opts.onNotFound] || null;
|
|
431
511
|
if (handlers && handlers.length > 0) {
|
|
@@ -459,6 +539,10 @@ var WooksWf = class extends WooksAdapterBase {
|
|
|
459
539
|
throw error;
|
|
460
540
|
}
|
|
461
541
|
clean();
|
|
542
|
+
if (result.inputRequired) {
|
|
543
|
+
const finalName = current().get(stateStrategyNameKey);
|
|
544
|
+
if (finalName !== void 0) result.inputRequired.stateStrategy = finalName;
|
|
545
|
+
}
|
|
462
546
|
if (result.resume) result.resume = (_input) => this.resume(result.state, {
|
|
463
547
|
input: _input,
|
|
464
548
|
spy,
|
|
@@ -503,4 +587,4 @@ function createWfApp(opts, wooks) {
|
|
|
503
587
|
}
|
|
504
588
|
|
|
505
589
|
//#endregion
|
|
506
|
-
export { EncapsulatedStateStrategy, HandleStateStrategy, StepRetriableError, WfStateStoreMemory, WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, createWfContext, handleWfOutletRequest, outlet, outletEmail, outletHttp, resumeKey, resumeWfContext, useLogger, useRouteParams, useWfFinished, useWfOutlet, useWfState, wfKind, wfShortcuts };
|
|
590
|
+
export { EncapsulatedStateStrategy, HandleStateStrategy, StepRetriableError, WfStateStoreMemory, WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, createWfContext, handleWfOutletRequest, outlet, outletEmail, outletHttp, resumeKey, resumeWfContext, swapStrategy, useLogger, useRouteParams, useWfFinished, useWfOutlet, useWfState, useWfStrategy, wfKind, wfShortcuts };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-wf",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.16",
|
|
4
4
|
"description": "@wooksjs/event-wf",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -42,17 +42,17 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"vitest": "^3.2.4",
|
|
45
|
-
"@wooksjs/event-core": "^0.7.
|
|
46
|
-
"@wooksjs/event-http": "^0.7.
|
|
47
|
-
"wooks": "^0.7.
|
|
48
|
-
"@wooksjs/http-body": "^0.7.
|
|
45
|
+
"@wooksjs/event-core": "^0.7.16",
|
|
46
|
+
"@wooksjs/event-http": "^0.7.16",
|
|
47
|
+
"wooks": "^0.7.16",
|
|
48
|
+
"@wooksjs/http-body": "^0.7.16"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@prostojs/logger": "^0.4.3",
|
|
52
|
-
"@wooksjs/event-core": "^0.7.
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"@wooksjs/http
|
|
52
|
+
"@wooksjs/event-core": "^0.7.16",
|
|
53
|
+
"@wooksjs/http-body": "^0.7.16",
|
|
54
|
+
"wooks": "^0.7.16",
|
|
55
|
+
"@wooksjs/event-http": "^0.7.16"
|
|
56
56
|
},
|
|
57
57
|
"peerDependenciesMeta": {
|
|
58
58
|
"@wooksjs/event-http": {
|