@yieldstar/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +21 -5
- package/dist/base/step.d.ts.map +1 -1
- package/dist/base/store.d.ts +135 -21
- package/dist/base/store.d.ts.map +1 -1
- package/dist/base/workflow.d.ts +15 -0
- package/dist/base/workflow.d.ts.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1647 -172
- 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 -9
- package/dist/lib/workflow-runner.d.ts.map +1 -1
- package/package.json +1 -1
- 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 +29 -7
- package/src/base/store.ts +339 -49
- package/src/base/workflow.ts +15 -0
- package/src/index.ts +137 -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 +75 -43
- package/src/base/scheduler.ts +0 -8
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, test } from "vitest";
|
|
2
|
+
import { type Durable, durableValueCodec } from "./durable-value";
|
|
3
|
+
|
|
4
|
+
function canonical(value: unknown) {
|
|
5
|
+
const encoding = durableValueCodec.encode(value);
|
|
6
|
+
if (encoding.status !== "encoded") {
|
|
7
|
+
throw new Error(`expected encoded, got: ${encoding.message}`);
|
|
8
|
+
}
|
|
9
|
+
return encoding.canonical;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function invalidMessage(value: unknown) {
|
|
13
|
+
const encoding = durableValueCodec.encode(value);
|
|
14
|
+
if (encoding.status !== "invalid") {
|
|
15
|
+
throw new Error(`expected invalid, got: ${encoding.canonical}`);
|
|
16
|
+
}
|
|
17
|
+
return encoding.message;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("durableValueCodec.encode accepts", () => {
|
|
21
|
+
test.each([
|
|
22
|
+
[null, "null"],
|
|
23
|
+
[true, "true"],
|
|
24
|
+
[false, "false"],
|
|
25
|
+
[0, "0"],
|
|
26
|
+
[1.5, "1.5"],
|
|
27
|
+
[-12, "-12"],
|
|
28
|
+
[1e21, "1e+21"],
|
|
29
|
+
["", '""'],
|
|
30
|
+
['quote " and \n newline', JSON.stringify('quote " and \n newline')],
|
|
31
|
+
[[], "[]"],
|
|
32
|
+
[{}, "{}"],
|
|
33
|
+
])("%j", (value, expected) => {
|
|
34
|
+
expect(canonical(value)).toBe(expected);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("nested arrays and objects", () => {
|
|
38
|
+
expect(canonical({ a: [1, { b: null }], c: "x" })).toBe(
|
|
39
|
+
'{"a":[1,{"b":null}],"c":"x"}'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("objects with a null prototype", () => {
|
|
44
|
+
const value = Object.create(null);
|
|
45
|
+
value.a = 1;
|
|
46
|
+
expect(canonical(value)).toBe('{"a":1}');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("the same object in sibling branches", () => {
|
|
50
|
+
const shared = { x: 1 };
|
|
51
|
+
expect(canonical({ a: shared, b: [shared, shared] })).toBe(
|
|
52
|
+
'{"a":{"x":1},"b":[{"x":1},{"x":1}]}'
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("an own __proto__ data property", () => {
|
|
57
|
+
const value = JSON.parse('{"__proto__":1}');
|
|
58
|
+
expect(canonical(value)).toBe('{"__proto__":1}');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("durableValueCodec.encode is canonical", () => {
|
|
63
|
+
test("object keys are sorted, including integer-like keys", () => {
|
|
64
|
+
expect(canonical({ b: 1, a: 2, 10: 3, 2: 4 })).toBe(
|
|
65
|
+
'{"10":3,"2":4,"a":2,"b":1}'
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("different key order encodes to the same string", () => {
|
|
70
|
+
expect(canonical({ a: 1, b: { c: 2, d: 3 } })).toBe(
|
|
71
|
+
canonical({ b: { d: 3, c: 2 }, a: 1 })
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("-0 is encoded as 0", () => {
|
|
76
|
+
expect(canonical(-0)).toBe("0");
|
|
77
|
+
expect(canonical({ n: -0 })).toBe(canonical({ n: 0 }));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("array order is preserved", () => {
|
|
81
|
+
expect(canonical([2, 1])).not.toBe(canonical([1, 2]));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("durableValueCodec.encode rejects", () => {
|
|
86
|
+
test.each([
|
|
87
|
+
["functions", () => {}],
|
|
88
|
+
["symbols", Symbol("s")],
|
|
89
|
+
["bigints", 1n],
|
|
90
|
+
["NaN", Number.NaN],
|
|
91
|
+
["Infinity", Number.POSITIVE_INFINITY],
|
|
92
|
+
["-Infinity", Number.NEGATIVE_INFINITY],
|
|
93
|
+
["Dates", new Date(0)],
|
|
94
|
+
["Maps", new Map()],
|
|
95
|
+
["class instances", new (class Point {})()],
|
|
96
|
+
["array subclasses", new (class List extends Array {})()],
|
|
97
|
+
])("%s", (_label, value) => {
|
|
98
|
+
expect(durableValueCodec.encode(value).status).toBe("invalid");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("nested invalid values, naming their path", () => {
|
|
102
|
+
expect(invalidMessage({ a: { b: new Date(0) } })).toMatch(/^\$\["a"\]\["b"\]: /);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("missing array items", () => {
|
|
106
|
+
expect(invalidMessage([1, , 3])).toMatch(/^\$\[1\]: missing array item/);
|
|
107
|
+
expect(invalidMessage(new Array(2))).toMatch(/missing array item/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("extra array properties", () => {
|
|
111
|
+
const value = Object.assign([1], { extra: true });
|
|
112
|
+
expect(invalidMessage(value)).toMatch(/arrays must only have items/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("accessors, without invoking them", () => {
|
|
116
|
+
let calls = 0;
|
|
117
|
+
const value = {
|
|
118
|
+
get a() {
|
|
119
|
+
calls++;
|
|
120
|
+
return 1;
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
expect(invalidMessage(value)).toMatch(/accessor/);
|
|
124
|
+
expect(calls).toBe(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("non-enumerable properties", () => {
|
|
128
|
+
const value = {};
|
|
129
|
+
Object.defineProperty(value, "hidden", { value: 1, enumerable: false });
|
|
130
|
+
expect(invalidMessage(value)).toMatch(/non-enumerable/);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("symbol properties", () => {
|
|
134
|
+
expect(invalidMessage({ [Symbol("s")]: 1 })).toMatch(/symbol/);
|
|
135
|
+
const array = [1];
|
|
136
|
+
Object.defineProperty(array, Symbol("s"), { value: 1 });
|
|
137
|
+
expect(invalidMessage(array)).toMatch(/symbol/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("reference cycles", () => {
|
|
141
|
+
const object: Record<string, unknown> = {};
|
|
142
|
+
object.self = object;
|
|
143
|
+
expect(invalidMessage(object)).toMatch(/cycle/);
|
|
144
|
+
|
|
145
|
+
const array: unknown[] = [];
|
|
146
|
+
array.push({ back: array });
|
|
147
|
+
expect(invalidMessage(array)).toMatch(/cycle/);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
function roundTrip(value: unknown) {
|
|
152
|
+
const decoding = durableValueCodec.decode(canonical(value));
|
|
153
|
+
if (decoding.status !== "decoded") {
|
|
154
|
+
throw new Error(`expected decoded, got: ${decoding.message}`);
|
|
155
|
+
}
|
|
156
|
+
return decoding.value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function decodeInvalid(text: string) {
|
|
160
|
+
const decoding = durableValueCodec.decode(text);
|
|
161
|
+
if (decoding.status !== "invalid") {
|
|
162
|
+
throw new Error("expected invalid");
|
|
163
|
+
}
|
|
164
|
+
return decoding.message;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
describe("durableValueCodec tags undefined", () => {
|
|
168
|
+
test("as a whole value, a property and an array item", () => {
|
|
169
|
+
expect(canonical(undefined)).toBe('{"$type":"undefined"}');
|
|
170
|
+
expect(canonical({ a: undefined })).toBe('{"a":{"$type":"undefined"}}');
|
|
171
|
+
expect(canonical([1, undefined])).toBe('[1,{"$type":"undefined"}]');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("keeping { a: undefined } distinct from {}", () => {
|
|
175
|
+
expect(canonical({ a: undefined })).not.toBe(canonical({}));
|
|
176
|
+
const decoded = roundTrip({ a: undefined });
|
|
177
|
+
expect(decoded).toHaveProperty("a");
|
|
178
|
+
expect(Object.keys(decoded as object)).toEqual(["a"]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("round trip", () => {
|
|
182
|
+
expect(roundTrip(undefined)).toBeUndefined();
|
|
183
|
+
const list = roundTrip([1, undefined]) as unknown[];
|
|
184
|
+
expect(list).toEqual([1, undefined]);
|
|
185
|
+
expect(1 in list).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("durableValueCodec wraps plain objects with a $type key", () => {
|
|
190
|
+
test("so they read back as written", () => {
|
|
191
|
+
const value = { $type: "undefined" };
|
|
192
|
+
expect(canonical(value)).toBe('{"$type":"object","value":{"$type":"undefined"}}');
|
|
193
|
+
expect(roundTrip(value)).toEqual(value);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("including tag-shaped objects nested inside values", () => {
|
|
197
|
+
const value = {
|
|
198
|
+
list: [{ $type: "error", name: "E", message: "m" }],
|
|
199
|
+
wrapped: { $type: "object", value: { $type: "undefined" } },
|
|
200
|
+
};
|
|
201
|
+
expect(roundTrip(value)).toEqual(value);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("but leaves other objects unwrapped", () => {
|
|
205
|
+
expect(canonical({ type: "x" })).toBe('{"type":"x"}');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("durableValueCodec.decode", () => {
|
|
210
|
+
test.each([
|
|
211
|
+
null,
|
|
212
|
+
true,
|
|
213
|
+
0,
|
|
214
|
+
1.5,
|
|
215
|
+
"text",
|
|
216
|
+
[],
|
|
217
|
+
{},
|
|
218
|
+
{ a: [1, { b: null }], c: "x" },
|
|
219
|
+
])("round trips %j", (value) => {
|
|
220
|
+
expect(roundTrip(value)).toEqual(value);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("returns a copy", () => {
|
|
224
|
+
const value = { nested: { a: 1 } };
|
|
225
|
+
const decoded = roundTrip(value) as typeof value;
|
|
226
|
+
expect(decoded).not.toBe(value);
|
|
227
|
+
expect(decoded.nested).not.toBe(value.nested);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("keeps an own __proto__ key as data", () => {
|
|
231
|
+
const decoded = roundTrip(JSON.parse('{"__proto__":{"polluted":true}}')) as object;
|
|
232
|
+
expect(Object.getPrototypeOf(decoded)).toBe(Object.prototype);
|
|
233
|
+
expect(Object.keys(decoded)).toEqual(["__proto__"]);
|
|
234
|
+
expect((decoded as { polluted?: boolean }).polluted).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("leaves error-shaped plain objects as plain objects", () => {
|
|
238
|
+
const report = { name: "launch", message: "Ship it", stack: "web" };
|
|
239
|
+
const decoded = roundTrip(report);
|
|
240
|
+
expect(decoded).not.toBeInstanceOf(Error);
|
|
241
|
+
expect(decoded).toEqual(report);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test.each([
|
|
245
|
+
["text that is not JSON", "{"],
|
|
246
|
+
["an unknown tag", '{"$type":"date","value":0}'],
|
|
247
|
+
["a tag with extra keys", '{"$type":"undefined","value":1}'],
|
|
248
|
+
["a wrapped value that is not an object", '{"$type":"object","value":[1]}'],
|
|
249
|
+
["an error without a message", '{"$type":"error","name":"Error"}'],
|
|
250
|
+
["an error with a non-string code", '{"$type":"error","name":"E","message":"m","code":1}'],
|
|
251
|
+
])("rejects %s", (_label, text) => {
|
|
252
|
+
expect(typeof decodeInvalid(text)).toBe("string");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("Durable<T>", () => {
|
|
257
|
+
function record<T>(value: T & Durable<T>): T {
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test("accepts interfaces, undefined and errors", () => {
|
|
262
|
+
interface Reservation {
|
|
263
|
+
id: string;
|
|
264
|
+
note?: string;
|
|
265
|
+
lines: { sku: string; qty: number }[];
|
|
266
|
+
error: Error | null;
|
|
267
|
+
}
|
|
268
|
+
const reservation: Reservation = { id: "r", lines: [], error: null };
|
|
269
|
+
expectTypeOf(record(reservation)).toEqualTypeOf<Reservation>();
|
|
270
|
+
expectTypeOf(record(undefined)).toEqualTypeOf<undefined>();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("rejects Dates, Maps, Sets, bigints and functions", () => {
|
|
274
|
+
// @ts-expect-error
|
|
275
|
+
record({ at: new Date() });
|
|
276
|
+
// @ts-expect-error
|
|
277
|
+
record({ map: new Map<string, number>() });
|
|
278
|
+
// @ts-expect-error
|
|
279
|
+
record(new Set<number>());
|
|
280
|
+
// @ts-expect-error
|
|
281
|
+
record(1n);
|
|
282
|
+
// @ts-expect-error
|
|
283
|
+
record({ fn: () => 1 });
|
|
284
|
+
});
|
|
285
|
+
});
|