yieldstar 0.5.0 → 0.6.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 +43 -52
- package/dist/exports/errors.d.ts.map +1 -1
- package/dist/exports/sdk-http.d.ts +8 -0
- package/dist/exports/sdk-http.d.ts.map +1 -1
- package/dist/exports/sdk-local.d.ts +19 -2
- package/dist/exports/sdk-local.d.ts.map +1 -1
- package/dist/exports/workflow.d.ts +14 -2
- package/dist/exports/workflow.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +664 -214
- package/dist/internal/effect-call.d.ts +60 -0
- package/dist/internal/effect-call.d.ts.map +1 -0
- package/dist/internal/runtime-call.d.ts +39 -0
- package/dist/internal/runtime-call.d.ts.map +1 -0
- package/dist/internal/serialise.d.ts.map +1 -1
- package/dist/internal/step-record.d.ts +13 -0
- package/dist/internal/step-record.d.ts.map +1 -0
- package/dist/internal/step-runner.d.ts +77 -14
- package/dist/internal/step-runner.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/exports/errors.ts +4 -0
- package/src/exports/sdk-http.ts +60 -27
- package/src/exports/sdk-local.ts +41 -15
- package/src/exports/workflow.ts +157 -101
- package/src/index.ts +34 -2
- package/src/internal/effect-call.ts +217 -0
- package/src/internal/runtime-call.ts +95 -0
- package/src/internal/step-record.ts +110 -0
- package/src/internal/step-runner.ts +515 -191
- package/src/internal/serialise.ts +0 -47
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
assertSynchronousClaim,
|
|
3
|
+
assertSynchronousUpdater,
|
|
2
4
|
cloneStoreState,
|
|
3
5
|
defineStore,
|
|
4
6
|
type Draft,
|
|
7
|
+
type DurableObject,
|
|
8
|
+
type DurableStoreSchema,
|
|
9
|
+
type IfDurable,
|
|
10
|
+
type ExecutionStartRequest,
|
|
11
|
+
type ExecutionStartResult,
|
|
12
|
+
InfrastructureError,
|
|
5
13
|
type StandardSchemaV1,
|
|
6
14
|
StepResponse,
|
|
7
15
|
StepKey,
|
|
@@ -10,7 +18,6 @@ import {
|
|
|
10
18
|
StepDelay,
|
|
11
19
|
StepCacheCheck,
|
|
12
20
|
StepStoreWait,
|
|
13
|
-
type StoreClient,
|
|
14
21
|
type StoreDefinition,
|
|
15
22
|
type StoreDeleteFromResult,
|
|
16
23
|
type StoreKey,
|
|
@@ -20,11 +27,32 @@ import {
|
|
|
20
27
|
type StoreTakeResult,
|
|
21
28
|
type StoreUpdateFromResult,
|
|
22
29
|
type StoreUpdateResult,
|
|
30
|
+
type StoreWaiter,
|
|
23
31
|
trackStoreSelector,
|
|
32
|
+
type Durable,
|
|
33
|
+
type StepSchemaVerdict,
|
|
34
|
+
durableValueCodec,
|
|
35
|
+
StepEffectWait,
|
|
36
|
+
validateEffectRequest,
|
|
37
|
+
WorkflowEffectError,
|
|
38
|
+
WorkflowStartError,
|
|
39
|
+
WorkflowValueError,
|
|
24
40
|
} from "@yieldstar/core";
|
|
25
41
|
import { RetryableError } from "../exports/errors";
|
|
26
42
|
import { getCallSiteHash } from "./utils";
|
|
43
|
+
import {
|
|
44
|
+
StepRuntimeCall,
|
|
45
|
+
workflowCallback,
|
|
46
|
+
type RuntimeCallResult,
|
|
47
|
+
} from "./runtime-call";
|
|
27
48
|
import type { WorkflowEvent } from "@yieldstar/core";
|
|
49
|
+
import {
|
|
50
|
+
StepEffectCall,
|
|
51
|
+
type EffectCallOutcome,
|
|
52
|
+
type EffectLookup,
|
|
53
|
+
} from "./effect-call";
|
|
54
|
+
|
|
55
|
+
export type { EffectLookup };
|
|
28
56
|
|
|
29
57
|
/**
|
|
30
58
|
* @description A library of step generators, each of which:
|
|
@@ -43,7 +71,7 @@ export type WorkflowStore<T> = {
|
|
|
43
71
|
get(key?: string): AsyncGenerator<StepResponse, StoreSnapshot<T>>;
|
|
44
72
|
select<R>(
|
|
45
73
|
key: string,
|
|
46
|
-
selector: StoreSelector<T, R>
|
|
74
|
+
selector: StoreSelector<T, R> & DurableSelector<T, R>
|
|
47
75
|
): AsyncGenerator<StepResponse, R>;
|
|
48
76
|
update(
|
|
49
77
|
key: string,
|
|
@@ -59,158 +87,318 @@ export type WorkflowStore<T> = {
|
|
|
59
87
|
snapshot: StoreSnapshot<T>
|
|
60
88
|
): AsyncGenerator<StepResponse, StoreDeleteFromResult>;
|
|
61
89
|
when<R>(
|
|
62
|
-
selector: StoreSelector<T, R | undefined | null | false>
|
|
90
|
+
selector: StoreSelector<T, R | undefined | null | false> &
|
|
91
|
+
DurableMatchSelector<T, R>
|
|
63
92
|
): AsyncGenerator<StepResponse, NonNullable<R>>;
|
|
64
93
|
when<R>(
|
|
65
94
|
key: string,
|
|
66
|
-
selector: StoreSelector<T, R | undefined | null | false>
|
|
95
|
+
selector: StoreSelector<T, R | undefined | null | false> &
|
|
96
|
+
DurableMatchSelector<T, R>
|
|
67
97
|
): AsyncGenerator<StepResponse, NonNullable<R>>;
|
|
68
98
|
take<R>(
|
|
69
99
|
key: string,
|
|
70
|
-
selector: StoreSelector<T, R | undefined | null | false
|
|
100
|
+
selector: StoreSelector<T, R | undefined | null | false> &
|
|
101
|
+
DurableMatchSelector<T, R>,
|
|
71
102
|
claim: (draft: Draft<T>, selected: NonNullable<R>) => void
|
|
72
103
|
): AsyncGenerator<StepResponse, NonNullable<R>>;
|
|
73
104
|
};
|
|
74
105
|
|
|
106
|
+
/** A store selector whose result, which the step records, is durable. */
|
|
107
|
+
type DurableSelector<T, R> = IfDurable<R, StoreSelector<T, Durable<R>>>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A `when` or `take` selector whose match is durable. `false` is excluded
|
|
111
|
+
* before the check so that `R` is inferred as it would be without it.
|
|
112
|
+
*/
|
|
113
|
+
type DurableMatchSelector<T, R> = IfDurable<
|
|
114
|
+
Exclude<R, false>,
|
|
115
|
+
StoreSelector<T, Durable<Exclude<R, false>> | undefined | null | false>
|
|
116
|
+
>;
|
|
117
|
+
|
|
75
118
|
export type StepRunner = ReturnType<typeof createStepRunner>;
|
|
76
119
|
|
|
77
120
|
export function createStepRunner(params: {
|
|
78
121
|
event: WorkflowEvent;
|
|
79
|
-
|
|
122
|
+
clock: () => number;
|
|
80
123
|
}) {
|
|
81
|
-
const { event,
|
|
82
|
-
|
|
124
|
+
const { event, clock } = params;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns a handle to the store `definition.name` + `id`, where `id`
|
|
128
|
+
* defaults to this execution's ID. Obtaining a handle reads, creates and
|
|
129
|
+
* records nothing, so any caller may obtain the same handle again. Each
|
|
130
|
+
* operation on a missing store first creates it from `definition.initial`.
|
|
131
|
+
*/
|
|
83
132
|
function store<Schema extends StandardSchemaV1>(
|
|
84
|
-
definition: StoreDefinition<Schema
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
): AsyncGenerator<StepResponse, WorkflowStore<StoreState<Schema>>> {
|
|
92
|
-
const id = params?.id ?? event.executionId;
|
|
93
|
-
const key = `store:${definition.name}:${id}`;
|
|
94
|
-
|
|
95
|
-
return storeStep({
|
|
96
|
-
key,
|
|
133
|
+
definition: StoreDefinition<Schema> & {
|
|
134
|
+
schema: DurableStoreSchema<Schema>;
|
|
135
|
+
},
|
|
136
|
+
params?: { id?: string }
|
|
137
|
+
): WorkflowStore<StoreState<Schema>> {
|
|
138
|
+
return createWorkflowStore({
|
|
97
139
|
definition,
|
|
98
|
-
id,
|
|
140
|
+
id: params?.id ?? event.executionId,
|
|
99
141
|
event,
|
|
100
|
-
storeClient,
|
|
101
|
-
initial: params?.initial,
|
|
102
142
|
});
|
|
103
143
|
}
|
|
104
144
|
|
|
105
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Starts another workflow as a durable step and returns its execution ID.
|
|
147
|
+
* The start is identified by this execution's ID and `stepKey`, so every
|
|
148
|
+
* replay of the step names the same target. A rejected start throws a
|
|
149
|
+
* `WorkflowStartError`.
|
|
150
|
+
*/
|
|
151
|
+
function start<Params extends Record<string, any>>(
|
|
152
|
+
stepKey: string,
|
|
153
|
+
target: { workflowId: string; params?: Params & Durable<Params> }
|
|
154
|
+
): AsyncGenerator<
|
|
155
|
+
StepResponse,
|
|
156
|
+
{ executionId: string },
|
|
157
|
+
StepResult | StepError
|
|
158
|
+
> {
|
|
159
|
+
if (stepKey === "") {
|
|
160
|
+
throw new Error("step.start requires a non-empty step key");
|
|
161
|
+
}
|
|
162
|
+
const request: ExecutionStartRequest = {
|
|
163
|
+
requestId: { executionId: event.executionId, stepKey },
|
|
164
|
+
workflowId: target.workflowId,
|
|
165
|
+
};
|
|
166
|
+
// A missing params and an empty object are different requests.
|
|
167
|
+
if (target.params !== undefined) {
|
|
168
|
+
request.params = target.params as DurableObject;
|
|
169
|
+
}
|
|
170
|
+
return startStep(stepKey, request);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Runs an external operation identified by `descriptor.effectId`, at most
|
|
175
|
+
* once in effect: after an uncertain attempt, `lookup` decides whether the
|
|
176
|
+
* operation already happened before `perform` runs again (RFC 005).
|
|
177
|
+
*/
|
|
178
|
+
function effect<T, Input extends Record<string, any> = DurableObject>(
|
|
179
|
+
stepKey: string,
|
|
180
|
+
descriptor: EffectDescriptor<T, Input> & DurableEffectResult<T>
|
|
181
|
+
): AsyncGenerator<StepResponse, T, StepResult | StepError> {
|
|
182
|
+
if (typeof stepKey !== "string" || stepKey === "") {
|
|
183
|
+
throw new Error("step.effect requires a non-empty step key");
|
|
184
|
+
}
|
|
185
|
+
return effectStep(stepKey, descriptor) as AsyncGenerator<
|
|
186
|
+
StepResponse,
|
|
187
|
+
T,
|
|
188
|
+
StepResult | StepError
|
|
189
|
+
>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { run, delay: createDelay(clock), poll, store, start, effect };
|
|
106
193
|
}
|
|
107
194
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Describes one external operation for `step.effect`. `effectId` names the
|
|
197
|
+
* operation in the external system, so every caller that means it, in any
|
|
198
|
+
* execution or runtime, uses the same ID, and no caller reuses it for another
|
|
199
|
+
* operation. The external system must guarantee that repeated `perform` calls
|
|
200
|
+
* for one ID make one operation with one result, that `not-found` proves no
|
|
201
|
+
* call exists or can still finish, and that `settled` never changes.
|
|
202
|
+
*/
|
|
203
|
+
export type EffectDescriptor<T, Input = DurableObject> = {
|
|
204
|
+
effectId: string;
|
|
205
|
+
/** Names the version of the behaviour of `input`, `lookup` and `perform`. */
|
|
206
|
+
operation: string;
|
|
207
|
+
input: Input & Durable<Input>;
|
|
208
|
+
/** Milliseconds before an uncertain outcome is looked up again. */
|
|
209
|
+
retryIn: number;
|
|
210
|
+
/** Reports what the external system knows about `effectId`. */
|
|
211
|
+
lookup(effectId: string): Promise<EffectLookup<T>>;
|
|
212
|
+
/** Performs the operation under `effectId`. */
|
|
213
|
+
perform(effectId: string): Promise<T>;
|
|
214
|
+
};
|
|
123
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Checks the effect's result type without taking part in its inference. A
|
|
218
|
+
* `perform` that resolves with nothing settles the effect with `undefined`.
|
|
219
|
+
*/
|
|
220
|
+
type DurableEffectResult<T> = [T] extends [void]
|
|
221
|
+
? unknown
|
|
222
|
+
: IfDurable<T, { perform(effectId: string): Promise<Durable<T>> }>;
|
|
223
|
+
|
|
224
|
+
async function* effectStep(
|
|
225
|
+
key: string,
|
|
226
|
+
descriptor: EffectDescriptor<unknown, unknown>
|
|
227
|
+
): AsyncGenerator<StepResponse, unknown, any> {
|
|
124
228
|
yield new StepKey(key);
|
|
125
229
|
|
|
126
230
|
const cached = yield new StepCacheCheck();
|
|
127
231
|
|
|
128
|
-
if (cached)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
232
|
+
if (cached) return yield* replay(cached);
|
|
233
|
+
|
|
234
|
+
const { effectId, operation, input, retryIn, lookup, perform } =
|
|
235
|
+
(descriptor ?? {}) as Partial<EffectDescriptor<unknown, unknown>>;
|
|
236
|
+
const invalid = invalidEffectRequest({
|
|
237
|
+
effectId,
|
|
238
|
+
operation,
|
|
239
|
+
input,
|
|
240
|
+
retryIn,
|
|
241
|
+
lookup,
|
|
242
|
+
perform,
|
|
243
|
+
});
|
|
244
|
+
if (invalid) {
|
|
245
|
+
const err = new WorkflowEffectError(
|
|
246
|
+
"INVALID_EFFECT_REQUEST",
|
|
247
|
+
`step.effect "${key}": ${invalid}`
|
|
248
|
+
);
|
|
249
|
+
yield new StepError(err);
|
|
250
|
+
// unreachable – consumer calls throw() on the generator first
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const outcome: EffectCallOutcome = yield new StepEffectCall({
|
|
255
|
+
stepKey: key,
|
|
256
|
+
effectId: effectId!,
|
|
257
|
+
operation: operation!,
|
|
258
|
+
input: input as DurableObject,
|
|
259
|
+
retryIn: retryIn!,
|
|
260
|
+
lookup: (id) => lookup!.call(descriptor, id),
|
|
261
|
+
perform: (id) => perform!.call(descriptor, id),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
switch (outcome.status) {
|
|
265
|
+
case "settled":
|
|
266
|
+
return yield* record(outcome.result);
|
|
267
|
+
case "rejected": {
|
|
268
|
+
const err = new WorkflowEffectError(
|
|
269
|
+
outcome.code,
|
|
270
|
+
`Effect "${effectId}" was begun with a different operation or input`
|
|
271
|
+
);
|
|
272
|
+
yield new StepError(err);
|
|
273
|
+
throw err;
|
|
132
274
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
event,
|
|
137
|
-
storeClient,
|
|
138
|
-
});
|
|
275
|
+
case "suspended":
|
|
276
|
+
yield new StepEffectWait(outcome.wakeAt);
|
|
277
|
+
throw new Error("Effect wait yielded control unexpectedly");
|
|
139
278
|
}
|
|
279
|
+
}
|
|
140
280
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
281
|
+
function invalidEffectRequest(request: {
|
|
282
|
+
effectId: unknown;
|
|
283
|
+
operation: unknown;
|
|
284
|
+
input: unknown;
|
|
285
|
+
retryIn: unknown;
|
|
286
|
+
lookup: unknown;
|
|
287
|
+
perform: unknown;
|
|
288
|
+
}): string | undefined {
|
|
289
|
+
const validated = validateEffectRequest(request);
|
|
290
|
+
if (!validated.ok) return validated.message;
|
|
291
|
+
const { retryIn } = request;
|
|
292
|
+
if (typeof retryIn !== "number" || !Number.isFinite(retryIn) || retryIn < 0) {
|
|
293
|
+
return "retryIn must be a finite, non-negative number";
|
|
294
|
+
}
|
|
295
|
+
if (typeof request.lookup !== "function") return "lookup must be a function";
|
|
296
|
+
if (typeof request.perform !== "function") return "perform must be a function";
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function* startStep(
|
|
301
|
+
key: string,
|
|
302
|
+
request: ExecutionStartRequest
|
|
303
|
+
): AsyncGenerator<StepResponse, { executionId: string }, any> {
|
|
304
|
+
yield new StepKey(key);
|
|
305
|
+
|
|
306
|
+
const cached = yield new StepCacheCheck();
|
|
307
|
+
|
|
308
|
+
if (cached) return yield* replay(cached);
|
|
309
|
+
|
|
310
|
+
const started: RuntimeCallResult<ExecutionStartResult> =
|
|
311
|
+
yield new StepRuntimeCall(async ({ executions }) => {
|
|
312
|
+
// The client's failures are never the workflow's to catch: whatever it
|
|
313
|
+
// throws, the delivery ends and the next attempt calls it again.
|
|
314
|
+
try {
|
|
315
|
+
return await executions.start(request);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
throw InfrastructureError.is(err) ? err : new InfrastructureError(err);
|
|
318
|
+
}
|
|
153
319
|
});
|
|
154
|
-
|
|
320
|
+
|
|
321
|
+
if (!started.ok) {
|
|
322
|
+
throw new Error("ExecutionClient.start failed without an infrastructure error");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const outcome = started.value;
|
|
326
|
+
|
|
327
|
+
if (outcome.status === "rejected") {
|
|
328
|
+
const err = new WorkflowStartError(outcome.code, outcome.message);
|
|
155
329
|
yield new StepError(err);
|
|
330
|
+
// unreachable – consumer calls throw() on the generator first
|
|
156
331
|
throw err;
|
|
157
332
|
}
|
|
333
|
+
|
|
334
|
+
return yield* record({ executionId: outcome.executionId });
|
|
158
335
|
}
|
|
159
336
|
|
|
160
337
|
function createWorkflowStore<T>(params: {
|
|
161
338
|
definition: StoreDefinition<any>;
|
|
162
339
|
id: string;
|
|
163
340
|
event: WorkflowEvent;
|
|
164
|
-
storeClient: StoreClient;
|
|
165
341
|
}): WorkflowStore<T> {
|
|
166
|
-
const {
|
|
167
|
-
const key = { storeName: definition.name, storeId: id };
|
|
342
|
+
const { id, event } = params;
|
|
343
|
+
const key = { storeName: params.definition.name, storeId: id };
|
|
344
|
+
// Any operation may run `initial` on a missing store, so its errors must
|
|
345
|
+
// reach workflow code instead of ending the delivery as infrastructure.
|
|
346
|
+
const definition: StoreDefinition<any> = {
|
|
347
|
+
...params.definition,
|
|
348
|
+
initial: workflowCallback(params.definition.initial),
|
|
349
|
+
};
|
|
168
350
|
|
|
169
351
|
return {
|
|
170
|
-
definition,
|
|
352
|
+
definition: params.definition,
|
|
171
353
|
id,
|
|
172
354
|
key,
|
|
173
355
|
get(stepKey?: string) {
|
|
174
|
-
return
|
|
175
|
-
(
|
|
356
|
+
return runtimeStep(
|
|
357
|
+
stepKey ?? getCallSiteHash(this.get),
|
|
358
|
+
async ({ stores }) =>
|
|
359
|
+
(await stores.getStore({ definition, id })) as StoreSnapshot<T>
|
|
176
360
|
);
|
|
177
361
|
},
|
|
178
362
|
select<R>(stepKey: string, selector: StoreSelector<T, R>) {
|
|
179
|
-
return
|
|
180
|
-
const snapshot = await
|
|
363
|
+
return runtimeStep(stepKey, async ({ stores }) => {
|
|
364
|
+
const snapshot = await stores.getStore({ definition, id });
|
|
181
365
|
// Run the selector against a clone so accidental mutation cannot
|
|
182
366
|
// corrupt shared state (consistent with when, which throws via
|
|
183
367
|
// its tracking proxy).
|
|
184
|
-
return selector(cloneStoreState(snapshot.state as T));
|
|
368
|
+
return workflowCallback(selector)(cloneStoreState(snapshot.state as T));
|
|
185
369
|
});
|
|
186
370
|
},
|
|
187
371
|
update(stepKey, updater) {
|
|
188
|
-
return
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
372
|
+
return runtimeStep(
|
|
373
|
+
stepKey,
|
|
374
|
+
async ({ stores }) =>
|
|
375
|
+
(await stores.updateStore({
|
|
376
|
+
definition,
|
|
377
|
+
id,
|
|
378
|
+
updater: workflowCallback(updater, assertSynchronousUpdater) as any,
|
|
379
|
+
// Exactly-once across the store-commit/heap-write gap: on replay
|
|
380
|
+
// after a crash, the store's applied-steps ledger returns the
|
|
381
|
+
// recorded result instead of re-running the updater.
|
|
382
|
+
stepId: { executionId: event.executionId, stepKey },
|
|
383
|
+
})) as StoreUpdateResult<T>
|
|
198
384
|
);
|
|
199
385
|
},
|
|
200
386
|
updateFrom(stepKey, snapshot, updater) {
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
387
|
+
return runtimeStep(
|
|
388
|
+
stepKey,
|
|
389
|
+
async ({ stores }) =>
|
|
390
|
+
(await stores.updateStoreFrom({
|
|
391
|
+
definition,
|
|
392
|
+
id,
|
|
393
|
+
snapshot,
|
|
394
|
+
updater: workflowCallback(updater, assertSynchronousUpdater) as any,
|
|
395
|
+
stepId: { executionId: event.executionId, stepKey },
|
|
396
|
+
})) as StoreUpdateFromResult<T>
|
|
209
397
|
);
|
|
210
398
|
},
|
|
211
399
|
deleteFrom(stepKey, snapshot) {
|
|
212
|
-
return
|
|
213
|
-
|
|
400
|
+
return runtimeStep<StoreDeleteFromResult>(stepKey, ({ stores }) =>
|
|
401
|
+
stores.deleteStoreFrom({
|
|
214
402
|
definition,
|
|
215
403
|
id,
|
|
216
404
|
snapshot,
|
|
@@ -234,7 +422,6 @@ function createWorkflowStore<T>(params: {
|
|
|
234
422
|
definition,
|
|
235
423
|
id,
|
|
236
424
|
event,
|
|
237
|
-
storeClient,
|
|
238
425
|
stepKey,
|
|
239
426
|
selector,
|
|
240
427
|
});
|
|
@@ -248,7 +435,6 @@ function createWorkflowStore<T>(params: {
|
|
|
248
435
|
definition,
|
|
249
436
|
id,
|
|
250
437
|
event,
|
|
251
|
-
storeClient,
|
|
252
438
|
stepKey,
|
|
253
439
|
selector,
|
|
254
440
|
claim,
|
|
@@ -257,14 +443,45 @@ function createWorkflowStore<T>(params: {
|
|
|
257
443
|
};
|
|
258
444
|
}
|
|
259
445
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
446
|
+
export type StepRunOptions<S extends StandardSchemaV1> = {
|
|
447
|
+
/**
|
|
448
|
+
* Validates the step's decoded value, on the first run and on every
|
|
449
|
+
* replay; the step returns the schema's output. The step function must
|
|
450
|
+
* return the schema's input as a durable value.
|
|
451
|
+
*/
|
|
452
|
+
schema: S;
|
|
453
|
+
};
|
|
263
454
|
|
|
264
|
-
|
|
455
|
+
/**
|
|
456
|
+
* A step function whose result, or the value its promise resolves to, is a
|
|
457
|
+
* durable value. Checking the whole function type keeps the result's type as
|
|
458
|
+
* TypeScript infers it, and names the offending property in the error.
|
|
459
|
+
*/
|
|
460
|
+
export type DurableStepFn<F> = F extends () => infer R
|
|
461
|
+
? () => R extends PromiseLike<infer U> ? PromiseLike<DurableUnion<U>> : DurableUnion<R>
|
|
462
|
+
: never;
|
|
463
|
+
|
|
464
|
+
type DurableUnion<T> = T extends unknown ? Durable<T> : never;
|
|
465
|
+
|
|
466
|
+
function run<F extends () => unknown>(
|
|
467
|
+
fn: F & DurableStepFn<F>
|
|
468
|
+
): AsyncGenerator<StepResponse, Awaited<ReturnType<F>>, StepResponse>;
|
|
469
|
+
|
|
470
|
+
function run<F extends () => unknown>(
|
|
471
|
+
key: string,
|
|
472
|
+
fn: F & DurableStepFn<F>
|
|
473
|
+
): AsyncGenerator<StepResponse, Awaited<ReturnType<F>>, StepResponse>;
|
|
474
|
+
|
|
475
|
+
function run<S extends StandardSchemaV1>(
|
|
476
|
+
fn: () => StepSchemaInput<S> | PromiseLike<StepSchemaInput<S>>,
|
|
477
|
+
options: StepRunOptions<S>
|
|
478
|
+
): AsyncGenerator<StepResponse, StandardSchemaV1.InferOutput<S>, StepResponse>;
|
|
479
|
+
|
|
480
|
+
function run<S extends StandardSchemaV1>(
|
|
265
481
|
key: string,
|
|
266
|
-
fn: () =>
|
|
267
|
-
|
|
482
|
+
fn: () => StepSchemaInput<S> | PromiseLike<StepSchemaInput<S>>,
|
|
483
|
+
options: StepRunOptions<S>
|
|
484
|
+
): AsyncGenerator<StepResponse, StandardSchemaV1.InferOutput<S>, StepResponse>;
|
|
268
485
|
|
|
269
486
|
// NOTE: run/delay/poll are plain (synchronous) functions that delegate to
|
|
270
487
|
// async generators. This matters for keyless steps: the call-site hash must be
|
|
@@ -272,59 +489,72 @@ function run<T extends any>(
|
|
|
272
489
|
// generator body doesn't execute until its first .next(), which happens inside
|
|
273
490
|
// the workflow driver, so hashing there would give every keyless step the same
|
|
274
491
|
// key.
|
|
275
|
-
function run
|
|
276
|
-
arg1: string | (() =>
|
|
277
|
-
arg2?: (() =>
|
|
278
|
-
|
|
492
|
+
function run(
|
|
493
|
+
arg1: string | (() => unknown),
|
|
494
|
+
arg2?: (() => unknown) | StepRunOptions<StandardSchemaV1>,
|
|
495
|
+
arg3?: StepRunOptions<StandardSchemaV1>
|
|
496
|
+
): AsyncGenerator<StepResponse, any, StepResponse> {
|
|
279
497
|
let key: string | null = null;
|
|
280
|
-
let fn: () =>
|
|
498
|
+
let fn: () => unknown;
|
|
499
|
+
let options: StepRunOptions<StandardSchemaV1> | undefined;
|
|
281
500
|
|
|
282
501
|
if (typeof arg1 === "string") {
|
|
283
502
|
key = arg1;
|
|
284
|
-
fn = arg2 as () =>
|
|
503
|
+
fn = arg2 as () => unknown;
|
|
504
|
+
options = arg3;
|
|
285
505
|
} else {
|
|
286
506
|
fn = arg1;
|
|
507
|
+
options = arg2 as StepRunOptions<StandardSchemaV1> | undefined;
|
|
287
508
|
}
|
|
288
509
|
|
|
289
510
|
if (!key) key = getCallSiteHash(run);
|
|
290
511
|
|
|
291
|
-
return durableStep(key, fn);
|
|
512
|
+
return durableStep(key, fn, options?.schema);
|
|
292
513
|
}
|
|
293
514
|
|
|
294
|
-
|
|
295
|
-
|
|
515
|
+
type StepSchemaInput<S extends StandardSchemaV1> = Durable<
|
|
516
|
+
StandardSchemaV1.InferInput<S>
|
|
517
|
+
>;
|
|
518
|
+
|
|
519
|
+
/** `step.delay`, reading its wake time from the runtime clock. */
|
|
520
|
+
function createDelay(clock: () => number) {
|
|
521
|
+
function delay(retryInterval: number): any;
|
|
522
|
+
function delay(key: string, retryInterval: number): any;
|
|
523
|
+
function delay(
|
|
524
|
+
arg1: string | number,
|
|
525
|
+
arg2?: number
|
|
526
|
+
): AsyncGenerator<any, void, StepDelay> {
|
|
527
|
+
let key: string | null = null;
|
|
528
|
+
let retryInterval: number;
|
|
529
|
+
|
|
530
|
+
if (typeof arg1 === "string") {
|
|
531
|
+
key = arg1;
|
|
532
|
+
retryInterval = arg2!;
|
|
533
|
+
} else {
|
|
534
|
+
retryInterval = arg1;
|
|
535
|
+
}
|
|
296
536
|
|
|
297
|
-
|
|
298
|
-
arg1: string | number,
|
|
299
|
-
arg2?: number
|
|
300
|
-
): AsyncGenerator<any, void, StepDelay> {
|
|
301
|
-
let key: string | null = null;
|
|
302
|
-
let retryInterval: number;
|
|
537
|
+
if (!key) key = getCallSiteHash(delay);
|
|
303
538
|
|
|
304
|
-
|
|
305
|
-
key = arg1;
|
|
306
|
-
retryInterval = arg2!;
|
|
307
|
-
} else {
|
|
308
|
-
retryInterval = arg1;
|
|
539
|
+
return delayStep(key, retryInterval, clock);
|
|
309
540
|
}
|
|
310
541
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
return delayStep(key, retryInterval);
|
|
542
|
+
return delay;
|
|
314
543
|
}
|
|
315
544
|
|
|
316
545
|
async function* delayStep(
|
|
317
546
|
key: string,
|
|
318
|
-
retryInterval: number
|
|
547
|
+
retryInterval: number,
|
|
548
|
+
clock: () => number
|
|
319
549
|
): AsyncGenerator<any, void, StepDelay> {
|
|
320
550
|
yield new StepKey(key);
|
|
321
551
|
|
|
322
552
|
const cached = yield new StepCacheCheck();
|
|
323
553
|
|
|
324
554
|
if (cached) {
|
|
325
|
-
yield new StepDelay(cached.
|
|
555
|
+
yield new StepDelay(cached.wakeAt);
|
|
326
556
|
} else {
|
|
327
|
-
yield new StepDelay(
|
|
557
|
+
yield new StepDelay(clock() + retryInterval);
|
|
328
558
|
}
|
|
329
559
|
}
|
|
330
560
|
|
|
@@ -366,27 +596,32 @@ function poll(
|
|
|
366
596
|
return run(key, task);
|
|
367
597
|
}
|
|
368
598
|
|
|
369
|
-
async function* durableStep
|
|
599
|
+
async function* durableStep(
|
|
370
600
|
key: string,
|
|
371
|
-
fn: () =>
|
|
372
|
-
|
|
601
|
+
fn: () => unknown,
|
|
602
|
+
schema: StandardSchemaV1 | undefined
|
|
603
|
+
): AsyncGenerator<StepResponse, any, any> {
|
|
373
604
|
yield new StepKey(key);
|
|
374
605
|
|
|
375
606
|
const cached = yield new StepCacheCheck();
|
|
376
607
|
|
|
377
608
|
if (cached) {
|
|
378
|
-
yield cached;
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
|
|
609
|
+
const value = yield* replay(cached);
|
|
610
|
+
if (!schema) return value;
|
|
611
|
+
const validated = await validateStepValue(key, schema, value);
|
|
612
|
+
if (cached.schema === "rejected" && validated.ok) {
|
|
613
|
+
throw new WorkflowValueError(
|
|
614
|
+
"STEP_SCHEMA_INVALID",
|
|
615
|
+
key,
|
|
616
|
+
`Step "${key}" schema rejected its value on the first run`
|
|
617
|
+
);
|
|
382
618
|
}
|
|
383
|
-
return
|
|
619
|
+
return unwrapValidated(validated);
|
|
384
620
|
}
|
|
385
621
|
|
|
622
|
+
let result: unknown;
|
|
386
623
|
try {
|
|
387
|
-
|
|
388
|
-
yield new StepResult(result);
|
|
389
|
-
return result;
|
|
624
|
+
result = await fn();
|
|
390
625
|
} catch (err: unknown) {
|
|
391
626
|
if (err instanceof RetryableError) {
|
|
392
627
|
yield new StepError(err, {
|
|
@@ -399,47 +634,139 @@ async function* durableStep<T extends any>(
|
|
|
399
634
|
// unreachable – consumer calls throw() on the generator first
|
|
400
635
|
throw err;
|
|
401
636
|
}
|
|
637
|
+
|
|
638
|
+
if (!schema) return yield* record(result);
|
|
639
|
+
|
|
640
|
+
// The schema validates the value as replay will decode it. A value the
|
|
641
|
+
// codec rejects is left for the driver to record as INVALID_STEP_VALUE.
|
|
642
|
+
const encoding = durableValueCodec.encode(result);
|
|
643
|
+
if (encoding.status === "invalid") return yield* record(result);
|
|
644
|
+
const decoding = durableValueCodec.decode(encoding.canonical);
|
|
645
|
+
if (decoding.status === "invalid") throw new Error(decoding.message);
|
|
646
|
+
const validated = await validateStepValue(key, schema, decoding.value);
|
|
647
|
+
yield* record(result, validated.ok ? "accepted" : "rejected");
|
|
648
|
+
return unwrapValidated(validated);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
type ValidatedStepValue =
|
|
652
|
+
| { ok: true; value: unknown }
|
|
653
|
+
| { ok: false; error: WorkflowValueError };
|
|
654
|
+
|
|
655
|
+
async function validateStepValue(
|
|
656
|
+
key: string,
|
|
657
|
+
schema: StandardSchemaV1,
|
|
658
|
+
value: unknown
|
|
659
|
+
): Promise<ValidatedStepValue> {
|
|
660
|
+
const result = await schema["~standard"].validate(value);
|
|
661
|
+
if (result.issues) {
|
|
662
|
+
const messages = result.issues.map((issue) => issue.message).join("; ");
|
|
663
|
+
return {
|
|
664
|
+
ok: false,
|
|
665
|
+
error: new WorkflowValueError(
|
|
666
|
+
"STEP_SCHEMA_INVALID",
|
|
667
|
+
key,
|
|
668
|
+
`Step "${key}" value failed its schema: ${messages}`
|
|
669
|
+
),
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
return { ok: true, value: result.value };
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function unwrapValidated(validated: ValidatedStepValue) {
|
|
676
|
+
if (!validated.ok) throw validated.error;
|
|
677
|
+
return validated.value;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Yields a step's result for the driver to record, and returns the decoded
|
|
682
|
+
* record the driver resumes the step with, so that the first run returns
|
|
683
|
+
* what replay will.
|
|
684
|
+
*/
|
|
685
|
+
async function* record<T>(
|
|
686
|
+
result: T,
|
|
687
|
+
schema?: StepSchemaVerdict
|
|
688
|
+
): AsyncGenerator<StepResponse, T, any> {
|
|
689
|
+
const recorded: StepResult = yield new StepResult(result, schema);
|
|
690
|
+
return recorded.result;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Yields a recorded response; the driver throws a recorded error into the step. */
|
|
694
|
+
async function* replay(cached: StepResponse): AsyncGenerator<StepResponse, any, any> {
|
|
695
|
+
const recorded: StepResult = yield cached;
|
|
696
|
+
return recorded.result;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Yields a store step's error for the driver to record and throw. A value
|
|
701
|
+
* error raised by a store client is given this step's key.
|
|
702
|
+
*/
|
|
703
|
+
async function* fail(key: string, error: unknown): AsyncGenerator<StepResponse, never, any> {
|
|
704
|
+
const err =
|
|
705
|
+
error instanceof WorkflowValueError && error.stepKey !== key
|
|
706
|
+
? new WorkflowValueError(error.code, key, error.message)
|
|
707
|
+
: error;
|
|
708
|
+
yield new StepError(err);
|
|
709
|
+
// unreachable – consumer calls throw() on the generator first
|
|
710
|
+
throw err;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* A durable step whose work is a runtime client call. The driver makes the
|
|
715
|
+
* call and resumes the step with its result.
|
|
716
|
+
*/
|
|
717
|
+
async function* runtimeStep<T>(
|
|
718
|
+
key: string,
|
|
719
|
+
call: StepRuntimeCall<T>["call"]
|
|
720
|
+
): AsyncGenerator<StepResponse, T, any> {
|
|
721
|
+
yield new StepKey(key);
|
|
722
|
+
|
|
723
|
+
const cached = yield new StepCacheCheck();
|
|
724
|
+
|
|
725
|
+
if (cached) return yield* replay(cached);
|
|
726
|
+
|
|
727
|
+
const outcome: RuntimeCallResult<T> = yield new StepRuntimeCall(call);
|
|
728
|
+
|
|
729
|
+
if (!outcome.ok) return yield* fail(key, outcome.error);
|
|
730
|
+
|
|
731
|
+
return yield* record(outcome.value);
|
|
402
732
|
}
|
|
403
733
|
|
|
404
734
|
async function* whenStep<T, R>(params: {
|
|
405
735
|
definition: StoreDefinition<any>;
|
|
406
736
|
id: string;
|
|
407
737
|
event: WorkflowEvent;
|
|
408
|
-
storeClient: StoreClient;
|
|
409
738
|
stepKey: string;
|
|
410
739
|
selector: StoreSelector<T, R | undefined | null | false>;
|
|
411
|
-
}): AsyncGenerator<StepResponse, NonNullable<R>,
|
|
412
|
-
const { definition, id, event,
|
|
740
|
+
}): AsyncGenerator<StepResponse, NonNullable<R>, any> {
|
|
741
|
+
const { definition, id, event, stepKey, selector } = params;
|
|
413
742
|
|
|
414
743
|
yield new StepKey(stepKey);
|
|
415
744
|
|
|
416
745
|
const cached = yield new StepCacheCheck();
|
|
417
746
|
|
|
418
|
-
if (cached)
|
|
419
|
-
yield cached;
|
|
420
|
-
if (!(cached instanceof StepResult)) {
|
|
421
|
-
throw new Error("Store wait step cache must resolve to a step result");
|
|
422
|
-
}
|
|
423
|
-
return cached.result;
|
|
424
|
-
}
|
|
747
|
+
if (cached) return yield* replay(cached);
|
|
425
748
|
|
|
426
|
-
const
|
|
749
|
+
const read: RuntimeCallResult<StoreSnapshot<unknown>> =
|
|
750
|
+
yield new StepRuntimeCall(({ stores }) =>
|
|
751
|
+
stores.getStore({ definition, id })
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
if (!read.ok) return yield* fail(stepKey, read.error);
|
|
755
|
+
|
|
756
|
+
const snapshot = read.value;
|
|
427
757
|
const { result, readPaths } = trackStoreSelector(
|
|
428
758
|
cloneStoreState(snapshot.state as T),
|
|
429
759
|
selector
|
|
430
760
|
);
|
|
431
761
|
|
|
432
762
|
if (result !== undefined && result !== null && result !== false) {
|
|
433
|
-
|
|
434
|
-
yield new StepResult(clonedResult);
|
|
435
|
-
return clonedResult as NonNullable<R>;
|
|
763
|
+
return yield* record(result as NonNullable<R>);
|
|
436
764
|
}
|
|
437
765
|
|
|
438
|
-
|
|
766
|
+
yield* registerWaiter({
|
|
439
767
|
workflowId: event.workflowId,
|
|
440
768
|
executionId: event.executionId,
|
|
441
769
|
stepKey,
|
|
442
|
-
event,
|
|
443
770
|
storeName: definition.name,
|
|
444
771
|
storeId: id,
|
|
445
772
|
instanceId: snapshot.instanceId,
|
|
@@ -447,8 +774,6 @@ async function* whenStep<T, R>(params: {
|
|
|
447
774
|
readPaths,
|
|
448
775
|
});
|
|
449
776
|
|
|
450
|
-
yield new StepStoreWait();
|
|
451
|
-
|
|
452
777
|
throw new Error("Store wait yielded control unexpectedly");
|
|
453
778
|
}
|
|
454
779
|
|
|
@@ -456,54 +781,42 @@ async function* takeStep<T, R>(params: {
|
|
|
456
781
|
definition: StoreDefinition<any>;
|
|
457
782
|
id: string;
|
|
458
783
|
event: WorkflowEvent;
|
|
459
|
-
storeClient: StoreClient;
|
|
460
784
|
stepKey: string;
|
|
461
785
|
selector: StoreSelector<T, R | undefined | null | false>;
|
|
462
786
|
claim: (draft: Draft<T>, selected: NonNullable<R>) => void;
|
|
463
|
-
}): AsyncGenerator<StepResponse, NonNullable<R>,
|
|
464
|
-
const { definition, id, event,
|
|
465
|
-
params;
|
|
787
|
+
}): AsyncGenerator<StepResponse, NonNullable<R>, any> {
|
|
788
|
+
const { definition, id, event, stepKey, selector, claim } = params;
|
|
466
789
|
|
|
467
790
|
yield new StepKey(stepKey);
|
|
468
791
|
|
|
469
792
|
const cached = yield new StepCacheCheck();
|
|
470
793
|
|
|
471
|
-
if (cached)
|
|
472
|
-
yield cached;
|
|
473
|
-
if (!(cached instanceof StepResult)) {
|
|
474
|
-
throw new Error("Store take step cache must resolve to a step result");
|
|
475
|
-
}
|
|
476
|
-
return cached.result;
|
|
477
|
-
}
|
|
794
|
+
if (cached) return yield* replay(cached);
|
|
478
795
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
yield new StepError(err);
|
|
493
|
-
// unreachable – consumer calls throw() on the generator first
|
|
494
|
-
throw err;
|
|
495
|
-
}
|
|
796
|
+
const taken: RuntimeCallResult<StoreTakeResult<R>> =
|
|
797
|
+
yield new StepRuntimeCall(({ stores }) =>
|
|
798
|
+
stores.takeFromStore({
|
|
799
|
+
definition,
|
|
800
|
+
id,
|
|
801
|
+
selector: workflowCallback(selector) as any,
|
|
802
|
+
claim: workflowCallback(claim, assertSynchronousClaim) as any,
|
|
803
|
+
// Exactly-once across the store-commit/heap-write gap: on replay
|
|
804
|
+
// after a crash, the store's applied-steps ledger returns the
|
|
805
|
+
// recorded (matched) outcome instead of re-running selector/claim.
|
|
806
|
+
stepId: { executionId: event.executionId, stepKey },
|
|
807
|
+
})
|
|
808
|
+
);
|
|
496
809
|
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
810
|
+
if (!taken.ok) return yield* fail(stepKey, taken.error);
|
|
811
|
+
|
|
812
|
+
const outcome = taken.value;
|
|
501
813
|
|
|
502
|
-
|
|
814
|
+
if (outcome.matched) return yield* record(outcome.selected as NonNullable<R>);
|
|
815
|
+
|
|
816
|
+
yield* registerWaiter({
|
|
503
817
|
workflowId: event.workflowId,
|
|
504
818
|
executionId: event.executionId,
|
|
505
819
|
stepKey,
|
|
506
|
-
event,
|
|
507
820
|
storeName: definition.name,
|
|
508
821
|
storeId: id,
|
|
509
822
|
instanceId: outcome.instanceId,
|
|
@@ -511,7 +824,18 @@ async function* takeStep<T, R>(params: {
|
|
|
511
824
|
readPaths: outcome.readPaths,
|
|
512
825
|
});
|
|
513
826
|
|
|
514
|
-
yield new StepStoreWait();
|
|
515
|
-
|
|
516
827
|
throw new Error("Store take yielded control unexpectedly");
|
|
517
828
|
}
|
|
829
|
+
|
|
830
|
+
/** Registers a waiter, then suspends the step until a store change wakes it. */
|
|
831
|
+
async function* registerWaiter(
|
|
832
|
+
waiter: StoreWaiter
|
|
833
|
+
): AsyncGenerator<StepResponse, void, any> {
|
|
834
|
+
const registered: RuntimeCallResult<void> = yield new StepRuntimeCall(
|
|
835
|
+
({ stores }) => stores.registerWaiter(waiter)
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
if (!registered.ok) return yield* fail(waiter.stepKey, registered.error);
|
|
839
|
+
|
|
840
|
+
yield new StepStoreWait();
|
|
841
|
+
}
|