@fkws/klonk 0.0.21 → 0.0.22

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
@@ -48,20 +48,65 @@ A `Task` is the smallest unit of work. It's an abstract class with two main meth
48
48
  - `validateInput(input)`: Runtime validation of the task's input (on top of strong typing).
49
49
  - `run(input)`: Executes the task's logic.
50
50
 
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!
51
+ 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.
52
+
53
+ ### Railroad (Rust-inspired Result Type)
54
+
55
+ `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:
56
+
57
+ ```typescript
58
+ type Railroad<T> =
59
+ | { success: true, data: T }
60
+ | { success: false, error: Error }
61
+ ```
62
+
63
+ #### Helper Functions
64
+
65
+ Klonk provides Rust-inspired helper functions for working with `Railroad`:
66
+
67
+ ```typescript
68
+ import { unwrap, unwrapOr, unwrapOrElse, isOk, isErr } from "@fkws/klonk";
69
+
70
+ // unwrap: Get data or throw error (like Rust's .unwrap())
71
+ const data = unwrap(result); // Returns T or throws
72
+
73
+ // unwrapOr: Get data or return a default value
74
+ const data = unwrapOr(result, defaultValue); // Returns T
75
+
76
+ // unwrapOrElse: Get data or compute a fallback from the error
77
+ const data = unwrapOrElse(result, (err) => computeFallback(err));
78
+
79
+ // isOk / isErr: Type guards for narrowing
80
+ if (isOk(result)) {
81
+ console.log(result.data); // TypeScript knows it's success
82
+ }
83
+ if (isErr(result)) {
84
+ console.log(result.error); // TypeScript knows it's error
85
+ }
86
+ ```
87
+
88
+ #### Why Railroad?
89
+
90
+ The name "Railroad" comes from Railway Oriented Programming - a functional approach where success travels the "happy path" and errors get shunted to the "error track". Combined with TypeScript's type narrowing, you get:
91
+
92
+ - **No uncaught exceptions** - errors are values, not thrown
93
+ - **Explicit error handling** - the type system forces you to handle failures
94
+ - **Familiar patterns** - if you love Rust's `Result`, you'll feel right at home
52
95
 
53
96
  ### Playlist
54
97
 
55
98
  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:
56
99
 
57
100
  ```typescript
101
+ import { isOk } from "@fkws/klonk";
102
+
58
103
  playlist
59
104
  .addTask(new FetchTask("fetch"))
60
105
  .input((source) => ({ url: source.targetUrl }))
61
106
  .addTask(new ParseTask("parse"))
62
107
  .input((source, outputs) => ({
63
- // Full autocomplete! outputs.fetch?.success, outputs.fetch?.data, etc.
64
- html: outputs.fetch?.success ? outputs.fetch.data.body : ""
108
+ // Use isOk for Rust-style type narrowing!
109
+ html: outputs.fetch && isOk(outputs.fetch) ? outputs.fetch.data.body : ""
65
110
  }))
66
111
  ```
67
112
 
@@ -72,11 +117,13 @@ playlist
72
117
  Need to conditionally skip a task? Just return `null` from the input builder:
73
118
 
74
119
  ```typescript
120
+ import { isOk } from "@fkws/klonk";
121
+
75
122
  playlist
76
123
  .addTask(new NotifyTask("notify"))
77
124
  .input((source, outputs) => {
78
- // Skip notification if previous task failed
79
- if (!outputs.fetch?.success) {
125
+ // Skip notification if previous task failed - using isOk!
126
+ if (!outputs.fetch || !isOk(outputs.fetch)) {
80
127
  return null; // Task will be skipped!
81
128
  }
82
129
  return { message: "Success!", level: "info" };
@@ -283,7 +330,7 @@ Notice the fluent `.addTask(task).input(builder)` syntax - each task's input bui
283
330
 
284
331
  ```typescript
285
332
  import { z } from 'zod';
286
- import { Workflow } from '@fkws/klonk';
333
+ import { Workflow, isOk } from '@fkws/klonk';
287
334
 
288
335
  // The following example requires tasks, integrations and a trigger.
289
336
  // Soon, you will be able to import these from @fkws/klonkworks.
@@ -337,17 +384,17 @@ const workflow = Workflow.create()
337
384
  // Access outputs of previous tasks - fully typed!
338
385
  // Check for null (skipped) and success
339
386
  const downloadResult = outputs['download-invoice-pdf'];
340
- if (!downloadResult?.success) {
387
+ if (!downloadResult || !isOk(downloadResult)) {
341
388
  throw downloadResult?.error ?? new Error('Failed to download invoice PDF');
342
389
  }
343
390
 
344
391
  const payeesResult = outputs['get-payees'];
345
- if (!payeesResult?.success) {
392
+ if (!payeesResult || !isOk(payeesResult)) {
346
393
  throw payeesResult?.error ?? new Error('Failed to load payees');
347
394
  }
348
395
 
349
396
  const expenseTypesResult = outputs['get-expense-types'];
350
- if (!expenseTypesResult?.success) {
397
+ if (!expenseTypesResult || !isOk(expenseTypesResult)) {
351
398
  throw expenseTypesResult?.error ?? new Error('Failed to load expense types');
352
399
  }
353
400
 
@@ -372,7 +419,7 @@ const workflow = Workflow.create()
372
419
  .addTask(new TACreateNotionDatabaseItem("create-notion-invoice", notionProvider))
373
420
  .input((source, outputs) => {
374
421
  const invoiceResult = outputs['parse-invoice'];
375
- if (!invoiceResult?.success) {
422
+ if (!invoiceResult || !isOk(invoiceResult)) {
376
423
  throw invoiceResult?.error ?? new Error('Failed to parse invoice');
377
424
  }
378
425
  const invoiceData = invoiceResult.data;
@@ -408,7 +455,7 @@ workflow.start({
408
455
  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
456
 
410
457
  ```typescript
411
- import { Machine } from "@fkws/klonk"
458
+ import { Machine, isOk } from "@fkws/klonk"
412
459
  import { OpenRouterClient } from "./tasks/common/OpenrouterClient"
413
460
  import { Model } from "./tasks/common/models"
414
461
  import { TABasicTextInference } from "./tasks/TABasicTextInference"
@@ -457,17 +504,17 @@ const webSearchAgent = Machine
457
504
  // Extract search terms from refined input
458
505
  .addTask(new TABasicTextInference("extract_search_terms", client))
459
506
  .input((state, outputs) => ({
460
- inputText: `Original: ${state.input}\n\nRefined: ${outputs.refine?.success ? outputs.refine.data.text : state.input}`,
507
+ inputText: `Original: ${state.input}\n\nRefined: ${outputs.refine && isOk(outputs.refine) ? outputs.refine.data.text : state.input}`,
461
508
  model: state.model ?? "openai/gpt-5.2",
462
509
  instructions: `Extract one short web search query from the user request and refined prompt.`
463
510
  }))
464
511
 
465
- // Update state with results
512
+ // Update state with results - using isOk for type narrowing
466
513
  .finally((state, outputs) => {
467
- if (outputs.refine?.success) {
514
+ if (outputs.refine && isOk(outputs.refine)) {
468
515
  state.refinedInput = outputs.refine.data.text;
469
516
  }
470
- if (outputs.extract_search_terms?.success) {
517
+ if (outputs.extract_search_terms && isOk(outputs.extract_search_terms)) {
471
518
  state.searchTerm = outputs.extract_search_terms.data.text;
472
519
  }
473
520
  })
@@ -492,7 +539,7 @@ const webSearchAgent = Machine
492
539
  query: state.searchTerm!
493
540
  }))
494
541
  .finally((state, outputs) => {
495
- if (outputs.search?.success) {
542
+ if (outputs.search && isOk(outputs.search)) {
496
543
  state.searchResults = outputs.search.data;
497
544
  }
498
545
  })
@@ -515,7 +562,7 @@ const webSearchAgent = Machine
515
562
  Write a professional response.`
516
563
  }))
517
564
  .finally((state, outputs) => {
518
- state.finalResponse = outputs.generate_response?.success
565
+ state.finalResponse = outputs.generate_response && isOk(outputs.generate_response)
519
566
  ? outputs.generate_response.data.text
520
567
  : "Sorry, an error occurred: " + (outputs.generate_response?.error ?? "unknown");
521
568
  })
@@ -549,13 +596,23 @@ Klonk's type system is designed to be minimal yet powerful. Here's what makes it
549
596
  | Type | Parameters | Purpose |
550
597
  |------|------------|---------|
551
598
  | `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 |
599
+ | `Railroad<Output>` | Success data type | Discriminated union for success/error results (like Rust's `Result`) |
553
600
  | `Playlist<AllOutputs, Source>` | Accumulated output map, source data type | Ordered task sequence with typed chaining |
554
601
  | `Trigger<Ident, Data>` | String literal ident, event payload type | Event source for workflows |
555
602
  | `Workflow<Events>` | Union of trigger event types | Connects triggers to playlists |
556
603
  | `Machine<StateData, AllStateIdents>` | Mutable state shape, union of state idents | Finite state machine with typed transitions |
557
604
  | `StateNode<StateData, Ident, AllStateIdents>` | State shape, this node's ident, all valid transition targets | Individual state with playlist and transitions |
558
605
 
606
+ ### Railroad Helper Functions
607
+
608
+ | Function | Signature | Behavior |
609
+ |----------|-----------|----------|
610
+ | `unwrap(r)` | `Railroad<T> → T` | Returns data or throws error |
611
+ | `unwrapOr(r, default)` | `Railroad<T>, T → T` | Returns data or default value |
612
+ | `unwrapOrElse(r, fn)` | `Railroad<T>, (E) → T → T` | Returns data or result of fn(error) |
613
+ | `isOk(r)` | `Railroad<T> → boolean` | Type guard for success case |
614
+ | `isErr(r)` | `Railroad<T> → boolean` | Type guard for error case |
615
+
559
616
  ### How Output Chaining Works
560
617
 
561
618
  When you add a task to a playlist, Klonk extends the output type:
@@ -570,7 +627,7 @@ Playlist<{ fetch: Railroad<FetchOutput> | null }, Source>
570
627
  // Now outputs include both: { fetch: ..., parse: Railroad<ParseOutput> | null }
571
628
  ```
572
629
 
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!
630
+ 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
631
 
575
632
  This maps cleanly to Rust's types:
576
633
  | 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) {
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.
@@ -575,4 +606,4 @@ type RunOptions = {
575
606
  mode: "infinitely"
576
607
  interval?: number
577
608
  });
578
- export { Workflow, TriggerEvent, Trigger, Task, StateNode, RunOptions, Railroad, PlaylistRunOptions, Playlist, Machine };
609
+ 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) {
@@ -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.22",
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",