yieldstar 0.0.0 → 0.1.0-alpha.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/index.js +326 -0
- package/package.json +11 -6
- package/src/exports/errors.ts +13 -0
- package/src/exports/router.ts +7 -0
- package/src/exports/sdk-http.ts +67 -0
- package/src/exports/sdk-local.ts +51 -0
- package/src/exports/workflow.ts +220 -0
- package/src/index.ts +6 -0
- package/src/internal/serialise.ts +44 -0
- package/src/internal/step-runner.ts +143 -0
- package/src/internal/utils.ts +5 -0
- package/tsconfig.json +1 -2
- package/yieldstar-0.1.0-alpha.0.tgz +0 -0
- package/README.md +0 -15
- package/index.ts +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/exports/sdk-local.ts
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
function createLocalSdk(workflowRouter, invoker) {
|
|
5
|
+
return {
|
|
6
|
+
async trigger(workflowId, opts) {
|
|
7
|
+
const { workflowParams } = opts ?? {};
|
|
8
|
+
const executionId = opts?.executionId ?? randomUUID();
|
|
9
|
+
await invoker.execute({
|
|
10
|
+
executionId,
|
|
11
|
+
workflowId,
|
|
12
|
+
params: workflowParams
|
|
13
|
+
});
|
|
14
|
+
return { executionId };
|
|
15
|
+
},
|
|
16
|
+
async triggerAndWait(workflowId, opts) {
|
|
17
|
+
const { workflowParams } = opts ?? {};
|
|
18
|
+
const executionId = opts?.executionId ?? randomUUID();
|
|
19
|
+
const workflowCompletePromise = new Promise((resolve) => {
|
|
20
|
+
invoker.workflowEndEmitter.once(executionId, resolve);
|
|
21
|
+
});
|
|
22
|
+
await this.trigger(workflowId, { executionId, workflowParams });
|
|
23
|
+
return workflowCompletePromise;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// src/exports/sdk-http.ts
|
|
28
|
+
import { deserializeError, isErrorLike } from "serialize-error";
|
|
29
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
30
|
+
function createHttpSdkFactory(workflowRouter) {
|
|
31
|
+
return (params) => {
|
|
32
|
+
return {
|
|
33
|
+
async trigger(workflowId, opts) {
|
|
34
|
+
const { workflowParams } = opts ?? {};
|
|
35
|
+
const executionId = opts?.executionId ?? randomUUID2();
|
|
36
|
+
const res = await fetch(`${params.host}:${params.port}/trigger`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
executionId,
|
|
40
|
+
workflowId,
|
|
41
|
+
params: workflowParams
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
if (res.ok) {
|
|
45
|
+
return res.json();
|
|
46
|
+
} else {
|
|
47
|
+
throw res.statusText;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async triggerAndWait(workflowId, opts) {
|
|
51
|
+
const { workflowParams } = opts ?? {};
|
|
52
|
+
const executionId = opts?.executionId ?? randomUUID2();
|
|
53
|
+
await this.trigger(workflowId, { executionId, workflowParams });
|
|
54
|
+
const result = await fetch(`${params.host}:${params.port}/events`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: JSON.stringify({ executionId })
|
|
57
|
+
});
|
|
58
|
+
const json = await result.json();
|
|
59
|
+
if (isErrorLike(json)) {
|
|
60
|
+
throw deserializeError(json);
|
|
61
|
+
}
|
|
62
|
+
return json;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// src/exports/router.ts
|
|
68
|
+
function createWorkflowRouter(workflows) {
|
|
69
|
+
return workflows;
|
|
70
|
+
}
|
|
71
|
+
// src/exports/errors.ts
|
|
72
|
+
class RetryableError extends Error {
|
|
73
|
+
maxAttempts;
|
|
74
|
+
retryInterval;
|
|
75
|
+
constructor(message, opts) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "RetryableError";
|
|
78
|
+
this.maxAttempts = opts.maxAttempts;
|
|
79
|
+
this.retryInterval = opts.retryInterval;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// src/internal/utils.ts
|
|
83
|
+
var isIterable = (i) => {
|
|
84
|
+
return typeof i === "object" && i != null && Symbol.iterator in i;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/internal/serialise.ts
|
|
88
|
+
import { serializeError, deserializeError as deserializeError2, isErrorLike as isErrorLike2 } from "serialize-error";
|
|
89
|
+
import {
|
|
90
|
+
StepDelay,
|
|
91
|
+
StepError,
|
|
92
|
+
StepResult,
|
|
93
|
+
WorkflowResult
|
|
94
|
+
} from "@yieldstar/core";
|
|
95
|
+
function serialize(data) {
|
|
96
|
+
const replaceErrors = (key, value) => {
|
|
97
|
+
if (value instanceof Error) {
|
|
98
|
+
return serializeError(value);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
return JSON.stringify(data, replaceErrors);
|
|
103
|
+
}
|
|
104
|
+
function deserialize(jsonString) {
|
|
105
|
+
const reviveErrors = (key, value) => {
|
|
106
|
+
if (isErrorLike2(value)) {
|
|
107
|
+
return deserializeError2(value);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
};
|
|
111
|
+
const stepResponse = JSON.parse(jsonString, reviveErrors);
|
|
112
|
+
switch (stepResponse.type) {
|
|
113
|
+
case "step-error":
|
|
114
|
+
const { err, ...params } = stepResponse;
|
|
115
|
+
return new StepError(stepResponse.err, params);
|
|
116
|
+
case "step-delay":
|
|
117
|
+
return new StepDelay(stepResponse.resumeIn);
|
|
118
|
+
case "step-result":
|
|
119
|
+
return new StepResult(stepResponse.result);
|
|
120
|
+
case "workflow-result":
|
|
121
|
+
return new WorkflowResult(stepResponse.result);
|
|
122
|
+
default:
|
|
123
|
+
throw new Error("Unknown step result type: " + stepResponse.type);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/internal/step-runner.ts
|
|
128
|
+
import {
|
|
129
|
+
StepKey,
|
|
130
|
+
StepError as StepError2,
|
|
131
|
+
StepResult as StepResult2,
|
|
132
|
+
StepDelay as StepDelay2,
|
|
133
|
+
StepCacheCheck
|
|
134
|
+
} from "@yieldstar/core";
|
|
135
|
+
async function* run(arg1, arg2) {
|
|
136
|
+
let key = null;
|
|
137
|
+
let fn;
|
|
138
|
+
if (typeof arg1 === "string") {
|
|
139
|
+
key = arg1;
|
|
140
|
+
fn = arg2;
|
|
141
|
+
} else {
|
|
142
|
+
fn = arg1;
|
|
143
|
+
}
|
|
144
|
+
yield new StepKey(key);
|
|
145
|
+
const cached = yield new StepCacheCheck;
|
|
146
|
+
if (cached) {
|
|
147
|
+
yield cached;
|
|
148
|
+
if (cached instanceof StepError2) {
|
|
149
|
+
throw cached.err;
|
|
150
|
+
}
|
|
151
|
+
return cached.result;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const result = await fn();
|
|
155
|
+
yield new StepResult2(result);
|
|
156
|
+
return result;
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof RetryableError) {
|
|
159
|
+
yield new StepError2(err, {
|
|
160
|
+
maxAttempts: err.maxAttempts,
|
|
161
|
+
retryInterval: err.retryInterval
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
yield new StepError2(err);
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function* delay(arg1, arg2) {
|
|
170
|
+
let key = null;
|
|
171
|
+
let retryInterval;
|
|
172
|
+
if (typeof arg1 === "string") {
|
|
173
|
+
key = arg1;
|
|
174
|
+
retryInterval = arg2;
|
|
175
|
+
} else {
|
|
176
|
+
retryInterval = arg1;
|
|
177
|
+
}
|
|
178
|
+
yield new StepKey(key);
|
|
179
|
+
const cached = yield new StepCacheCheck;
|
|
180
|
+
if (cached) {
|
|
181
|
+
yield new StepDelay2(cached.resumeIn);
|
|
182
|
+
} else {
|
|
183
|
+
yield new StepDelay2(Date.now() + retryInterval);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function* poll(arg1, arg2, arg3) {
|
|
187
|
+
let key = null;
|
|
188
|
+
let opts;
|
|
189
|
+
let predicate;
|
|
190
|
+
if (typeof arg1 === "string") {
|
|
191
|
+
key = arg1;
|
|
192
|
+
opts = arg2;
|
|
193
|
+
predicate = arg3;
|
|
194
|
+
} else {
|
|
195
|
+
opts = arg1;
|
|
196
|
+
predicate = arg2;
|
|
197
|
+
}
|
|
198
|
+
const task = async () => {
|
|
199
|
+
if (!await predicate()) {
|
|
200
|
+
throw new RetryableError("Polling reached max retries", {
|
|
201
|
+
maxAttempts: opts.maxAttempts,
|
|
202
|
+
retryInterval: opts.retryInterval
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
if (key) {
|
|
207
|
+
yield* run(key, task);
|
|
208
|
+
} else {
|
|
209
|
+
yield* run(task);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
var stepRunner = { run, delay, poll };
|
|
213
|
+
|
|
214
|
+
// src/exports/workflow.ts
|
|
215
|
+
import {
|
|
216
|
+
StepResponse as StepResponse3,
|
|
217
|
+
StepKey as StepKey2,
|
|
218
|
+
StepError as StepError3,
|
|
219
|
+
StepResult as StepResult3,
|
|
220
|
+
StepDelay as StepDelay3,
|
|
221
|
+
StepCacheCheck as StepCacheCheck2,
|
|
222
|
+
StepInvalid,
|
|
223
|
+
WorkflowResult as WorkflowResult2
|
|
224
|
+
} from "@yieldstar/core";
|
|
225
|
+
function createWorkflow(workflowFn) {
|
|
226
|
+
return async function* workflowGenerator(params) {
|
|
227
|
+
const workflowIterator = workflowFn(stepRunner);
|
|
228
|
+
const { executionId, heapClient } = params;
|
|
229
|
+
let keylessStepIndex = -1;
|
|
230
|
+
let iteratorResult = null;
|
|
231
|
+
let nextIteratorResult = null;
|
|
232
|
+
while (true) {
|
|
233
|
+
let stepAttempt = 0;
|
|
234
|
+
let stepKey;
|
|
235
|
+
if (nextIteratorResult) {
|
|
236
|
+
iteratorResult = nextIteratorResult;
|
|
237
|
+
nextIteratorResult = null;
|
|
238
|
+
} else {
|
|
239
|
+
iteratorResult = await workflowIterator.next();
|
|
240
|
+
}
|
|
241
|
+
if (iteratorResult.done) {
|
|
242
|
+
stepKey = "$$workflow-result$$";
|
|
243
|
+
} else if (!(iteratorResult.value instanceof StepKey2)) {
|
|
244
|
+
throw new Error("Step runners must yield a StepKey object");
|
|
245
|
+
} else if (iteratorResult.value.key) {
|
|
246
|
+
stepKey = iteratorResult.value.key;
|
|
247
|
+
} else {
|
|
248
|
+
keylessStepIndex++;
|
|
249
|
+
stepKey = `\$\$step-index-${keylessStepIndex}\$\$`;
|
|
250
|
+
}
|
|
251
|
+
let cached = await heapClient.readStep({
|
|
252
|
+
executionId,
|
|
253
|
+
stepKey
|
|
254
|
+
});
|
|
255
|
+
if (cached?.meta) {
|
|
256
|
+
stepAttempt = cached.meta.attempt + 1;
|
|
257
|
+
}
|
|
258
|
+
if (cached?.meta.done === false) {
|
|
259
|
+
cached = null;
|
|
260
|
+
}
|
|
261
|
+
if (!iteratorResult.done) {
|
|
262
|
+
iteratorResult = await workflowIterator.next();
|
|
263
|
+
}
|
|
264
|
+
let stepResponse;
|
|
265
|
+
if (iteratorResult.done) {
|
|
266
|
+
stepResponse = new WorkflowResult2(iteratorResult.value);
|
|
267
|
+
} else {
|
|
268
|
+
stepResponse = iteratorResult.value;
|
|
269
|
+
}
|
|
270
|
+
if (stepResponse instanceof StepCacheCheck2) {
|
|
271
|
+
const cachedResponse = cached ? deserialize(cached.stepResponseJson) : null;
|
|
272
|
+
iteratorResult = await workflowIterator.next(cachedResponse);
|
|
273
|
+
stepResponse = iteratorResult.value;
|
|
274
|
+
} else if (!(stepResponse instanceof WorkflowResult2)) {
|
|
275
|
+
throw new Error("Step runners must yield a CacheCheck object");
|
|
276
|
+
}
|
|
277
|
+
if (isIterable(stepResponse)) {
|
|
278
|
+
console.log("[ERR] Steps must be yielded using yield*\n");
|
|
279
|
+
stepResponse = new StepInvalid;
|
|
280
|
+
} else if (!(stepResponse instanceof StepResponse3)) {
|
|
281
|
+
console.log(`[ERR] Iterators should yield a StepResponse. Got ${JSON.stringify(stepResponse)}`);
|
|
282
|
+
stepResponse = new StepInvalid;
|
|
283
|
+
}
|
|
284
|
+
const needsRetry = !cached?.meta.done && stepResponse instanceof StepError3 && stepResponse.maxAttempts > stepAttempt + 1;
|
|
285
|
+
if (!cached) {
|
|
286
|
+
await heapClient.writeStep({
|
|
287
|
+
executionId,
|
|
288
|
+
stepKey,
|
|
289
|
+
stepAttempt,
|
|
290
|
+
stepDone: !needsRetry,
|
|
291
|
+
stepResponseJson: serialize(stepResponse)
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (stepResponse instanceof WorkflowResult2) {
|
|
295
|
+
return stepResponse;
|
|
296
|
+
}
|
|
297
|
+
if (stepResponse instanceof StepInvalid) {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
if (stepResponse instanceof StepError3) {
|
|
301
|
+
if (needsRetry) {
|
|
302
|
+
yield new StepDelay3(Date.now() + stepResponse.retryInterval);
|
|
303
|
+
} else {
|
|
304
|
+
nextIteratorResult = await workflowIterator.throw(stepResponse.err);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (stepResponse instanceof StepDelay3) {
|
|
309
|
+
if (stepResponse.resumeIn <= Date.now()) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (!(stepResponse instanceof StepResult3)) {
|
|
314
|
+
yield stepResponse;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
throw new Error("Critical error");
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export {
|
|
321
|
+
createWorkflowRouter,
|
|
322
|
+
createWorkflow,
|
|
323
|
+
createLocalSdk,
|
|
324
|
+
createHttpSdkFactory,
|
|
325
|
+
RetryableError
|
|
326
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yieldstar",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"module": "index.ts",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
5
4
|
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"serialize-error": "^11.0.3"
|
|
9
|
+
},
|
|
6
10
|
"devDependencies": {
|
|
7
|
-
"@types/bun": "latest"
|
|
11
|
+
"@types/bun": "latest",
|
|
12
|
+
"@yieldstar/core": "0.1.0"
|
|
8
13
|
},
|
|
9
|
-
"
|
|
10
|
-
"
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target bun --packages=external"
|
|
11
16
|
}
|
|
12
|
-
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class RetryableError extends Error {
|
|
2
|
+
maxAttempts: number;
|
|
3
|
+
retryInterval: number;
|
|
4
|
+
constructor(
|
|
5
|
+
message: string,
|
|
6
|
+
opts: { maxAttempts: number; retryInterval: number }
|
|
7
|
+
) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "RetryableError";
|
|
10
|
+
this.maxAttempts = opts.maxAttempts;
|
|
11
|
+
this.retryInterval = opts.retryInterval;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowRouter,
|
|
3
|
+
WorkflowGeneratorReturnType,
|
|
4
|
+
} from "@yieldstar/core";
|
|
5
|
+
import { deserializeError, isErrorLike } from "serialize-error";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
export function createHttpSdkFactory<W extends WorkflowRouter>(
|
|
9
|
+
workflowRouter: W
|
|
10
|
+
) {
|
|
11
|
+
return (params: { host: string; port: number }) => {
|
|
12
|
+
return {
|
|
13
|
+
async trigger<K extends keyof W>(
|
|
14
|
+
workflowId: K,
|
|
15
|
+
opts?: {
|
|
16
|
+
executionId?: string;
|
|
17
|
+
workflowParams?: Parameters<W[K]>[0] extends never
|
|
18
|
+
? never
|
|
19
|
+
: Parameters<W[K]>[0];
|
|
20
|
+
}
|
|
21
|
+
) {
|
|
22
|
+
const { workflowParams } = opts ?? {};
|
|
23
|
+
const executionId = opts?.executionId ?? randomUUID();
|
|
24
|
+
const res = await fetch(`${params.host}:${params.port}/trigger`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
executionId,
|
|
28
|
+
workflowId,
|
|
29
|
+
params: workflowParams,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
if (res.ok) {
|
|
33
|
+
return res.json() as Promise<{ executionId: string }>;
|
|
34
|
+
} else {
|
|
35
|
+
throw res.statusText;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
async triggerAndWait<K extends keyof W>(
|
|
39
|
+
workflowId: K,
|
|
40
|
+
opts?: {
|
|
41
|
+
executionId?: string;
|
|
42
|
+
workflowParams?: Parameters<W[K]>[0] extends never
|
|
43
|
+
? never
|
|
44
|
+
: Parameters<W[K]>[0];
|
|
45
|
+
}
|
|
46
|
+
) {
|
|
47
|
+
const { workflowParams } = opts ?? {};
|
|
48
|
+
const executionId = opts?.executionId ?? randomUUID();
|
|
49
|
+
// todo: add timeout and cancel request
|
|
50
|
+
await this.trigger(workflowId, { executionId, workflowParams });
|
|
51
|
+
// todo: set up subscription first (maybe just use ws)
|
|
52
|
+
const result = await fetch(`${params.host}:${params.port}/events`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: JSON.stringify({ executionId }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const json = await result.json();
|
|
58
|
+
|
|
59
|
+
if (isErrorLike(json)) {
|
|
60
|
+
throw deserializeError(json);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return json as Promise<WorkflowGeneratorReturnType<W[K]>>;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkflowRouter,
|
|
3
|
+
WorkflowGeneratorReturnType,
|
|
4
|
+
WorkflowInvoker,
|
|
5
|
+
} from "@yieldstar/core";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
export function createLocalSdk<W extends WorkflowRouter>(
|
|
9
|
+
workflowRouter: W,
|
|
10
|
+
invoker: WorkflowInvoker
|
|
11
|
+
) {
|
|
12
|
+
return {
|
|
13
|
+
async trigger<K extends keyof W>(
|
|
14
|
+
workflowId: K,
|
|
15
|
+
opts?: {
|
|
16
|
+
executionId?: string;
|
|
17
|
+
workflowParams?: Parameters<W[K]>[0] extends never
|
|
18
|
+
? never
|
|
19
|
+
: Parameters<W[K]>[0];
|
|
20
|
+
}
|
|
21
|
+
) {
|
|
22
|
+
const { workflowParams } = opts ?? {};
|
|
23
|
+
const executionId = opts?.executionId ?? randomUUID();
|
|
24
|
+
await invoker.execute({
|
|
25
|
+
executionId,
|
|
26
|
+
workflowId: workflowId as string,
|
|
27
|
+
params: workflowParams,
|
|
28
|
+
});
|
|
29
|
+
return { executionId };
|
|
30
|
+
},
|
|
31
|
+
async triggerAndWait<K extends keyof W>(
|
|
32
|
+
workflowId: K,
|
|
33
|
+
opts?: {
|
|
34
|
+
executionId?: string;
|
|
35
|
+
workflowParams?: Parameters<W[K]>[0] extends never
|
|
36
|
+
? never
|
|
37
|
+
: Parameters<W[K]>[0];
|
|
38
|
+
}
|
|
39
|
+
) {
|
|
40
|
+
const { workflowParams } = opts ?? {};
|
|
41
|
+
const executionId = opts?.executionId ?? randomUUID();
|
|
42
|
+
const workflowCompletePromise = new Promise((resolve) => {
|
|
43
|
+
invoker.workflowEndEmitter.once(executionId, resolve);
|
|
44
|
+
});
|
|
45
|
+
await this.trigger(workflowId, { executionId, workflowParams });
|
|
46
|
+
return workflowCompletePromise as Promise<
|
|
47
|
+
WorkflowGeneratorReturnType<W[K]>
|
|
48
|
+
>;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { WorkflowGenerator } from "@yieldstar/core";
|
|
2
|
+
import type { StepRunner } from "../internal/step-runner";
|
|
3
|
+
import { isIterable } from "../internal/utils";
|
|
4
|
+
import { deserialize, serialize } from "../internal/serialise";
|
|
5
|
+
import { stepRunner } from "../internal/step-runner";
|
|
6
|
+
import {
|
|
7
|
+
StepResponse,
|
|
8
|
+
StepKey,
|
|
9
|
+
StepError,
|
|
10
|
+
StepResult,
|
|
11
|
+
StepDelay,
|
|
12
|
+
StepCacheCheck,
|
|
13
|
+
StepInvalid,
|
|
14
|
+
WorkflowResult,
|
|
15
|
+
} from "@yieldstar/core";
|
|
16
|
+
|
|
17
|
+
export type WorkflowFn<T> = (step: StepRunner) => AsyncGenerator<any, T>;
|
|
18
|
+
|
|
19
|
+
export function createWorkflow<T>(
|
|
20
|
+
workflowFn: WorkflowFn<T>
|
|
21
|
+
): WorkflowGenerator<T> {
|
|
22
|
+
/**
|
|
23
|
+
* @description Advances workflow steps, handling any workflow logic, and
|
|
24
|
+
* yielding control to a workflow executor to do async work
|
|
25
|
+
* @yields {StepResponse}
|
|
26
|
+
*/
|
|
27
|
+
return async function* workflowGenerator(params) {
|
|
28
|
+
const workflowIterator = workflowFn(stepRunner);
|
|
29
|
+
const { executionId, heapClient } = params;
|
|
30
|
+
|
|
31
|
+
let keylessStepIndex = -1;
|
|
32
|
+
let iteratorResult: IteratorResult<any> | null = null;
|
|
33
|
+
let nextIteratorResult: IteratorResult<any> | null = null;
|
|
34
|
+
|
|
35
|
+
while (true) {
|
|
36
|
+
let stepAttempt = 0;
|
|
37
|
+
let stepKey: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* If we already have the next iterator result from a previous iteration
|
|
41
|
+
* (e.g. it threw an error and advanced the workflow), use that, otherwise,
|
|
42
|
+
* advance the workflow generator
|
|
43
|
+
*/
|
|
44
|
+
if (nextIteratorResult) {
|
|
45
|
+
iteratorResult = nextIteratorResult;
|
|
46
|
+
nextIteratorResult = null;
|
|
47
|
+
} else {
|
|
48
|
+
iteratorResult = await workflowIterator.next();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get the StepKey from the step runner.
|
|
53
|
+
* If the StepKey value is null, assume deterministic ordering, and use
|
|
54
|
+
* the step index as a key.
|
|
55
|
+
* If the generator is done, the workflow has returned, so we set a
|
|
56
|
+
* special key for that.
|
|
57
|
+
*/
|
|
58
|
+
if (iteratorResult.done) {
|
|
59
|
+
stepKey = "$$workflow-result$$";
|
|
60
|
+
} else if (!(iteratorResult.value instanceof StepKey)) {
|
|
61
|
+
throw new Error("Step runners must yield a StepKey object");
|
|
62
|
+
} else if (iteratorResult.value.key) {
|
|
63
|
+
stepKey = iteratorResult.value.key;
|
|
64
|
+
} else {
|
|
65
|
+
keylessStepIndex++;
|
|
66
|
+
stepKey = `$$step-index-${keylessStepIndex}$$`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Using the StepKey, retrieve the previous response for this step from
|
|
71
|
+
* the cache
|
|
72
|
+
*/
|
|
73
|
+
let cached = await heapClient.readStep({
|
|
74
|
+
executionId,
|
|
75
|
+
stepKey,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Increment attempt counter. We'll save this after execution.
|
|
80
|
+
*/
|
|
81
|
+
if (cached?.meta) {
|
|
82
|
+
stepAttempt = cached.meta.attempt + 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* If the step needs to be retried, ignore the cached value
|
|
87
|
+
*/
|
|
88
|
+
if (cached?.meta.done === false) {
|
|
89
|
+
cached = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* If the workflow generator hasn't returned, advance the step runner
|
|
94
|
+
*/
|
|
95
|
+
if (!iteratorResult.done) {
|
|
96
|
+
iteratorResult = await workflowIterator.next();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* We can now get a StepResponse object from the iterator result.
|
|
101
|
+
* The step runner generator always yields a StepResponse, whereas
|
|
102
|
+
* the workflow generator return value needs to be wrapped.
|
|
103
|
+
*/
|
|
104
|
+
let stepResponse: StepResponse;
|
|
105
|
+
|
|
106
|
+
if (iteratorResult.done) {
|
|
107
|
+
stepResponse = new WorkflowResult(iteratorResult.value);
|
|
108
|
+
} else {
|
|
109
|
+
stepResponse = iteratorResult.value;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* If the first yielded value is a cache check, advance to actual step,
|
|
114
|
+
* passing it the cached value
|
|
115
|
+
*/
|
|
116
|
+
if (stepResponse instanceof StepCacheCheck) {
|
|
117
|
+
const cachedResponse = cached
|
|
118
|
+
? deserialize(cached.stepResponseJson)
|
|
119
|
+
: null;
|
|
120
|
+
iteratorResult = await workflowIterator.next(cachedResponse);
|
|
121
|
+
stepResponse = iteratorResult.value;
|
|
122
|
+
} else if (!(stepResponse instanceof WorkflowResult)) {
|
|
123
|
+
throw new Error("Step runners must yield a CacheCheck object");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check for invalid iterators e.g. developer didn't use yield* or didn't
|
|
128
|
+
* use a step runner
|
|
129
|
+
* @todo: iterable check should be before StepKey check
|
|
130
|
+
* @todo: not throwing here causes a critical error – should workflow break?
|
|
131
|
+
*/
|
|
132
|
+
if (isIterable(stepResponse)) {
|
|
133
|
+
console.log("[ERR] Steps must be yielded using yield*\n");
|
|
134
|
+
stepResponse = new StepInvalid();
|
|
135
|
+
} else if (!(stepResponse instanceof StepResponse)) {
|
|
136
|
+
console.log(
|
|
137
|
+
`[ERR] Iterators should yield a StepResponse. Got ${JSON.stringify(
|
|
138
|
+
stepResponse
|
|
139
|
+
)}`
|
|
140
|
+
);
|
|
141
|
+
stepResponse = new StepInvalid();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Determine if step needs to be retried
|
|
146
|
+
*/
|
|
147
|
+
const needsRetry =
|
|
148
|
+
!cached?.meta.done &&
|
|
149
|
+
stepResponse instanceof StepError &&
|
|
150
|
+
// 1-indexed vs 0-indexed
|
|
151
|
+
stepResponse.maxAttempts > stepAttempt + 1;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* If this step attempt is not already cached, cache it
|
|
155
|
+
*/
|
|
156
|
+
if (!cached) {
|
|
157
|
+
await heapClient.writeStep({
|
|
158
|
+
executionId,
|
|
159
|
+
stepKey,
|
|
160
|
+
stepAttempt,
|
|
161
|
+
stepDone: !needsRetry,
|
|
162
|
+
stepResponseJson: serialize(stepResponse),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Once the WorkflowResult is cached, return the WorkflowResult to the
|
|
168
|
+
* upstream consumer e.g the worker
|
|
169
|
+
*/
|
|
170
|
+
if (stepResponse instanceof WorkflowResult) {
|
|
171
|
+
return stepResponse;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Once invalid iterator steps have been cached, stop execution
|
|
176
|
+
*/
|
|
177
|
+
if (stepResponse instanceof StepInvalid) {
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* If step runner yielded a step error, either throw the error back for
|
|
183
|
+
* user to handle, or yield to worker, if it should be retried.
|
|
184
|
+
* (Throwing also advances the workflow generator, so, in that case, we
|
|
185
|
+
* store the next iterator result, and continue to the next step).
|
|
186
|
+
*/
|
|
187
|
+
if (stepResponse instanceof StepError) {
|
|
188
|
+
if (needsRetry) {
|
|
189
|
+
yield new StepDelay(Date.now() + stepResponse.retryInterval);
|
|
190
|
+
} else {
|
|
191
|
+
nextIteratorResult = await workflowIterator.throw(stepResponse.err);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* If the delay has already elapsed, continue straight to next step
|
|
198
|
+
*/
|
|
199
|
+
if (stepResponse instanceof StepDelay) {
|
|
200
|
+
if (stepResponse.resumeIn <= Date.now()) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Yield control back to the worker if the stepResponse is not a result
|
|
207
|
+
* i.e. async work is required. (Whenever we receive a StepResult, it's
|
|
208
|
+
* always possible to continue straight to the next step).
|
|
209
|
+
*/
|
|
210
|
+
if (!(stepResponse instanceof StepResult)) {
|
|
211
|
+
yield stepResponse;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* If we arrive here, something has gone wrong internally
|
|
217
|
+
*/
|
|
218
|
+
throw new Error("Critical error");
|
|
219
|
+
};
|
|
220
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createLocalSdk } from "./exports/sdk-local";
|
|
2
|
+
export { createHttpSdkFactory } from "./exports/sdk-http";
|
|
3
|
+
export { createWorkflowRouter } from "./exports/router";
|
|
4
|
+
export { RetryableError } from "./exports/errors";
|
|
5
|
+
export { createWorkflow } from "./exports/workflow";
|
|
6
|
+
export type { WorkflowFn } from "./exports/workflow";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { serializeError, deserializeError, isErrorLike } from "serialize-error";
|
|
2
|
+
import {
|
|
3
|
+
StepDelay,
|
|
4
|
+
StepError,
|
|
5
|
+
StepResult,
|
|
6
|
+
StepResponse,
|
|
7
|
+
WorkflowResult,
|
|
8
|
+
} from "@yieldstar/core";
|
|
9
|
+
|
|
10
|
+
export function serialize(data: StepResponse): string {
|
|
11
|
+
const replaceErrors = (key: string, value: any) => {
|
|
12
|
+
if (value instanceof Error) {
|
|
13
|
+
return serializeError(value);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return JSON.stringify(data, replaceErrors);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function deserialize(jsonString: string): StepResponse {
|
|
22
|
+
const reviveErrors = (key: string, value: any) => {
|
|
23
|
+
if (isErrorLike(value)) {
|
|
24
|
+
return deserializeError(value);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const stepResponse = JSON.parse(jsonString, reviveErrors);
|
|
30
|
+
|
|
31
|
+
switch (stepResponse.type) {
|
|
32
|
+
case "step-error":
|
|
33
|
+
const { err, ...params } = stepResponse;
|
|
34
|
+
return new StepError(stepResponse.err, params);
|
|
35
|
+
case "step-delay":
|
|
36
|
+
return new StepDelay(stepResponse.resumeIn);
|
|
37
|
+
case "step-result":
|
|
38
|
+
return new StepResult(stepResponse.result);
|
|
39
|
+
case "workflow-result":
|
|
40
|
+
return new WorkflowResult(stepResponse.result);
|
|
41
|
+
default:
|
|
42
|
+
throw new Error("Unknown step result type: " + stepResponse.type);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StepResponse,
|
|
3
|
+
StepKey,
|
|
4
|
+
StepError,
|
|
5
|
+
StepResult,
|
|
6
|
+
StepDelay,
|
|
7
|
+
StepCacheCheck,
|
|
8
|
+
} from "@yieldstar/core";
|
|
9
|
+
import { RetryableError } from "../exports/errors";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @description A library of step generators, each of which:
|
|
13
|
+
* @yields a StepResponse to workflow consumers
|
|
14
|
+
* @returns the result of executing a user-defined function, or
|
|
15
|
+
* @returns the cached result from a previous run, or
|
|
16
|
+
* @throws any error caught when running the user-defined function,
|
|
17
|
+
* once retries have been exhausted, or if there are no retry semantics
|
|
18
|
+
*/
|
|
19
|
+
export const stepRunner = { run, delay, poll };
|
|
20
|
+
|
|
21
|
+
export type StepRunner = typeof stepRunner;
|
|
22
|
+
|
|
23
|
+
function run<T extends any>(
|
|
24
|
+
fn: () => T | Promise<T>
|
|
25
|
+
): AsyncGenerator<StepResponse, T, StepResult | StepError>;
|
|
26
|
+
|
|
27
|
+
function run<T extends any>(
|
|
28
|
+
key: string,
|
|
29
|
+
fn: () => T | Promise<T>
|
|
30
|
+
): AsyncGenerator<StepResponse, T, StepResult | StepError>;
|
|
31
|
+
|
|
32
|
+
async function* run<T extends any>(
|
|
33
|
+
arg1: string | (() => T | Promise<T>),
|
|
34
|
+
arg2?: () => T | Promise<T>
|
|
35
|
+
): AsyncGenerator<StepResponse, T, StepResult | StepError> {
|
|
36
|
+
let key: string | null = null;
|
|
37
|
+
let fn: () => T | Promise<T>;
|
|
38
|
+
|
|
39
|
+
if (typeof arg1 === "string") {
|
|
40
|
+
key = arg1;
|
|
41
|
+
fn = arg2!;
|
|
42
|
+
} else {
|
|
43
|
+
fn = arg1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
yield new StepKey(key);
|
|
47
|
+
|
|
48
|
+
const cached = yield new StepCacheCheck();
|
|
49
|
+
|
|
50
|
+
if (cached) {
|
|
51
|
+
yield cached;
|
|
52
|
+
if (cached instanceof StepError) {
|
|
53
|
+
// unreachable – consumer calls throw() on the generator first
|
|
54
|
+
throw cached.err;
|
|
55
|
+
}
|
|
56
|
+
return cached.result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const result = await fn();
|
|
61
|
+
yield new StepResult(result);
|
|
62
|
+
return result;
|
|
63
|
+
} catch (err: unknown) {
|
|
64
|
+
if (err instanceof RetryableError) {
|
|
65
|
+
yield new StepError(err, {
|
|
66
|
+
maxAttempts: err.maxAttempts,
|
|
67
|
+
retryInterval: err.retryInterval,
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
yield new StepError(err);
|
|
71
|
+
}
|
|
72
|
+
// unreachable – consumer calls throw() on the generator first
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function delay(retryInterval: number): any;
|
|
78
|
+
function delay(key: string, retryInterval: number): any;
|
|
79
|
+
|
|
80
|
+
async function* delay(
|
|
81
|
+
arg1: string | number,
|
|
82
|
+
arg2?: number
|
|
83
|
+
): AsyncGenerator<any, void, StepDelay> {
|
|
84
|
+
let key: string | null = null;
|
|
85
|
+
let retryInterval: number;
|
|
86
|
+
|
|
87
|
+
if (typeof arg1 === "string") {
|
|
88
|
+
key = arg1;
|
|
89
|
+
retryInterval = arg2!;
|
|
90
|
+
} else {
|
|
91
|
+
retryInterval = arg1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
yield new StepKey(key);
|
|
95
|
+
|
|
96
|
+
const cached = yield new StepCacheCheck();
|
|
97
|
+
|
|
98
|
+
if (cached) {
|
|
99
|
+
yield new StepDelay(cached.resumeIn);
|
|
100
|
+
} else {
|
|
101
|
+
yield new StepDelay(Date.now() + retryInterval);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type PollOpts = { maxAttempts: number; retryInterval: number };
|
|
106
|
+
type PollPredicate = () => boolean | Promise<boolean>;
|
|
107
|
+
|
|
108
|
+
function poll(opts: PollOpts, predicate: PollPredicate): any;
|
|
109
|
+
function poll(key: string, opts: PollOpts, predicate: PollPredicate): any;
|
|
110
|
+
|
|
111
|
+
async function* poll(
|
|
112
|
+
arg1: string | PollOpts,
|
|
113
|
+
arg2: PollOpts | PollPredicate,
|
|
114
|
+
arg3?: PollPredicate
|
|
115
|
+
) {
|
|
116
|
+
let key: string | null = null;
|
|
117
|
+
let opts: PollOpts;
|
|
118
|
+
let predicate: PollPredicate;
|
|
119
|
+
|
|
120
|
+
if (typeof arg1 === "string") {
|
|
121
|
+
key = arg1;
|
|
122
|
+
opts = arg2 as PollOpts;
|
|
123
|
+
predicate = arg3 as PollPredicate;
|
|
124
|
+
} else {
|
|
125
|
+
opts = arg1;
|
|
126
|
+
predicate = arg2 as PollPredicate;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const task = async () => {
|
|
130
|
+
if (!(await predicate())) {
|
|
131
|
+
throw new RetryableError("Polling reached max retries", {
|
|
132
|
+
maxAttempts: opts.maxAttempts,
|
|
133
|
+
retryInterval: opts.retryInterval,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (key) {
|
|
139
|
+
yield* run(key, task);
|
|
140
|
+
} else {
|
|
141
|
+
yield* run(task);
|
|
142
|
+
}
|
|
143
|
+
}
|
package/tsconfig.json
CHANGED
|
File without changes
|
package/README.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# yieldstar
|
|
2
|
-
|
|
3
|
-
To install dependencies:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
bun install
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
To run:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
bun run index.ts
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
This project was created using `bun init` in bun v1.1.20. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
package/index.ts
DELETED