@kaleido-io/workflow-engine-sdk 0.0.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1121 -0
  3. package/bin/init.js +200 -0
  4. package/bin/wesdk.js +57 -0
  5. package/dist/package.json +3 -0
  6. package/dist/src/client/client.d.ts +24 -0
  7. package/dist/src/client/client.d.ts.map +1 -0
  8. package/dist/src/client/client.js +58 -0
  9. package/dist/src/client/client.js.map +1 -0
  10. package/dist/src/client/rest-client.d.ts +222 -0
  11. package/dist/src/client/rest-client.d.ts.map +1 -0
  12. package/dist/src/client/rest-client.js +242 -0
  13. package/dist/src/client/rest-client.js.map +1 -0
  14. package/dist/src/config/config.d.ts +60 -0
  15. package/dist/src/config/config.d.ts.map +1 -0
  16. package/dist/src/config/config.js +117 -0
  17. package/dist/src/config/config.js.map +1 -0
  18. package/dist/src/factories/event_source.d.ts +54 -0
  19. package/dist/src/factories/event_source.d.ts.map +1 -0
  20. package/dist/src/factories/event_source.js +170 -0
  21. package/dist/src/factories/event_source.js.map +1 -0
  22. package/dist/src/factories/transaction_handler.d.ts +27 -0
  23. package/dist/src/factories/transaction_handler.d.ts.map +1 -0
  24. package/dist/src/factories/transaction_handler.js +66 -0
  25. package/dist/src/factories/transaction_handler.js.map +1 -0
  26. package/dist/src/helpers/stage_director.d.ts +42 -0
  27. package/dist/src/helpers/stage_director.d.ts.map +1 -0
  28. package/dist/src/helpers/stage_director.js +304 -0
  29. package/dist/src/helpers/stage_director.js.map +1 -0
  30. package/dist/src/i18n/errors.d.ts +61 -0
  31. package/dist/src/i18n/errors.d.ts.map +1 -0
  32. package/dist/src/i18n/errors.js +90 -0
  33. package/dist/src/i18n/errors.js.map +1 -0
  34. package/dist/src/index.d.ts +20 -0
  35. package/dist/src/index.d.ts.map +1 -0
  36. package/dist/src/index.js +85 -0
  37. package/dist/src/index.js.map +1 -0
  38. package/dist/src/interfaces/handlers.d.ts +112 -0
  39. package/dist/src/interfaces/handlers.d.ts.map +1 -0
  40. package/dist/src/interfaces/handlers.js +18 -0
  41. package/dist/src/interfaces/handlers.js.map +1 -0
  42. package/dist/src/interfaces/messages.d.ts +6 -0
  43. package/dist/src/interfaces/messages.d.ts.map +1 -0
  44. package/dist/src/interfaces/messages.js +18 -0
  45. package/dist/src/interfaces/messages.js.map +1 -0
  46. package/dist/src/log/logger.d.ts +8 -0
  47. package/dist/src/log/logger.d.ts.map +1 -0
  48. package/dist/src/log/logger.js +39 -0
  49. package/dist/src/log/logger.js.map +1 -0
  50. package/dist/src/runtime/engine_client.d.ts +37 -0
  51. package/dist/src/runtime/engine_client.d.ts.map +1 -0
  52. package/dist/src/runtime/engine_client.js +99 -0
  53. package/dist/src/runtime/engine_client.js.map +1 -0
  54. package/dist/src/runtime/handler_runtime.d.ts +124 -0
  55. package/dist/src/runtime/handler_runtime.d.ts.map +1 -0
  56. package/dist/src/runtime/handler_runtime.js +623 -0
  57. package/dist/src/runtime/handler_runtime.js.map +1 -0
  58. package/dist/src/types/core.d.ts +258 -0
  59. package/dist/src/types/core.d.ts.map +1 -0
  60. package/dist/src/types/core.js +71 -0
  61. package/dist/src/types/core.js.map +1 -0
  62. package/dist/src/types/flows.d.ts +144 -0
  63. package/dist/src/types/flows.d.ts.map +1 -0
  64. package/dist/src/types/flows.js +30 -0
  65. package/dist/src/types/flows.js.map +1 -0
  66. package/dist/src/utils/errors.d.ts +2 -0
  67. package/dist/src/utils/errors.d.ts.map +1 -0
  68. package/dist/src/utils/errors.js +23 -0
  69. package/dist/src/utils/errors.js.map +1 -0
  70. package/dist/src/utils/patch.d.ts +9 -0
  71. package/dist/src/utils/patch.d.ts.map +1 -0
  72. package/dist/src/utils/patch.js +99 -0
  73. package/dist/src/utils/patch.js.map +1 -0
  74. package/dist-esm/src/client/client.js +54 -0
  75. package/dist-esm/src/client/client.js.map +1 -0
  76. package/dist-esm/src/client/rest-client.js +238 -0
  77. package/dist-esm/src/client/rest-client.js.map +1 -0
  78. package/dist-esm/src/config/config.js +113 -0
  79. package/dist-esm/src/config/config.js.map +1 -0
  80. package/dist-esm/src/factories/event_source.js +167 -0
  81. package/dist-esm/src/factories/event_source.js.map +1 -0
  82. package/dist-esm/src/factories/transaction_handler.js +63 -0
  83. package/dist-esm/src/factories/transaction_handler.js.map +1 -0
  84. package/dist-esm/src/helpers/stage_director.js +298 -0
  85. package/dist-esm/src/helpers/stage_director.js.map +1 -0
  86. package/dist-esm/src/i18n/errors.js +85 -0
  87. package/dist-esm/src/i18n/errors.js.map +1 -0
  88. package/dist-esm/src/index.js +51 -0
  89. package/dist-esm/src/index.js.map +1 -0
  90. package/dist-esm/src/interfaces/handlers.js +17 -0
  91. package/dist-esm/src/interfaces/handlers.js.map +1 -0
  92. package/dist-esm/src/interfaces/messages.js +17 -0
  93. package/dist-esm/src/interfaces/messages.js.map +1 -0
  94. package/dist-esm/src/log/logger.js +36 -0
  95. package/dist-esm/src/log/logger.js.map +1 -0
  96. package/dist-esm/src/runtime/engine_client.js +95 -0
  97. package/dist-esm/src/runtime/engine_client.js.map +1 -0
  98. package/dist-esm/src/runtime/handler_runtime.js +586 -0
  99. package/dist-esm/src/runtime/handler_runtime.js.map +1 -0
  100. package/dist-esm/src/types/core.js +68 -0
  101. package/dist-esm/src/types/core.js.map +1 -0
  102. package/dist-esm/src/types/flows.js +27 -0
  103. package/dist-esm/src/types/flows.js.map +1 -0
  104. package/dist-esm/src/utils/errors.js +19 -0
  105. package/dist-esm/src/utils/errors.js.map +1 -0
  106. package/dist-esm/src/utils/patch.js +56 -0
  107. package/dist-esm/src/utils/patch.js.map +1 -0
  108. package/package.json +79 -11
  109. package/template/.env.sample +14 -0
  110. package/template/.vscode/launch.json +23 -0
  111. package/template/README.md +37 -0
  112. package/template/package.json +36 -0
  113. package/template/src/connect.ts +58 -0
  114. package/template/src/provider.ts +24 -0
  115. package/template/src/samples/event-source/README.md +50 -0
  116. package/template/src/samples/event-source/echo-handler.ts +65 -0
  117. package/template/src/samples/event-source/event-processor.ts +46 -0
  118. package/template/src/samples/event-source/event-source.ts +70 -0
  119. package/template/src/samples/event-source/stream.ts +34 -0
  120. package/template/src/samples/hello/README.md +52 -0
  121. package/template/src/samples/hello/flow.ts +74 -0
  122. package/template/src/samples/hello/handlers.test.ts +147 -0
  123. package/template/src/samples/hello/handlers.ts +72 -0
  124. package/template/src/samples/hello/transaction.ts +24 -0
  125. package/template/src/samples/http-invoke/README.md +42 -0
  126. package/template/src/samples/http-invoke/flow.ts +63 -0
  127. package/template/src/samples/http-invoke/handlers.ts +66 -0
  128. package/template/src/samples/http-invoke/transaction.ts +22 -0
  129. package/template/src/samples/snap/README.md +98 -0
  130. package/template/src/samples/snap/event-source.ts +104 -0
  131. package/template/src/samples/snap/flow.ts +85 -0
  132. package/template/src/samples/snap/snap-handler.ts +84 -0
  133. package/template/src/samples/snap/stream.ts +26 -0
  134. package/template/src/samples/snap/transaction.ts +26 -0
  135. package/template/src/utils/post-stream.ts +67 -0
  136. package/template/src/utils/post-transaction.ts +64 -0
  137. package/template/src/utils/post-workflow.ts +63 -0
  138. package/template/tsconfig.json +24 -0
  139. package/template/vitest.config.ts +42 -0
  140. package/CODEOWNERS +0 -5
package/README.md ADDED
@@ -0,0 +1,1121 @@
1
+ # Kaleido Workflow Engine TypeScript SDK
2
+
3
+ A TypeScript SDK for building handlers that integrate with the Kaleido workflow engine. Build transaction handlers, event sources, and event processors that participate in workflows with full type safety and automatic reconnection.
4
+
5
+ ## Quick start
6
+
7
+ ### Installation
8
+
9
+ ```bash
10
+ npm install @kaleido-io/workflow-engine-sdk
11
+ ```
12
+
13
+ ### Create a new project
14
+
15
+ To get up and running with a sample project, you can use:
16
+
17
+ ```bash
18
+ npx @kaleido-io/workflow-engine-sdk init <project-name>
19
+ ```
20
+
21
+ This will create a new project in a directory named for project-name, and in a few short steps it can be up and connecting in to your Kaleido workflow engine.
22
+
23
+ ### Integrating into an existing project
24
+
25
+ ```typescript
26
+ import {
27
+ WorkflowEngineClient,
28
+ ConfigLoader,
29
+ WorkflowEngineConfig,
30
+ newDirectedTransactionHandler,
31
+ InvocationMode,
32
+ EvalResult
33
+ } from '@kaleido-io/workflow-engine-sdk';
34
+ import * as fs from 'fs';
35
+ import * as yaml from 'js-yaml';
36
+
37
+ // 1. Load configuration (your application handles file loading)
38
+ const configFile = fs.readFileSync('./config.yaml', 'utf8');
39
+ const config: WorkflowEngineConfig = yaml.load(configFile) as WorkflowEngineConfig;
40
+
41
+ // 2. Use SDK's ConfigLoader to create client config with your provider name
42
+ // The SDK handles authentication header setup and URL conversion automatically
43
+ const clientConfig = ConfigLoader.createClientConfig(config, 'my-service');
44
+
45
+ // 3. Create client
46
+ const client = new WorkflowEngineClient(clientConfig);
47
+
48
+ // 4. Create and register transaction handler
49
+ const actionMap = new Map([
50
+ ['myAction', {
51
+ invocationMode: InvocationMode.PARALLEL,
52
+ handler: async (transaction, input) => {
53
+ return {
54
+ result: EvalResult.COMPLETE,
55
+ output: { success: true }
56
+ };
57
+ }
58
+ }]
59
+ ]);
60
+
61
+ const handler = newDirectedTransactionHandler('my-handler', actionMap);
62
+ client.registerTransactionHandler('my-handler', handler);
63
+
64
+ // 5. Connect
65
+ await client.connect();
66
+ ```
67
+
68
+ ## Core concepts
69
+
70
+ ### WorkflowEngineClient
71
+
72
+ The main entry point that manages:
73
+ - Handler registration (transaction handlers and event sources)
74
+ - WebSocket connection lifecycle
75
+ - Automatic reconnection and re-registration
76
+ - Message routing between engine and handlers
77
+
78
+ ```typescript
79
+ const client = new WorkflowEngineClient({
80
+ url: 'ws://localhost:5503/ws',
81
+ providerName: 'my-service',
82
+ authToken: 'your-token',
83
+ authHeaderName: 'X-Kld-Authz', // Optional, defaults to X-Kld-Authz
84
+ reconnectDelay: 2000, // Optional, ms between reconnect attempts
85
+ maxAttempts: undefined // Optional, undefined = infinite retries (recommended)
86
+ });
87
+
88
+ // Register handlers
89
+ client.registerTransactionHandler('handler-name', transactionHandler);
90
+ client.registerEventSource('source-name', eventSource);
91
+
92
+ // Connect
93
+ await client.connect();
94
+
95
+ // Check connection status
96
+ if (client.isConnected()) {
97
+ console.log('Connected!');
98
+ }
99
+
100
+ // Disconnect
101
+ client.disconnect();
102
+ ```
103
+
104
+ ### Configuration file format
105
+
106
+ If you choose to use YAML files, create a configuration file like this:
107
+
108
+ ```yaml
109
+ # Basic authentication (username/password)
110
+ workflowEngine:
111
+ url: http://localhost:5503
112
+ auth:
113
+ type: basic
114
+ username: my-user
115
+ password: my-password
116
+ # maxRetries: undefined = infinite reconnection (recommended)
117
+ # maxRetries: 5 # Optional: limit reconnection attempts
118
+ retryDelay: 2s
119
+ timeout: 30s
120
+ batchSize: 10
121
+ batchTimeout: 500ms
122
+ pollDuration: 2s
123
+ ```
124
+
125
+ **Or use token authentication:**
126
+
127
+ ```yaml
128
+ # Token authentication (API key, JWT, etc.)
129
+ workflowEngine:
130
+ url: http://localhost:5503
131
+ auth:
132
+ type: token
133
+ token: dev-token-123
134
+ header: X-Kld-Authz # Optional, defaults to Authorization
135
+ scheme: "" # Optional, e.g. "Bearer" for "Bearer <token>"
136
+ # maxRetries: undefined = infinite reconnection (recommended for long-running services)
137
+ retryDelay: 2s
138
+ ```
139
+
140
+ Load and use configuration:
141
+
142
+ ```typescript
143
+ import {
144
+ ConfigLoader,
145
+ WorkflowEngineConfig
146
+ } from '@kaleido-io/workflow-engine-sdk';
147
+ import * as fs from 'fs';
148
+ import * as yaml from 'js-yaml';
149
+
150
+ // Your application loads configuration (the SDK doesn't load files)
151
+ const configFile = fs.readFileSync('./config.yaml', 'utf8');
152
+ const config: WorkflowEngineConfig = yaml.load(configFile) as WorkflowEngineConfig;
153
+
154
+ // Use SDK's ConfigLoader to create client config with provider name (REQUIRED)
155
+ // Note: SDK automatically converts http:// to ws:// and adds /ws path
156
+ const clientConfig = ConfigLoader.createClientConfig(config, 'my-service');
157
+
158
+ // Optionally log summary (without sensitive data)
159
+ ConfigLoader.logConfigSummary(config);
160
+
161
+ // Create client
162
+ const client = new WorkflowEngineClient(clientConfig);
163
+ ```
164
+
165
+ **URL Handling:**
166
+ - Config file uses HTTP URL: `http://localhost:5503` or `https://example.com`
167
+ - SDK automatically converts to WebSocket: `ws://localhost:5503/ws` or `wss://example.com/ws`
168
+ - `/ws` path is automatically added if not present
169
+
170
+ ### Configuration Schema
171
+
172
+ ```typescript
173
+ interface WorkflowEngineConfig {
174
+ workflowEngine: {
175
+ mode?: HandlerRuntimeMode; // Defaults to outbound
176
+ port?: number; // port used for the web socket server in inbound mode
177
+ url?: string; // Workflow engine URL
178
+ auth?: AuthConfig; // Authentication (see below)
179
+ timeout?: string; // Request timeout (e.g. "30s")
180
+ maxRetries?: number; // Max reconnection attempts (undefined = infinite)
181
+ retryDelay?: string; // Delay between retries (e.g. "2s")
182
+ batchSize?: number; // Batch size for handlers
183
+ batchTimeout?: string; // Batch timeout (e.g. "500ms")
184
+ pollDuration?: string; // Event source poll duration
185
+ };
186
+ }
187
+
188
+ // Authentication types
189
+ type AuthConfig = BasicAuth | TokenAuth;
190
+
191
+ interface BasicAuth {
192
+ type: 'basic'; // Must be 'basic'
193
+ username: string; // Username
194
+ password: string; // Password
195
+ }
196
+
197
+ interface TokenAuth {
198
+ type: 'token'; // Must be 'token'
199
+ token: string; // API token
200
+ header?: string; // Header name (default: 'Authorization')
201
+ scheme?: string; // Scheme (e.g. 'Bearer', default: '')
202
+ }
203
+ ```
204
+
205
+ ### Configuration examples
206
+
207
+ **Outbound, basic auth:**
208
+ ```yaml
209
+ workflowEngine:
210
+ url: http://localhost:5503
211
+ auth:
212
+ type: basic
213
+ username: admin
214
+ password: secret123
215
+ ```
216
+
217
+ **Outbound, token auth (raw token):**
218
+ ```yaml
219
+ workflowEngine:
220
+ url: http://localhost:5503
221
+ auth:
222
+ type: token
223
+ token: dev-token-123
224
+ header: X-Kld-Authz
225
+ scheme: "" # Empty string = raw token
226
+ ```
227
+
228
+ **Outbound, token auth (bearer token):**
229
+ ```yaml
230
+ workflowEngine:
231
+ url: http://localhost:5503
232
+ auth:
233
+ type: token
234
+ token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
235
+ scheme: Bearer # Sends "Bearer <token>"
236
+ ```
237
+
238
+ **Inbound:**
239
+
240
+ The client will wait for an inbound connection from the workflow engine
241
+ ```yaml
242
+ workflowEngine:
243
+ mode: inbound
244
+ port: 12345
245
+ ```
246
+
247
+ **With environment variable overrides:**
248
+ ```typescript
249
+ import * as fs from 'fs';
250
+ import * as yaml from 'js-yaml';
251
+ import { ConfigLoader, WorkflowEngineConfig } from '@kaleido-io/workflow-engine-sdk';
252
+
253
+ // Your application loads and merges config with env vars
254
+ const configFile = fs.readFileSync('./config.yaml', 'utf8');
255
+ const config: WorkflowEngineConfig = yaml.load(configFile) as WorkflowEngineConfig;
256
+
257
+ // Override URL from environment
258
+ if (process.env.WORKFLOW_ENGINE_URL) {
259
+ config.workflowEngine.url = process.env.WORKFLOW_ENGINE_URL;
260
+ }
261
+
262
+ // Override token from environment
263
+ if (process.env.WORKFLOW_ENGINE_TOKEN &&
264
+ config.workflowEngine.auth.type === 'token') {
265
+ config.workflowEngine.auth.token = process.env.WORKFLOW_ENGINE_TOKEN;
266
+ }
267
+
268
+ // SDK transforms config into client config
269
+ const clientConfig = ConfigLoader.createClientConfig(config, 'my-service');
270
+ ```
271
+
272
+ ## Transaction handlers
273
+
274
+ ### Using the factory pattern
275
+
276
+ The recommended approach for building transaction handlers:
277
+
278
+ ```typescript
279
+ import {
280
+ newDirectedTransactionHandler,
281
+ InvocationMode,
282
+ EvalResult,
283
+ Patch
284
+ } from '@kaleido-io/workflow-engine-sdk';
285
+
286
+ // Define your input type
287
+ interface MyInput {
288
+ action: string;
289
+ data: string;
290
+ }
291
+
292
+ // Create action map
293
+ const actionMap = new Map([
294
+ ['processData', {
295
+ invocationMode: InvocationMode.PARALLEL,
296
+ handler: async (transaction, input: MyInput) => {
297
+ // Process the data
298
+ const result = processData(input.data);
299
+
300
+ return {
301
+ result: EvalResult.COMPLETE,
302
+ output: { processed: result },
303
+ extraUpdates: [
304
+ Patch.add('/processedData', result)
305
+ ]
306
+ };
307
+ }
308
+ }],
309
+
310
+ ['batchProcess', {
311
+ invocationMode: InvocationMode.BATCH,
312
+ batchHandler: async (transactions) => {
313
+ // Process all transactions together
314
+ const results = await processBatch(transactions.map(r => r.value));
315
+
316
+ return results.map(result => ({
317
+ result: EvalResult.COMPLETE,
318
+ output: result
319
+ }));
320
+ }
321
+ }]
322
+ ]);
323
+
324
+ // Create handler
325
+ const handler = newDirectedTransactionHandler('my-handler', actionMap)
326
+ .withInitFn(async (engAPI) => {
327
+ // Initialize resources
328
+ console.log('Handler initialized');
329
+ })
330
+ .withCloseFn(() => {
331
+ // Cleanup resources
332
+ console.log('Handler closed');
333
+ });
334
+
335
+ client.registerTransactionHandler('my-handler', handler);
336
+ ```
337
+
338
+ ### Invocation modes
339
+
340
+ **PARALLEL**: Each transaction processed independently in parallel
341
+ ```typescript
342
+ {
343
+ invocationMode: InvocationMode.PARALLEL,
344
+ handler: async (transaction, input) => {
345
+ // Process single transaction
346
+ return { result: EvalResult.COMPLETE };
347
+ }
348
+ }
349
+ ```
350
+
351
+ **BATCH**: All transactions in batch processed together
352
+ ```typescript
353
+ {
354
+ invocationMode: InvocationMode.BATCH,
355
+ batchHandler: async (transactions) => {
356
+ // Process all transactions at once
357
+ const results = await batchProcess(transactions);
358
+ return results;
359
+ }
360
+ }
361
+ ```
362
+
363
+ ### Eval results
364
+
365
+ Return appropriate result based on outcome:
366
+
367
+ - `EvalResult.COMPLETE` - Success, proceed to next stage
368
+ - `EvalResult.WAITING` - Stay in current stage (waiting for event)
369
+ - `EvalResult.FIXABLE_ERROR` - Retry later
370
+ - `EvalResult.TRANSIENT_ERROR` - Temporary error, retry
371
+ - `EvalResult.HARD_FAILURE` - Permanent failure, go to failure stage
372
+
373
+ ### State updates
374
+
375
+ Use JSON Patch operations to update workflow state:
376
+
377
+ ```typescript
378
+ import { Patch } from '@kaleido-io/workflow-engine-sdk';
379
+
380
+ return {
381
+ result: EvalResult.COMPLETE,
382
+ stateUpdates: [
383
+ Patch.add('/newField', 'value'),
384
+ Patch.replace('/existingField', 'newValue'),
385
+ Patch.remove('/oldField'),
386
+ Patch.add('/array/-', 'append to array')
387
+ ]
388
+ };
389
+ ```
390
+
391
+ ### Custom stage transitions
392
+
393
+ Override the default next stage:
394
+
395
+ ```typescript
396
+ return {
397
+ result: EvalResult.COMPLETE,
398
+ customStage: 'custom-next-stage', // Override default nextStage
399
+ output: { data: 'result' }
400
+ };
401
+ ```
402
+
403
+ ### Triggers
404
+
405
+ Emit events to trigger other workflows:
406
+
407
+ ```typescript
408
+ return {
409
+ result: EvalResult.COMPLETE,
410
+ triggers: [
411
+ { topic: 'user.created' },
412
+ { topic: 'notification.send', ephemeral: true }
413
+ ]
414
+ };
415
+ ```
416
+
417
+ ### Handler events
418
+
419
+ Emit events directly from handlers:
420
+
421
+ ```typescript
422
+ return {
423
+ result: EvalResult.COMPLETE,
424
+ events: [
425
+ { topic: 'something-happened', data: {} }
426
+ ]
427
+ };
428
+ ```
429
+
430
+ ## Event sources
431
+
432
+ Event sources poll external systems and emit events to the workflow engine.
433
+
434
+ ### Creating an Event Source
435
+
436
+ ```typescript
437
+ import { newEventSource } from '@kaleido-io/workflow-engine-sdk';
438
+
439
+ // Define your types
440
+ interface MyCheckpoint {
441
+ lastId: number;
442
+ }
443
+
444
+ interface MyConfig {
445
+ topic: string;
446
+ pollInterval: number;
447
+ }
448
+
449
+ interface MyEventData {
450
+ id: number;
451
+ data: string;
452
+ }
453
+
454
+ // Create event source
455
+ const eventSource = newEventSource<MyCheckpoint, MyConfig, MyEventData>(
456
+ 'my-event-source',
457
+ async (config, checkpointIn) => {
458
+ // Poll for events
459
+ const events = await fetchNewEvents(
460
+ config.config.topic,
461
+ checkpointIn?.lastId || 0
462
+ );
463
+
464
+ // Return checkpoint and events
465
+ return {
466
+ checkpointOut: {
467
+ lastId: events[events.length - 1]?.id || checkpointIn?.lastId || 0
468
+ },
469
+ events: events.map(e => ({
470
+ idempotencyKey: `event-${e.id}`,
471
+ topic: config.config.topic,
472
+ data: e
473
+ }))
474
+ };
475
+ }
476
+ )
477
+ .withInitialCheckpoint(async (config) => {
478
+ // Build initial checkpoint
479
+ return { lastId: 0 };
480
+ })
481
+ .withConfigParser(async (info, configData) => {
482
+ // Parse and validate config
483
+ const config = configData as MyConfig;
484
+ if (!config.topic) {
485
+ throw new Error('topic is required');
486
+ }
487
+ return config;
488
+ })
489
+ .withDeleteFn(async (info) => {
490
+ // Cleanup on deletion
491
+ console.log(`Deleting event source: ${info.streamName}`);
492
+ })
493
+ .withInitFn(async (engAPI) => {
494
+ // Initialize resources
495
+ console.log('Event source initialized');
496
+ })
497
+ .withCloseFn(() => {
498
+ // Cleanup resources
499
+ console.log('Event source closed');
500
+ });
501
+
502
+ // Register event source
503
+ client.registerEventSource('my-event-source', eventSource);
504
+ ```
505
+
506
+ ### Event source lifecycle
507
+
508
+ 1. **Validation**: `withConfigParser` validates stream configuration
509
+ 2. **Initial checkpoint**: `withInitialCheckpoint` creates starting point
510
+ 3. **Polling**: Poll function called repeatedly to fetch events
511
+ 4. **Checkpoint update**: Checkpoint saved after each successful poll
512
+ 5. **Resumption**: On restart, polling resumes from last checkpoint
513
+
514
+ ### Real-world example: stellar ledgers
515
+
516
+ ```typescript
517
+ interface StellarBlockCheckpoint {
518
+ lastLedger: number;
519
+ }
520
+
521
+ interface StellarBlockConfig {
522
+ topic: string;
523
+ fromLedger?: string;
524
+ batchSize?: number;
525
+ }
526
+
527
+ interface MinimalLedger {
528
+ sequence: number;
529
+ hash: string;
530
+ closedAt: string;
531
+ }
532
+
533
+ const stellarBlocks = newEventSource<
534
+ StellarBlockCheckpoint,
535
+ StellarBlockConfig,
536
+ MinimalLedger
537
+ >(
538
+ 'stellarBlocks',
539
+ async (config, checkpointIn) => {
540
+ const startLedger = checkpointIn ? checkpointIn.lastLedger + 1 : await getLatestLedger();
541
+ const batchSize = config.config.batchSize || 10;
542
+
543
+ const events = [];
544
+ let newCheckpoint = startLedger - 1;
545
+
546
+ for (let i = 0; i < batchSize; i++) {
547
+ try {
548
+ const ledger = await fetchLedger(startLedger + i);
549
+ events.push({
550
+ idempotencyKey: ledger.hash,
551
+ topic: config.config.topic,
552
+ data: {
553
+ sequence: ledger.sequence,
554
+ hash: ledger.hash,
555
+ closedAt: ledger.closed_at
556
+ }
557
+ });
558
+ newCheckpoint = ledger.sequence;
559
+ } catch (error) {
560
+ break; // Ledger not yet available
561
+ }
562
+ }
563
+
564
+ return {
565
+ checkpointOut: { lastLedger: newCheckpoint },
566
+ events
567
+ };
568
+ }
569
+ )
570
+ .withInitialCheckpoint(async (config) => {
571
+ const ledgerNum = config.fromLedger === 'latest'
572
+ ? await getLatestLedger()
573
+ : parseInt(config.fromLedger || '0', 10);
574
+ return { lastLedger: ledgerNum };
575
+ })
576
+ .withConfigParser(async (info, configData) => {
577
+ const config = configData as StellarBlockConfig;
578
+ if (!config.topic) {
579
+ throw new Error('topic is required');
580
+ }
581
+ return config;
582
+ });
583
+ ```
584
+
585
+ ### Creating event streams
586
+
587
+ Event streams connect event sources to workflows:
588
+
589
+ ```bash
590
+ curl -X PUT http://localhost:5503/api/v1/streams/my-stream \
591
+ -H "Content-Type: application/json" \
592
+ -H "X-Kld-Authz: dev-token-123" \
593
+ -d '{
594
+ "name": "my-stream",
595
+ "started": true,
596
+ "type": "correlation_stream",
597
+ "listenerHandler": "my-event-source",
598
+ "listenerHandlerProvider": "my-service",
599
+ "config": {
600
+ "topic": "my-topic",
601
+ "pollInterval": 1000
602
+ }
603
+ }'
604
+ ```
605
+
606
+ ## EngineAPI
607
+
608
+ The `EngineAPI` interface allows handlers to make synchronous API calls back to the workflow engine during transaction processing.
609
+
610
+ ### Submitting Async Transactions
611
+
612
+ ```typescript
613
+ async function myHandler(transaction, input, engAPI: EngineAPI) {
614
+ // Submit transactions to the engine
615
+ const results = await engAPI.submitAsyncTransactions(
616
+ transaction.authRef,
617
+ [
618
+ {
619
+ workflowId: 'flw:abc123',
620
+ operation: 'process',
621
+ input: { data: 'value' }
622
+ }
623
+ ]
624
+ );
625
+
626
+ return {
627
+ result: EvalResult.COMPLETE,
628
+ output: { submittedTxs: results }
629
+ };
630
+ }
631
+ ```
632
+
633
+ ## StageDirector pattern
634
+
635
+ For workflows with action-based routing and automatic stage transitions:
636
+
637
+ ```typescript
638
+ import { BasicStageDirector, WithStageDirector } from '@kaleido-io/workflow-engine-sdk';
639
+
640
+ interface MyInput extends WithStageDirector {
641
+ data: string;
642
+ }
643
+
644
+ class MyInputImpl implements MyInput {
645
+ public stageDirector: BasicStageDirector;
646
+ public data: string;
647
+
648
+ constructor(input: any) {
649
+ this.stageDirector = new BasicStageDirector(
650
+ input.action, // Action to execute
651
+ input.outputPath, // Where to store output
652
+ input.nextStage, // Stage on success
653
+ input.failureStage // Stage on failure
654
+ );
655
+ this.data = input.data;
656
+ }
657
+
658
+ getStageDirector() {
659
+ return this.stageDirector;
660
+ }
661
+ }
662
+
663
+ // The SDK automatically wraps plain JSON objects from the engine
664
+ // with a getStageDirector() method, so you can also use plain objects:
665
+ const actionMap = new Map([
666
+ ['myAction', {
667
+ invocationMode: InvocationMode.PARALLEL,
668
+ handler: async (transaction, input: any) => {
669
+ // input.action, input.outputPath, input.nextStage are available
670
+ return {
671
+ result: EvalResult.COMPLETE,
672
+ output: { processed: input.data }
673
+ };
674
+ }
675
+ }]
676
+ ]);
677
+ ```
678
+
679
+ ## Error handling
680
+
681
+ ### Handler errors
682
+
683
+ Return appropriate error results:
684
+
685
+ ```typescript
686
+ handler: async (transaction, input) => {
687
+ try {
688
+ const result = await riskyOperation(input);
689
+ return {
690
+ result: EvalResult.COMPLETE,
691
+ output: result
692
+ };
693
+ } catch (error) {
694
+ if (isTransient(error)) {
695
+ return {
696
+ result: EvalResult.TRANSIENT_ERROR,
697
+ error: error as Error
698
+ };
699
+ } else {
700
+ return {
701
+ result: EvalResult.HARD_FAILURE,
702
+ error: error as Error
703
+ };
704
+ }
705
+ }
706
+ }
707
+ ```
708
+
709
+ ### Connection errors
710
+
711
+ The client automatically handles:
712
+ - WebSocket disconnections
713
+ - Automatic reconnection with exponential backoff
714
+ - Handler re-registration on reconnect
715
+ - Connection health monitoring
716
+
717
+ Monitor connection events:
718
+
719
+ ```typescript
720
+ // The SDK logs connection events automatically
721
+ // Check connection status programmatically:
722
+ if (!client.isConnected()) {
723
+ console.warn('Client disconnected, will auto-reconnect');
724
+ }
725
+ ```
726
+
727
+ ## Logging
728
+
729
+ The SDK uses a structured logger:
730
+
731
+ ```typescript
732
+ import { newLogger } from '@kaleido-io/workflow-engine-sdk';
733
+
734
+ const log = newLogger('my-component');
735
+
736
+ log.debug('Debug message', { metadata: 'value' });
737
+ log.info('Info message', { userId: 123 });
738
+ log.warn('Warning message', { reason: 'low memory' });
739
+ log.error('Error message', { error: err.message });
740
+ ```
741
+
742
+ ## Testing
743
+
744
+ ### Unit tests
745
+
746
+ Mock the EngineAPI and test handlers in isolation:
747
+
748
+ ```typescript
749
+ import { jest } from '@jest/globals';
750
+
751
+ describe('MyHandler', () => {
752
+ it('should process data correctly', async () => {
753
+ const mockEngAPI = {
754
+ submitAsyncTransactions: jest.fn().mockResolvedValue([])
755
+ };
756
+
757
+ const transaction = {
758
+ transactionId: 'ftx:test123',
759
+ workflowId: 'flw:test',
760
+ input: { action: 'process', data: 'test' }
761
+ };
762
+
763
+ const result = await myHandler(transaction, transaction.input, mockEngAPI);
764
+
765
+ expect(result.result).toBe(EvalResult.COMPLETE);
766
+ expect(result.output).toBeDefined();
767
+ });
768
+ });
769
+ ```
770
+
771
+ ### Component tests
772
+
773
+ Test with a running workflow engine:
774
+
775
+ ```typescript
776
+ import {
777
+ WorkflowEngineClient,
778
+ ConfigLoader,
779
+ WorkflowEngineConfig
780
+ } from '@kaleido-io/workflow-engine-sdk';
781
+ import * as fs from 'fs';
782
+ import * as yaml from 'js-yaml';
783
+
784
+ // Helper to load test config (your test infrastructure)
785
+ function loadTestConfig(): WorkflowEngineConfig {
786
+ const configFile = fs.readFileSync('./test-config.yaml', 'utf8');
787
+ const config: WorkflowEngineConfig = yaml.load(configFile) as WorkflowEngineConfig;
788
+
789
+ // Override with environment variables if present
790
+ if (process.env.WORKFLOW_ENGINE_URL) {
791
+ config.workflowEngine.url = process.env.WORKFLOW_ENGINE_URL;
792
+ }
793
+
794
+ return config;
795
+ }
796
+
797
+ describe('Component Test', () => {
798
+ let client: WorkflowEngineClient;
799
+ const testConfig = loadTestConfig();
800
+
801
+ beforeAll(async () => {
802
+ // Use SDK's ConfigLoader to transform config
803
+ const clientConfig = ConfigLoader.createClientConfig(testConfig, 'test-provider');
804
+ client = new WorkflowEngineClient(clientConfig);
805
+
806
+ client.registerTransactionHandler('my-handler', handler);
807
+ await client.connect();
808
+ });
809
+
810
+ afterAll(() => {
811
+ client.disconnect();
812
+ });
813
+
814
+ it('should process workflow end-to-end', async () => {
815
+ // For REST API calls, extract auth headers from SDK config
816
+ function getAuthHeaders(): Record<string, string> {
817
+ const clientConfig = ConfigLoader.createClientConfig(testConfig, 'test-client');
818
+ return clientConfig.options?.headers || {};
819
+ }
820
+
821
+ const authHeaders = getAuthHeaders();
822
+
823
+ // Create workflow
824
+ const workflowResponse = await fetch('http://localhost:5503/api/v1/workflows', {
825
+ method: 'POST',
826
+ headers: {
827
+ 'Content-Type': 'application/x-yaml',
828
+ ...authHeaders // SDK handles auth automatically
829
+ },
830
+ body: workflowYAML
831
+ });
832
+
833
+ // Wait for completion and verify results
834
+ });
835
+ });
836
+ ```
837
+
838
+ ## Examples
839
+
840
+ ### Complete transaction handler example
841
+
842
+ ```typescript
843
+ import {
844
+ WorkflowEngineClient,
845
+ WorkflowEngineConfig,
846
+ newDirectedTransactionHandler,
847
+ InvocationMode,
848
+ EvalResult,
849
+ Patch,
850
+ ConfigLoader
851
+ } from '@kaleido-io/workflow-engine-sdk';
852
+ import * as fs from 'fs';
853
+ import * as yaml from 'js-yaml';
854
+
855
+ interface ProcessInput {
856
+ action: string;
857
+ userId: string;
858
+ amount: number;
859
+ }
860
+
861
+ async function main() {
862
+ // Load config (your application handles file loading)
863
+ const configFile = fs.readFileSync('./config.yaml', 'utf8');
864
+ const config: WorkflowEngineConfig = yaml.load(configFile) as WorkflowEngineConfig;
865
+
866
+ // SDK transforms config
867
+ const clientConfig = ConfigLoader.createClientConfig(config, 'payment-service');
868
+
869
+ // Create client
870
+ const client = new WorkflowEngineClient(clientConfig);
871
+
872
+ // Define actions
873
+ const actionMap = new Map([
874
+ ['validatePayment', {
875
+ invocationMode: InvocationMode.PARALLEL,
876
+ handler: async (transaction, input: ProcessInput) => {
877
+ if (input.amount <= 0) {
878
+ return {
879
+ result: EvalResult.HARD_FAILURE,
880
+ error: new Error('Invalid amount')
881
+ };
882
+ }
883
+
884
+ return {
885
+ result: EvalResult.COMPLETE,
886
+ output: { validated: true },
887
+ extraUpdates: [
888
+ Patch.add('/validation', { valid: true, timestamp: new Date() })
889
+ ]
890
+ };
891
+ }
892
+ }],
893
+
894
+ ['processPayment', {
895
+ invocationMode: InvocationMode.PARALLEL,
896
+ handler: async (transaction, input: ProcessInput) => {
897
+ const paymentResult = await processPayment(input.userId, input.amount);
898
+
899
+ return {
900
+ result: EvalResult.COMPLETE,
901
+ output: paymentResult,
902
+ triggers: [
903
+ { topic: 'payment.completed' }
904
+ ]
905
+ };
906
+ }
907
+ }]
908
+ ]);
909
+
910
+ // Create handler
911
+ const handler = newDirectedTransactionHandler('payment-handler', actionMap)
912
+ .withInitFn(async (engAPI) => {
913
+ console.log('Payment handler initialized');
914
+ })
915
+ .withCloseFn(() => {
916
+ console.log('Payment handler closed');
917
+ });
918
+
919
+ // Register and connect
920
+ client.registerTransactionHandler('payment-handler', handler);
921
+ await client.connect();
922
+
923
+ console.log('Payment service ready');
924
+ }
925
+
926
+ main().catch(console.error);
927
+ ```
928
+
929
+ ### Complete event source example
930
+
931
+ See the Stellar blocks example in the Event Sources section above for a complete real-world event source implementation.
932
+
933
+ ## Architecture
934
+
935
+ ### Client architecture
936
+
937
+ ```
938
+ WorkflowEngineClient (Public API)
939
+
940
+ HandlerRuntime (Connection Management)
941
+
942
+ WebSocket Connection
943
+
944
+ Workflow Engine
945
+ ```
946
+
947
+ ### Handler execution flow
948
+
949
+ ```
950
+ 1. Workflow Engine sends WSHandleTransactions
951
+ 2. HandlerRuntime routes to registered handler
952
+ 3. Handler processes transactions
953
+ 4. Handler returns WSHandleTransactionsResult with results
954
+ 5. Runtime sends reply back to engine
955
+ 6. Engine updates workflow state
956
+ ```
957
+
958
+ ### Event source flow
959
+
960
+ ```
961
+ 1. Engine sends WSListenerPollRequest
962
+ 2. HandlerRuntime routes to event source
963
+ 3. Event source polls external system
964
+ 4. Event source returns events + checkpoint
965
+ 5. Engine processes events
966
+ 6. Engine triggers workflows matching topics
967
+ 7. Engine saves checkpoint
968
+ ```
969
+
970
+ ## Advanced topics
971
+
972
+ ### Custom authentication
973
+
974
+ ```typescript
975
+ const client = new WorkflowEngineClient({
976
+ url: 'ws://localhost:5503/ws',
977
+ providerName: 'my-service',
978
+ options: {
979
+ headers: {
980
+ 'Authorization': `Bearer ${process.env.AUTH_TOKEN}`
981
+ }
982
+ }
983
+ });
984
+ ```
985
+
986
+ ### Multiple handlers
987
+
988
+ ```typescript
989
+ // Register multiple handlers
990
+ client.registerTransactionHandler('handler1', handler1);
991
+ client.registerTransactionHandler('handler2', handler2);
992
+ client.registerEventSource('source1', source1);
993
+ client.registerEventSource('source2', source2);
994
+
995
+ // All handlers use the same WebSocket connection
996
+ await client.connect();
997
+ ```
998
+
999
+ ### Configuration validation
1000
+
1001
+ ```typescript
1002
+ import { ConfigLoader, WorkflowEngineConfig } from '@kaleido-io/workflow-engine-sdk';
1003
+ import * as fs from 'fs';
1004
+ import * as yaml from 'js-yaml';
1005
+
1006
+ try {
1007
+ // Your application loads config
1008
+ const configFile = fs.readFileSync('./config.yaml', 'utf8');
1009
+ const config: WorkflowEngineConfig = yaml.load(configFile) as WorkflowEngineConfig;
1010
+
1011
+ // Validate required fields
1012
+ if (!config.workflowEngine) {
1013
+ throw new Error('Missing workflowEngine configuration');
1014
+ }
1015
+ if (!config.workflowEngine.url) {
1016
+ throw new Error('Missing workflowEngine.url');
1017
+ }
1018
+ if (!config.workflowEngine.auth) {
1019
+ throw new Error('Missing workflowEngine.auth');
1020
+ }
1021
+
1022
+ // SDK logs summary (without sensitive data)
1023
+ ConfigLoader.logConfigSummary(config);
1024
+ } catch (error) {
1025
+ console.error('Invalid configuration:', error.message);
1026
+ process.exit(1);
1027
+ }
1028
+ ```
1029
+
1030
+ ## Best practices
1031
+
1032
+ 1. **Use the factory pattern**: `newDirectedTransactionHandler` and `newEventSource` provide clean, type-safe APIs
1033
+ 2. **Handle errors gracefully**: Return appropriate `EvalResult` values
1034
+ 3. **Use state updates**: Keep workflow state synchronized with JSON Patch
1035
+ 4. **Implement idempotency**: Event sources should use checkpoints for resumability
1036
+ 5. **Log structured data**: Use the built-in logger with metadata
1037
+ 6. **Test thoroughly**: Unit test handlers, component test with real engine
1038
+ 7. **Monitor connections**: Check `isConnected()` and handle reconnection
1039
+ 8. **Clean up resources**: Implement `withCloseFn` for proper cleanup
1040
+
1041
+ ## Troubleshooting
1042
+
1043
+ ### Handler not registered
1044
+
1045
+ **Problem**: `No connections for handler 'my-handler'`
1046
+
1047
+ **Solution**: Ensure handler is registered before creating workflow or ensure connector is running
1048
+
1049
+ ```typescript
1050
+ // Register BEFORE submitting workflows
1051
+ client.registerTransactionHandler('my-handler', handler);
1052
+ await client.connect();
1053
+ // Now workflows can use this handler
1054
+ ```
1055
+
1056
+ ### Connection timeouts
1057
+
1058
+ **Problem**: Client fails to connect or times out
1059
+
1060
+ **Solution**: Check workflow engine URL and authentication
1061
+
1062
+ ```typescript
1063
+ // Verify URL format (should include ws:// or wss://)
1064
+ url: 'ws://localhost:5503/ws' // ✓ Correct
1065
+ url: 'localhost:5503' // ✗ Wrong
1066
+
1067
+ // Check authentication
1068
+ authToken: process.env.AUTH_TOKEN // Ensure token is valid
1069
+ ```
1070
+
1071
+ ### Event source not polling
1072
+
1073
+ **Problem**: Event stream created but no events emitted
1074
+
1075
+ **Solution**:
1076
+ 1. Check stream is started: `"started": true`
1077
+ 2. Verify handler name matches: `listenerHandler: 'my-event-source'`
1078
+ 3. Check provider name matches: `listenerHandlerProvider: 'my-service'`
1079
+ 4. Ensure event source is registered before creating stream
1080
+
1081
+ ### State Updates Not Applied
1082
+
1083
+ **Problem**: JSON Patch operations fail silently
1084
+
1085
+ **Solution**: Ensure paths are valid and operations are correct
1086
+
1087
+ ```typescript
1088
+ // Use helper functions
1089
+ Patch.add('/newField', value) // ✓ Correct
1090
+ { op: 'add', path: '/newField' } // ✗ Missing value
1091
+
1092
+ // Array append
1093
+ Patch.add('/array/-', item) // ✓ Correct
1094
+ Patch.add('/array/999', item) // ✗ Wrong index
1095
+ ```
1096
+
1097
+ ## API reference
1098
+
1099
+ See the TypeScript type definitions for complete API documentation:
1100
+
1101
+ - `WorkflowEngineClient` - Main client class
1102
+ - `WorkflowEngineConfig` - Configuration interface
1103
+ - `ConfigLoader` - Configuration transformation utilities
1104
+ - `TransactionHandler` - Handler interface
1105
+ - `EventSource` - Event source interface
1106
+ - `EngineAPI` - Engine API interface
1107
+ - `EvalResult` - Result enum
1108
+ - `InvocationMode` - Invocation mode enum
1109
+ - `Patch` - JSON Patch helpers
1110
+
1111
+ ### ConfigLoader
1112
+
1113
+ The `ConfigLoader` class provides utilities for transforming configuration:
1114
+
1115
+ - `createClientConfig(config, providerName)` - Transforms `WorkflowEngineConfig` into `WorkflowEngineClientConfig`
1116
+ - Converts HTTP URLs to WebSocket URLs
1117
+ - Sets up authentication headers based on auth type
1118
+ - Handles retry and timeout settings
1119
+ - `logConfigSummary(config)` - Logs configuration summary (without sensitive data)
1120
+
1121
+ **Note:** The SDK does not load configuration from files. Your application should load configuration and pass it to these utilities.