yieldstar 0.0.0 → 0.1.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 ADDED
@@ -0,0 +1,327 @@
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
+ var stepRunner = { run, delay, poll };
136
+ async function* run(arg1, arg2) {
137
+ let key = null;
138
+ let fn;
139
+ if (typeof arg1 === "string") {
140
+ key = arg1;
141
+ fn = arg2;
142
+ } else {
143
+ fn = arg1;
144
+ }
145
+ yield new StepKey(key);
146
+ const cached = yield new StepCacheCheck;
147
+ if (cached) {
148
+ yield cached;
149
+ if (cached instanceof StepError2) {
150
+ throw cached.err;
151
+ }
152
+ return cached.result;
153
+ }
154
+ try {
155
+ const result = await fn();
156
+ yield new StepResult2(result);
157
+ return result;
158
+ } catch (err) {
159
+ if (err instanceof RetryableError) {
160
+ yield new StepError2(err, {
161
+ maxAttempts: err.maxAttempts,
162
+ retryInterval: err.retryInterval
163
+ });
164
+ } else {
165
+ yield new StepError2(err);
166
+ }
167
+ throw err;
168
+ }
169
+ }
170
+ async function* delay(arg1, arg2) {
171
+ let key = null;
172
+ let retryInterval;
173
+ if (typeof arg1 === "string") {
174
+ key = arg1;
175
+ retryInterval = arg2;
176
+ } else {
177
+ retryInterval = arg1;
178
+ }
179
+ yield new StepKey(key);
180
+ const cached = yield new StepCacheCheck;
181
+ if (cached) {
182
+ yield new StepDelay2(cached.resumeIn);
183
+ } else {
184
+ yield new StepDelay2(Date.now() + retryInterval);
185
+ }
186
+ }
187
+ async function* poll(arg1, arg2, arg3) {
188
+ let key = null;
189
+ let opts;
190
+ let predicate;
191
+ if (typeof arg1 === "string") {
192
+ key = arg1;
193
+ opts = arg2;
194
+ predicate = arg3;
195
+ } else {
196
+ opts = arg1;
197
+ predicate = arg2;
198
+ }
199
+ const task = async () => {
200
+ if (!await predicate()) {
201
+ throw new RetryableError("Polling reached max retries", {
202
+ maxAttempts: opts.maxAttempts,
203
+ retryInterval: opts.retryInterval
204
+ });
205
+ }
206
+ };
207
+ if (key) {
208
+ yield* run(key, task);
209
+ } else {
210
+ yield* run(task);
211
+ }
212
+ }
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*
279
+ `);
280
+ stepResponse = new StepInvalid;
281
+ } else if (!(stepResponse instanceof StepResponse3)) {
282
+ console.log(`[ERR] Iterators should yield a StepResponse. Got ${JSON.stringify(stepResponse)}`);
283
+ stepResponse = new StepInvalid;
284
+ }
285
+ const needsRetry = !cached?.meta.done && stepResponse instanceof StepError3 && stepResponse.maxAttempts > stepAttempt + 1;
286
+ if (!cached) {
287
+ await heapClient.writeStep({
288
+ executionId,
289
+ stepKey,
290
+ stepAttempt,
291
+ stepDone: !needsRetry,
292
+ stepResponseJson: serialize(stepResponse)
293
+ });
294
+ }
295
+ if (stepResponse instanceof WorkflowResult2) {
296
+ return stepResponse;
297
+ }
298
+ if (stepResponse instanceof StepInvalid) {
299
+ break;
300
+ }
301
+ if (stepResponse instanceof StepError3) {
302
+ if (needsRetry) {
303
+ yield new StepDelay3(Date.now() + stepResponse.retryInterval);
304
+ } else {
305
+ nextIteratorResult = await workflowIterator.throw(stepResponse.err);
306
+ continue;
307
+ }
308
+ }
309
+ if (stepResponse instanceof StepDelay3) {
310
+ if (stepResponse.resumeIn <= Date.now()) {
311
+ continue;
312
+ }
313
+ }
314
+ if (!(stepResponse instanceof StepResult3)) {
315
+ yield stepResponse;
316
+ }
317
+ }
318
+ throw new Error("Critical error");
319
+ };
320
+ }
321
+ export {
322
+ createWorkflowRouter,
323
+ createWorkflow,
324
+ createLocalSdk,
325
+ createHttpSdkFactory,
326
+ RetryableError
327
+ };
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",
5
4
  "type": "module",
6
- "devDependencies": {
7
- "@types/bun": "latest"
5
+ "main": "dist/index.js",
6
+ "types": "src/index.ts",
7
+ "scripts": {
8
+ "build": "bun build src/index.ts --outfile dist/index.js --target bun --packages=external"
9
+ },
10
+ "dependencies": {
11
+ "serialize-error": "^11.0.3"
8
12
  },
9
- "peerDependencies": {
10
- "typescript": "^5.0.0"
13
+ "devDependencies": {
14
+ "@types/bun": "latest",
15
+ "@yieldstar/core": "workspace:*"
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,7 @@
1
+ import type { WorkflowRouter } from "@yieldstar/core";
2
+
3
+ export function createWorkflowRouter<W extends WorkflowRouter>(
4
+ workflows: W
5
+ ): W {
6
+ return workflows;
7
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export const isIterable = <T>(i: T | Iterable<T>): i is Iterable<T> => {
2
+ return (
3
+ typeof i === "object" && i != null && Symbol.iterator in (i as Iterable<T>)
4
+ );
5
+ };
package/tsconfig.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  // Enable latest features
4
- "lib": ["ESNext", "DOM"],
4
+ "lib": ["ESNext"],
5
5
  "target": "ESNext",
6
6
  "module": "ESNext",
7
7
  "moduleDetection": "force",
8
- "jsx": "react-jsx",
9
8
  "allowJs": true,
10
9
 
11
10
  // Bundler mode
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
@@ -1,3 +0,0 @@
1
- import { version } from "./package.json";
2
-
3
- console.log(`yieldstar version ${version}`);