yieldstar 0.4.6 → 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.
Files changed (43) hide show
  1. package/README.md +57 -64
  2. package/dist/exports/errors.d.ts.map +1 -1
  3. package/dist/exports/sdk-http.d.ts +8 -0
  4. package/dist/exports/sdk-http.d.ts.map +1 -1
  5. package/dist/exports/sdk-local.d.ts +19 -2
  6. package/dist/exports/sdk-local.d.ts.map +1 -1
  7. package/dist/exports/workflow.d.ts +14 -2
  8. package/dist/exports/workflow.d.ts.map +1 -1
  9. package/dist/index.d.ts +5 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +791 -137
  12. package/dist/internal/effect-call.d.ts +60 -0
  13. package/dist/internal/effect-call.d.ts.map +1 -0
  14. package/dist/internal/runtime-call.d.ts +39 -0
  15. package/dist/internal/runtime-call.d.ts.map +1 -0
  16. package/dist/internal/serialise.d.ts.map +1 -1
  17. package/dist/internal/step-record.d.ts +13 -0
  18. package/dist/internal/step-record.d.ts.map +1 -0
  19. package/dist/internal/step-runner.d.ts +93 -9
  20. package/dist/internal/step-runner.d.ts.map +1 -1
  21. package/dist/internal/utils.d.ts +11 -0
  22. package/dist/internal/utils.d.ts.map +1 -1
  23. package/package.json +8 -4
  24. package/src/exports/errors.ts +4 -0
  25. package/src/exports/sdk-http.ts +60 -27
  26. package/src/exports/sdk-local.ts +41 -15
  27. package/src/exports/workflow.ts +159 -99
  28. package/src/index.ts +51 -1
  29. package/src/internal/effect-call.ts +217 -0
  30. package/src/internal/runtime-call.ts +95 -0
  31. package/src/internal/step-record.ts +110 -0
  32. package/src/internal/step-runner.ts +748 -53
  33. package/src/internal/utils.ts +24 -10
  34. package/dist/exports/scheduler.d.ts +0 -49
  35. package/dist/exports/scheduler.d.ts.map +0 -1
  36. package/dist/exports/simplified-types.d.ts +0 -31
  37. package/dist/exports/simplified-types.d.ts.map +0 -1
  38. package/dist/exports/types.d.ts +0 -6
  39. package/dist/exports/types.d.ts.map +0 -1
  40. package/dist/internal/types.d.ts +0 -9
  41. package/dist/internal/types.d.ts.map +0 -1
  42. package/src/internal/serialise.ts +0 -44
  43. package/tsconfig.tsbuildinfo +0 -1
package/dist/index.js CHANGED
@@ -1,57 +1,83 @@
1
- // @bun
2
1
  // src/exports/sdk-local.ts
3
- import { nanoid } from "nanoid";
4
- function createLocalSdk(invoker) {
2
+ import {
3
+ ExecutionAdmissionError,
4
+ waitForExecutionOutcome
5
+ } from "@yieldstar/core";
6
+ function createLocalSdk(runtime, options) {
7
+ const { executions } = runtime;
5
8
  return {
6
9
  async trigger(event) {
7
- const executionEvent = {
8
- executionId: event.executionId ?? nanoid(),
10
+ const admission = await executions.admit({
11
+ executionId: event.executionId,
9
12
  workflowId: event.workflowId,
10
- params: event.params
11
- };
12
- await invoker.execute(executionEvent);
13
- return { executionId: executionEvent.executionId };
13
+ params: event.params,
14
+ context: new Map(Object.entries(event.context ?? {}))
15
+ });
16
+ if (admission.status === "rejected") {
17
+ throw new ExecutionAdmissionError(admission.code, admission.message);
18
+ }
19
+ return { executionId: admission.executionId };
14
20
  },
15
21
  async triggerAndWait(event) {
16
- const executionId = event.executionId ?? nanoid();
17
- const workflowCompletePromise = new Promise((resolve) => {
18
- invoker.workflowEndEmitter.once(executionId, resolve);
22
+ const { executionId } = await this.trigger(event);
23
+ const outcome = await waitForExecutionOutcome(executions, executionId, {
24
+ emitter: options?.invoker?.workflowEndEmitter,
25
+ pollInterval: options?.pollInterval
19
26
  });
20
- await this.trigger({ ...event, executionId });
21
- return workflowCompletePromise;
27
+ if (!outcome) {
28
+ throw new Error(`Execution "${executionId}" has no record`);
29
+ }
30
+ if (outcome.status === "failed")
31
+ throw outcome.error;
32
+ return outcome.result;
22
33
  }
23
34
  };
24
35
  }
25
36
  // src/exports/sdk-http.ts
26
- import { nanoid as nanoid2 } from "nanoid";
27
- import { deserializeError } from "serialize-error";
28
- import { errorWithOriginalStack } from "@yieldstar/core";
37
+ import {
38
+ ExecutionAdmissionError as ExecutionAdmissionError2,
39
+ durableValueCodec
40
+ } from "@yieldstar/core";
41
+ var admissionErrorCodes = [
42
+ "EXECUTION_ID_CONFLICT",
43
+ "INVALID_EXECUTION_EVENT"
44
+ ];
29
45
  function createHttpSdkFactory() {
30
46
  return (params) => {
31
47
  return {
32
48
  async trigger(event) {
33
- const executionEvent = {
34
- executionId: event?.executionId ?? nanoid2(),
35
- workflowId: event?.workflowId,
36
- params: event?.params,
37
- context: event?.context
38
- };
49
+ const request = { workflowId: event?.workflowId };
50
+ if (event?.executionId !== undefined)
51
+ request.executionId = event.executionId;
52
+ if (event && "params" in event)
53
+ request.params = event.params;
54
+ if (event?.context !== undefined)
55
+ request.context = event.context;
56
+ const encoding = durableValueCodec.encode(request);
57
+ if (encoding.status === "invalid") {
58
+ throw new ExecutionAdmissionError2("INVALID_EXECUTION_EVENT", `Event cannot be stored without loss: ${encoding.message}`);
59
+ }
39
60
  const res = await fetch(`${params.url}/trigger`, {
40
61
  ...params.fetchOptions,
41
62
  method: "POST",
42
- body: JSON.stringify(executionEvent)
63
+ headers: { "content-type": "application/json" },
64
+ body: encoding.canonical
43
65
  });
44
66
  if (res.ok) {
67
+ const ack = await res.json();
45
68
  return {
46
- executionId: executionEvent.executionId,
47
- ack: () => res.json(),
48
- waitForResult: () => this.waitForResult({
49
- executionId: executionEvent.executionId
50
- })
69
+ executionId: ack.executionId,
70
+ ack: async () => ack,
71
+ waitForResult: () => this.waitForResult(ack)
51
72
  };
52
- } else {
53
- throw res.statusText;
54
73
  }
74
+ const body = await res.json().catch(() => {
75
+ return;
76
+ });
77
+ if (body?.code && admissionErrorCodes.includes(body.code)) {
78
+ throw new ExecutionAdmissionError2(body.code, body.message ?? res.statusText);
79
+ }
80
+ throw res.statusText;
55
81
  },
56
82
  async waitForResult(ack) {
57
83
  const res = await fetch(`${params.url}/events`, {
@@ -59,11 +85,18 @@ function createHttpSdkFactory() {
59
85
  body: JSON.stringify({ executionId: ack.executionId }),
60
86
  ...params.fetchOptions
61
87
  });
88
+ if (res.status !== 500 && !res.ok) {
89
+ throw res.statusText;
90
+ }
91
+ const decoding = durableValueCodec.decode(await res.text());
92
+ if (decoding.status === "invalid") {
93
+ throw new Error(`Invalid workflow outcome: ${decoding.message}`);
94
+ }
62
95
  if (res.status === 500) {
63
- const error = await res.json();
64
- throw errorWithOriginalStack(deserializeError(error), this.waitForResult);
96
+ const error = decoding.value instanceof Error ? decoding.value : new Error(String(decoding.value));
97
+ throw error;
65
98
  }
66
- return res.json();
99
+ return decoding.value;
67
100
  }
68
101
  };
69
102
  };
@@ -73,6 +106,8 @@ function createWorkflowRouter(workflows) {
73
106
  return workflows;
74
107
  }
75
108
  // src/exports/errors.ts
109
+ import { registerErrorClass } from "@yieldstar/core";
110
+
76
111
  class RetryableError extends Error {
77
112
  maxAttempts;
78
113
  retryInterval;
@@ -83,129 +118,537 @@ class RetryableError extends Error {
83
118
  this.retryInterval = opts.retryInterval;
84
119
  }
85
120
  }
121
+ registerErrorClass("RetryableError", RetryableError);
122
+
123
+ // src/index.ts
124
+ import {
125
+ EffectClientInvariantError as EffectClientInvariantError2,
126
+ EffectDeliveryError as EffectDeliveryError2,
127
+ ExecutionAdmissionError as ExecutionAdmissionError3,
128
+ WORKFLOW_RESULT_STEP_KEY as WORKFLOW_RESULT_STEP_KEY2,
129
+ WorkflowEffectError as WorkflowEffectError2,
130
+ WorkflowStartError as WorkflowStartError2,
131
+ WorkflowValueError as WorkflowValueError4,
132
+ registerErrorClass as registerErrorClass2
133
+ } from "@yieldstar/core";
134
+
86
135
  // src/internal/utils.ts
87
136
  import { createHash } from "crypto";
88
137
  var isIterable = (i) => {
89
138
  return typeof i === "object" && i != null && Symbol.iterator in i;
90
139
  };
91
140
  function getCallSiteHash(stepFn) {
92
- const originalStack = stepFn.stack;
93
- Error.captureStackTrace(stepFn, getCallSiteHash);
94
- const stack = stepFn.stack;
95
- const stepFnCallSite = stack.split(")")[1];
96
- if (originalStack === undefined) {
97
- delete stepFn.stack;
98
- } else {
99
- stepFn.stack = originalStack;
141
+ const holder = {};
142
+ Error.captureStackTrace(holder, stepFn);
143
+ const frames = (holder.stack ?? "").split(`
144
+ `).filter((line) => /^\s*at\s/.test(line));
145
+ const callSite = frames[0]?.trim();
146
+ if (!callSite) {
147
+ throw new Error("Could not derive a cache key from the call site. Pass an explicit key to this step.");
100
148
  }
101
- return createHash("sha1").update(stepFnCallSite).digest("hex");
149
+ return createHash("sha1").update(callSite).digest("hex");
102
150
  }
103
151
 
104
- // src/internal/serialise.ts
152
+ // src/internal/step-record.ts
105
153
  import {
154
+ DurableValueError,
106
155
  StepDelay,
156
+ StepEffectWait,
107
157
  StepError,
158
+ StepInvalid,
108
159
  StepResult,
109
- WorkflowResult
160
+ StepStoreWait,
161
+ WorkflowResult,
162
+ WorkflowValueError,
163
+ decodeParsedValue,
164
+ durableValueCodec as durableValueCodec2,
165
+ encodeThrownValue
110
166
  } from "@yieldstar/core";
111
- import { isErrorLike, serializeError, deserializeError as deserializeError2 } from "serialize-error";
112
- function serializeStepResponse(data) {
113
- const replaceErrors = (key, value) => {
114
- if (value instanceof Error) {
115
- return serializeError(value);
167
+ function encodeStepRecord(response, stepKey) {
168
+ const json = encodeResponse(response, stepKey);
169
+ return { json, response: decodeStepRecord(json) };
170
+ }
171
+ function encodeResponse(response, stepKey) {
172
+ if (response instanceof StepResult || response instanceof WorkflowResult) {
173
+ const result = durableValueCodec2.encode(response.result);
174
+ if (result.status === "invalid") {
175
+ const label = response instanceof WorkflowResult ? "Workflow result" : `Step "${stepKey}" result`;
176
+ return encodeResponse(new StepError(new WorkflowValueError("INVALID_STEP_VALUE", stepKey, `${label} is not a durable value: ${result.message}`)), stepKey);
116
177
  }
117
- return value;
118
- };
119
- return JSON.stringify(data, replaceErrors);
178
+ const schema = response instanceof StepResult && response.schema !== undefined ? `,"schema":${JSON.stringify(response.schema)}` : "";
179
+ return `{"result":${result.canonical}${schema},"type":${JSON.stringify(response.type)}}`;
180
+ }
181
+ if (response instanceof StepError) {
182
+ return `{"backOffStrategy":${JSON.stringify(response.backOffStrategy)}` + `,"err":${encodeThrownValue(response.err)}` + `,"maxAttempts":${JSON.stringify(response.maxAttempts)}` + `,"retryInterval":${JSON.stringify(response.retryInterval)}` + `,"type":"step-error"}`;
183
+ }
184
+ if (response instanceof StepDelay) {
185
+ return `{"type":"step-delay","wakeAt":${JSON.stringify(response.wakeAt)}}`;
186
+ }
187
+ if (response instanceof StepEffectWait) {
188
+ return `{"type":"effect-wait","wakeAt":${JSON.stringify(response.wakeAt)}}`;
189
+ }
190
+ if (response instanceof StepStoreWait || response instanceof StepInvalid) {
191
+ return `{"type":${JSON.stringify(response.type)}}`;
192
+ }
193
+ throw new Error(`Cannot record a ${response.type} step response`);
120
194
  }
121
- function deserializeStepResponse(jsonString) {
122
- const reviveErrors = (key, value) => {
123
- if (isErrorLike(value)) {
124
- return deserializeError2(value);
195
+ function decodeStepRecord(json) {
196
+ const record = JSON.parse(json);
197
+ try {
198
+ switch (record.type) {
199
+ case "step-result":
200
+ return new StepResult(decodeParsedValue(record.result, "result"), record.schema === "accepted" || record.schema === "rejected" ? record.schema : undefined);
201
+ case "workflow-result":
202
+ return new WorkflowResult(decodeParsedValue(record.result, "result"));
203
+ case "step-error":
204
+ return new StepError(decodeParsedValue(record.err, "err"), {
205
+ maxAttempts: record.maxAttempts,
206
+ retryInterval: record.retryInterval,
207
+ backoffStrategy: record.backOffStrategy
208
+ });
209
+ case "step-delay":
210
+ return new StepDelay(record.wakeAt);
211
+ case "effect-wait":
212
+ return new StepEffectWait(record.wakeAt);
213
+ case "store-wait":
214
+ return new StepStoreWait;
215
+ case "step-invalid":
216
+ return new StepInvalid;
125
217
  }
126
- return value;
127
- };
128
- const stepResponse = JSON.parse(jsonString, reviveErrors);
129
- switch (stepResponse.type) {
130
- case "step-error":
131
- const { err, ...params } = stepResponse;
132
- return new StepError(stepResponse.err, params);
133
- case "step-delay":
134
- return new StepDelay(stepResponse.resumeIn);
135
- case "step-result":
136
- return new StepResult(stepResponse.result);
137
- case "workflow-result":
138
- return new WorkflowResult(stepResponse.result);
139
- default:
140
- throw new Error("Unknown step result type: " + stepResponse.type);
218
+ } catch (err) {
219
+ if (err instanceof DurableValueError) {
220
+ throw new Error(`Invalid step record: ${err.message}`);
221
+ }
222
+ throw err;
141
223
  }
224
+ throw new Error(`Unknown step record type: ${record.type}`);
142
225
  }
143
226
 
144
227
  // src/internal/step-runner.ts
145
228
  import {
229
+ assertSynchronousClaim,
230
+ assertSynchronousUpdater,
231
+ cloneStoreState,
232
+ defineStore,
233
+ InfrastructureError as InfrastructureError3,
146
234
  StepKey,
147
235
  StepError as StepError2,
148
236
  StepResult as StepResult2,
149
237
  StepDelay as StepDelay2,
150
- StepCacheCheck
238
+ StepCacheCheck,
239
+ StepStoreWait as StepStoreWait2,
240
+ trackStoreSelector,
241
+ durableValueCodec as durableValueCodec4,
242
+ StepEffectWait as StepEffectWait2,
243
+ validateEffectRequest,
244
+ WorkflowEffectError,
245
+ WorkflowStartError,
246
+ WorkflowValueError as WorkflowValueError3
151
247
  } from "@yieldstar/core";
152
- var stepRunner = { run, delay, poll };
153
- async function* run(arg1, arg2) {
154
- let key = null;
155
- let fn;
156
- if (typeof arg1 === "string") {
157
- key = arg1;
158
- fn = arg2;
159
- } else {
160
- fn = arg1;
248
+
249
+ // src/internal/runtime-call.ts
250
+ import {
251
+ InfrastructureError,
252
+ StepResponse as StepResponse2,
253
+ StoreValidationError,
254
+ WorkflowValueError as WorkflowValueError2
255
+ } from "@yieldstar/core";
256
+
257
+ class StepRuntimeCall extends StepResponse2 {
258
+ static type = "runtime-call";
259
+ type = "runtime-call";
260
+ call;
261
+ constructor(call) {
262
+ super();
263
+ this.call = call;
161
264
  }
162
- if (!key)
163
- key = getCallSiteHash(run);
164
- yield new StepKey(key);
165
- const cached = yield new StepCacheCheck;
166
- if (cached) {
167
- yield cached;
168
- if (cached instanceof StepError2) {
169
- throw cached.err;
265
+ }
266
+
267
+ class StepCallbackError extends Error {
268
+ constructor(cause) {
269
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
270
+ this.name = "StepCallbackError";
271
+ }
272
+ }
273
+ function workflowCallback(fn, assertResult) {
274
+ return (...args) => {
275
+ try {
276
+ const result = fn(...args);
277
+ assertResult?.(result);
278
+ return result;
279
+ } catch (err) {
280
+ throw new StepCallbackError(err);
170
281
  }
171
- return cached.result;
282
+ };
283
+ }
284
+ async function executeRuntimeCall(request, clients) {
285
+ try {
286
+ return { ok: true, value: await request.call(clients) };
287
+ } catch (err) {
288
+ if (InfrastructureError.is(err))
289
+ throw err;
290
+ if (err instanceof StepCallbackError)
291
+ return { ok: false, error: err.cause };
292
+ if (isWorkflowFacingError(err))
293
+ return { ok: false, error: err };
294
+ throw new InfrastructureError(err);
172
295
  }
296
+ }
297
+ function isWorkflowFacingError(err) {
298
+ return err instanceof StoreValidationError || err instanceof WorkflowValueError2 || err instanceof Error && err.name === "StoreValidationError";
299
+ }
300
+ async function infrastructure(fn) {
173
301
  try {
174
- const result = await fn();
175
- yield new StepResult2(result);
176
- return result;
302
+ return await fn();
177
303
  } catch (err) {
178
- if (err instanceof RetryableError) {
179
- yield new StepError2(err, {
180
- maxAttempts: err.maxAttempts,
181
- retryInterval: err.retryInterval
182
- });
183
- } else {
304
+ throw InfrastructureError.is(err) ? err : new InfrastructureError(err);
305
+ }
306
+ }
307
+
308
+ // src/internal/effect-call.ts
309
+ import {
310
+ EffectClientInvariantError,
311
+ EffectDeliveryError,
312
+ InfrastructureError as InfrastructureError2,
313
+ StepResponse as StepResponse3,
314
+ durableValueCodec as durableValueCodec3
315
+ } from "@yieldstar/core";
316
+
317
+ class StepEffectCall extends StepResponse3 {
318
+ static type = "effect-call";
319
+ type = "effect-call";
320
+ request;
321
+ constructor(request) {
322
+ super();
323
+ this.request = request;
324
+ }
325
+ }
326
+ async function executeEffectCall(request, context) {
327
+ const { effects, executionId, clock, assertLease, logger } = context;
328
+ const { stepKey, effectId, operation, input, retryIn } = request;
329
+ const fail = (code, message, cause) => new InfrastructureError2(new EffectDeliveryError(code, { effectId, stepKey, message }, cause === undefined ? undefined : { cause }));
330
+ async function client(method, call) {
331
+ try {
332
+ return await call();
333
+ } catch (err) {
334
+ const message = `EffectClient.${method} failed for effect "${effectId}": ${errorMessage(err)}`;
335
+ if (isInvariantError(err))
336
+ throw fail("EFFECT_RUNTIME_INVARIANT", message, err);
337
+ throw fail("EFFECT_CLIENT_UNAVAILABLE", message, err);
338
+ }
339
+ }
340
+ const malformedClient = (method) => fail("EFFECT_RUNTIME_INVARIANT", `EffectClient.${method} returned a malformed response for effect "${effectId}"`);
341
+ async function suspend() {
342
+ await client("watch", () => effects.watch({ effectId, executionId, stepKey }));
343
+ return { status: "suspended", wakeAt: clock() + retryIn };
344
+ }
345
+ async function settle(result) {
346
+ const encoding = durableValueCodec3.encode(result);
347
+ if (encoding.status === "invalid") {
348
+ throw fail("EFFECT_HANDLER_INVALID", `Effect "${effectId}" result is not a durable value: ${encoding.message}`);
349
+ }
350
+ const settled = await client("settle", () => effects.settle({ operation, effectId, result }));
351
+ if (!isObject(settled) || !("result" in settled))
352
+ throw malformedClient("settle");
353
+ if (settled.status === "conflict") {
354
+ logger.warn({
355
+ executionId,
356
+ stepKey,
357
+ effectId,
358
+ result: settled.result,
359
+ conflictingResult: settled.conflictingResult
360
+ }, `Effect "${effectId}" settled with a different result; the first result stands`);
361
+ } else if (settled.status !== "settled") {
362
+ throw malformedClient("settle");
363
+ }
364
+ return { status: "settled", result: settled.result };
365
+ }
366
+ const begun = await client("begin", () => effects.begin({ operation, effectId, input, executionId, stepKey }));
367
+ if (!isObject(begun))
368
+ throw malformedClient("begin");
369
+ let next;
370
+ switch (begun.status) {
371
+ case "settled":
372
+ if (!("result" in begun))
373
+ throw malformedClient("begin");
374
+ return { status: "settled", result: begun.result };
375
+ case "rejected":
376
+ if (begun.code !== "EFFECT_REQUEST_MISMATCH")
377
+ throw malformedClient("begin");
378
+ return { status: "rejected", code: begun.code };
379
+ case "new":
380
+ next = "perform";
381
+ break;
382
+ case "pending":
383
+ next = "lookup";
384
+ break;
385
+ default:
386
+ throw malformedClient("begin");
387
+ }
388
+ if (next === "lookup") {
389
+ assertLease();
390
+ let found;
391
+ try {
392
+ found = await request.lookup(effectId);
393
+ } catch {
394
+ return suspend();
395
+ }
396
+ if (!isObject(found))
397
+ throw malformedLookup(fail, effectId);
398
+ switch (found.status) {
399
+ case "settled":
400
+ if (!("result" in found))
401
+ throw malformedLookup(fail, effectId);
402
+ return settle(found.result);
403
+ case "unknown":
404
+ return suspend();
405
+ case "not-found":
406
+ break;
407
+ default:
408
+ throw malformedLookup(fail, effectId);
409
+ }
410
+ }
411
+ assertLease();
412
+ let performed;
413
+ try {
414
+ performed = await request.perform(effectId);
415
+ } catch {
416
+ return suspend();
417
+ }
418
+ return settle(performed);
419
+ }
420
+ function malformedLookup(fail, effectId) {
421
+ return fail("EFFECT_HANDLER_INVALID", `lookup for effect "${effectId}" returned a malformed response: expected { status: "settled", result }, { status: "not-found" } or { status: "unknown" }`);
422
+ }
423
+ function isInvariantError(err) {
424
+ return err instanceof EffectClientInvariantError || typeof err === "object" && err !== null && err.code === "EFFECT_RUNTIME_INVARIANT";
425
+ }
426
+ function isObject(value) {
427
+ return typeof value === "object" && value !== null;
428
+ }
429
+ function errorMessage(err) {
430
+ return err instanceof Error ? err.message : String(err);
431
+ }
432
+
433
+ // src/internal/step-runner.ts
434
+ function createStepRunner(params) {
435
+ const { event, clock } = params;
436
+ function store(definition, params) {
437
+ return createWorkflowStore({
438
+ definition,
439
+ id: params?.id ?? event.executionId,
440
+ event
441
+ });
442
+ }
443
+ function start(stepKey, target) {
444
+ if (stepKey === "") {
445
+ throw new Error("step.start requires a non-empty step key");
446
+ }
447
+ const request = {
448
+ requestId: { executionId: event.executionId, stepKey },
449
+ workflowId: target.workflowId
450
+ };
451
+ if (target.params !== undefined) {
452
+ request.params = target.params;
453
+ }
454
+ return startStep(stepKey, request);
455
+ }
456
+ function effect(stepKey, descriptor) {
457
+ if (typeof stepKey !== "string" || stepKey === "") {
458
+ throw new Error("step.effect requires a non-empty step key");
459
+ }
460
+ return effectStep(stepKey, descriptor);
461
+ }
462
+ return { run, delay: createDelay(clock), poll, store, start, effect };
463
+ }
464
+ async function* effectStep(key, descriptor) {
465
+ yield new StepKey(key);
466
+ const cached = yield new StepCacheCheck;
467
+ if (cached)
468
+ return yield* replay(cached);
469
+ const { effectId, operation, input, retryIn, lookup, perform } = descriptor ?? {};
470
+ const invalid = invalidEffectRequest({
471
+ effectId,
472
+ operation,
473
+ input,
474
+ retryIn,
475
+ lookup,
476
+ perform
477
+ });
478
+ if (invalid) {
479
+ const err = new WorkflowEffectError("INVALID_EFFECT_REQUEST", `step.effect "${key}": ${invalid}`);
480
+ yield new StepError2(err);
481
+ throw err;
482
+ }
483
+ const outcome = yield new StepEffectCall({
484
+ stepKey: key,
485
+ effectId,
486
+ operation,
487
+ input,
488
+ retryIn,
489
+ lookup: (id) => lookup.call(descriptor, id),
490
+ perform: (id) => perform.call(descriptor, id)
491
+ });
492
+ switch (outcome.status) {
493
+ case "settled":
494
+ return yield* record(outcome.result);
495
+ case "rejected": {
496
+ const err = new WorkflowEffectError(outcome.code, `Effect "${effectId}" was begun with a different operation or input`);
184
497
  yield new StepError2(err);
498
+ throw err;
185
499
  }
500
+ case "suspended":
501
+ yield new StepEffectWait2(outcome.wakeAt);
502
+ throw new Error("Effect wait yielded control unexpectedly");
503
+ }
504
+ }
505
+ function invalidEffectRequest(request) {
506
+ const validated = validateEffectRequest(request);
507
+ if (!validated.ok)
508
+ return validated.message;
509
+ const { retryIn } = request;
510
+ if (typeof retryIn !== "number" || !Number.isFinite(retryIn) || retryIn < 0) {
511
+ return "retryIn must be a finite, non-negative number";
512
+ }
513
+ if (typeof request.lookup !== "function")
514
+ return "lookup must be a function";
515
+ if (typeof request.perform !== "function")
516
+ return "perform must be a function";
517
+ return;
518
+ }
519
+ async function* startStep(key, request) {
520
+ yield new StepKey(key);
521
+ const cached = yield new StepCacheCheck;
522
+ if (cached)
523
+ return yield* replay(cached);
524
+ const started = yield new StepRuntimeCall(async ({ executions }) => {
525
+ try {
526
+ return await executions.start(request);
527
+ } catch (err) {
528
+ throw InfrastructureError3.is(err) ? err : new InfrastructureError3(err);
529
+ }
530
+ });
531
+ if (!started.ok) {
532
+ throw new Error("ExecutionClient.start failed without an infrastructure error");
533
+ }
534
+ const outcome = started.value;
535
+ if (outcome.status === "rejected") {
536
+ const err = new WorkflowStartError(outcome.code, outcome.message);
537
+ yield new StepError2(err);
186
538
  throw err;
187
539
  }
540
+ return yield* record({ executionId: outcome.executionId });
188
541
  }
189
- async function* delay(arg1, arg2) {
542
+ function createWorkflowStore(params) {
543
+ const { id, event } = params;
544
+ const key = { storeName: params.definition.name, storeId: id };
545
+ const definition = {
546
+ ...params.definition,
547
+ initial: workflowCallback(params.definition.initial)
548
+ };
549
+ return {
550
+ definition: params.definition,
551
+ id,
552
+ key,
553
+ get(stepKey) {
554
+ return runtimeStep(stepKey ?? getCallSiteHash(this.get), async ({ stores }) => await stores.getStore({ definition, id }));
555
+ },
556
+ select(stepKey, selector) {
557
+ return runtimeStep(stepKey, async ({ stores }) => {
558
+ const snapshot = await stores.getStore({ definition, id });
559
+ return workflowCallback(selector)(cloneStoreState(snapshot.state));
560
+ });
561
+ },
562
+ update(stepKey, updater) {
563
+ return runtimeStep(stepKey, async ({ stores }) => await stores.updateStore({
564
+ definition,
565
+ id,
566
+ updater: workflowCallback(updater, assertSynchronousUpdater),
567
+ stepId: { executionId: event.executionId, stepKey }
568
+ }));
569
+ },
570
+ updateFrom(stepKey, snapshot, updater) {
571
+ return runtimeStep(stepKey, async ({ stores }) => await stores.updateStoreFrom({
572
+ definition,
573
+ id,
574
+ snapshot,
575
+ updater: workflowCallback(updater, assertSynchronousUpdater),
576
+ stepId: { executionId: event.executionId, stepKey }
577
+ }));
578
+ },
579
+ deleteFrom(stepKey, snapshot) {
580
+ return runtimeStep(stepKey, ({ stores }) => stores.deleteStoreFrom({
581
+ definition,
582
+ id,
583
+ snapshot,
584
+ stepId: { executionId: event.executionId, stepKey }
585
+ }));
586
+ },
587
+ when(arg1, arg2) {
588
+ const stepKey = typeof arg1 === "string" ? arg1 : getCallSiteHash(this.when);
589
+ const selector = typeof arg1 === "string" ? arg2 : arg1;
590
+ return whenStep({
591
+ definition,
592
+ id,
593
+ event,
594
+ stepKey,
595
+ selector
596
+ });
597
+ },
598
+ take(stepKey, selector, claim) {
599
+ return takeStep({
600
+ definition,
601
+ id,
602
+ event,
603
+ stepKey,
604
+ selector,
605
+ claim
606
+ });
607
+ }
608
+ };
609
+ }
610
+ function run(arg1, arg2, arg3) {
190
611
  let key = null;
191
- let retryInterval;
612
+ let fn;
613
+ let options;
192
614
  if (typeof arg1 === "string") {
193
615
  key = arg1;
194
- retryInterval = arg2;
616
+ fn = arg2;
617
+ options = arg3;
195
618
  } else {
196
- retryInterval = arg1;
619
+ fn = arg1;
620
+ options = arg2;
197
621
  }
198
622
  if (!key)
199
- key = getCallSiteHash(delay);
623
+ key = getCallSiteHash(run);
624
+ return durableStep(key, fn, options?.schema);
625
+ }
626
+ function createDelay(clock) {
627
+ function delay(arg1, arg2) {
628
+ let key = null;
629
+ let retryInterval;
630
+ if (typeof arg1 === "string") {
631
+ key = arg1;
632
+ retryInterval = arg2;
633
+ } else {
634
+ retryInterval = arg1;
635
+ }
636
+ if (!key)
637
+ key = getCallSiteHash(delay);
638
+ return delayStep(key, retryInterval, clock);
639
+ }
640
+ return delay;
641
+ }
642
+ async function* delayStep(key, retryInterval, clock) {
200
643
  yield new StepKey(key);
201
644
  const cached = yield new StepCacheCheck;
202
645
  if (cached) {
203
- yield new StepDelay2(cached.resumeIn);
646
+ yield new StepDelay2(cached.wakeAt);
204
647
  } else {
205
- yield new StepDelay2(Date.now() + retryInterval);
648
+ yield new StepDelay2(clock() + retryInterval);
206
649
  }
207
650
  }
208
- async function* poll(arg1, arg2, arg3) {
651
+ function poll(arg1, arg2, arg3) {
209
652
  let key = null;
210
653
  let opts;
211
654
  let predicate;
@@ -227,26 +670,185 @@ async function* poll(arg1, arg2, arg3) {
227
670
  });
228
671
  }
229
672
  };
230
- yield* run(key, task);
673
+ return run(key, task);
674
+ }
675
+ async function* durableStep(key, fn, schema) {
676
+ yield new StepKey(key);
677
+ const cached = yield new StepCacheCheck;
678
+ if (cached) {
679
+ const value = yield* replay(cached);
680
+ if (!schema)
681
+ return value;
682
+ const validated = await validateStepValue(key, schema, value);
683
+ if (cached.schema === "rejected" && validated.ok) {
684
+ throw new WorkflowValueError3("STEP_SCHEMA_INVALID", key, `Step "${key}" schema rejected its value on the first run`);
685
+ }
686
+ return unwrapValidated(validated);
687
+ }
688
+ let result;
689
+ try {
690
+ result = await fn();
691
+ } catch (err) {
692
+ if (err instanceof RetryableError) {
693
+ yield new StepError2(err, {
694
+ maxAttempts: err.maxAttempts,
695
+ retryInterval: err.retryInterval
696
+ });
697
+ } else {
698
+ yield new StepError2(err);
699
+ }
700
+ throw err;
701
+ }
702
+ if (!schema)
703
+ return yield* record(result);
704
+ const encoding = durableValueCodec4.encode(result);
705
+ if (encoding.status === "invalid")
706
+ return yield* record(result);
707
+ const decoding = durableValueCodec4.decode(encoding.canonical);
708
+ if (decoding.status === "invalid")
709
+ throw new Error(decoding.message);
710
+ const validated = await validateStepValue(key, schema, decoding.value);
711
+ yield* record(result, validated.ok ? "accepted" : "rejected");
712
+ return unwrapValidated(validated);
713
+ }
714
+ async function validateStepValue(key, schema, value) {
715
+ const result = await schema["~standard"].validate(value);
716
+ if (result.issues) {
717
+ const messages = result.issues.map((issue) => issue.message).join("; ");
718
+ return {
719
+ ok: false,
720
+ error: new WorkflowValueError3("STEP_SCHEMA_INVALID", key, `Step "${key}" value failed its schema: ${messages}`)
721
+ };
722
+ }
723
+ return { ok: true, value: result.value };
724
+ }
725
+ function unwrapValidated(validated) {
726
+ if (!validated.ok)
727
+ throw validated.error;
728
+ return validated.value;
729
+ }
730
+ async function* record(result, schema) {
731
+ const recorded = yield new StepResult2(result, schema);
732
+ return recorded.result;
733
+ }
734
+ async function* replay(cached) {
735
+ const recorded = yield cached;
736
+ return recorded.result;
737
+ }
738
+ async function* fail(key, error) {
739
+ const err = error instanceof WorkflowValueError3 && error.stepKey !== key ? new WorkflowValueError3(error.code, key, error.message) : error;
740
+ yield new StepError2(err);
741
+ throw err;
742
+ }
743
+ async function* runtimeStep(key, call) {
744
+ yield new StepKey(key);
745
+ const cached = yield new StepCacheCheck;
746
+ if (cached)
747
+ return yield* replay(cached);
748
+ const outcome = yield new StepRuntimeCall(call);
749
+ if (!outcome.ok)
750
+ return yield* fail(key, outcome.error);
751
+ return yield* record(outcome.value);
752
+ }
753
+ async function* whenStep(params) {
754
+ const { definition, id, event, stepKey, selector } = params;
755
+ yield new StepKey(stepKey);
756
+ const cached = yield new StepCacheCheck;
757
+ if (cached)
758
+ return yield* replay(cached);
759
+ const read = yield new StepRuntimeCall(({ stores }) => stores.getStore({ definition, id }));
760
+ if (!read.ok)
761
+ return yield* fail(stepKey, read.error);
762
+ const snapshot = read.value;
763
+ const { result, readPaths } = trackStoreSelector(cloneStoreState(snapshot.state), selector);
764
+ if (result !== undefined && result !== null && result !== false) {
765
+ return yield* record(result);
766
+ }
767
+ yield* registerWaiter({
768
+ workflowId: event.workflowId,
769
+ executionId: event.executionId,
770
+ stepKey,
771
+ storeName: definition.name,
772
+ storeId: id,
773
+ instanceId: snapshot.instanceId,
774
+ sinceVersion: snapshot.version,
775
+ readPaths
776
+ });
777
+ throw new Error("Store wait yielded control unexpectedly");
778
+ }
779
+ async function* takeStep(params) {
780
+ const { definition, id, event, stepKey, selector, claim } = params;
781
+ yield new StepKey(stepKey);
782
+ const cached = yield new StepCacheCheck;
783
+ if (cached)
784
+ return yield* replay(cached);
785
+ const taken = yield new StepRuntimeCall(({ stores }) => stores.takeFromStore({
786
+ definition,
787
+ id,
788
+ selector: workflowCallback(selector),
789
+ claim: workflowCallback(claim, assertSynchronousClaim),
790
+ stepId: { executionId: event.executionId, stepKey }
791
+ }));
792
+ if (!taken.ok)
793
+ return yield* fail(stepKey, taken.error);
794
+ const outcome = taken.value;
795
+ if (outcome.matched)
796
+ return yield* record(outcome.selected);
797
+ yield* registerWaiter({
798
+ workflowId: event.workflowId,
799
+ executionId: event.executionId,
800
+ stepKey,
801
+ storeName: definition.name,
802
+ storeId: id,
803
+ instanceId: outcome.instanceId,
804
+ sinceVersion: outcome.version,
805
+ readPaths: outcome.readPaths
806
+ });
807
+ throw new Error("Store take yielded control unexpectedly");
808
+ }
809
+ async function* registerWaiter(waiter) {
810
+ const registered = yield new StepRuntimeCall(({ stores }) => stores.registerWaiter(waiter));
811
+ if (!registered.ok)
812
+ return yield* fail(waiter.stepKey, registered.error);
813
+ yield new StepStoreWait2;
231
814
  }
232
815
 
233
816
  // src/exports/workflow.ts
234
817
  import {
235
- StepResponse as StepResponse3,
818
+ StepResponse as StepResponse5,
236
819
  StepKey as StepKey2,
237
820
  StepError as StepError3,
238
821
  StepResult as StepResult3,
239
822
  StepDelay as StepDelay3,
240
823
  StepCacheCheck as StepCacheCheck2,
241
- StepInvalid,
242
- WorkflowResult as WorkflowResult2
824
+ StepEffectWait as StepEffectWait3,
825
+ StepInvalid as StepInvalid2,
826
+ StepStoreWait as StepStoreWait3,
827
+ WorkflowResult as WorkflowResult2,
828
+ InfrastructureError as InfrastructureError4,
829
+ LeaseLostError,
830
+ StepConflictError,
831
+ isLeaseLost,
832
+ WORKFLOW_RESULT_STEP_KEY
243
833
  } from "@yieldstar/core";
244
834
  function workflow(workflowFn) {
245
- return async function* workflowGenerator({ event, heapClient, logger }) {
835
+ return async function* workflowGenerator({
836
+ event,
837
+ lease,
838
+ signal,
839
+ heapClient,
840
+ storeClient,
841
+ executionClient,
842
+ effectClient,
843
+ clock,
844
+ logger
845
+ }) {
846
+ const stepRunner = createStepRunner({ event, clock });
246
847
  const workflowIterator = workflowFn(stepRunner, event, logger);
247
848
  const { executionId } = event;
248
849
  let iteratorResult = null;
249
850
  let nextIteratorResult = null;
851
+ let resumeWith;
250
852
  const stepKeys = new Set;
251
853
  while (true) {
252
854
  let stepAttempt = 0;
@@ -255,10 +857,12 @@ function workflow(workflowFn) {
255
857
  iteratorResult = nextIteratorResult;
256
858
  nextIteratorResult = null;
257
859
  } else {
258
- iteratorResult = await workflowIterator.next();
860
+ stopIfAborted(signal);
861
+ iteratorResult = await workflowIterator.next(resumeWith);
259
862
  }
863
+ resumeWith = undefined;
260
864
  if (iteratorResult.done) {
261
- stepKey = "$$workflow-result$$";
865
+ stepKey = WORKFLOW_RESULT_STEP_KEY;
262
866
  } else if (!(iteratorResult.value instanceof StepKey2)) {
263
867
  throw new Error("Step runners must yield a StepKey object");
264
868
  } else {
@@ -268,10 +872,10 @@ function workflow(workflowFn) {
268
872
  }
269
873
  stepKeys.add(stepKey);
270
874
  }
271
- let cached = await heapClient.readStep({
875
+ let cached = await infrastructure(() => heapClient.readStep({
272
876
  executionId,
273
877
  stepKey
274
- });
878
+ }));
275
879
  if (cached?.meta) {
276
880
  stepAttempt = cached.meta.attempt + 1;
277
881
  }
@@ -288,62 +892,112 @@ function workflow(workflowFn) {
288
892
  stepResponse = iteratorResult.value;
289
893
  }
290
894
  if (stepResponse instanceof StepCacheCheck2) {
291
- const cachedResponse = cached ? deserializeStepResponse(cached.stepResponseJson) : null;
895
+ const cachedResponse = cached ? decodeStepRecord(cached.stepResponseJson) : null;
896
+ stopIfAborted(signal);
292
897
  iteratorResult = await workflowIterator.next(cachedResponse);
293
898
  stepResponse = iteratorResult.value;
899
+ while (stepResponse instanceof StepRuntimeCall || stepResponse instanceof StepEffectCall) {
900
+ stopIfAborted(signal);
901
+ const callResult = stepResponse instanceof StepEffectCall ? await executeEffectCall(stepResponse.request, {
902
+ effects: effectClient,
903
+ executionId,
904
+ clock,
905
+ assertLease: () => stopIfAborted(signal),
906
+ logger
907
+ }) : await executeRuntimeCall(stepResponse, {
908
+ stores: storeClient,
909
+ executions: executionClient
910
+ });
911
+ iteratorResult = await workflowIterator.next(callResult);
912
+ stepResponse = iteratorResult.value;
913
+ }
294
914
  } else if (!(stepResponse instanceof WorkflowResult2)) {
295
915
  throw new Error("Step runners must yield a CacheCheck object");
296
916
  }
297
917
  if (isIterable(stepResponse)) {
298
918
  console.log(`[ERR] Steps must be yielded using yield*
299
919
  `);
300
- stepResponse = new StepInvalid;
301
- } else if (!(stepResponse instanceof StepResponse3)) {
920
+ stepResponse = new StepInvalid2;
921
+ } else if (!(stepResponse instanceof StepResponse5)) {
302
922
  console.log(`[ERR] Iterators should yield a StepResponse. Got ${JSON.stringify(stepResponse)}`);
303
- stepResponse = new StepInvalid;
923
+ stepResponse = new StepInvalid2;
924
+ }
925
+ let stepResponseJson;
926
+ if (!cached || stepResponse instanceof WorkflowResult2) {
927
+ const record = encodeStepRecord(stepResponse, stepKey);
928
+ stepResponseJson = record.json;
929
+ stepResponse = record.response;
304
930
  }
305
931
  const needsRetry = !cached?.meta.done && stepResponse instanceof StepError3 && stepResponse.maxAttempts > stepAttempt + 1;
306
- if (!cached) {
307
- await heapClient.writeStep({
308
- executionId,
932
+ const needsWake = !cached?.meta.done && (stepResponse instanceof StepStoreWait3 || stepResponse instanceof StepEffectWait3);
933
+ if (!cached && stepResponseJson !== undefined) {
934
+ const done = !needsRetry && !needsWake;
935
+ stopIfAborted(signal);
936
+ const written = await infrastructure(() => heapClient.writeStep({
937
+ lease,
309
938
  stepKey,
310
939
  stepAttempt,
311
- stepDone: !needsRetry,
312
- stepResponseJson: serializeStepResponse(stepResponse)
313
- });
940
+ done,
941
+ stepResponseJson
942
+ }));
943
+ if (isLeaseLost(written)) {
944
+ throw new InfrastructureError4(new LeaseLostError(executionId));
945
+ }
946
+ if (written.stepResponseJson !== stepResponseJson || written.meta.done !== done) {
947
+ throw new InfrastructureError4(new StepConflictError(executionId, stepKey));
948
+ }
314
949
  }
315
950
  if (stepResponse instanceof WorkflowResult2) {
316
951
  return stepResponse;
317
952
  }
318
- if (stepResponse instanceof StepInvalid) {
953
+ if (iteratorResult.done && stepResponse instanceof StepError3) {
954
+ throw stepResponse.err;
955
+ }
956
+ if (stepResponse instanceof StepInvalid2) {
319
957
  break;
320
958
  }
321
959
  if (stepResponse instanceof StepError3) {
322
960
  if (needsRetry) {
323
- yield new StepDelay3(Date.now() + stepResponse.retryInterval);
961
+ yield new StepDelay3(clock() + stepResponse.retryInterval);
324
962
  } else {
963
+ stopIfAborted(signal);
325
964
  nextIteratorResult = await workflowIterator.throw(stepResponse.err);
326
965
  continue;
327
966
  }
328
967
  }
329
968
  if (stepResponse instanceof StepDelay3) {
330
- if (stepResponse.resumeIn <= Date.now()) {
969
+ if (stepResponse.wakeAt <= clock()) {
331
970
  continue;
332
971
  }
333
972
  }
334
- if (!(stepResponse instanceof StepResult3)) {
973
+ if (stepResponse instanceof StepResult3) {
974
+ resumeWith = stepResponse;
975
+ } else {
335
976
  yield stepResponse;
336
977
  }
337
978
  }
338
- throw new Error("Critical error");
979
+ throw new Error("Workflow yielded an invalid step. Steps must be yielded with yield* from a step runner.");
339
980
  };
340
981
  }
341
982
  var createWorkflow = workflow;
983
+ function stopIfAborted(signal) {
984
+ if (signal.aborted)
985
+ throw new InfrastructureError4(signal.reason);
986
+ }
342
987
  export {
343
- workflow,
344
- createWorkflowRouter,
345
- createWorkflow,
346
- createLocalSdk,
988
+ EffectClientInvariantError2 as EffectClientInvariantError,
989
+ EffectDeliveryError2 as EffectDeliveryError,
990
+ ExecutionAdmissionError3 as ExecutionAdmissionError,
991
+ RetryableError,
992
+ WORKFLOW_RESULT_STEP_KEY2 as WORKFLOW_RESULT_STEP_KEY,
993
+ WorkflowEffectError2 as WorkflowEffectError,
994
+ WorkflowStartError2 as WorkflowStartError,
995
+ WorkflowValueError4 as WorkflowValueError,
347
996
  createHttpSdkFactory,
348
- RetryableError
997
+ createLocalSdk,
998
+ createWorkflow,
999
+ createWorkflowRouter,
1000
+ defineStore,
1001
+ registerErrorClass2 as registerErrorClass,
1002
+ workflow
349
1003
  };