@fkws/klonk 0.0.6 → 0.0.8

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
@@ -1,4 +1,16 @@
1
- # Klonk
1
+ ![Klonk](./.github/assets/logo-full.png)
2
+
3
+ ---
4
+
5
+ ![npm version](https://img.shields.io/npm/v/@fkws/klonk)
6
+ ![npm downloads](https://img.shields.io/npm/dm/@fkws/klonk)
7
+ [![codecov](https://codecov.io/gh/klar-web-services/klonk/branch/main/graph/badge.svg?token=2R145SOCWH)](https://codecov.io/gh/klar-web-services/klonk)
8
+ ---
9
+
10
+ ![License](https://img.shields.io/github/license/klar-web-services/klonk)
11
+
12
+ ---
13
+
2
14
  *A code-first, type-safe automation engine for TypeScript.*
3
15
 
4
16
  ## Introduction
package/dist/index.cjs CHANGED
@@ -451,25 +451,20 @@ class Machine {
451
451
  }
452
452
  retries++;
453
453
  }
454
- if (!next) {
455
- logger?.info({ phase: "end" }, "No next state after retries, run complete.");
456
- return stateData;
457
- }
458
454
  }
459
- if (next === this.initialState) {
455
+ const resolvedNext = next;
456
+ if (resolvedNext === this.initialState) {
460
457
  logger?.info({ phase: "end" }, "Next state is initial state, run complete.");
461
458
  return stateData;
462
459
  }
463
- logger?.info({ phase: "progress", from: current.ident, to: next.ident }, "Transitioning state.");
464
- current = next;
460
+ logger?.info({ phase: "progress", from: current.ident, to: resolvedNext.ident }, "Transitioning state.");
461
+ current = resolvedNext;
465
462
  await current.playlist.run(stateData);
466
- if (current.ident)
467
- visitedIdents.add(current.ident);
463
+ visitedIdents.add(current.ident);
468
464
  if (visitedIdents.size >= allStates.length) {
469
465
  logger?.info({ phase: "end" }, "All reachable states visited, run complete.");
470
466
  return stateData;
471
467
  }
472
468
  }
473
- return stateData;
474
469
  }
475
470
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * A simple discriminated-union result type used by Tasks.
3
+ * Prefer returning a `Railroad` from `Task.run` instead of throwing exceptions.
4
+ *
5
+ * Example:
6
+ * - Success: `{ success: true, data: value }`
7
+ * - Failure: `{ success: false, error }`
8
+ *
9
+ * @template OutputType - The success payload type.
10
+ * @template ErrorType - Optional error payload type (default `Error`).
11
+ */
1
12
  type Railroad<
2
13
  OutputType,
3
14
  ErrorType = Error
@@ -8,20 +19,55 @@ type Railroad<
8
19
  readonly success: false
9
20
  readonly error: ErrorType
10
21
  };
22
+ /**
23
+ * Base class for all executable units in Klonk.
24
+ * Implement `validateInput` for runtime checks and `run` for the actual work.
25
+ *
26
+ * The type parameters power Klonk's autocomplete across Playlists and Workflows:
27
+ * - `InputType` is inferred at call sites when you build a Playlist.
28
+ * - `OutputType` becomes available to subsequent tasks under this task's `ident`.
29
+ * - `IdentType` should be a string literal to provide strongly-typed keys in outputs.
30
+ *
31
+ * See README Code Examples for end-to-end usage in Playlists and Machines.
32
+ *
33
+ * @template InputType - Runtime input shape expected by the task.
34
+ * @template OutputType - Result shape produced by the task.
35
+ * @template IdentType - Unique task identifier (use a string literal).
36
+ */
11
37
  declare abstract class Task<
12
38
  InputType,
13
39
  OutputType,
14
40
  IdentType extends string
15
41
  > {
16
42
  ident: IdentType;
43
+ /**
44
+ * Unique identifier for the task. Also used as the key in Playlist outputs.
45
+ */
17
46
  constructor(ident: IdentType);
47
+ /**
48
+ * Execute the task logic.
49
+ * Return a `Railroad` to encode success or failure without throwing.
50
+ *
51
+ * @param input - The input object provided by the Playlist builder.
52
+ * @returns A `Railroad` containing output data on success, or an error on failure.
53
+ */
18
54
  abstract run(input: InputType): Promise<Railroad<OutputType>>;
19
55
  }
56
+ /**
57
+ * Function used to build the input for a Task from the Playlist context.
58
+ *
59
+ * @template SourceType - The source object passed to `run` (e.g., trigger event or machine state).
60
+ * @template AllOutputTypes - Accumulated outputs from previously executed tasks.
61
+ * @template TaskInputType - Concrete input type required by the target Task.
62
+ */
20
63
  type InputBuilder<
21
64
  SourceType,
22
65
  AllOutputTypes,
23
66
  TaskInputType
24
67
  > = (source: SourceType, outputs: AllOutputTypes) => TaskInputType;
68
+ /**
69
+ * @internal Internal assembly type that couples a task with its input builder.
70
+ */
25
71
  interface Machine<
26
72
  SourceType,
27
73
  AllOutputTypes,
@@ -32,14 +78,56 @@ interface Machine<
32
78
  task: Task<TaskInputType, TaskOutputType, IdentType>;
33
79
  builder: InputBuilder<SourceType, AllOutputTypes, TaskInputType>;
34
80
  }
81
+ /**
82
+ * Prevent TypeScript from inferring `T` from a builder argument so that the
83
+ * task definition remains the source of truth. Useful for preserving safety
84
+ * when chaining `.addTask` calls.
85
+ */
35
86
  type NoInfer<T> = [T][T extends any ? 0 : never];
87
+ /**
88
+ * An ordered sequence of Tasks executed with strong type inference.
89
+ *
90
+ * As tasks are added via `.addTask`, their outputs are merged into the
91
+ * accumulated `AllOutputTypes` map using the task's `ident` as a key. The
92
+ * next task's input builder receives both the original `source` and the
93
+ * strongly-typed `outputs` from all previous tasks.
94
+ *
95
+ * Typical sources:
96
+ * - Workflow: the trigger event object.
97
+ * - Machine: the current mutable state object.
98
+ *
99
+ * See README Code Examples for how to chain tasks and consume outputs.
100
+ *
101
+ * @template AllOutputTypes - Map of task idents to their `Railroad<Output>` results.
102
+ * @template SourceType - The source object provided to `run`.
103
+ */
36
104
  declare class Playlist<
37
105
  AllOutputTypes extends Record<string, any>,
38
106
  SourceType = unknown
39
107
  > {
108
+ /**
109
+ * Internal list of task + builder pairs in the order they will run.
110
+ */
40
111
  machines: Machine<any, any, any, any, string>[];
112
+ /**
113
+ * Optional finalizer invoked after all tasks complete (successfully or not).
114
+ */
41
115
  finalizer?: (source: SourceType, outputs: Record<string, any>) => void | Promise<void>;
42
116
  constructor(machines?: Machine<any, any, any, any, string>[], finalizer?: (source: SourceType, outputs: Record<string, any>) => void | Promise<void>);
117
+ /**
118
+ * Append a task to the end of the playlist.
119
+ *
120
+ * The task's `ident` is used as a key in the aggregated `outputs` object made
121
+ * available to subsequent builders. The value under that key is the task's
122
+ * `Railroad<Output>` result, enabling type-safe success/error handling.
123
+ *
124
+ * @template TaskInputType - Input required by the task.
125
+ * @template TaskOutputType - Output produced by the task.
126
+ * @template IdentType - The task's ident (string literal recommended).
127
+ * @param task - The task instance to run at this step.
128
+ * @param builder - Function that builds the task input from `source` and prior `outputs`.
129
+ * @returns A new Playlist with the output map extended to include this task's result.
130
+ */
43
131
  addTask<
44
132
  TaskInputType,
45
133
  TaskOutputType,
@@ -47,9 +135,35 @@ declare class Playlist<
47
135
  >(task: Task<TaskInputType, TaskOutputType, IdentType> & {
48
136
  ident: IdentType
49
137
  }, builder: (source: SourceType, outputs: AllOutputTypes) => NoInfer<TaskInputType>): Playlist<AllOutputTypes & { [K in IdentType] : Railroad<TaskOutputType> }, SourceType>;
138
+ /**
139
+ * Register a callback to run after the playlist finishes. Use this hook to
140
+ * react to the last task or to adjust machine state before a transition.
141
+ *
142
+ * Note: The callback receives the strongly-typed `outputs` object.
143
+ *
144
+ * @param finalizer - Callback executed once after all tasks complete.
145
+ * @returns This playlist for chaining.
146
+ */
50
147
  finally(finalizer: (source: SourceType, outputs: AllOutputTypes) => void | Promise<void>): this;
148
+ /**
149
+ * Execute all tasks in order, building each task's input via its builder
150
+ * and storing each result under the task's ident in the outputs map.
151
+ * If a task's `validateInput` returns false, execution stops with an error.
152
+ *
153
+ * @param source - The source object for this run (e.g., trigger event or machine state).
154
+ * @returns The aggregated, strongly-typed outputs map.
155
+ */
51
156
  run(source: SourceType): Promise<AllOutputTypes>;
52
157
  }
158
+ /**
159
+ * Event object produced by a `Trigger` and consumed by a `Workflow`.
160
+ *
161
+ * - `triggerIdent` lets a workflow that has multiple triggers disambiguate the source.
162
+ * - `data` is the trigger-specific payload (e.g., webhook body, file metadata, etc.).
163
+ *
164
+ * @template IdentType - Trigger identifier (use a string literal).
165
+ * @template T - Payload type emitted by this trigger.
166
+ */
53
167
  type TriggerEvent<
54
168
  IdentType extends string,
55
169
  T
@@ -57,6 +171,9 @@ type TriggerEvent<
57
171
  triggerIdent: IdentType
58
172
  data: T
59
173
  };
174
+ /**
175
+ * @internal Small FIFO queue used by Triggers to buffer events.
176
+ */
60
177
  declare class EventQueue<TEventType> {
61
178
  size: number;
62
179
  queue: TEventType[];
@@ -71,11 +188,46 @@ declare abstract class Trigger<
71
188
  > {
72
189
  readonly ident: IdentType;
73
190
  protected readonly queue: EventQueue<TriggerEvent<IdentType, TData>>;
191
+ /**
192
+ * Base class for event sources that feed Workflows.
193
+ * Implementations should acquire resources in `start` and release them in `stop`.
194
+ * Use `pushEvent` to enqueue new events; Workflows poll with `poll`.
195
+ *
196
+ * @param ident - Unique identifier for this trigger (use a string literal).
197
+ * @param queueSize - Max events buffered (oldest are dropped when at capacity). Default: 50.
198
+ */
74
199
  constructor(ident: IdentType, queueSize?: number);
200
+ /**
201
+ * Stop the trigger and release any resources.
202
+ */
75
203
  abstract stop(): Promise<void>;
204
+ /**
205
+ * Enqueue an event for consumption by a Workflow.
206
+ * Protected to avoid manual emission from outside trigger implementations.
207
+ *
208
+ * @param data - Event payload emitted by this trigger.
209
+ */
76
210
  protected pushEvent(data: TData): void;
211
+ /**
212
+ * Retrieve the next event in the buffer (or null if none available).
213
+ * Workflows call this in their polling loop.
214
+ */
77
215
  poll(): TriggerEvent<IdentType, TData> | null;
78
216
  }
217
+ /**
218
+ * Connects one or more `Trigger`s to a `Playlist`.
219
+ * When a trigger emits an event, the playlist runs with that event as `source`.
220
+ *
221
+ * - Add triggers with `addTrigger`.
222
+ * - Configure the playlist using `setPlaylist(p => p.addTask(...))`.
223
+ * - Start polling with `start`, optionally receiving a callback when a run completes.
224
+ *
225
+ * See README Code Examples for building a full workflow.
226
+ *
227
+ * @template AllTriggerEvents - Union of all trigger event shapes in this workflow.
228
+ * @template TAllOutputs - Aggregated outputs map shape produced by the playlist.
229
+ * @template TPlaylist - Concrete playlist type or `null` before configuration.
230
+ */
79
231
  declare class Workflow<
80
232
  AllTriggerEvents extends TriggerEvent<string, any>,
81
233
  TAllOutputs extends Record<string, any>,
@@ -84,24 +236,65 @@ declare class Workflow<
84
236
  playlist: TPlaylist;
85
237
  triggers: Trigger<string, any>[];
86
238
  constructor(triggers: Trigger<string, any>[], playlist: TPlaylist);
239
+ /**
240
+ * Register a new trigger to feed events into the workflow.
241
+ * The resulting workflow type widens its `AllTriggerEvents` union accordingly.
242
+ *
243
+ * @template TIdent - Trigger ident (string literal recommended).
244
+ * @template TData - Payload type emitted by the trigger.
245
+ * @param trigger - The trigger instance to add.
246
+ * @returns A new Workflow instance with updated event type.
247
+ */
87
248
  addTrigger<
88
249
  const TIdent extends string,
89
250
  TData
90
251
  >(trigger: Trigger<TIdent, TData>): Workflow<AllTriggerEvents | TriggerEvent<TIdent, TData>, TAllOutputs, Playlist<TAllOutputs, AllTriggerEvents | TriggerEvent<TIdent, TData>> | null>;
252
+ /**
253
+ * Configure the playlist by providing a builder that starts from an empty
254
+ * `Playlist<{}, AllTriggerEvents>` and returns your fully configured playlist.
255
+ *
256
+ * This method ensures type inference flows from your tasks and idents into
257
+ * the resulting `TBuilderOutputs` map used by the workflow's callback.
258
+ *
259
+ * @template TBuilderOutputs - Aggregated outputs map (deduced from your tasks).
260
+ * @template TFinalPlaylist - Concrete playlist type returned by the builder.
261
+ * @param builder - Receives an empty playlist and must return a configured one.
262
+ * @returns A new Workflow with the concrete playlist and output types.
263
+ */
91
264
  setPlaylist<
92
265
  TBuilderOutputs extends Record<string, any>,
93
266
  TFinalPlaylist extends Playlist<TBuilderOutputs, AllTriggerEvents>
94
267
  >(builder: (p: Playlist<{}, AllTriggerEvents>) => TFinalPlaylist): Workflow<AllTriggerEvents, TBuilderOutputs, TFinalPlaylist>;
268
+ /**
269
+ * Begin polling triggers and run the playlist whenever an event is available.
270
+ * The loop uses `setTimeout` with the given `interval` and returns immediately.
271
+ *
272
+ * @param interval - Polling interval in milliseconds (default 5000ms).
273
+ * @param callback - Optional callback executed after each successful playlist run.
274
+ * @throws If called before a playlist is configured.
275
+ */
95
276
  start({ interval, callback }?: {
96
277
  interval?: number
97
278
  callback?: (source: AllTriggerEvents, outputs: TAllOutputs) => any
98
279
  }): Promise<void>;
280
+ /**
281
+ * Create a new, empty workflow. Add triggers and set a playlist before starting.
282
+ */
99
283
  static create(): Workflow<never, {}, null>;
100
284
  }
101
285
  import { Logger } from "pino";
286
+ /**
287
+ * A weighted, conditional edge to a target state.
288
+ * Transitions are evaluated in descending `weight` order, then by insertion order.
289
+ *
290
+ * @template TStateData - Mutable state shared across the machine.
291
+ */
102
292
  type Transition<TStateData> = {
293
+ /** Target state node, resolved during `finalize`. */
103
294
  to: StateNode<TStateData> | null
295
+ /** Async predicate that decides whether to take this transition. */
104
296
  condition: (stateData: TStateData) => Promise<boolean>
297
+ /** Higher values are tried first; defaults to 0. */
105
298
  weight?: number
106
299
  };
107
300
  /**
package/dist/index.js CHANGED
@@ -397,26 +397,21 @@ class Machine {
397
397
  }
398
398
  retries++;
399
399
  }
400
- if (!next) {
401
- logger?.info({ phase: "end" }, "No next state after retries, run complete.");
402
- return stateData;
403
- }
404
400
  }
405
- if (next === this.initialState) {
401
+ const resolvedNext = next;
402
+ if (resolvedNext === this.initialState) {
406
403
  logger?.info({ phase: "end" }, "Next state is initial state, run complete.");
407
404
  return stateData;
408
405
  }
409
- logger?.info({ phase: "progress", from: current.ident, to: next.ident }, "Transitioning state.");
410
- current = next;
406
+ logger?.info({ phase: "progress", from: current.ident, to: resolvedNext.ident }, "Transitioning state.");
407
+ current = resolvedNext;
411
408
  await current.playlist.run(stateData);
412
- if (current.ident)
413
- visitedIdents.add(current.ident);
409
+ visitedIdents.add(current.ident);
414
410
  if (visitedIdents.size >= allStates.length) {
415
411
  logger?.info({ phase: "end" }, "All reachable states visited, run complete.");
416
412
  return stateData;
417
413
  }
418
414
  }
419
- return stateData;
420
415
  }
421
416
  }
422
417
  export {
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@fkws/klonk",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "A lightweight, extensible workflow automation engine for Node.js and Bun",
5
+ "repository": "https://github.com/klar-web-services/klonk",
5
6
  "module": "dist/index.js",
6
7
  "main": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
@@ -25,6 +26,9 @@
25
26
  "start": "bun index.ts",
26
27
  "build": "bunx rimraf dist && bunup",
27
28
  "prepublishOnly": "bun run build",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "test:coverage": "vitest run --coverage",
28
32
  "release": "bun scripts/release.js",
29
33
  "release:patch": "bun scripts/release.js patch",
30
34
  "release:minor": "bun scripts/release.js minor",
@@ -41,7 +45,9 @@
41
45
  "devDependencies": {
42
46
  "@types/bun": "latest",
43
47
  "@types/node": "latest",
44
- "bunup": "^0.6.2"
48
+ "@vitest/coverage-v8": "^4.0.3",
49
+ "bunup": "^0.6.2",
50
+ "vitest": "^4.0.3"
45
51
  },
46
52
  "peerDependencies": {
47
53
  "typescript": "^5"