@fkws/klonk 0.0.21 → 0.0.23

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 CHANGED
@@ -20,15 +20,14 @@
20
20
 
21
21
  ## Introduction
22
22
 
23
- Klonk is a code-first, type-safe automation engine designed with developer experience as a top priority. It provides powerful, composable primitives to build complex workflows and state machines with world-class autocomplete and type inference. If you've ever wanted to build event-driven automations or a stateful agent, but in code, with all the benefits of TypeScript, Klonk is for you.
23
+ Klonk is a code-first, type-safe automation engine. It provides composable primitives to build workflows and state machines with autocomplete and type inference. If you've ever wanted to build event-driven automations or a stateful agent in code, with all the benefits of TypeScript, Klonk is for you.
24
24
 
25
- ![Code](./.github/assets/blurry.png)
26
25
  ![Skip to code examples ->](https://github.com/klar-web-services/klonk?tab=readme-ov-file#code-examples)
27
26
 
28
27
  The two main features are **Workflows** and **Machines**.
29
28
 
30
- - **Workflows**: Combine triggers with a series of tasks (a `Playlist`) to automate processes. Perfect for event-driven automation, like "when a file is added to Dropbox, parse it, and create an entry in Notion."
31
- - **Machines**: Create finite state machines where each state has its own `Playlist` of tasks and conditional transitions to other states. Ideal for building agents, multi-step processes, or any system with complex, stateful logic.
29
+ - **Workflows**: Combine triggers with a series of tasks (a `Playlist`) to automate processes. Example: "when a file is added to Dropbox, parse it, and create an entry in Notion."
30
+ - **Machines**: Finite state machines where each state has its own `Playlist` of tasks and conditional transitions to other states. Useful for agents, multi-step processes, or systems with stateful logic.
32
31
 
33
32
  ## Installation
34
33
 
@@ -38,9 +37,98 @@ bun add @fkws/klonk
38
37
  npm i @fkws/klonk
39
38
  ```
40
39
 
40
+ ### Compatibility
41
+
42
+ | Requirement | Support |
43
+ |-------------|---------|
44
+ | **Runtimes** | Node.js 18+, Bun 1.0+, Deno (via npm specifier, best-effort) |
45
+ | **Module** | ESM (native) and CJS (via bundled `/dist`) |
46
+ | **TypeScript** | 5.0+ (required for full type inference) |
47
+ | **Dependencies** | Zero runtime dependencies |
48
+
49
+ **Status:** Pre-1.0, API may change between minor versions. Aiming for stability by 1.0.
50
+
51
+ ## Quickstart
52
+
53
+ Copy-paste this to see Klonk in action. One trigger, two tasks, fully typed outputs:
54
+
55
+ ```typescript
56
+ import { Task, Trigger, Workflow, Railroad, isOk } from "@fkws/klonk";
57
+
58
+ // 1. Define two simple tasks
59
+ class FetchUser<I extends string> extends Task<{ userId: string }, { name: string; email: string }, I> {
60
+ async validateInput(input: { userId: string }) { return !!input.userId; }
61
+ async run(input: { userId: string }): Promise<Railroad<{ name: string; email: string }>> {
62
+ if (input.userId !== "123") return { success: false, error: new Error("User not found") };
63
+ return { success: true, data: { name: "Alice", email: "alice@example.com" } };
64
+ }
65
+ }
66
+
67
+ class SendEmail<I extends string> extends Task<{ to: string; subject: string }, { sent: boolean }, I> {
68
+ async validateInput(input: { to: string; subject: string }) { return !!input.to; }
69
+ async run(input: { to: string; subject: string }): Promise<Railroad<{ sent: boolean }>> {
70
+ console.log(`📧 Sending "${input.subject}" to ${input.to}`);
71
+ return { success: true, data: { sent: true } };
72
+ }
73
+ }
74
+
75
+ // 2. Create a trigger (fires once with a userId)
76
+ class ManualTrigger<I extends string> extends Trigger<I, { userId: string }> {
77
+ async start() { this.pushEvent({ userId: "123" }); }
78
+ async stop() {}
79
+ }
80
+
81
+ // 3. Wire it up: trigger → playlist with typed outputs
82
+ const workflow = Workflow.create()
83
+ .addTrigger(new ManualTrigger("manual"))
84
+ .setPlaylist(p => p
85
+ .addTask(new FetchUser("fetch-user"))
86
+ .input((source) => ({ userId: source.data.userId })) // ← source.data is typed!
87
+
88
+ .addTask(new SendEmail("send-email"))
89
+ .input((source, outputs) => {
90
+ // outputs["fetch-user"] is typed as Railroad<{ name, email }> | null
91
+ const user = outputs["fetch-user"];
92
+ if (!user || !isOk(user)) return null; // skip if failed
93
+ return { to: user.data.email, subject: `Welcome, ${user.data.name}!` };
94
+ })
95
+ );
96
+
97
+ workflow.start({ callback: (src, out) => console.log("✅ Done!", out) });
98
+ ```
99
+
100
+ **What you just saw:**
101
+ - `source.data.userId` is typed from the trigger
102
+ - `outputs["fetch-user"]` is typed by the task's ident string literal
103
+ - `user.data.email` is narrowed after the `isOk()` check
104
+
105
+ ## TypeScript Magic Moment
106
+
107
+ Klonk's type inference isn't marketing. Here's proof:
108
+
109
+ ```typescript
110
+ import { Machine } from "@fkws/klonk";
111
+
112
+ // Declare states upfront → autocomplete for ALL transitions
113
+ const machine = Machine.create<{ count: number }>()
114
+ .withStates("idle", "processing", "done") // ← These drive autocomplete
115
+ .addState("idle", node => node
116
+ .setPlaylist(p => p/* ... */)
117
+ .addTransition({
118
+ to: "processing", // ← Type "pro" and your IDE suggests "processing"
119
+ condition: async () => true,
120
+ weight: 1
121
+ })
122
+ // @ts-expect-error - "typo-state" is not a valid state
123
+ .addTransition({ to: "typo-state", condition: async () => true, weight: 1 })
124
+ , { initial: true });
125
+ ```
126
+
127
+ The `withStates<...>()` pattern means **you can't transition to a state that doesn't exist**. TypeScript catches it at compile time, not runtime.
128
+
41
129
  ## Core Concepts
42
130
 
43
- At the heart of Klonk are a few key concepts that work together.
131
+ Klonk has a few concepts that work together.
44
132
 
45
133
  ### Task
46
134
 
@@ -48,20 +136,61 @@ A `Task` is the smallest unit of work. It's an abstract class with two main meth
48
136
  - `validateInput(input)`: Runtime validation of the task's input (on top of strong typing).
49
137
  - `run(input)`: Executes the task's logic.
50
138
 
51
- Tasks use a `Railroad` return type, which is a simple discriminated union for handling success and error states without throwing exceptions. Shoutout to Rust!
139
+ Tasks use a `Railroad` return type - a discriminated union for handling success and error states without throwing exceptions. Inspired by Rust's `Result<T, E>` type, it comes with familiar helper functions like `unwrap()`, `unwrapOr()`, and more.
140
+
141
+ ### Railroad (Rust-inspired Result Type)
142
+
143
+ `Railroad<T>` is Klonk's version of Rust's `Result<T, E>`. It's a discriminated union that forces you to handle both success and error cases:
144
+
145
+ ```typescript
146
+ type Railroad<T> =
147
+ | { success: true, data: T }
148
+ | { success: false, error: Error }
149
+ ```
150
+
151
+ #### Helper Functions
152
+
153
+ Klonk provides Rust-inspired helper functions for working with `Railroad`:
154
+
155
+ ```typescript
156
+ import { unwrap, unwrapOr, unwrapOrElse, isOk, isErr } from "@fkws/klonk";
157
+
158
+ // unwrap: Get data or throw error (like Rust's .unwrap())
159
+ const data = unwrap(result); // Returns T or throws
160
+
161
+ // unwrapOr: Get data or return a default value
162
+ const data = unwrapOr(result, defaultValue); // Returns T
163
+
164
+ // unwrapOrElse: Get data or compute a fallback from the error
165
+ const data = unwrapOrElse(result, (err) => computeFallback(err));
166
+
167
+ // isOk / isErr: Type guards for narrowing
168
+ if (isOk(result)) {
169
+ console.log(result.data); // TypeScript knows it's success
170
+ }
171
+ if (isErr(result)) {
172
+ console.log(result.error); // TypeScript knows it's error
173
+ }
174
+ ```
175
+
176
+ #### Why Railroad?
177
+
178
+ The name "Railroad" comes from Railway Oriented Programming, where success travels the "happy path" and errors get shunted to the "error track". Combined with TypeScript's type narrowing, you get explicit error handling without exceptions. If you like Rust's `Result`, you'll feel at home.
52
179
 
53
180
  ### Playlist
54
181
 
55
- A `Playlist` is a sequence of `Tasks` executed in order. The magic of a `Playlist` is that each task has access to the outputs of all previous tasks, in a fully type-safe way. You build a `Playlist` by chaining `.addTask().input()` calls:
182
+ A `Playlist` is a sequence of `Tasks` executed in order. Each task has access to the outputs of all previous tasks, in a fully type-safe way. You build a `Playlist` by chaining `.addTask().input()` calls:
56
183
 
57
184
  ```typescript
185
+ import { isOk } from "@fkws/klonk";
186
+
58
187
  playlist
59
188
  .addTask(new FetchTask("fetch"))
60
189
  .input((source) => ({ url: source.targetUrl }))
61
190
  .addTask(new ParseTask("parse"))
62
191
  .input((source, outputs) => ({
63
- // Full autocomplete! outputs.fetch?.success, outputs.fetch?.data, etc.
64
- html: outputs.fetch?.success ? outputs.fetch.data.body : ""
192
+ // Use isOk for Rust-style type narrowing!
193
+ html: outputs.fetch && isOk(outputs.fetch) ? outputs.fetch.data.body : ""
65
194
  }))
66
195
  ```
67
196
 
@@ -72,11 +201,13 @@ playlist
72
201
  Need to conditionally skip a task? Just return `null` from the input builder:
73
202
 
74
203
  ```typescript
204
+ import { isOk } from "@fkws/klonk";
205
+
75
206
  playlist
76
207
  .addTask(new NotifyTask("notify"))
77
208
  .input((source, outputs) => {
78
- // Skip notification if previous task failed
79
- if (!outputs.fetch?.success) {
209
+ // Skip notification if previous task failed - using isOk!
210
+ if (!outputs.fetch || !isOk(outputs.fetch)) {
80
211
  return null; // Task will be skipped!
81
212
  }
82
213
  return { message: "Success!", level: "info" };
@@ -114,7 +245,7 @@ Workflow.create()
114
245
  node.preventRetry() // Task failures throw immediately
115
246
  ```
116
247
 
117
- Default behavior: infinite retries at 1000ms delay. Use `.preventRetry()` to fail fast, or `.retryLimit(n)` to cap attempts.
248
+ Default behavior: infinite retries at 1000ms delay. This is designed for long-running daemons and background workers where resilience matters. **For request/response contexts** (APIs, CLIs, one-shot scripts), set `.retryLimit(n)` to cap attempts or use `.preventRetry()` to fail fast.
118
249
 
119
250
  ### Trigger
120
251
 
@@ -126,11 +257,11 @@ A `Workflow` connects one or more `Triggers` to a `Playlist`. When a trigger fir
126
257
 
127
258
  ### Machine
128
259
 
129
- A `Machine` is a finite state machine. You build it by declaring all state identifiers upfront with `.withStates<...>()`, then adding states with `.addState()`:
260
+ A `Machine` is a finite state machine. You build it by declaring all state identifiers upfront with `.withStates(...)`, then adding states with `.addState()`:
130
261
 
131
262
  ```typescript
132
263
  Machine.create<MyStateData>()
133
- .withStates<"idle" | "running" | "complete">() // Declare all states
264
+ .withStates("idle", "running", "complete") // Declare all states
134
265
  .addState("idle", node => node
135
266
  .setPlaylist(p => p.addTask(...).input(...))
136
267
  .addTransition({ to: "running", condition: ..., weight: 1 }) // Autocomplete!
@@ -159,14 +290,14 @@ Notes:
159
290
 
160
291
  ## Features
161
292
 
162
- - **Type-Safe & Autocompleted**: Klonk leverages TypeScript's inference to provide a world-class developer experience. The inputs and outputs of every step are strongly typed, so you'll know at compile time if your logic is sound.
163
- - **Code-First**: Define your automations directly in TypeScript. No YAML, no drag-and-drop UIs. Just the full power of a real programming language.
164
- - **Composable & Extensible**: The core primitives (`Task`, `Trigger`) are simple abstract classes, making it easy to create your own reusable components and integrations.
293
+ - **Type-Safe & Autocompleted**: Klonk uses TypeScript's inference so the inputs and outputs of every step are strongly typed. You'll know at compile time if your logic is sound.
294
+ - **Code-First**: Define your automations directly in TypeScript. No YAML, no drag-and-drop UIs.
295
+ - **Composable & Extensible**: The core primitives (`Task`, `Trigger`) are simple abstract classes, so you can create your own reusable components.
165
296
  - **Flexible Execution**: `Machines` run with configurable modes via `run(state, options)`: `any`, `leaf`, `roundtrip`, or `infinitely` (with optional `interval`).
166
297
 
167
298
  ## Klonkworks: Pre-built Components
168
299
 
169
- Coming soon(ish)! Klonkworks will be a large collection of pre-built Tasks, Triggers, and integrations. This will allow you to quickly assemble powerful automations that connect to a wide variety of services, often without needing to build your own components from scratch.
300
+ Coming soon(ish)! Klonkworks will be a collection of pre-built Tasks, Triggers, and integrations that connect to various services, so you don't have to build everything from scratch.
170
301
 
171
302
  ## Code Examples
172
303
 
@@ -277,13 +408,13 @@ export class IntervalTrigger<TIdent extends string> extends Trigger<TIdent, { no
277
408
  <details>
278
409
  <summary><b>Building a Workflow</b></summary>
279
410
 
280
- Workflows are perfect for event-driven automations. This example creates a workflow that triggers when a new invoice PDF is added to a Dropbox folder. It then parses the invoice and creates a new item in a Notion database.
411
+ Workflows work well for event-driven automations. This example triggers when a new invoice PDF is added to a Dropbox folder, parses the invoice, and creates a new item in a Notion database.
281
412
 
282
413
  Notice the fluent `.addTask(task).input(builder)` syntax - each task's input builder has access to `source` (trigger data) and `outputs` (all previous task results), with full type inference!
283
414
 
284
415
  ```typescript
285
416
  import { z } from 'zod';
286
- import { Workflow } from '@fkws/klonk';
417
+ import { Workflow, isOk } from '@fkws/klonk';
287
418
 
288
419
  // The following example requires tasks, integrations and a trigger.
289
420
  // Soon, you will be able to import these from @fkws/klonkworks.
@@ -337,17 +468,17 @@ const workflow = Workflow.create()
337
468
  // Access outputs of previous tasks - fully typed!
338
469
  // Check for null (skipped) and success
339
470
  const downloadResult = outputs['download-invoice-pdf'];
340
- if (!downloadResult?.success) {
471
+ if (!downloadResult || !isOk(downloadResult)) {
341
472
  throw downloadResult?.error ?? new Error('Failed to download invoice PDF');
342
473
  }
343
474
 
344
475
  const payeesResult = outputs['get-payees'];
345
- if (!payeesResult?.success) {
476
+ if (!payeesResult || !isOk(payeesResult)) {
346
477
  throw payeesResult?.error ?? new Error('Failed to load payees');
347
478
  }
348
479
 
349
480
  const expenseTypesResult = outputs['get-expense-types'];
350
- if (!expenseTypesResult?.success) {
481
+ if (!expenseTypesResult || !isOk(expenseTypesResult)) {
351
482
  throw expenseTypesResult?.error ?? new Error('Failed to load expense types');
352
483
  }
353
484
 
@@ -372,7 +503,7 @@ const workflow = Workflow.create()
372
503
  .addTask(new TACreateNotionDatabaseItem("create-notion-invoice", notionProvider))
373
504
  .input((source, outputs) => {
374
505
  const invoiceResult = outputs['parse-invoice'];
375
- if (!invoiceResult?.success) {
506
+ if (!invoiceResult || !isOk(invoiceResult)) {
376
507
  throw invoiceResult?.error ?? new Error('Failed to parse invoice');
377
508
  }
378
509
  const invoiceData = invoiceResult.data;
@@ -403,12 +534,12 @@ workflow.start({
403
534
  <details>
404
535
  <summary><b>Building a Machine</b></summary>
405
536
 
406
- `Machines` are ideal for building complex, stateful agents. This example shows an AI agent that takes a user's query, refines it, performs a web search, and generates a final response.
537
+ `Machines` work well for stateful agents. This example shows an AI agent that takes a user's query, refines it, performs a web search, and generates a response.
407
538
 
408
539
  The `Machine` manages a `StateData` object. Each `StateNode`'s `Playlist` can modify this state, and the `Transitions` between states use it to decide which state to move to next.
409
540
 
410
541
  ```typescript
411
- import { Machine } from "@fkws/klonk"
542
+ import { Machine, isOk } from "@fkws/klonk"
412
543
  import { OpenRouterClient } from "./tasks/common/OpenrouterClient"
413
544
  import { Model } from "./tasks/common/models"
414
545
  import { TABasicTextInference } from "./tasks/TABasicTextInference"
@@ -442,7 +573,7 @@ const client = new OpenRouterClient(process.env.OPENROUTER_API_KEY!)
442
573
  const webSearchAgent = Machine
443
574
  .create<StateData>()
444
575
  // Declare all states upfront for transition autocomplete
445
- .withStates<"refine_and_extract" | "search_web" | "generate_response">()
576
+ .withStates("refine_and_extract", "search_web", "generate_response")
446
577
  .addState("refine_and_extract", node => node
447
578
  .setPlaylist(p => p
448
579
  // Refine the user's input
@@ -457,17 +588,17 @@ const webSearchAgent = Machine
457
588
  // Extract search terms from refined input
458
589
  .addTask(new TABasicTextInference("extract_search_terms", client))
459
590
  .input((state, outputs) => ({
460
- inputText: `Original: ${state.input}\n\nRefined: ${outputs.refine?.success ? outputs.refine.data.text : state.input}`,
591
+ inputText: `Original: ${state.input}\n\nRefined: ${outputs.refine && isOk(outputs.refine) ? outputs.refine.data.text : state.input}`,
461
592
  model: state.model ?? "openai/gpt-5.2",
462
593
  instructions: `Extract one short web search query from the user request and refined prompt.`
463
594
  }))
464
595
 
465
- // Update state with results
596
+ // Update state with results - using isOk for type narrowing
466
597
  .finally((state, outputs) => {
467
- if (outputs.refine?.success) {
598
+ if (outputs.refine && isOk(outputs.refine)) {
468
599
  state.refinedInput = outputs.refine.data.text;
469
600
  }
470
- if (outputs.extract_search_terms?.success) {
601
+ if (outputs.extract_search_terms && isOk(outputs.extract_search_terms)) {
471
602
  state.searchTerm = outputs.extract_search_terms.data.text;
472
603
  }
473
604
  })
@@ -492,7 +623,7 @@ const webSearchAgent = Machine
492
623
  query: state.searchTerm!
493
624
  }))
494
625
  .finally((state, outputs) => {
495
- if (outputs.search?.success) {
626
+ if (outputs.search && isOk(outputs.search)) {
496
627
  state.searchResults = outputs.search.data;
497
628
  }
498
629
  })
@@ -515,7 +646,7 @@ const webSearchAgent = Machine
515
646
  Write a professional response.`
516
647
  }))
517
648
  .finally((state, outputs) => {
518
- state.finalResponse = outputs.generate_response?.success
649
+ state.finalResponse = outputs.generate_response && isOk(outputs.generate_response)
519
650
  ? outputs.generate_response.data.text
520
651
  : "Sorry, an error occurred: " + (outputs.generate_response?.error ?? "unknown");
521
652
  })
@@ -542,20 +673,30 @@ console.log(state.finalResponse);
542
673
 
543
674
  ## Type System
544
675
 
545
- Klonk's type system is designed to be minimal yet powerful. Here's what makes it tick:
676
+ Klonk's type system is minimal. Here's how it works:
546
677
 
547
678
  ### Core Types
548
679
 
549
680
  | Type | Parameters | Purpose |
550
681
  |------|------------|---------|
551
682
  | `Task<Input, Output, Ident>` | Input shape, output shape, string literal ident | Base class for all tasks |
552
- | `Railroad<Output>` | Success data type | Discriminated union for success/error results |
683
+ | `Railroad<Output>` | Success data type | Discriminated union for success/error results (like Rust's `Result`) |
553
684
  | `Playlist<AllOutputs, Source>` | Accumulated output map, source data type | Ordered task sequence with typed chaining |
554
685
  | `Trigger<Ident, Data>` | String literal ident, event payload type | Event source for workflows |
555
686
  | `Workflow<Events>` | Union of trigger event types | Connects triggers to playlists |
556
687
  | `Machine<StateData, AllStateIdents>` | Mutable state shape, union of state idents | Finite state machine with typed transitions |
557
688
  | `StateNode<StateData, Ident, AllStateIdents>` | State shape, this node's ident, all valid transition targets | Individual state with playlist and transitions |
558
689
 
690
+ ### Railroad Helper Functions
691
+
692
+ | Function | Signature | Behavior |
693
+ |----------|-----------|----------|
694
+ | `unwrap(r)` | `Railroad<T> → T` | Returns data or throws error |
695
+ | `unwrapOr(r, default)` | `Railroad<T>, T → T` | Returns data or default value |
696
+ | `unwrapOrElse(r, fn)` | `Railroad<T>, (E) → T → T` | Returns data or result of fn(error) |
697
+ | `isOk(r)` | `Railroad<T> → boolean` | Type guard for success case |
698
+ | `isErr(r)` | `Railroad<T> → boolean` | Type guard for error case |
699
+
559
700
  ### How Output Chaining Works
560
701
 
561
702
  When you add a task to a playlist, Klonk extends the output type:
@@ -570,7 +711,7 @@ Playlist<{ fetch: Railroad<FetchOutput> | null }, Source>
570
711
  // Now outputs include both: { fetch: ..., parse: Railroad<ParseOutput> | null }
571
712
  ```
572
713
 
573
- The `| null` accounts for the possibility that a task was skipped (when its input builder returns `null`). This is why you'll use optional chaining like `outputs.fetch?.success` - TypeScript knows the output could be `null` if the task was skipped!
714
+ The `| null` accounts for the possibility that a task was skipped (when its input builder returns `null`). This is why you'll check for null before using `isOk()` - for example: `outputs.fetch && isOk(outputs.fetch)`. TypeScript then narrows the type so you can safely access `.data`!
574
715
 
575
716
  This maps cleanly to Rust's types:
576
717
  | Rust | Klonk (TypeScript) |
package/dist/index.cjs CHANGED
@@ -30,6 +30,11 @@ var __export = (target, all) => {
30
30
  // src/index.ts
31
31
  var exports_src = {};
32
32
  __export(exports_src, {
33
+ unwrapOrElse: () => unwrapOrElse,
34
+ unwrapOr: () => unwrapOr,
35
+ unwrap: () => unwrap,
36
+ isOk: () => isOk,
37
+ isErr: () => isErr,
33
38
  Workflow: () => Workflow,
34
39
  Trigger: () => Trigger,
35
40
  Task: () => Task,
@@ -165,6 +170,28 @@ class Workflow {
165
170
  }
166
171
  }
167
172
  // src/prototypes/Task.ts
173
+ function isOk(r) {
174
+ return r.success === true;
175
+ }
176
+ function isErr(r) {
177
+ return r.success === false;
178
+ }
179
+ function unwrap(r) {
180
+ if (r.success)
181
+ return r.data;
182
+ throw r.error;
183
+ }
184
+ function unwrapOr(r, defaultValue) {
185
+ if (r.success)
186
+ return r.data;
187
+ return defaultValue;
188
+ }
189
+ function unwrapOrElse(r, fn) {
190
+ if (r.success)
191
+ return r.data;
192
+ return fn(r.error);
193
+ }
194
+
168
195
  class Task {
169
196
  ident;
170
197
  constructor(ident) {
@@ -330,7 +357,7 @@ class Machine {
330
357
  static create() {
331
358
  return new Machine;
332
359
  }
333
- withStates() {
360
+ withStates(..._states) {
334
361
  return this;
335
362
  }
336
363
  finalize({
package/dist/index.d.ts CHANGED
@@ -19,6 +19,37 @@ type Railroad<
19
19
  readonly success: false
20
20
  readonly error: ErrorType
21
21
  };
22
+ /** Type guard: returns true if the Railroad is a success */
23
+ declare function isOk<
24
+ T,
25
+ E
26
+ >(r: Railroad<T, E>): r is {
27
+ success: true
28
+ data: T
29
+ };
30
+ /** Type guard: returns true if the Railroad is an error */
31
+ declare function isErr<
32
+ T,
33
+ E
34
+ >(r: Railroad<T, E>): r is {
35
+ success: false
36
+ error: E
37
+ };
38
+ /** Returns the data if success, throws the error if failure (like Rust's unwrap) */
39
+ declare function unwrap<
40
+ T,
41
+ E
42
+ >(r: Railroad<T, E>): T;
43
+ /** Returns the data if success, or the default value if failure */
44
+ declare function unwrapOr<
45
+ T,
46
+ E
47
+ >(r: Railroad<T, E>, defaultValue: T): T;
48
+ /** Returns the data if success, or calls the function with the error if failure */
49
+ declare function unwrapOrElse<
50
+ T,
51
+ E
52
+ >(r: Railroad<T, E>, fn: (error: E) => T): T;
22
53
  /**
23
54
  * Base class for all executable units in Klonk.
24
55
  * Implement `validateInput` for runtime checks and `run` for the actual work.
@@ -432,7 +463,7 @@ declare class StateNode<
432
463
  next(data: TStateData): Promise<StateNode<TStateData> | null>;
433
464
  }
434
465
  /**
435
- * Returned by `Machine.create()` - you must call `.withStates<...>()` to declare state idents.
466
+ * Returned by `Machine.create()` - you must call `.withStates(...)` to declare state idents.
436
467
  *
437
468
  * This ensures all state identifiers are known upfront for full transition autocomplete.
438
469
  */
@@ -441,17 +472,18 @@ interface MachineNeedsStates<TStateData> {
441
472
  * Declare all state identifiers that will be used in this machine.
442
473
  * This enables full autocomplete for transition targets.
443
474
  *
444
- * @template TIdents - Union of all state idents (e.g., `"idle" | "running" | "complete"`).
475
+ * @param states - All state idents as string arguments. If omitted, any string is allowed (useful for tests).
445
476
  * @returns The machine, ready for adding states.
446
477
  *
447
478
  * @example
448
479
  * Machine.create<MyState>()
449
- * .withStates<"idle" | "running" | "complete">()
480
+ * .withStates("idle", "running", "complete")
450
481
  * .addState("idle", node => node
451
482
  * .addTransition({ to: "running", ... }) // Autocomplete works!
452
483
  * )
453
484
  */
454
- withStates<TIdents extends string>(): Machine<TStateData, TIdents>;
485
+ withStates(): Machine<TStateData, string>;
486
+ withStates<const T extends readonly string[]>(...states: T): Machine<TStateData, T[number]>;
455
487
  }
456
488
  /**
457
489
  * A finite state machine that coordinates execution of `StateNode` playlists
@@ -487,7 +519,7 @@ declare class Machine<
487
519
  */
488
520
  private sleep;
489
521
  /**
490
- * Create a new Machine. You must call `.withStates<...>()` next to declare
522
+ * Create a new Machine. You must call `.withStates(...)` next to declare
491
523
  * all state identifiers before adding states.
492
524
  *
493
525
  * @template TStateData - The shape of the mutable state carried through the machine.
@@ -495,7 +527,7 @@ declare class Machine<
495
527
  *
496
528
  * @example
497
529
  * Machine.create<MyState>()
498
- * .withStates<"idle" | "running">()
530
+ * .withStates("idle", "running")
499
531
  * .addState("idle", node => ...)
500
532
  */
501
533
  static create<TStateData>(): MachineNeedsStates<TStateData>;
@@ -505,7 +537,7 @@ declare class Machine<
505
537
  *
506
538
  * @internal
507
539
  */
508
- withStates<TIdents extends string>(): Machine<TStateData, TIdents>;
540
+ withStates<const T extends readonly string[]>(..._states: T): Machine<TStateData, T[number]>;
509
541
  /**
510
542
  * Finalize the machine by resolving state transitions and locking configuration.
511
543
  * Must be called before `start` or `run`.
@@ -541,9 +573,9 @@ declare class Machine<
541
573
  * .addTransition({ to: "idle", condition: async () => true, weight: 1 })
542
574
  * )
543
575
  */
544
- addState<const TIdent extends string>(ident: TIdent, builder: (node: StateNode<TStateData, TIdent, AllStateIdents | TIdent>) => StateNode<TStateData, TIdent, AllStateIdents | TIdent>, options?: {
576
+ addState<const TIdent extends AllStateIdents>(ident: TIdent, builder: (node: StateNode<TStateData, TIdent, AllStateIdents>) => StateNode<TStateData, TIdent, AllStateIdents>, options?: {
545
577
  initial?: boolean
546
- }): Machine<TStateData, AllStateIdents | TIdent>;
578
+ }): Machine<TStateData, AllStateIdents>;
547
579
  /**
548
580
  * Attach a logger to this machine. If the machine has an initial state set,
549
581
  * the logger will be propagated to all currently reachable states.
@@ -575,4 +607,4 @@ type RunOptions = {
575
607
  mode: "infinitely"
576
608
  interval?: number
577
609
  });
578
- export { Workflow, TriggerEvent, Trigger, Task, StateNode, RunOptions, Railroad, PlaylistRunOptions, Playlist, Machine };
610
+ export { unwrapOrElse, unwrapOr, unwrap, isOk, isErr, Workflow, TriggerEvent, Trigger, Task, StateNode, RunOptions, Railroad, PlaylistRunOptions, Playlist, Machine };
package/dist/index.js CHANGED
@@ -124,6 +124,28 @@ class Workflow {
124
124
  }
125
125
  }
126
126
  // src/prototypes/Task.ts
127
+ function isOk(r) {
128
+ return r.success === true;
129
+ }
130
+ function isErr(r) {
131
+ return r.success === false;
132
+ }
133
+ function unwrap(r) {
134
+ if (r.success)
135
+ return r.data;
136
+ throw r.error;
137
+ }
138
+ function unwrapOr(r, defaultValue) {
139
+ if (r.success)
140
+ return r.data;
141
+ return defaultValue;
142
+ }
143
+ function unwrapOrElse(r, fn) {
144
+ if (r.success)
145
+ return r.data;
146
+ return fn(r.error);
147
+ }
148
+
127
149
  class Task {
128
150
  ident;
129
151
  constructor(ident) {
@@ -289,7 +311,7 @@ class Machine {
289
311
  static create() {
290
312
  return new Machine;
291
313
  }
292
- withStates() {
314
+ withStates(..._states) {
293
315
  return this;
294
316
  }
295
317
  finalize({
@@ -465,6 +487,11 @@ class Machine {
465
487
  }
466
488
  }
467
489
  export {
490
+ unwrapOrElse,
491
+ unwrapOr,
492
+ unwrap,
493
+ isOk,
494
+ isErr,
468
495
  Workflow,
469
496
  Trigger,
470
497
  Task,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fkws/klonk",
3
- "version": "0.0.21",
3
+ "version": "0.0.23",
4
4
  "description": "A lightweight, extensible workflow automation engine for Node.js and Bun",
5
5
  "repository": "https://github.com/klar-web-services/klonk",
6
6
  "homepage": "https://klonk.dev",