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.
Files changed (2) hide show
  1. package/README.md +408 -0
  2. 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.5",
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.5"
13
+ "@yieldstar/core": "0.4.6"
12
14
  },
13
15
  "devDependencies": {
14
16
  "@types/node": "^22.18.0"