@yieldstar/core 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.
Files changed (64) hide show
  1. package/dist/base/durable-value.d.ts +71 -0
  2. package/dist/base/durable-value.d.ts.map +1 -0
  3. package/dist/base/durable-value.test.d.ts +2 -0
  4. package/dist/base/durable-value.test.d.ts.map +1 -0
  5. package/dist/base/effect.d.ts +136 -0
  6. package/dist/base/effect.d.ts.map +1 -0
  7. package/dist/base/errors.d.ts +23 -0
  8. package/dist/base/errors.d.ts.map +1 -0
  9. package/dist/base/errors.test.d.ts +2 -0
  10. package/dist/base/errors.test.d.ts.map +1 -0
  11. package/dist/base/event.d.ts +6 -1
  12. package/dist/base/event.d.ts.map +1 -1
  13. package/dist/base/execution.d.ts +255 -0
  14. package/dist/base/execution.d.ts.map +1 -0
  15. package/dist/base/heap.d.ts +15 -3
  16. package/dist/base/heap.d.ts.map +1 -1
  17. package/dist/base/invoker.d.ts +47 -2
  18. package/dist/base/invoker.d.ts.map +1 -1
  19. package/dist/base/runtime.d.ts +27 -4
  20. package/dist/base/runtime.d.ts.map +1 -1
  21. package/dist/base/step.d.ts +21 -5
  22. package/dist/base/step.d.ts.map +1 -1
  23. package/dist/base/store.d.ts +135 -21
  24. package/dist/base/store.d.ts.map +1 -1
  25. package/dist/base/workflow.d.ts +15 -0
  26. package/dist/base/workflow.d.ts.map +1 -1
  27. package/dist/index.d.ts +23 -6
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1653 -172
  30. package/dist/lib/effect-state.d.ts +78 -0
  31. package/dist/lib/effect-state.d.ts.map +1 -0
  32. package/dist/lib/execution-dispatcher.d.ts +112 -0
  33. package/dist/lib/execution-dispatcher.d.ts.map +1 -0
  34. package/dist/lib/execution-state.d.ts +248 -0
  35. package/dist/lib/execution-state.d.ts.map +1 -0
  36. package/dist/lib/execution-state.test.d.ts +2 -0
  37. package/dist/lib/execution-state.test.d.ts.map +1 -0
  38. package/dist/lib/wait-for-outcome.d.ts +15 -0
  39. package/dist/lib/wait-for-outcome.d.ts.map +1 -0
  40. package/dist/lib/workflow-runner.d.ts +14 -9
  41. package/dist/lib/workflow-runner.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/base/durable-value.test.ts +285 -0
  44. package/src/base/durable-value.ts +442 -0
  45. package/src/base/effect.ts +165 -0
  46. package/src/base/errors.test.ts +210 -0
  47. package/src/base/errors.ts +72 -0
  48. package/src/base/event.ts +6 -1
  49. package/src/base/execution.ts +315 -0
  50. package/src/base/heap.ts +17 -3
  51. package/src/base/invoker.ts +60 -2
  52. package/src/base/runtime.ts +28 -7
  53. package/src/base/step.ts +29 -7
  54. package/src/base/store.ts +339 -49
  55. package/src/base/workflow.ts +15 -0
  56. package/src/index.ts +138 -3
  57. package/src/lib/effect-state.ts +254 -0
  58. package/src/lib/execution-dispatcher.ts +408 -0
  59. package/src/lib/execution-state.test.ts +249 -0
  60. package/src/lib/execution-state.ts +650 -0
  61. package/src/lib/wait-for-outcome.ts +40 -0
  62. package/src/lib/workflow-runner.ts +75 -43
  63. package/src/base/scheduler.ts +0 -8
  64. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,442 @@
1
+ import { registeredErrorClass } from "./errors";
2
+
3
+ export type DurableValue =
4
+ | null
5
+ | boolean
6
+ | number
7
+ | string
8
+ | undefined
9
+ | Error
10
+ | DurableValue[]
11
+ | DurableObject;
12
+
13
+ export type DurableObject = { [key: string]: DurableValue };
14
+
15
+ /**
16
+ * Compile-time check for a value that will be recorded. Unlike an index
17
+ * signature, it accepts interfaces. The codec still validates every value at
18
+ * runtime.
19
+ */
20
+ export type Durable<T> = T extends
21
+ | string
22
+ | number
23
+ | boolean
24
+ | null
25
+ | undefined
26
+ | Error
27
+ ? T
28
+ : T extends readonly (infer U)[]
29
+ ? readonly Durable<U>[]
30
+ : T extends Function | Date | Map<any, any> | Set<any> | bigint
31
+ ? never
32
+ : { [K in keyof T]: Durable<T[K]> };
33
+
34
+ /**
35
+ * `unknown` when `T` is a durable value, and `Otherwise` when it is not.
36
+ * Intersect a parameter with it to check a type inferred from the rest of
37
+ * that parameter: the check takes no part in inference, so literal widening
38
+ * and union inference are unchanged. `Otherwise` should restate the
39
+ * parameter with `Durable<T>` in place of `T`, so the error names the
40
+ * offending property.
41
+ */
42
+ export type IfDurable<T, Otherwise> = [T] extends [Durable<T>] ? unknown : Otherwise;
43
+
44
+ export type DurableValueEncoding =
45
+ | { status: "encoded"; canonical: string }
46
+ | { status: "invalid"; message: string };
47
+
48
+ export type DurableDecoding =
49
+ | { status: "decoded"; value: DurableValue }
50
+ | { status: "invalid"; message: string };
51
+
52
+ export interface DurableValueCodec {
53
+ /**
54
+ * Validates `value` recursively and returns its canonical JSON: object keys
55
+ * sorted by UTF-16 code unit and `-0` written as `0`, so equal values always
56
+ * encode to the same string. `undefined`, errors and plain objects with an
57
+ * own `$type` key are written as tagged objects. Never throws for an invalid
58
+ * value.
59
+ */
60
+ encode(value: unknown): DurableValueEncoding;
61
+
62
+ /**
63
+ * Reads an encoding back into the value that was encoded. Errors are revived
64
+ * as the class registered under their `name`, without calling its
65
+ * constructor. Never throws for an invalid encoding.
66
+ */
67
+ decode(canonical: string): DurableDecoding;
68
+ }
69
+
70
+ export class DurableValueError extends Error {}
71
+
72
+ export const durableValueCodec: DurableValueCodec = {
73
+ encode(value) {
74
+ try {
75
+ return { status: "encoded", canonical: encodeValue(value, "$", new Set()) };
76
+ } catch (err) {
77
+ if (!(err instanceof DurableValueError)) throw err;
78
+ return { status: "invalid", message: err.message };
79
+ }
80
+ },
81
+
82
+ decode(canonical) {
83
+ let parsed: unknown;
84
+ try {
85
+ parsed = JSON.parse(canonical);
86
+ } catch (err) {
87
+ return { status: "invalid", message: `not JSON: ${(err as Error).message}` };
88
+ }
89
+ try {
90
+ return { status: "decoded", value: decodeParsedValue(parsed) };
91
+ } catch (err) {
92
+ if (!(err instanceof DurableValueError)) throw err;
93
+ return { status: "invalid", message: err.message };
94
+ }
95
+ },
96
+ };
97
+
98
+ /**
99
+ * Encodes a thrown value: as thrown when it is a `DurableValue`, and as
100
+ * `Error(String(value))` otherwise. Always succeeds.
101
+ */
102
+ export function encodeThrownValue(value: unknown): string {
103
+ const encoding = durableValueCodec.encode(value);
104
+ if (encoding.status === "encoded") return encoding.canonical;
105
+ let message: string;
106
+ try {
107
+ message = String(value);
108
+ } catch {
109
+ message = "Thrown value is not a durable value";
110
+ }
111
+ return encodeValue(new Error(message), "$", new Set());
112
+ }
113
+
114
+ /**
115
+ * Whether `value` is an object with the default object prototype or `null`.
116
+ * It says nothing about the object's properties.
117
+ */
118
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
119
+ if (typeof value !== "object" || value === null) return false;
120
+ const proto = Object.getPrototypeOf(value);
121
+ return proto === Object.prototype || proto === null;
122
+ }
123
+
124
+ const UNDEFINED_TAG = '{"$type":"undefined"}';
125
+
126
+ const ERROR_KEYS = new Set(["name", "message", "stack", "code", "cause"]);
127
+
128
+ function encodeValue(value: unknown, path: string, ancestors: Set<object>): string {
129
+ if (value === null) return "null";
130
+ switch (typeof value) {
131
+ case "undefined":
132
+ return UNDEFINED_TAG;
133
+ case "boolean":
134
+ return value ? "true" : "false";
135
+ case "string":
136
+ return JSON.stringify(value);
137
+ case "number":
138
+ if (!Number.isFinite(value)) {
139
+ throw new DurableValueError(`${path}: ${value} is not a finite number`);
140
+ }
141
+ // JSON.stringify(-0) is "0".
142
+ return JSON.stringify(value);
143
+ case "object":
144
+ break;
145
+ default:
146
+ throw new DurableValueError(`${path}: ${typeof value} is not a durable value`);
147
+ }
148
+
149
+ // Only cycles are rejected: the same object may appear in sibling branches.
150
+ if (ancestors.has(value)) {
151
+ throw new DurableValueError(`${path}: reference cycle`);
152
+ }
153
+ ancestors.add(value);
154
+ try {
155
+ if (Array.isArray(value)) return encodeArray(value, path, ancestors);
156
+ if (value instanceof Error) return encodeError(value, path, ancestors);
157
+ return encodeObject(value, path, ancestors);
158
+ } finally {
159
+ // Error encoding recovers from invalid properties, so the set must be
160
+ // restored when a nested value throws.
161
+ ancestors.delete(value);
162
+ }
163
+ }
164
+
165
+ function encodeArray(value: unknown[], path: string, ancestors: Set<object>) {
166
+ if (Object.getPrototypeOf(value) !== Array.prototype) {
167
+ throw new DurableValueError(`${path}: arrays must not be subclassed`);
168
+ }
169
+ const items: string[] = [];
170
+ for (const key of Reflect.ownKeys(value)) {
171
+ if (key === "length") continue;
172
+ if (typeof key === "symbol") {
173
+ throw new DurableValueError(`${path}: symbol properties are not durable`);
174
+ }
175
+ if (!isArrayIndex(key, value.length)) {
176
+ throw new DurableValueError(`${path}.${key}: arrays must only have items`);
177
+ }
178
+ }
179
+ for (let i = 0; i < value.length; i++) {
180
+ const itemPath = `${path}[${i}]`;
181
+ const descriptor = Object.getOwnPropertyDescriptor(value, i);
182
+ if (!descriptor) {
183
+ throw new DurableValueError(`${itemPath}: missing array item`);
184
+ }
185
+ items.push(encodeValue(dataValue(descriptor, itemPath), itemPath, ancestors));
186
+ }
187
+ return `[${items.join(",")}]`;
188
+ }
189
+
190
+ function encodeObject(value: object, path: string, ancestors: Set<object>) {
191
+ if (!isPlainObject(value)) {
192
+ const name = Object.getPrototypeOf(value)?.constructor?.name ?? "object";
193
+ throw new DurableValueError(`${path}: ${name} instances are not plain objects`);
194
+ }
195
+ const entries: [string, string][] = [];
196
+ for (const key of Reflect.ownKeys(value)) {
197
+ if (typeof key === "symbol") {
198
+ throw new DurableValueError(`${path}: symbol properties are not durable`);
199
+ }
200
+ const keyPath = `${path}[${JSON.stringify(key)}]`;
201
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
202
+ entries.push([key, encodeValue(dataValue(descriptor, keyPath), keyPath, ancestors)]);
203
+ }
204
+ return objectLiteral(entries);
205
+ }
206
+
207
+ /**
208
+ * Writes a plain object from encoded entries, wrapping it as
209
+ * `{ $type: "object", value }` when it has its own `$type` key so that it can
210
+ * never be read as a tag.
211
+ */
212
+ function objectLiteral(entries: [string, string][]) {
213
+ const literal = sortedLiteral(entries);
214
+ return entries.some(([key]) => key === "$type")
215
+ ? `{"$type":"object","value":${literal}}`
216
+ : literal;
217
+ }
218
+
219
+ function sortedLiteral(entries: [string, string][]) {
220
+ const sorted = [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
221
+ return `{${sorted.map(([key, value]) => `${JSON.stringify(key)}:${value}`).join(",")}}`;
222
+ }
223
+
224
+ /**
225
+ * Tags an error with its name, message, string `stack` and `code`, its
226
+ * `cause`, a non-enumerable `errors` array (as on `AggregateError`) and its
227
+ * own enumerable fields. Properties that are not durable are left out rather
228
+ * than rejecting the error.
229
+ */
230
+ function encodeError(error: Error, path: string, ancestors: Set<object>) {
231
+ const name = readProperty(error, "name");
232
+ const message = readProperty(error, "message");
233
+ const stack = readProperty(error, "stack");
234
+ const code = readProperty(error, "code");
235
+ const tag: [string, string][] = [
236
+ ["$type", '"error"'],
237
+ ["name", JSON.stringify(typeof name === "string" ? name : "Error")],
238
+ ["message", JSON.stringify(typeof message === "string" ? message : "")],
239
+ ];
240
+ if (typeof stack === "string") tag.push(["stack", JSON.stringify(stack)]);
241
+ if (typeof code === "string") tag.push(["code", JSON.stringify(code)]);
242
+
243
+ if (Object.hasOwn(error, "cause")) {
244
+ const cause = encodeOptional(readProperty(error, "cause"), `${path}.cause`, ancestors);
245
+ if (cause !== undefined) tag.push(["cause", cause]);
246
+ }
247
+
248
+ const errors = Object.getOwnPropertyDescriptor(error, "errors");
249
+ if (errors && !errors.enumerable && Array.isArray(errors.value)) {
250
+ const encoded = encodeOptional(errors.value, `${path}.errors`, ancestors);
251
+ if (encoded !== undefined) tag.push(["errors", encoded]);
252
+ }
253
+
254
+ const fields: [string, string][] = [];
255
+ for (const key of Object.keys(error)) {
256
+ if (ERROR_KEYS.has(key)) continue;
257
+ const descriptor = Object.getOwnPropertyDescriptor(error, key)!;
258
+ if (!("value" in descriptor)) continue;
259
+ const encoded = encodeOptional(descriptor.value, `${path}.${key}`, ancestors);
260
+ if (encoded !== undefined) fields.push([key, encoded]);
261
+ }
262
+ if (fields.length > 0) tag.push(["fields", objectLiteral(fields)]);
263
+
264
+ return sortedLiteral(tag);
265
+ }
266
+
267
+ function encodeOptional(value: unknown, path: string, ancestors: Set<object>) {
268
+ try {
269
+ return encodeValue(value, path, ancestors);
270
+ } catch (err) {
271
+ if (!(err instanceof DurableValueError)) throw err;
272
+ return undefined;
273
+ }
274
+ }
275
+
276
+ function readProperty(error: Error, key: string): unknown {
277
+ try {
278
+ return (error as unknown as Record<string, unknown>)[key];
279
+ } catch {
280
+ return undefined;
281
+ }
282
+ }
283
+
284
+ /** Reads a property without invoking accessors. */
285
+ function dataValue(descriptor: PropertyDescriptor, path: string) {
286
+ if (!("value" in descriptor)) {
287
+ throw new DurableValueError(`${path}: accessor properties are not durable`);
288
+ }
289
+ if (!descriptor.enumerable) {
290
+ throw new DurableValueError(`${path}: non-enumerable properties are not durable`);
291
+ }
292
+ return descriptor.value;
293
+ }
294
+
295
+ function isArrayIndex(key: string, length: number) {
296
+ const index = Number(key);
297
+ return Number.isInteger(index) && index >= 0 && index < length && String(index) === key;
298
+ }
299
+
300
+ /**
301
+ * Decodes a value already parsed from an encoding's JSON, so that adapters can
302
+ * embed encodings in a larger JSON document. Throws `DurableValueError` for an
303
+ * invalid encoding.
304
+ */
305
+ export function decodeParsedValue(parsed: unknown, path = "$"): DurableValue {
306
+ if (parsed === null) return null;
307
+ switch (typeof parsed) {
308
+ case "boolean":
309
+ case "string":
310
+ return parsed;
311
+ case "number":
312
+ if (!Number.isFinite(parsed)) {
313
+ throw new DurableValueError(`${path}: ${parsed} is not a finite number`);
314
+ }
315
+ return parsed;
316
+ case "object":
317
+ break;
318
+ default:
319
+ throw new DurableValueError(`${path}: ${typeof parsed} is not a durable value`);
320
+ }
321
+ if (Array.isArray(parsed)) {
322
+ return parsed.map((item, i) => decodeParsedValue(item, `${path}[${i}]`));
323
+ }
324
+ const object = parsed as Record<string, unknown>;
325
+ if (!Object.hasOwn(object, "$type")) return decodeObject(object, path);
326
+
327
+ switch (object.$type) {
328
+ case "undefined":
329
+ expectKeys(object, path, ["$type"], []);
330
+ return undefined;
331
+ case "object": {
332
+ expectKeys(object, path, ["$type", "value"], []);
333
+ const value = object.value;
334
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
335
+ throw new DurableValueError(`${path}.value: a wrapped object must be an object`);
336
+ }
337
+ return decodeObject(value as Record<string, unknown>, `${path}.value`);
338
+ }
339
+ case "error":
340
+ return decodeError(object, path);
341
+ default:
342
+ throw new DurableValueError(`${path}: unknown $type ${JSON.stringify(object.$type)}`);
343
+ }
344
+ }
345
+
346
+ function decodeObject(object: Record<string, unknown>, path: string): DurableObject {
347
+ const decoded: DurableObject = {};
348
+ for (const key of Object.keys(object)) {
349
+ // Defined rather than assigned, so an own `__proto__` key stays data.
350
+ Object.defineProperty(decoded, key, {
351
+ value: decodeParsedValue(object[key], `${path}[${JSON.stringify(key)}]`),
352
+ enumerable: true,
353
+ writable: true,
354
+ configurable: true,
355
+ });
356
+ }
357
+ return decoded;
358
+ }
359
+
360
+ function decodeError(tag: Record<string, unknown>, path: string): Error {
361
+ expectKeys(
362
+ tag,
363
+ path,
364
+ ["$type", "name", "message"],
365
+ ["stack", "code", "cause", "errors", "fields"]
366
+ );
367
+ const { name, message, stack, code } = tag;
368
+ for (const [key, value, optional] of [
369
+ ["name", name, false],
370
+ ["message", message, false],
371
+ ["stack", stack, true],
372
+ ["code", code, true],
373
+ ] as const) {
374
+ if (typeof value !== "string" && !(optional && value === undefined)) {
375
+ throw new DurableValueError(`${path}.${key}: must be a string`);
376
+ }
377
+ }
378
+
379
+ const errorClass = registeredErrorClass(name as string) ?? Error;
380
+ // Constructing a base Error with the class as new.target gives a native error
381
+ // (one `Error.isError` accepts) with the class's prototype, without calling
382
+ // the class's constructor, which may require arguments or have side effects.
383
+ const error: Error = Reflect.construct(Error, [], errorClass);
384
+ const hidden = (key: string, value: unknown) =>
385
+ Object.defineProperty(error, key, {
386
+ value,
387
+ enumerable: false,
388
+ writable: true,
389
+ configurable: true,
390
+ });
391
+ const visible = (key: string, value: unknown) =>
392
+ Object.defineProperty(error, key, {
393
+ value,
394
+ enumerable: true,
395
+ writable: true,
396
+ configurable: true,
397
+ });
398
+
399
+ // Classes that inherit their name have it set in the constructor, which
400
+ // makes it an own enumerable property.
401
+ if (error.name !== name) visible("name", name);
402
+ hidden("message", message);
403
+ // Otherwise the stack captured here would stand in for the original.
404
+ if (stack !== undefined) hidden("stack", stack);
405
+ else delete error.stack;
406
+ if (code !== undefined) visible("code", code);
407
+ if (Object.hasOwn(tag, "cause")) {
408
+ hidden("cause", decodeParsedValue(tag.cause, `${path}.cause`));
409
+ }
410
+ if (Object.hasOwn(tag, "errors")) {
411
+ if (!Array.isArray(tag.errors)) {
412
+ throw new DurableValueError(`${path}.errors: must be an array`);
413
+ }
414
+ hidden("errors", decodeParsedValue(tag.errors, `${path}.errors`));
415
+ }
416
+ if (Object.hasOwn(tag, "fields")) {
417
+ const fields = decodeParsedValue(tag.fields, `${path}.fields`);
418
+ if (!isPlainObject(fields)) {
419
+ throw new DurableValueError(`${path}.fields: must be an object`);
420
+ }
421
+ for (const key of Object.keys(fields)) visible(key, fields[key]);
422
+ }
423
+ return error;
424
+ }
425
+
426
+ function expectKeys(
427
+ object: Record<string, unknown>,
428
+ path: string,
429
+ required: string[],
430
+ optional: string[]
431
+ ) {
432
+ for (const key of required) {
433
+ if (!Object.hasOwn(object, key)) {
434
+ throw new DurableValueError(`${path}: tag is missing ${key}`);
435
+ }
436
+ }
437
+ for (const key of Object.keys(object)) {
438
+ if (!required.includes(key) && !optional.includes(key)) {
439
+ throw new DurableValueError(`${path}: unexpected tag key ${JSON.stringify(key)}`);
440
+ }
441
+ }
442
+ }
@@ -0,0 +1,165 @@
1
+ import type { DurableObject, DurableValue } from "./durable-value";
2
+
3
+ export type EffectBeginRequest = {
4
+ operation: string;
5
+ /** Identifies the external operation across every execution and runtime. */
6
+ effectId: string;
7
+ input: DurableObject;
8
+ /** The execution and step that are beginning the effect. */
9
+ executionId: string;
10
+ stepKey: string;
11
+ };
12
+
13
+ export type EffectBeginResult =
14
+ | { status: "new" }
15
+ | { status: "pending" }
16
+ | { status: "settled"; result: DurableValue }
17
+ | { status: "rejected"; code: "EFFECT_REQUEST_MISMATCH" };
18
+
19
+ export type EffectSettleRequest = {
20
+ operation: string;
21
+ effectId: string;
22
+ result: DurableValue;
23
+ };
24
+
25
+ export type EffectSettleResult =
26
+ | { status: "settled"; result: DurableValue }
27
+ | {
28
+ status: "conflict";
29
+ /** The first result, which stays settled. */
30
+ result: DurableValue;
31
+ conflictingResult: DurableValue;
32
+ };
33
+
34
+ export type EffectWatchRequest = {
35
+ effectId: string;
36
+ executionId: string;
37
+ stepKey: string;
38
+ };
39
+
40
+ /** A settlement that reported a different result from the settled one. */
41
+ export type EffectConflictReport = {
42
+ result: DurableValue;
43
+ conflictingResult: DurableValue;
44
+ reportedAt: number;
45
+ };
46
+
47
+ export type EffectRecord = {
48
+ effectId: string;
49
+ operation: string;
50
+ input: DurableObject;
51
+ status: "pending" | "settled";
52
+ /** Present, possibly as `undefined`, only when `status` is `settled`. */
53
+ result?: DurableValue;
54
+ conflicts: EffectConflictReport[];
55
+ };
56
+
57
+ /**
58
+ * The runtime's record of external effects, keyed by effect ID (RFC 005).
59
+ * Effect records are never deleted, including when an execution that began
60
+ * one is cancelled or pruned.
61
+ *
62
+ * `begin`, `settle` and `watch` are not fenced: a stale lease holder's calls
63
+ * are accepted, and are safe because each is idempotent by effect ID. A
64
+ * client throws `EffectClientInvariantError` for a request the driver should
65
+ * never make, such as one with invalid values or for an unknown effect, and
66
+ * for stored data it cannot read. Any other rejection means the client may be
67
+ * unavailable.
68
+ */
69
+ export interface EffectClient {
70
+ /**
71
+ * Creates a pending effect before returning `new`. For an existing effect,
72
+ * returns its current state when `operation` and the canonical encoding of
73
+ * `input` match, and `EFFECT_REQUEST_MISMATCH` otherwise, leaving the
74
+ * effect unchanged.
75
+ */
76
+ begin(request: EffectBeginRequest): Promise<EffectBeginResult>;
77
+
78
+ /**
79
+ * Settles a pending effect with `result` and wakes every execution watching
80
+ * it, all-or-nothing. Settling an effect again with an equal result returns
81
+ * the settled result; a different result keeps the first, returns
82
+ * `conflict` and stores a permanent conflict report.
83
+ */
84
+ settle(request: EffectSettleRequest): Promise<EffectSettleResult>;
85
+
86
+ /**
87
+ * Registers the step as a waiter on the effect, so settlement wakes its
88
+ * execution. Repeating it keeps one registration. If the effect is already
89
+ * settled, wakes the execution now. The waiter is deleted by the heap write
90
+ * that records the step's result.
91
+ */
92
+ watch(request: EffectWatchRequest): Promise<void>;
93
+
94
+ /** For operators: the effect, its result and any conflict reports. */
95
+ getEffect(effectId: string): Promise<EffectRecord | undefined>;
96
+ }
97
+
98
+ /**
99
+ * Thrown by an `EffectClient` when a request or its stored data breaks the
100
+ * client's invariants. The driver requests another delivery instead of
101
+ * recording a step.
102
+ */
103
+ export class EffectClientInvariantError extends Error {
104
+ readonly code = "EFFECT_RUNTIME_INVARIANT" as const;
105
+
106
+ constructor(message: string, options?: ErrorOptions) {
107
+ super(message, options);
108
+ this.name = "EffectClientInvariantError";
109
+ }
110
+ }
111
+
112
+ export type WorkflowEffectErrorCode =
113
+ | "INVALID_EFFECT_REQUEST"
114
+ | "EFFECT_REQUEST_MISMATCH";
115
+
116
+ /** The codes with which `step.effect` requests another delivery. */
117
+ export type EffectDeliveryErrorCode =
118
+ | "EFFECT_CLIENT_UNAVAILABLE"
119
+ | "EFFECT_HANDLER_INVALID"
120
+ | "EFFECT_RUNTIME_INVARIANT";
121
+
122
+ /**
123
+ * The cause of the infrastructure error with which `step.effect` ends a
124
+ * delivery without recording its step. It never reaches workflow code: the
125
+ * delivery is not acknowledged, and the execution is delivered again.
126
+ *
127
+ * - `EFFECT_CLIENT_UNAVAILABLE`: an `EffectClient` call failed, and may
128
+ * recover without intervention.
129
+ * - `EFFECT_HANDLER_INVALID`: `lookup` or `perform` returned a malformed
130
+ * response or a value that is not durable.
131
+ * - `EFFECT_RUNTIME_INVARIANT`: the `EffectClient` threw
132
+ * `EffectClientInvariantError` or answered with a malformed response.
133
+ */
134
+ export class EffectDeliveryError extends Error {
135
+ readonly code: EffectDeliveryErrorCode;
136
+ readonly effectId: string;
137
+ readonly stepKey: string;
138
+
139
+ constructor(
140
+ code: EffectDeliveryErrorCode,
141
+ params: { effectId: string; stepKey: string; message: string },
142
+ options?: ErrorOptions
143
+ ) {
144
+ super(params.message, options);
145
+ this.name = "EffectDeliveryError";
146
+ this.code = code;
147
+ this.effectId = params.effectId;
148
+ this.stepKey = params.stepKey;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Thrown to workflow code by `step.effect` for an invalid request or one that
154
+ * does not match the recorded effect. It is recorded as the step's error, so
155
+ * replay throws it again.
156
+ */
157
+ export class WorkflowEffectError extends Error {
158
+ readonly code: WorkflowEffectErrorCode;
159
+
160
+ constructor(code: WorkflowEffectErrorCode, message: string) {
161
+ super(message);
162
+ this.name = "WorkflowEffectError";
163
+ this.code = code;
164
+ }
165
+ }