@yieldstar/core 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.
- package/dist/base/durable-value.d.ts +71 -0
- package/dist/base/durable-value.d.ts.map +1 -0
- package/dist/base/durable-value.test.d.ts +2 -0
- package/dist/base/durable-value.test.d.ts.map +1 -0
- package/dist/base/effect.d.ts +136 -0
- package/dist/base/effect.d.ts.map +1 -0
- package/dist/base/errors.d.ts +23 -0
- package/dist/base/errors.d.ts.map +1 -0
- package/dist/base/errors.test.d.ts +2 -0
- package/dist/base/errors.test.d.ts.map +1 -0
- package/dist/base/event.d.ts +6 -1
- package/dist/base/event.d.ts.map +1 -1
- package/dist/base/execution.d.ts +244 -0
- package/dist/base/execution.d.ts.map +1 -0
- package/dist/base/heap.d.ts +15 -3
- package/dist/base/heap.d.ts.map +1 -1
- package/dist/base/invoker.d.ts +47 -2
- package/dist/base/invoker.d.ts.map +1 -1
- package/dist/base/runtime.d.ts +27 -4
- package/dist/base/runtime.d.ts.map +1 -1
- package/dist/base/step.d.ts +26 -5
- package/dist/base/step.d.ts.map +1 -1
- package/dist/base/store.d.ts +431 -0
- package/dist/base/store.d.ts.map +1 -0
- package/dist/base/store.test.d.ts +2 -0
- package/dist/base/store.test.d.ts.map +1 -0
- package/dist/base/workflow.d.ts +17 -0
- package/dist/base/workflow.d.ts.map +1 -1
- package/dist/index.d.ts +24 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1986 -111
- package/dist/lib/effect-state.d.ts +78 -0
- package/dist/lib/effect-state.d.ts.map +1 -0
- package/dist/lib/execution-dispatcher.d.ts +102 -0
- package/dist/lib/execution-dispatcher.d.ts.map +1 -0
- package/dist/lib/execution-state.d.ts +248 -0
- package/dist/lib/execution-state.d.ts.map +1 -0
- package/dist/lib/execution-state.test.d.ts +2 -0
- package/dist/lib/execution-state.test.d.ts.map +1 -0
- package/dist/lib/wait-for-outcome.d.ts +15 -0
- package/dist/lib/wait-for-outcome.d.ts.map +1 -0
- package/dist/lib/workflow-runner.d.ts +14 -7
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/dist/utils/map.d.ts +1 -1
- package/dist/utils/map.d.ts.map +1 -1
- package/package.json +13 -3
- package/src/base/durable-value.test.ts +285 -0
- package/src/base/durable-value.ts +442 -0
- package/src/base/effect.ts +165 -0
- package/src/base/errors.test.ts +210 -0
- package/src/base/errors.ts +72 -0
- package/src/base/event.ts +6 -1
- package/src/base/execution.ts +303 -0
- package/src/base/heap.ts +17 -3
- package/src/base/invoker.ts +60 -2
- package/src/base/runtime.ts +28 -7
- package/src/base/step.ts +37 -7
- package/src/base/store.test.ts +176 -0
- package/src/base/store.ts +1123 -0
- package/src/base/workflow.ts +17 -0
- package/src/index.ts +174 -3
- package/src/lib/effect-state.ts +254 -0
- package/src/lib/execution-dispatcher.ts +393 -0
- package/src/lib/execution-state.test.ts +249 -0
- package/src/lib/execution-state.ts +650 -0
- package/src/lib/wait-for-outcome.ts +40 -0
- package/src/lib/workflow-runner.ts +78 -34
- package/src/utils/map.test.ts +1 -1
- package/src/utils/map.ts +8 -2
- package/dist/base/map.d.ts +0 -21
- package/dist/base/map.d.ts.map +0 -1
- package/dist/base/map.test.d.ts +0 -2
- package/dist/base/map.test.d.ts.map +0 -1
- package/dist/base/schedule.d.ts +0 -71
- package/dist/base/schedule.d.ts.map +0 -1
- package/dist/base/types.d.ts +0 -24
- package/dist/base/types.d.ts.map +0 -1
- package/src/base/scheduler.ts +0 -8
- 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
|
+
}
|