yieldstar 0.4.5 → 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/package.json +4 -2
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)
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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",
|
|
@@ -8,7 +10,7 @@
|
|
|
8
10
|
"nanoid": "^5.1.5",
|
|
9
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
16
|
"@types/node": "^22.18.0"
|