yieldstar 0.5.0 → 0.6.1

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.js CHANGED
@@ -1,56 +1,83 @@
1
1
  // src/exports/sdk-local.ts
2
- import { nanoid } from "nanoid";
3
- function createLocalSdk(invoker) {
2
+ import {
3
+ ExecutionAdmissionError,
4
+ waitForExecutionOutcome
5
+ } from "@yieldstar/core";
6
+ function createLocalSdk(runtime, options) {
7
+ const { executions } = runtime;
4
8
  return {
5
9
  async trigger(event) {
6
- const executionEvent = {
7
- executionId: event.executionId ?? nanoid(),
10
+ const admission = await executions.admit({
11
+ executionId: event.executionId,
8
12
  workflowId: event.workflowId,
9
- params: event.params
10
- };
11
- await invoker.execute(executionEvent);
12
- 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 };
13
20
  },
14
21
  async triggerAndWait(event) {
15
- const executionId = event.executionId ?? nanoid();
16
- const workflowCompletePromise = new Promise((resolve) => {
17
- 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
18
26
  });
19
- await this.trigger({ ...event, executionId });
20
- 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;
21
33
  }
22
34
  };
23
35
  }
24
36
  // src/exports/sdk-http.ts
25
- import { nanoid as nanoid2 } from "nanoid";
26
- import { deserializeError } from "serialize-error";
27
- 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
+ ];
28
45
  function createHttpSdkFactory() {
29
46
  return (params) => {
30
47
  return {
31
48
  async trigger(event) {
32
- const executionEvent = {
33
- executionId: event?.executionId ?? nanoid2(),
34
- workflowId: event?.workflowId,
35
- params: event?.params,
36
- context: event?.context
37
- };
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
+ }
38
60
  const res = await fetch(`${params.url}/trigger`, {
39
61
  ...params.fetchOptions,
40
62
  method: "POST",
41
- body: JSON.stringify(executionEvent)
63
+ headers: { "content-type": "application/json" },
64
+ body: encoding.canonical
42
65
  });
43
66
  if (res.ok) {
67
+ const ack = await res.json();
44
68
  return {
45
- executionId: executionEvent.executionId,
46
- ack: () => res.json(),
47
- waitForResult: () => this.waitForResult({
48
- executionId: executionEvent.executionId
49
- })
69
+ executionId: ack.executionId,
70
+ ack: async () => ack,
71
+ waitForResult: () => this.waitForResult(ack)
50
72
  };
51
- } else {
52
- throw res.statusText;
53
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;
54
81
  },
55
82
  async waitForResult(ack) {
56
83
  const res = await fetch(`${params.url}/events`, {
@@ -58,11 +85,18 @@ function createHttpSdkFactory() {
58
85
  body: JSON.stringify({ executionId: ack.executionId }),
59
86
  ...params.fetchOptions
60
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
+ }
61
95
  if (res.status === 500) {
62
- const error = await res.json();
63
- throw errorWithOriginalStack(deserializeError(error), this.waitForResult);
96
+ const error = decoding.value instanceof Error ? decoding.value : new Error(String(decoding.value));
97
+ throw error;
64
98
  }
65
- return res.json();
99
+ return decoding.value;
66
100
  }
67
101
  };
68
102
  };
@@ -72,6 +106,8 @@ function createWorkflowRouter(workflows) {
72
106
  return workflows;
73
107
  }
74
108
  // src/exports/errors.ts
109
+ import { registerErrorClass } from "@yieldstar/core";
110
+
75
111
  class RetryableError extends Error {
76
112
  maxAttempts;
77
113
  retryInterval;
@@ -82,6 +118,20 @@ class RetryableError extends Error {
82
118
  this.retryInterval = opts.retryInterval;
83
119
  }
84
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
+
85
135
  // src/internal/utils.ts
86
136
  import { createHash } from "crypto";
87
137
  var isIterable = (i) => {
@@ -99,146 +149,435 @@ function getCallSiteHash(stepFn) {
99
149
  return createHash("sha1").update(callSite).digest("hex");
100
150
  }
101
151
 
102
- // src/internal/serialise.ts
152
+ // src/internal/step-record.ts
103
153
  import {
154
+ DurableValueError,
104
155
  StepDelay,
156
+ StepEffectWait,
105
157
  StepError,
158
+ StepInvalid,
106
159
  StepResult,
107
160
  StepStoreWait,
108
- WorkflowResult
161
+ WorkflowResult,
162
+ WorkflowValueError,
163
+ decodeParsedValue,
164
+ durableValueCodec as durableValueCodec2,
165
+ encodeThrownValue
109
166
  } from "@yieldstar/core";
110
- import { isErrorLike, serializeError, deserializeError as deserializeError2 } from "serialize-error";
111
- function serializeStepResponse(data) {
112
- const replaceErrors = (key, value) => {
113
- if (value instanceof Error) {
114
- 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);
115
177
  }
116
- return value;
117
- };
118
- 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`);
119
194
  }
120
- function deserializeStepResponse(jsonString) {
121
- const reviveErrors = (key, value) => {
122
- if (isErrorLike(value)) {
123
- 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;
124
217
  }
125
- return value;
126
- };
127
- const stepResponse = JSON.parse(jsonString, reviveErrors);
128
- switch (stepResponse.type) {
129
- case "step-error":
130
- const { err, ...params } = stepResponse;
131
- return new StepError(stepResponse.err, params);
132
- case "step-delay":
133
- return new StepDelay(stepResponse.resumeIn);
134
- case "step-result":
135
- return new StepResult(stepResponse.result);
136
- case "store-wait":
137
- return new StepStoreWait;
138
- case "workflow-result":
139
- return new WorkflowResult(stepResponse.result);
140
- default:
141
- 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;
142
223
  }
224
+ throw new Error(`Unknown step record type: ${record.type}`);
143
225
  }
144
226
 
145
227
  // src/internal/step-runner.ts
146
228
  import {
229
+ assertSynchronousClaim,
230
+ assertSynchronousUpdater,
147
231
  cloneStoreState,
148
232
  defineStore,
233
+ InfrastructureError as InfrastructureError3,
149
234
  StepKey,
150
235
  StepError as StepError2,
151
236
  StepResult as StepResult2,
152
237
  StepDelay as StepDelay2,
153
238
  StepCacheCheck,
154
239
  StepStoreWait as StepStoreWait2,
155
- trackStoreSelector
240
+ trackStoreSelector,
241
+ durableValueCodec as durableValueCodec4,
242
+ StepEffectWait as StepEffectWait2,
243
+ validateEffectRequest,
244
+ WorkflowEffectError,
245
+ WorkflowStartError,
246
+ WorkflowValueError as WorkflowValueError3
156
247
  } from "@yieldstar/core";
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;
264
+ }
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);
281
+ }
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);
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) {
301
+ try {
302
+ return await fn();
303
+ } catch (err) {
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
157
434
  function createStepRunner(params) {
158
- const { event, storeClient } = params;
159
- function store(definition, params2) {
160
- const id = params2?.id ?? event.executionId;
161
- const key = `store:${definition.name}:${id}`;
162
- return storeStep({
163
- key,
435
+ const { event, clock } = params;
436
+ function store(definition, params) {
437
+ return createWorkflowStore({
164
438
  definition,
165
- id,
166
- event,
167
- storeClient,
168
- initial: params2?.initial
439
+ id: params?.id ?? event.executionId,
440
+ event
169
441
  });
170
442
  }
171
- return { run, delay, poll, store };
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 };
172
463
  }
173
- async function* storeStep(params) {
174
- const { key, definition, id, event, storeClient, initial } = params;
464
+ async function* effectStep(key, descriptor) {
175
465
  yield new StepKey(key);
176
466
  const cached = yield new StepCacheCheck;
177
- if (cached) {
178
- yield cached;
179
- if (cached instanceof StepError2) {
180
- throw cached.err;
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`);
497
+ yield new StepError2(err);
498
+ throw err;
181
499
  }
182
- return createWorkflowStore({
183
- definition,
184
- id,
185
- event,
186
- storeClient
187
- });
500
+ case "suspended":
501
+ yield new StepEffectWait2(outcome.wakeAt);
502
+ throw new Error("Effect wait yielded control unexpectedly");
188
503
  }
189
- try {
190
- await storeClient.getOrCreateStore({
191
- definition,
192
- id,
193
- initial
194
- });
195
- yield new StepResult2({ storeName: definition.name, storeId: id });
196
- return createWorkflowStore({
197
- definition,
198
- id,
199
- event,
200
- storeClient
201
- });
202
- } catch (err) {
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);
203
537
  yield new StepError2(err);
204
538
  throw err;
205
539
  }
540
+ return yield* record({ executionId: outcome.executionId });
206
541
  }
207
542
  function createWorkflowStore(params) {
208
- const { definition, id, event, storeClient } = params;
209
- const key = { storeName: definition.name, storeId: id };
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
+ };
210
549
  return {
211
- definition,
550
+ definition: params.definition,
212
551
  id,
213
552
  key,
214
553
  get(stepKey) {
215
- return durableStep(stepKey ?? getCallSiteHash(this.get), async () => await storeClient.getStore({ definition, id }));
554
+ return runtimeStep(stepKey ?? getCallSiteHash(this.get), async ({ stores }) => await stores.getStore({ definition, id }));
216
555
  },
217
556
  select(stepKey, selector) {
218
- return durableStep(stepKey, async () => {
219
- const snapshot = await storeClient.getStore({ definition, id });
220
- return selector(cloneStoreState(snapshot.state));
557
+ return runtimeStep(stepKey, async ({ stores }) => {
558
+ const snapshot = await stores.getStore({ definition, id });
559
+ return workflowCallback(selector)(cloneStoreState(snapshot.state));
221
560
  });
222
561
  },
223
562
  update(stepKey, updater) {
224
- return durableStep(stepKey, async () => await storeClient.updateStore({
563
+ return runtimeStep(stepKey, async ({ stores }) => await stores.updateStore({
225
564
  definition,
226
565
  id,
227
- updater,
566
+ updater: workflowCallback(updater, assertSynchronousUpdater),
228
567
  stepId: { executionId: event.executionId, stepKey }
229
568
  }));
230
569
  },
231
570
  updateFrom(stepKey, snapshot, updater) {
232
- return durableStep(stepKey, async () => await storeClient.updateStoreFrom({
571
+ return runtimeStep(stepKey, async ({ stores }) => await stores.updateStoreFrom({
233
572
  definition,
234
573
  id,
235
574
  snapshot,
236
- updater,
575
+ updater: workflowCallback(updater, assertSynchronousUpdater),
237
576
  stepId: { executionId: event.executionId, stepKey }
238
577
  }));
239
578
  },
240
579
  deleteFrom(stepKey, snapshot) {
241
- return durableStep(stepKey, () => storeClient.deleteStoreFrom({
580
+ return runtimeStep(stepKey, ({ stores }) => stores.deleteStoreFrom({
242
581
  definition,
243
582
  id,
244
583
  snapshot,
@@ -252,7 +591,6 @@ function createWorkflowStore(params) {
252
591
  definition,
253
592
  id,
254
593
  event,
255
- storeClient,
256
594
  stepKey,
257
595
  selector
258
596
  });
@@ -262,7 +600,6 @@ function createWorkflowStore(params) {
262
600
  definition,
263
601
  id,
264
602
  event,
265
- storeClient,
266
603
  stepKey,
267
604
  selector,
268
605
  claim
@@ -270,39 +607,45 @@ function createWorkflowStore(params) {
270
607
  }
271
608
  };
272
609
  }
273
- function run(arg1, arg2) {
610
+ function run(arg1, arg2, arg3) {
274
611
  let key = null;
275
612
  let fn;
613
+ let options;
276
614
  if (typeof arg1 === "string") {
277
615
  key = arg1;
278
616
  fn = arg2;
617
+ options = arg3;
279
618
  } else {
280
619
  fn = arg1;
620
+ options = arg2;
281
621
  }
282
622
  if (!key)
283
623
  key = getCallSiteHash(run);
284
- return durableStep(key, fn);
624
+ return durableStep(key, fn, options?.schema);
285
625
  }
286
- function delay(arg1, arg2) {
287
- let key = null;
288
- let retryInterval;
289
- if (typeof arg1 === "string") {
290
- key = arg1;
291
- retryInterval = arg2;
292
- } else {
293
- retryInterval = arg1;
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);
294
639
  }
295
- if (!key)
296
- key = getCallSiteHash(delay);
297
- return delayStep(key, retryInterval);
640
+ return delay;
298
641
  }
299
- async function* delayStep(key, retryInterval) {
642
+ async function* delayStep(key, retryInterval, clock) {
300
643
  yield new StepKey(key);
301
644
  const cached = yield new StepCacheCheck;
302
645
  if (cached) {
303
- yield new StepDelay2(cached.resumeIn);
646
+ yield new StepDelay2(cached.wakeAt);
304
647
  } else {
305
- yield new StepDelay2(Date.now() + retryInterval);
648
+ yield new StepDelay2(clock() + retryInterval);
306
649
  }
307
650
  }
308
651
  function poll(arg1, arg2, arg3) {
@@ -329,20 +672,22 @@ function poll(arg1, arg2, arg3) {
329
672
  };
330
673
  return run(key, task);
331
674
  }
332
- async function* durableStep(key, fn) {
675
+ async function* durableStep(key, fn, schema) {
333
676
  yield new StepKey(key);
334
677
  const cached = yield new StepCacheCheck;
335
678
  if (cached) {
336
- yield cached;
337
- if (cached instanceof StepError2) {
338
- throw cached.err;
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`);
339
685
  }
340
- return cached.result;
686
+ return unwrapValidated(validated);
341
687
  }
688
+ let result;
342
689
  try {
343
- const result = await fn();
344
- yield new StepResult2(result);
345
- return result;
690
+ result = await fn();
346
691
  } catch (err) {
347
692
  if (err instanceof RetryableError) {
348
693
  yield new StepError2(err, {
@@ -354,101 +699,156 @@ async function* durableStep(key, fn) {
354
699
  }
355
700
  throw err;
356
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);
357
752
  }
358
753
  async function* whenStep(params) {
359
- const { definition, id, event, storeClient, stepKey, selector } = params;
754
+ const { definition, id, event, stepKey, selector } = params;
360
755
  yield new StepKey(stepKey);
361
756
  const cached = yield new StepCacheCheck;
362
- if (cached) {
363
- yield cached;
364
- if (!(cached instanceof StepResult2)) {
365
- throw new Error("Store wait step cache must resolve to a step result");
366
- }
367
- return cached.result;
368
- }
369
- const snapshot = await storeClient.getStore({ definition, id });
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;
370
763
  const { result, readPaths } = trackStoreSelector(cloneStoreState(snapshot.state), selector);
371
764
  if (result !== undefined && result !== null && result !== false) {
372
- const clonedResult = cloneStoreState(result);
373
- yield new StepResult2(clonedResult);
374
- return clonedResult;
765
+ return yield* record(result);
375
766
  }
376
- await storeClient.registerWaiter({
767
+ yield* registerWaiter({
377
768
  workflowId: event.workflowId,
378
769
  executionId: event.executionId,
379
770
  stepKey,
380
- event,
381
771
  storeName: definition.name,
382
772
  storeId: id,
383
773
  instanceId: snapshot.instanceId,
384
774
  sinceVersion: snapshot.version,
385
775
  readPaths
386
776
  });
387
- yield new StepStoreWait2;
388
777
  throw new Error("Store wait yielded control unexpectedly");
389
778
  }
390
779
  async function* takeStep(params) {
391
- const { definition, id, event, storeClient, stepKey, selector, claim } = params;
780
+ const { definition, id, event, stepKey, selector, claim } = params;
392
781
  yield new StepKey(stepKey);
393
782
  const cached = yield new StepCacheCheck;
394
- if (cached) {
395
- yield cached;
396
- if (!(cached instanceof StepResult2)) {
397
- throw new Error("Store take step cache must resolve to a step result");
398
- }
399
- return cached.result;
400
- }
401
- let outcome;
402
- try {
403
- outcome = await storeClient.takeFromStore({
404
- definition,
405
- id,
406
- selector,
407
- claim,
408
- stepId: { executionId: event.executionId, stepKey }
409
- });
410
- } catch (err) {
411
- yield new StepError2(err);
412
- throw err;
413
- }
414
- if (outcome.matched) {
415
- yield new StepResult2(outcome.selected);
416
- return outcome.selected;
417
- }
418
- await storeClient.registerWaiter({
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({
419
798
  workflowId: event.workflowId,
420
799
  executionId: event.executionId,
421
800
  stepKey,
422
- event,
423
801
  storeName: definition.name,
424
802
  storeId: id,
425
803
  instanceId: outcome.instanceId,
426
804
  sinceVersion: outcome.version,
427
805
  readPaths: outcome.readPaths
428
806
  });
429
- yield new StepStoreWait2;
430
807
  throw new Error("Store take yielded control unexpectedly");
431
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;
814
+ }
432
815
 
433
816
  // src/exports/workflow.ts
434
817
  import {
435
- StepResponse as StepResponse3,
818
+ StepResponse as StepResponse5,
436
819
  StepKey as StepKey2,
437
820
  StepError as StepError3,
438
821
  StepResult as StepResult3,
439
822
  StepDelay as StepDelay3,
440
823
  StepCacheCheck as StepCacheCheck2,
441
- StepInvalid,
824
+ StepEffectWait as StepEffectWait3,
825
+ StepInvalid as StepInvalid2,
442
826
  StepStoreWait as StepStoreWait3,
443
- WorkflowResult as WorkflowResult2
827
+ WorkflowResult as WorkflowResult2,
828
+ InfrastructureError as InfrastructureError4,
829
+ LeaseLostError,
830
+ StepConflictError,
831
+ isLeaseLost,
832
+ WORKFLOW_RESULT_STEP_KEY
444
833
  } from "@yieldstar/core";
445
834
  function workflow(workflowFn) {
446
- return async function* workflowGenerator({ event, heapClient, storeClient, logger }) {
447
- const stepRunner = createStepRunner({ event, storeClient });
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 });
448
847
  const workflowIterator = workflowFn(stepRunner, event, logger);
449
848
  const { executionId } = event;
450
849
  let iteratorResult = null;
451
850
  let nextIteratorResult = null;
851
+ let resumeWith;
452
852
  const stepKeys = new Set;
453
853
  while (true) {
454
854
  let stepAttempt = 0;
@@ -457,10 +857,12 @@ function workflow(workflowFn) {
457
857
  iteratorResult = nextIteratorResult;
458
858
  nextIteratorResult = null;
459
859
  } else {
460
- iteratorResult = await workflowIterator.next();
860
+ stopIfAborted(signal);
861
+ iteratorResult = await workflowIterator.next(resumeWith);
461
862
  }
863
+ resumeWith = undefined;
462
864
  if (iteratorResult.done) {
463
- stepKey = "$$workflow-result$$";
865
+ stepKey = WORKFLOW_RESULT_STEP_KEY;
464
866
  } else if (!(iteratorResult.value instanceof StepKey2)) {
465
867
  throw new Error("Step runners must yield a StepKey object");
466
868
  } else {
@@ -470,10 +872,10 @@ function workflow(workflowFn) {
470
872
  }
471
873
  stepKeys.add(stepKey);
472
874
  }
473
- let cached = await heapClient.readStep({
875
+ let cached = await infrastructure(() => heapClient.readStep({
474
876
  executionId,
475
877
  stepKey
476
- });
878
+ }));
477
879
  if (cached?.meta) {
478
880
  stepAttempt = cached.meta.attempt + 1;
479
881
  }
@@ -490,64 +892,112 @@ function workflow(workflowFn) {
490
892
  stepResponse = iteratorResult.value;
491
893
  }
492
894
  if (stepResponse instanceof StepCacheCheck2) {
493
- const cachedResponse = cached ? deserializeStepResponse(cached.stepResponseJson) : null;
895
+ const cachedResponse = cached ? decodeStepRecord(cached.stepResponseJson) : null;
896
+ stopIfAborted(signal);
494
897
  iteratorResult = await workflowIterator.next(cachedResponse);
495
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
+ }
496
914
  } else if (!(stepResponse instanceof WorkflowResult2)) {
497
915
  throw new Error("Step runners must yield a CacheCheck object");
498
916
  }
499
917
  if (isIterable(stepResponse)) {
500
918
  console.log(`[ERR] Steps must be yielded using yield*
501
919
  `);
502
- stepResponse = new StepInvalid;
503
- } else if (!(stepResponse instanceof StepResponse3)) {
920
+ stepResponse = new StepInvalid2;
921
+ } else if (!(stepResponse instanceof StepResponse5)) {
504
922
  console.log(`[ERR] Iterators should yield a StepResponse. Got ${JSON.stringify(stepResponse)}`);
505
- 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;
506
930
  }
507
931
  const needsRetry = !cached?.meta.done && stepResponse instanceof StepError3 && stepResponse.maxAttempts > stepAttempt + 1;
508
- const needsWake = !cached?.meta.done && stepResponse instanceof StepStoreWait3;
509
- if (!cached) {
510
- await heapClient.writeStep({
511
- 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,
512
938
  stepKey,
513
939
  stepAttempt,
514
- stepDone: !needsRetry && !needsWake,
515
- stepResponseJson: serializeStepResponse(stepResponse)
516
- });
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
+ }
517
949
  }
518
950
  if (stepResponse instanceof WorkflowResult2) {
519
951
  return stepResponse;
520
952
  }
521
- if (stepResponse instanceof StepInvalid) {
953
+ if (iteratorResult.done && stepResponse instanceof StepError3) {
954
+ throw stepResponse.err;
955
+ }
956
+ if (stepResponse instanceof StepInvalid2) {
522
957
  break;
523
958
  }
524
959
  if (stepResponse instanceof StepError3) {
525
960
  if (needsRetry) {
526
- yield new StepDelay3(Date.now() + stepResponse.retryInterval);
961
+ yield new StepDelay3(clock() + stepResponse.retryInterval);
527
962
  } else {
963
+ stopIfAborted(signal);
528
964
  nextIteratorResult = await workflowIterator.throw(stepResponse.err);
529
965
  continue;
530
966
  }
531
967
  }
532
968
  if (stepResponse instanceof StepDelay3) {
533
- if (stepResponse.resumeIn <= Date.now()) {
969
+ if (stepResponse.wakeAt <= clock()) {
534
970
  continue;
535
971
  }
536
972
  }
537
- if (!(stepResponse instanceof StepResult3)) {
973
+ if (stepResponse instanceof StepResult3) {
974
+ resumeWith = stepResponse;
975
+ } else {
538
976
  yield stepResponse;
539
977
  }
540
978
  }
541
- throw new Error("Critical error");
979
+ throw new Error("Workflow yielded an invalid step. Steps must be yielded with yield* from a step runner.");
542
980
  };
543
981
  }
544
982
  var createWorkflow = workflow;
983
+ function stopIfAborted(signal) {
984
+ if (signal.aborted)
985
+ throw new InfrastructureError4(signal.reason);
986
+ }
545
987
  export {
546
- workflow,
547
- defineStore,
548
- createWorkflowRouter,
549
- createWorkflow,
550
- 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,
551
996
  createHttpSdkFactory,
552
- RetryableError
997
+ createLocalSdk,
998
+ createWorkflow,
999
+ createWorkflowRouter,
1000
+ defineStore,
1001
+ registerErrorClass2 as registerErrorClass,
1002
+ workflow
553
1003
  };