@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 +177 -36
- package/dist/index.cjs +28 -1
- package/dist/index.d.ts +42 -10
- package/dist/index.js +28 -1
- package/package.json +1 -1
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
|
|
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
|
-

|
|
26
25
|

|
|
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.
|
|
31
|
-
- **Machines**:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
64
|
-
html: outputs.fetch
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
163
|
-
- **Code-First**: Define your automations directly in TypeScript. No YAML, no drag-and-drop UIs.
|
|
164
|
-
- **Composable & Extensible**: The core primitives (`Task`, `Trigger`) are simple abstract classes,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
598
|
+
if (outputs.refine && isOk(outputs.refine)) {
|
|
468
599
|
state.refinedInput = outputs.refine.data.text;
|
|
469
600
|
}
|
|
470
|
-
if (outputs.extract_search_terms
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
480
|
+
* .withStates("idle", "running", "complete")
|
|
450
481
|
* .addState("idle", node => node
|
|
451
482
|
* .addTransition({ to: "running", ... }) // Autocomplete works!
|
|
452
483
|
* )
|
|
453
484
|
*/
|
|
454
|
-
withStates
|
|
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
|
|
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
|
|
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<
|
|
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
|
|
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
|
|
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