@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.
- package/LICENSE +201 -0
- package/README.md +1121 -0
- package/bin/init.js +200 -0
- package/bin/wesdk.js +57 -0
- package/dist/package.json +3 -0
- package/dist/src/client/client.d.ts +24 -0
- package/dist/src/client/client.d.ts.map +1 -0
- package/dist/src/client/client.js +58 -0
- package/dist/src/client/client.js.map +1 -0
- package/dist/src/client/rest-client.d.ts +222 -0
- package/dist/src/client/rest-client.d.ts.map +1 -0
- package/dist/src/client/rest-client.js +242 -0
- package/dist/src/client/rest-client.js.map +1 -0
- package/dist/src/config/config.d.ts +60 -0
- package/dist/src/config/config.d.ts.map +1 -0
- package/dist/src/config/config.js +117 -0
- package/dist/src/config/config.js.map +1 -0
- package/dist/src/factories/event_source.d.ts +54 -0
- package/dist/src/factories/event_source.d.ts.map +1 -0
- package/dist/src/factories/event_source.js +170 -0
- package/dist/src/factories/event_source.js.map +1 -0
- package/dist/src/factories/transaction_handler.d.ts +27 -0
- package/dist/src/factories/transaction_handler.d.ts.map +1 -0
- package/dist/src/factories/transaction_handler.js +66 -0
- package/dist/src/factories/transaction_handler.js.map +1 -0
- package/dist/src/helpers/stage_director.d.ts +42 -0
- package/dist/src/helpers/stage_director.d.ts.map +1 -0
- package/dist/src/helpers/stage_director.js +304 -0
- package/dist/src/helpers/stage_director.js.map +1 -0
- package/dist/src/i18n/errors.d.ts +61 -0
- package/dist/src/i18n/errors.d.ts.map +1 -0
- package/dist/src/i18n/errors.js +90 -0
- package/dist/src/i18n/errors.js.map +1 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +85 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces/handlers.d.ts +112 -0
- package/dist/src/interfaces/handlers.d.ts.map +1 -0
- package/dist/src/interfaces/handlers.js +18 -0
- package/dist/src/interfaces/handlers.js.map +1 -0
- package/dist/src/interfaces/messages.d.ts +6 -0
- package/dist/src/interfaces/messages.d.ts.map +1 -0
- package/dist/src/interfaces/messages.js +18 -0
- package/dist/src/interfaces/messages.js.map +1 -0
- package/dist/src/log/logger.d.ts +8 -0
- package/dist/src/log/logger.d.ts.map +1 -0
- package/dist/src/log/logger.js +39 -0
- package/dist/src/log/logger.js.map +1 -0
- package/dist/src/runtime/engine_client.d.ts +37 -0
- package/dist/src/runtime/engine_client.d.ts.map +1 -0
- package/dist/src/runtime/engine_client.js +99 -0
- package/dist/src/runtime/engine_client.js.map +1 -0
- package/dist/src/runtime/handler_runtime.d.ts +124 -0
- package/dist/src/runtime/handler_runtime.d.ts.map +1 -0
- package/dist/src/runtime/handler_runtime.js +623 -0
- package/dist/src/runtime/handler_runtime.js.map +1 -0
- package/dist/src/types/core.d.ts +258 -0
- package/dist/src/types/core.d.ts.map +1 -0
- package/dist/src/types/core.js +71 -0
- package/dist/src/types/core.js.map +1 -0
- package/dist/src/types/flows.d.ts +144 -0
- package/dist/src/types/flows.d.ts.map +1 -0
- package/dist/src/types/flows.js +30 -0
- package/dist/src/types/flows.js.map +1 -0
- package/dist/src/utils/errors.d.ts +2 -0
- package/dist/src/utils/errors.d.ts.map +1 -0
- package/dist/src/utils/errors.js +23 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/patch.d.ts +9 -0
- package/dist/src/utils/patch.d.ts.map +1 -0
- package/dist/src/utils/patch.js +99 -0
- package/dist/src/utils/patch.js.map +1 -0
- package/dist-esm/src/client/client.js +54 -0
- package/dist-esm/src/client/client.js.map +1 -0
- package/dist-esm/src/client/rest-client.js +238 -0
- package/dist-esm/src/client/rest-client.js.map +1 -0
- package/dist-esm/src/config/config.js +113 -0
- package/dist-esm/src/config/config.js.map +1 -0
- package/dist-esm/src/factories/event_source.js +167 -0
- package/dist-esm/src/factories/event_source.js.map +1 -0
- package/dist-esm/src/factories/transaction_handler.js +63 -0
- package/dist-esm/src/factories/transaction_handler.js.map +1 -0
- package/dist-esm/src/helpers/stage_director.js +298 -0
- package/dist-esm/src/helpers/stage_director.js.map +1 -0
- package/dist-esm/src/i18n/errors.js +85 -0
- package/dist-esm/src/i18n/errors.js.map +1 -0
- package/dist-esm/src/index.js +51 -0
- package/dist-esm/src/index.js.map +1 -0
- package/dist-esm/src/interfaces/handlers.js +17 -0
- package/dist-esm/src/interfaces/handlers.js.map +1 -0
- package/dist-esm/src/interfaces/messages.js +17 -0
- package/dist-esm/src/interfaces/messages.js.map +1 -0
- package/dist-esm/src/log/logger.js +36 -0
- package/dist-esm/src/log/logger.js.map +1 -0
- package/dist-esm/src/runtime/engine_client.js +95 -0
- package/dist-esm/src/runtime/engine_client.js.map +1 -0
- package/dist-esm/src/runtime/handler_runtime.js +586 -0
- package/dist-esm/src/runtime/handler_runtime.js.map +1 -0
- package/dist-esm/src/types/core.js +68 -0
- package/dist-esm/src/types/core.js.map +1 -0
- package/dist-esm/src/types/flows.js +27 -0
- package/dist-esm/src/types/flows.js.map +1 -0
- package/dist-esm/src/utils/errors.js +19 -0
- package/dist-esm/src/utils/errors.js.map +1 -0
- package/dist-esm/src/utils/patch.js +56 -0
- package/dist-esm/src/utils/patch.js.map +1 -0
- package/package.json +79 -11
- package/template/.env.sample +14 -0
- package/template/.vscode/launch.json +23 -0
- package/template/README.md +37 -0
- package/template/package.json +36 -0
- package/template/src/connect.ts +58 -0
- package/template/src/provider.ts +24 -0
- package/template/src/samples/event-source/README.md +50 -0
- package/template/src/samples/event-source/echo-handler.ts +65 -0
- package/template/src/samples/event-source/event-processor.ts +46 -0
- package/template/src/samples/event-source/event-source.ts +70 -0
- package/template/src/samples/event-source/stream.ts +34 -0
- package/template/src/samples/hello/README.md +52 -0
- package/template/src/samples/hello/flow.ts +74 -0
- package/template/src/samples/hello/handlers.test.ts +147 -0
- package/template/src/samples/hello/handlers.ts +72 -0
- package/template/src/samples/hello/transaction.ts +24 -0
- package/template/src/samples/http-invoke/README.md +42 -0
- package/template/src/samples/http-invoke/flow.ts +63 -0
- package/template/src/samples/http-invoke/handlers.ts +66 -0
- package/template/src/samples/http-invoke/transaction.ts +22 -0
- package/template/src/samples/snap/README.md +98 -0
- package/template/src/samples/snap/event-source.ts +104 -0
- package/template/src/samples/snap/flow.ts +85 -0
- package/template/src/samples/snap/snap-handler.ts +84 -0
- package/template/src/samples/snap/stream.ts +26 -0
- package/template/src/samples/snap/transaction.ts +26 -0
- package/template/src/utils/post-stream.ts +67 -0
- package/template/src/utils/post-transaction.ts +64 -0
- package/template/src/utils/post-workflow.ts +63 -0
- package/template/tsconfig.json +24 -0
- package/template/vitest.config.ts +42 -0
- 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.
|