yieldstar 0.4.4 → 0.4.6
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/README.md +408 -0
- package/dist/exports/sdk-http.d.ts.map +1 -1
- package/dist/index.js +26 -10
- package/dist/internal/step-runner.d.ts.map +1 -1
- package/dist/internal/utils.d.ts +1 -0
- package/dist/internal/utils.d.ts.map +1 -1
- package/package.json +7 -5
- package/src/exports/workflow.ts +7 -7
- package/src/internal/step-runner.ts +12 -9
- package/src/internal/utils.ts +20 -0
- package/tsconfig.tsbuildinfo +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# YieldStar 🤘
|
|
2
|
+
|
|
3
|
+
JavaScript‑native distributed workflows.
|
|
4
|
+
|
|
5
|
+
Write workflows as async generator functions that yield steps. Use first‑class primitives for retries, delays, and polling. .
|
|
6
|
+
|
|
7
|
+
Currently supports running on Bun and Sqlite. Postgres support will be added next.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Install the core package:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bun add yieldstar
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Depending on how you run workflows, also install:
|
|
18
|
+
|
|
19
|
+
- Local (recommended): `@yieldstar/bun-worker-invoker` and `@yieldstar/bun-sqlite-runtime`
|
|
20
|
+
- HTTP server (Bun): `@yieldstar/bun-http-server` and `@yieldstar/bun-worker-invoker`
|
|
21
|
+
|
|
22
|
+
Note: `@yieldstar/test-utils` is an internal testing helper used by this repository. For local application use, prefer the SQLite runtime.
|
|
23
|
+
|
|
24
|
+
## Quick Start: Local (SQLite)
|
|
25
|
+
|
|
26
|
+
Run a workflow locally with persistence and timers using a Bun worker and the SQLite runtime.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// router.ts
|
|
30
|
+
import { workflow, createWorkflowRouter } from "yieldstar";
|
|
31
|
+
|
|
32
|
+
export const simple = workflow(async function* (step, event, logger) {
|
|
33
|
+
const n = yield* step.run(() => 1);
|
|
34
|
+
yield* step.delay(1000);
|
|
35
|
+
return yield* step.run(() => n * 2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const router = createWorkflowRouter({ "simple-workflow": simple });
|
|
39
|
+
export type Router = typeof router;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// worker.ts
|
|
44
|
+
import pino from "pino";
|
|
45
|
+
import { WorkflowRunner } from "@yieldstar/core";
|
|
46
|
+
import { createWorkflowWorker } from "@yieldstar/bun-worker-invoker";
|
|
47
|
+
import { SqliteHeapClient, SqliteTimersClient, SqliteTaskQueueClient, SqliteSchedulerClient, createSqliteDb } from "@yieldstar/bun-sqlite-runtime";
|
|
48
|
+
import { router } from "./router";
|
|
49
|
+
|
|
50
|
+
const logger = pino();
|
|
51
|
+
const db = createSqliteDb({ path: "./.db/local.sqlite" });
|
|
52
|
+
|
|
53
|
+
const runner = new WorkflowRunner({
|
|
54
|
+
router,
|
|
55
|
+
heapClient: new SqliteHeapClient(db),
|
|
56
|
+
schedulerClient: new SqliteSchedulerClient({
|
|
57
|
+
taskQueueClient: new SqliteTaskQueueClient(db),
|
|
58
|
+
timersClient: new SqliteTimersClient(db),
|
|
59
|
+
}),
|
|
60
|
+
logger,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
createWorkflowWorker(runner, logger).listen();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// app.ts
|
|
68
|
+
import pino from "pino";
|
|
69
|
+
import { createWorkflowInvoker } from "@yieldstar/bun-worker-invoker";
|
|
70
|
+
import { createLocalSdk } from "yieldstar";
|
|
71
|
+
import { SqliteEventLoop, createSqliteDb } from "@yieldstar/bun-sqlite-runtime";
|
|
72
|
+
|
|
73
|
+
const logger = pino();
|
|
74
|
+
const workerPath = new URL("./worker.ts", import.meta.url).href;
|
|
75
|
+
const invoker = createWorkflowInvoker({ workerPath, logger });
|
|
76
|
+
|
|
77
|
+
const db = createSqliteDb({ path: "./.db/local.sqlite" });
|
|
78
|
+
const loop = new SqliteEventLoop(db);
|
|
79
|
+
loop.start({ onNewEvent: invoker.execute, logger });
|
|
80
|
+
|
|
81
|
+
const sdk = createLocalSdk<import("./router").Router>(invoker);
|
|
82
|
+
const result = await sdk.triggerAndWait({ workflowId: "simple-workflow" });
|
|
83
|
+
console.log(result); // 2
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Pick Your Runtime
|
|
87
|
+
|
|
88
|
+
### Option A: Local Worker + SQLite (single process)
|
|
89
|
+
|
|
90
|
+
Use a Bun worker to execute steps and a SQLite‑backed runtime for persistence, timers, and a task queue.
|
|
91
|
+
|
|
92
|
+
1) Define workflows and a router:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { workflow, createWorkflowRouter } from "yieldstar";
|
|
96
|
+
|
|
97
|
+
export const dynamic = workflow<{ msg: string }, void>(async function* (step, event, logger) {
|
|
98
|
+
const n = yield* step.run(() => Math.random());
|
|
99
|
+
yield* step.delay(1000);
|
|
100
|
+
yield* step.run(() => logger.info(`Hello ${event.params.msg}: ${n}`));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const router = createWorkflowRouter({ "dynamic-workflow": dynamic });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
2) Create worker and runtime wiring (worker thread):
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// worker.ts
|
|
110
|
+
import pino from "pino";
|
|
111
|
+
import { WorkflowRunner } from "@yieldstar/core";
|
|
112
|
+
import { createWorkflowWorker } from "@yieldstar/bun-worker-invoker";
|
|
113
|
+
import { SqliteHeapClient, SqliteTimersClient, SqliteTaskQueueClient, SqliteSchedulerClient } from "@yieldstar/bun-sqlite-runtime";
|
|
114
|
+
import { router } from "./router";
|
|
115
|
+
import { createSqliteDb } from "@yieldstar/bun-sqlite-runtime";
|
|
116
|
+
|
|
117
|
+
const logger = pino();
|
|
118
|
+
const db = createSqliteDb({ path: "./.db/local.sqlite" });
|
|
119
|
+
|
|
120
|
+
const runner = new WorkflowRunner({
|
|
121
|
+
router,
|
|
122
|
+
heapClient: new SqliteHeapClient(db),
|
|
123
|
+
schedulerClient: new SqliteSchedulerClient({
|
|
124
|
+
taskQueueClient: new SqliteTaskQueueClient(db),
|
|
125
|
+
timersClient: new SqliteTimersClient(db),
|
|
126
|
+
}),
|
|
127
|
+
logger,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
createWorkflowWorker(runner, logger).listen();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
3) Drive the worker from your app and call workflows locally:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// app.ts
|
|
137
|
+
import pino from "pino";
|
|
138
|
+
import { createWorkflowInvoker } from "@yieldstar/bun-worker-invoker";
|
|
139
|
+
import { createLocalSdk } from "yieldstar";
|
|
140
|
+
import { SqliteEventLoop, createSqliteDb } from "@yieldstar/bun-sqlite-runtime";
|
|
141
|
+
|
|
142
|
+
const logger = pino();
|
|
143
|
+
const workerPath = new URL("./worker.ts", import.meta.url).href;
|
|
144
|
+
const invoker = createWorkflowInvoker({ workerPath, logger });
|
|
145
|
+
|
|
146
|
+
const db = createSqliteDb({ path: "./.db/local.sqlite" });
|
|
147
|
+
const eventLoop = new SqliteEventLoop(db);
|
|
148
|
+
eventLoop.start({ onNewEvent: invoker.execute, logger });
|
|
149
|
+
|
|
150
|
+
const sdk = createLocalSdk<{ "dynamic-workflow": any }>(invoker);
|
|
151
|
+
const res = await sdk.triggerAndWait({ workflowId: "dynamic-workflow", params: { msg: "hello" } });
|
|
152
|
+
console.log(res);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
See a working reference in `examples/local-execution/`.
|
|
156
|
+
|
|
157
|
+
### Option B: HTTP Server (Bun)
|
|
158
|
+
|
|
159
|
+
Expose routes to trigger and await workflow results. Use the HTTP SDK as a client.
|
|
160
|
+
|
|
161
|
+
Server:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import pino from "pino";
|
|
165
|
+
import { createRoutes, createMiddleware } from "@yieldstar/bun-http-server";
|
|
166
|
+
import { createWorkflowInvoker } from "@yieldstar/bun-worker-invoker";
|
|
167
|
+
import { SqliteEventLoop, createSqliteDb } from "@yieldstar/bun-sqlite-runtime";
|
|
168
|
+
import { router } from "./router";
|
|
169
|
+
|
|
170
|
+
const logger = pino();
|
|
171
|
+
const workerPath = new URL("./worker.ts", import.meta.url).href;
|
|
172
|
+
const invoker = createWorkflowInvoker({ workerPath, logger });
|
|
173
|
+
|
|
174
|
+
const db = createSqliteDb({ path: "./.db/http.sqlite" });
|
|
175
|
+
new SqliteEventLoop(db).start({ onNewEvent: invoker.execute, logger });
|
|
176
|
+
|
|
177
|
+
const auth = createMiddleware(async (req, event, next) => {
|
|
178
|
+
if (!req.headers.get("Authorization")) return new Response("Unauthorized", { status: 401 });
|
|
179
|
+
return next();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
Bun.serve({
|
|
183
|
+
port: 8080,
|
|
184
|
+
routes: { "/status": new Response("OK"), ...createRoutes({ invoker, logger, middleware: [auth] }) },
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Client:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { createHttpSdkFactory } from "yieldstar";
|
|
192
|
+
const createSdk = createHttpSdkFactory<{ "dynamic-workflow": any }>();
|
|
193
|
+
const sdk = createSdk({ url: "http://localhost:8080" });
|
|
194
|
+
|
|
195
|
+
const exec = await sdk.trigger({ workflowId: "dynamic-workflow", params: { msg: "world" } });
|
|
196
|
+
await exec.ack();
|
|
197
|
+
const result = await exec.waitForResult();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
See `examples/http-server/` for a complete setup.
|
|
201
|
+
|
|
202
|
+
## Patterns You’ll Use
|
|
203
|
+
|
|
204
|
+
### Parameters
|
|
205
|
+
|
|
206
|
+
Pass data into a run and access it inside the workflow via `event.params`.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const result = await sdk.triggerAndWait({ workflowId: "w", params: { userId: "123" } });
|
|
210
|
+
|
|
211
|
+
export const w = workflow(async function* (step, event, logger) {
|
|
212
|
+
logger.info("user:", event.params?.userId);
|
|
213
|
+
return yield* step.run(() => event.params);
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
See: `test/params.test.ts`.
|
|
218
|
+
|
|
219
|
+
### Retries
|
|
220
|
+
|
|
221
|
+
Throw `RetryableError` inside `step.run` to retry the step with the configured policy.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { RetryableError } from "yieldstar";
|
|
225
|
+
|
|
226
|
+
yield* step.run(async () => {
|
|
227
|
+
// e.g. flaky fetch
|
|
228
|
+
throw new RetryableError("temporary", { maxAttempts: 4, retryInterval: 1000 });
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
See: `test/retries.test.ts`.
|
|
233
|
+
|
|
234
|
+
### Delays and Resumption
|
|
235
|
+
|
|
236
|
+
Pause until a time in the future and automatically resume.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
yield* step.delay(5000);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
See: `test/async.test.ts`.
|
|
243
|
+
|
|
244
|
+
### Polling
|
|
245
|
+
|
|
246
|
+
Keep checking a predicate until it passes or times out using structured retries.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
yield* step.poll({ retryInterval: 1000, maxAttempts: 10 }, () => isReady());
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
See: `test/polling.test.ts`.
|
|
253
|
+
|
|
254
|
+
### Cache Keys and Loops
|
|
255
|
+
|
|
256
|
+
In loops, add explicit cache keys so each iteration is distinct.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
for (let i = 0; i < 2; i++) {
|
|
260
|
+
yield* step.run(`step:${i}`, () => i);
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
If omitted, loop detection will raise: “Each step in a loop must have a unique cache key.” See: `test/loop-detection.test.ts`, `test/cache-keys.test.ts`.
|
|
265
|
+
|
|
266
|
+
### Custom Step‑Like Helpers
|
|
267
|
+
|
|
268
|
+
Compose your own helpers by yielding built‑in steps under the hood.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const waitForState = (step: any, logger: any) => async function* (state: string) {
|
|
272
|
+
yield* step.poll({ maxAttempts: 10, retryInterval: 1000 }, () => /* check */ true);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const coordinator = workflow(async function* (step) {
|
|
276
|
+
const a = yield* step.run(() => 2);
|
|
277
|
+
yield* (waitForState(step, console) as any)("enabled");
|
|
278
|
+
return yield* step.run(() => a * 3);
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
See: `examples/workflows/coordinator.ts`.
|
|
283
|
+
|
|
284
|
+
## API Reference
|
|
285
|
+
|
|
286
|
+
Top‑level exports from `yieldstar`:
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
export { createLocalSdk } from "yieldstar";
|
|
290
|
+
export { createHttpSdkFactory } from "yieldstar";
|
|
291
|
+
export { createWorkflowRouter } from "yieldstar";
|
|
292
|
+
export { RetryableError } from "yieldstar";
|
|
293
|
+
export { createWorkflow, workflow } from "yieldstar";
|
|
294
|
+
export type { WorkflowFn } from "yieldstar";
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### `workflow(fn)` / `createWorkflow(fn)`
|
|
298
|
+
|
|
299
|
+
Define a workflow. The function receives `(step, event, logger)` and must be an async generator that yields steps.
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
type WorkflowFn<Params, Result, Context> = (
|
|
303
|
+
step: StepRunner,
|
|
304
|
+
event: WorkflowEvent<Params, Context>,
|
|
305
|
+
logger: Logger
|
|
306
|
+
) => AsyncGenerator<any, Result>;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Return type is inferred; type parameters allow strong typing of `params` and `result`.
|
|
310
|
+
|
|
311
|
+
### Step Runner
|
|
312
|
+
|
|
313
|
+
Available on the first argument of a workflow:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
// run
|
|
317
|
+
yield* step.run(fn);
|
|
318
|
+
yield* step.run("key", fn);
|
|
319
|
+
|
|
320
|
+
// delay
|
|
321
|
+
yield* step.delay(ms);
|
|
322
|
+
yield* step.delay("key", ms);
|
|
323
|
+
|
|
324
|
+
// poll
|
|
325
|
+
yield* step.poll({ retryInterval, maxAttempts }, predicate);
|
|
326
|
+
yield* step.poll("key", { retryInterval, maxAttempts }, predicate);
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`RetryableError` inside `fn` signals structured retries:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
new RetryableError(message, { maxAttempts: number, retryInterval: number });
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Local SDK
|
|
336
|
+
|
|
337
|
+
Create a client that triggers workflows using an in‑process `WorkflowInvoker`.
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
const sdk = createLocalSdk<Router>(invoker);
|
|
341
|
+
await sdk.triggerAndWait({ workflowId, params? });
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### HTTP SDK
|
|
345
|
+
|
|
346
|
+
Factory returning a client bound to a base URL.
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
const createSdk = createHttpSdkFactory<Router>();
|
|
350
|
+
const sdk = createSdk({ url, fetchOptions? });
|
|
351
|
+
|
|
352
|
+
const exec = await sdk.trigger({ workflowId, params?, context?, executionId? });
|
|
353
|
+
await exec.ack();
|
|
354
|
+
const result = await exec.waitForResult();
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Router
|
|
358
|
+
|
|
359
|
+
Register your workflows with IDs:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
const router = createWorkflowRouter({ "id": workflow, ... });
|
|
363
|
+
type Router = typeof router;
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Use `Router` to type your SDKs so `workflowId` and `params` are enforced.
|
|
367
|
+
|
|
368
|
+
## Testing Your Workflows
|
|
369
|
+
|
|
370
|
+
For repository development we use an in‑memory runtime driven by `@yieldstar/test-utils` to `triggerAndWait` in tests. This package is internal; application users should test against the same runtime they use in production where possible. Example (internal):
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { test, expect } from "bun:test";
|
|
374
|
+
import { createTestSdkFactory } from "@yieldstar/test-utils";
|
|
375
|
+
import { createWorkflow } from "yieldstar";
|
|
376
|
+
|
|
377
|
+
test("data flow between steps", async () => {
|
|
378
|
+
const w = createWorkflow(async function* (step) {
|
|
379
|
+
let n = yield* step.run(() => 1);
|
|
380
|
+
n = yield* step.run(() => n * 2);
|
|
381
|
+
return n;
|
|
382
|
+
});
|
|
383
|
+
const createSdk = createTestSdkFactory();
|
|
384
|
+
const sdk = createSdk({ workflow: w });
|
|
385
|
+
expect(await sdk.triggerAndWait({ workflowId: "workflow" })).toBe(2);
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
See more examples in `test/*.test.ts`.
|
|
390
|
+
|
|
391
|
+
## Notes
|
|
392
|
+
|
|
393
|
+
- In loops, always add cache keys. Missing keys trigger loop detection.
|
|
394
|
+
- `logger` is a Pino logger passed into workflows for observability.
|
|
395
|
+
- HTTP middleware can read/set `event.context` before invoking your workflow.
|
|
396
|
+
|
|
397
|
+
## Contributing
|
|
398
|
+
|
|
399
|
+
- Please open an issue to discuss significant changes before submitting a PR.
|
|
400
|
+
- See CONTRIBUTING.md for repo layout, dev workflow, testing, and release details.
|
|
401
|
+
|
|
402
|
+
## License
|
|
403
|
+
|
|
404
|
+
MIT
|
|
405
|
+
|
|
406
|
+
## Author
|
|
407
|
+
|
|
408
|
+
[Daniel Grant](danielgrant.co)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk-http.d.ts","sourceRoot":"","sources":["../../src/exports/sdk-http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,YAAY,EAEZ,2BAA2B,EAC5B,MAAM,iBAAiB,CAAC;AAKzB,KAAK,UAAU,GAAG;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,eAAe,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,IAAI;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/B,aAAa,EAAE,MAAM,OAAO,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjE,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,cAAc,
|
|
1
|
+
{"version":3,"file":"sdk-http.d.ts","sourceRoot":"","sources":["../../src/exports/sdk-http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,cAAc,EACd,YAAY,EAEZ,2BAA2B,EAC5B,MAAM,iBAAiB,CAAC;AAKzB,KAAK,UAAU,GAAG;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,KAAK,eAAe,CAAC,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,IAAI;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/B,aAAa,EAAE,MAAM,OAAO,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACjE,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,cAAc,MACnD,QAAQ;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;CACrD;YAEiB,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,SAC/B,YAAY,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAC1C,OAAO,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;kBA2Bb,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,OACvC,UAAU,GACd,OAAO,CAAC,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;EAoBlD"}
|
package/dist/index.js
CHANGED
|
@@ -84,9 +84,22 @@ class RetryableError extends Error {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
// src/internal/utils.ts
|
|
87
|
+
import { createHash } from "crypto";
|
|
87
88
|
var isIterable = (i) => {
|
|
88
89
|
return typeof i === "object" && i != null && Symbol.iterator in i;
|
|
89
90
|
};
|
|
91
|
+
function getCallSiteHash(stepFn) {
|
|
92
|
+
const originalStack = stepFn.stack;
|
|
93
|
+
Error.captureStackTrace(stepFn, getCallSiteHash);
|
|
94
|
+
const stack = stepFn.stack;
|
|
95
|
+
const stepFnCallSite = stack.split(")")[1];
|
|
96
|
+
if (originalStack === undefined) {
|
|
97
|
+
delete stepFn.stack;
|
|
98
|
+
} else {
|
|
99
|
+
stepFn.stack = originalStack;
|
|
100
|
+
}
|
|
101
|
+
return createHash("sha1").update(stepFnCallSite).digest("hex");
|
|
102
|
+
}
|
|
90
103
|
|
|
91
104
|
// src/internal/serialise.ts
|
|
92
105
|
import {
|
|
@@ -146,6 +159,8 @@ async function* run(arg1, arg2) {
|
|
|
146
159
|
} else {
|
|
147
160
|
fn = arg1;
|
|
148
161
|
}
|
|
162
|
+
if (!key)
|
|
163
|
+
key = getCallSiteHash(run);
|
|
149
164
|
yield new StepKey(key);
|
|
150
165
|
const cached = yield new StepCacheCheck;
|
|
151
166
|
if (cached) {
|
|
@@ -180,6 +195,8 @@ async function* delay(arg1, arg2) {
|
|
|
180
195
|
} else {
|
|
181
196
|
retryInterval = arg1;
|
|
182
197
|
}
|
|
198
|
+
if (!key)
|
|
199
|
+
key = getCallSiteHash(delay);
|
|
183
200
|
yield new StepKey(key);
|
|
184
201
|
const cached = yield new StepCacheCheck;
|
|
185
202
|
if (cached) {
|
|
@@ -200,6 +217,8 @@ async function* poll(arg1, arg2, arg3) {
|
|
|
200
217
|
opts = arg1;
|
|
201
218
|
predicate = arg2;
|
|
202
219
|
}
|
|
220
|
+
if (!key)
|
|
221
|
+
key = getCallSiteHash(poll);
|
|
203
222
|
const task = async () => {
|
|
204
223
|
if (!await predicate()) {
|
|
205
224
|
throw new RetryableError("Polling reached max retries", {
|
|
@@ -208,11 +227,7 @@ async function* poll(arg1, arg2, arg3) {
|
|
|
208
227
|
});
|
|
209
228
|
}
|
|
210
229
|
};
|
|
211
|
-
|
|
212
|
-
yield* run(key, task);
|
|
213
|
-
} else {
|
|
214
|
-
yield* run(task);
|
|
215
|
-
}
|
|
230
|
+
yield* run(key, task);
|
|
216
231
|
}
|
|
217
232
|
|
|
218
233
|
// src/exports/workflow.ts
|
|
@@ -230,9 +245,9 @@ function workflow(workflowFn) {
|
|
|
230
245
|
return async function* workflowGenerator({ event, heapClient, logger }) {
|
|
231
246
|
const workflowIterator = workflowFn(stepRunner, event, logger);
|
|
232
247
|
const { executionId } = event;
|
|
233
|
-
let keylessStepIndex = -1;
|
|
234
248
|
let iteratorResult = null;
|
|
235
249
|
let nextIteratorResult = null;
|
|
250
|
+
const stepKeys = new Set;
|
|
236
251
|
while (true) {
|
|
237
252
|
let stepAttempt = 0;
|
|
238
253
|
let stepKey;
|
|
@@ -246,11 +261,12 @@ function workflow(workflowFn) {
|
|
|
246
261
|
stepKey = "$$workflow-result$$";
|
|
247
262
|
} else if (!(iteratorResult.value instanceof StepKey2)) {
|
|
248
263
|
throw new Error("Step runners must yield a StepKey object");
|
|
249
|
-
} else if (iteratorResult.value.key) {
|
|
250
|
-
stepKey = iteratorResult.value.key;
|
|
251
264
|
} else {
|
|
252
|
-
|
|
253
|
-
stepKey
|
|
265
|
+
stepKey = iteratorResult.value.key;
|
|
266
|
+
if (stepKeys.has(stepKey)) {
|
|
267
|
+
throw new Error("Each step in a loop must have a unique cache key.");
|
|
268
|
+
}
|
|
269
|
+
stepKeys.add(stepKey);
|
|
254
270
|
}
|
|
255
271
|
let cached = await heapClient.readStep({
|
|
256
272
|
executionId,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"step-runner.d.ts","sourceRoot":"","sources":["../../src/internal/step-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAEZ,SAAS,EACT,UAAU,EAGX,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"step-runner.d.ts","sourceRoot":"","sources":["../../src/internal/step-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAEZ,SAAS,EACT,UAAU,EAGX,MAAM,iBAAiB,CAAC;AAIzB;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU;;;;CAAuB,CAAC;AAE/C,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC;AAE3C,iBAAS,GAAG,CAAC,CAAC,SAAS,GAAG,EACxB,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvB,cAAc,CAAC,YAAY,EAAE,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC,CAAC;AAE3D,iBAAS,GAAG,CAAC,CAAC,SAAS,GAAG,EACxB,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACvB,cAAc,CAAC,YAAY,EAAE,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC,CAAC;AAiD3D,iBAAS,KAAK,CAAC,aAAa,EAAE,MAAM,GAAG,GAAG,CAAC;AAC3C,iBAAS,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,GAAG,CAAC;AA6BxD,KAAK,QAAQ,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC;AAC/D,KAAK,aAAa,GAAG,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtD,iBAAS,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,GAAG,GAAG,CAAC;AAC7D,iBAAS,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,aAAa,GAAG,GAAG,CAAC"}
|
package/dist/internal/utils.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/internal/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/internal/utils.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAG,CAAC,IAAI,QAAQ,CAAC,CAAC,CAIjE,CAAC;AAGF,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,UAe/C"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yieldstar",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Daniel Grant",
|
|
4
6
|
"type": "module",
|
|
5
7
|
"main": "dist/index.js",
|
|
6
8
|
"types": "dist/index.d.ts",
|
|
7
9
|
"dependencies": {
|
|
8
|
-
"nanoid": "^5.1.
|
|
9
|
-
"pino": "^9.
|
|
10
|
+
"nanoid": "^5.1.5",
|
|
11
|
+
"pino": "^9.9.0",
|
|
10
12
|
"serialize-error": "^11.0.3",
|
|
11
|
-
"@yieldstar/core": "0.4.
|
|
13
|
+
"@yieldstar/core": "0.4.6"
|
|
12
14
|
},
|
|
13
15
|
"devDependencies": {
|
|
14
|
-
"@types/node": "^22.
|
|
16
|
+
"@types/node": "^22.18.0"
|
|
15
17
|
},
|
|
16
18
|
"scripts": {
|
|
17
19
|
"build": "bun build src/index.ts --outfile dist/index.js --target bun --packages=external"
|
package/src/exports/workflow.ts
CHANGED
|
@@ -49,9 +49,9 @@ export function workflow<
|
|
|
49
49
|
const workflowIterator = workflowFn(stepRunner, event, logger);
|
|
50
50
|
const { executionId } = event;
|
|
51
51
|
|
|
52
|
-
let keylessStepIndex = -1;
|
|
53
52
|
let iteratorResult: IteratorResult<any> | null = null;
|
|
54
53
|
let nextIteratorResult: IteratorResult<any> | null = null;
|
|
54
|
+
const stepKeys = new Set<string>();
|
|
55
55
|
|
|
56
56
|
while (true) {
|
|
57
57
|
let stepAttempt = 0;
|
|
@@ -71,20 +71,20 @@ export function workflow<
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Get the StepKey from the step runner.
|
|
74
|
-
* If the StepKey value is null, assume deterministic ordering, and use
|
|
75
|
-
* the step index as a key.
|
|
76
74
|
* If the generator is done, the workflow has returned, so we set a
|
|
77
75
|
* special key for that.
|
|
76
|
+
* Finally, check that this call site wasn't reached before.
|
|
78
77
|
*/
|
|
79
78
|
if (iteratorResult.done) {
|
|
80
79
|
stepKey = "$$workflow-result$$";
|
|
81
80
|
} else if (!(iteratorResult.value instanceof StepKey)) {
|
|
82
81
|
throw new Error("Step runners must yield a StepKey object");
|
|
83
|
-
} else if (iteratorResult.value.key) {
|
|
84
|
-
stepKey = iteratorResult.value.key;
|
|
85
82
|
} else {
|
|
86
|
-
|
|
87
|
-
stepKey
|
|
83
|
+
stepKey = iteratorResult.value.key;
|
|
84
|
+
if (stepKeys.has(stepKey)) {
|
|
85
|
+
throw new Error("Each step in a loop must have a unique cache key.");
|
|
86
|
+
}
|
|
87
|
+
stepKeys.add(stepKey)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
StepCacheCheck,
|
|
8
8
|
} from "@yieldstar/core";
|
|
9
9
|
import { RetryableError } from "../exports/errors";
|
|
10
|
+
import { getCallSiteHash } from "./utils";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @description A library of step generators, each of which:
|
|
@@ -21,28 +22,30 @@ export const stepRunner = { run, delay, poll };
|
|
|
21
22
|
export type StepRunner = typeof stepRunner;
|
|
22
23
|
|
|
23
24
|
function run<T extends any>(
|
|
24
|
-
fn: () => T | Promise<T
|
|
25
|
+
fn: () => T | Promise<T>,
|
|
25
26
|
): AsyncGenerator<StepResponse, T, StepResult | StepError>;
|
|
26
27
|
|
|
27
28
|
function run<T extends any>(
|
|
28
29
|
key: string,
|
|
29
|
-
fn: () => T | Promise<T
|
|
30
|
+
fn: () => T | Promise<T>,
|
|
30
31
|
): AsyncGenerator<StepResponse, T, StepResult | StepError>;
|
|
31
32
|
|
|
32
33
|
async function* run<T extends any>(
|
|
33
34
|
arg1: string | (() => T | Promise<T>),
|
|
34
|
-
arg2?: () => T | Promise<T>
|
|
35
|
+
arg2?: (() => T | Promise<T>) | string,
|
|
35
36
|
): AsyncGenerator<StepResponse, T, StepResult | StepError> {
|
|
36
37
|
let key: string | null = null;
|
|
37
38
|
let fn: () => T | Promise<T>;
|
|
38
39
|
|
|
39
40
|
if (typeof arg1 === "string") {
|
|
40
41
|
key = arg1;
|
|
41
|
-
fn = arg2
|
|
42
|
+
fn = arg2 as () => T | Promise<T>;
|
|
42
43
|
} else {
|
|
43
44
|
fn = arg1;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
if (!key) key = getCallSiteHash(run);
|
|
48
|
+
|
|
46
49
|
yield new StepKey(key);
|
|
47
50
|
|
|
48
51
|
const cached = yield new StepCacheCheck();
|
|
@@ -91,6 +94,8 @@ async function* delay(
|
|
|
91
94
|
retryInterval = arg1;
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
if (!key) key = getCallSiteHash(delay);
|
|
98
|
+
|
|
94
99
|
yield new StepKey(key);
|
|
95
100
|
|
|
96
101
|
const cached = yield new StepCacheCheck();
|
|
@@ -126,6 +131,8 @@ async function* poll(
|
|
|
126
131
|
predicate = arg2 as PollPredicate;
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
if (!key) key = getCallSiteHash(poll);
|
|
135
|
+
|
|
129
136
|
const task = async () => {
|
|
130
137
|
if (!(await predicate())) {
|
|
131
138
|
throw new RetryableError("Polling reached max retries", {
|
|
@@ -135,9 +142,5 @@ async function* poll(
|
|
|
135
142
|
}
|
|
136
143
|
};
|
|
137
144
|
|
|
138
|
-
|
|
139
|
-
yield* run(key, task);
|
|
140
|
-
} else {
|
|
141
|
-
yield* run(task);
|
|
142
|
-
}
|
|
145
|
+
yield* run(key, task, );
|
|
143
146
|
}
|
package/src/internal/utils.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
|
|
1
3
|
export const isIterable = <T>(i: T | Iterable<T>): i is Iterable<T> => {
|
|
2
4
|
return (
|
|
3
5
|
typeof i === "object" && i != null && Symbol.iterator in (i as Iterable<T>)
|
|
4
6
|
);
|
|
5
7
|
};
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export function getCallSiteHash(stepFn: Function) {
|
|
11
|
+
const originalStack = (stepFn as any).stack;
|
|
12
|
+
|
|
13
|
+
Error.captureStackTrace(stepFn, getCallSiteHash);
|
|
14
|
+
|
|
15
|
+
const stack = (stepFn as any).stack
|
|
16
|
+
const stepFnCallSite = stack.split(')')[1]
|
|
17
|
+
|
|
18
|
+
if (originalStack === undefined) {
|
|
19
|
+
delete (stepFn as any).stack;
|
|
20
|
+
} else {
|
|
21
|
+
(stepFn as any).stack = originalStack;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return createHash("sha1").update(stepFnCallSite).digest("hex");
|
|
25
|
+
}
|