@kaleido-io/workflow-engine-sdk 0.9.3 → 0.9.4
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 +486 -362
- package/bin/init.js +14 -15
- package/dist/src/client/client.d.ts +22 -0
- package/dist/src/client/client.d.ts.map +1 -1
- package/dist/src/client/client.js +1 -0
- package/dist/src/client/client.js.map +1 -1
- package/dist/src/client/client_factory.d.ts +14 -0
- package/dist/src/client/client_factory.d.ts.map +1 -0
- package/dist/src/client/client_factory.js +64 -0
- package/dist/src/client/client_factory.js.map +1 -0
- package/dist/src/client/rest-client.d.ts.map +1 -1
- package/dist/src/client/rest-client.js +1 -1
- package/dist/src/client/rest-client.js.map +1 -1
- package/dist/src/config/config.d.ts +81 -1
- package/dist/src/config/config.d.ts.map +1 -1
- package/dist/src/config/config.js +269 -24
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config_helpers.d.ts +44 -0
- package/dist/src/config/config_helpers.d.ts.map +1 -0
- package/dist/src/config/config_helpers.js +68 -0
- package/dist/src/config/config_helpers.js.map +1 -0
- package/dist/src/helpers/stage_director.js +2 -2
- package/dist/src/i18n/errors.d.ts +5 -0
- package/dist/src/i18n/errors.d.ts.map +1 -1
- package/dist/src/i18n/errors.js +8 -3
- package/dist/src/i18n/errors.js.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/runtime/handler_runtime.d.ts +22 -0
- package/dist/src/runtime/handler_runtime.d.ts.map +1 -1
- package/dist/src/runtime/handler_runtime.js +43 -3
- package/dist/src/runtime/handler_runtime.js.map +1 -1
- package/dist/src/utils/patch.d.ts +1 -1
- package/dist-esm/src/client/client.js +2 -1
- package/dist-esm/src/client/client.js.map +1 -1
- package/dist-esm/src/client/client_factory.js +60 -0
- package/dist-esm/src/client/client_factory.js.map +1 -0
- package/dist-esm/src/client/rest-client.js +1 -1
- package/dist-esm/src/client/rest-client.js.map +1 -1
- package/dist-esm/src/config/config.js +235 -25
- package/dist-esm/src/config/config.js.map +1 -1
- package/dist-esm/src/config/config_helpers.js +61 -0
- package/dist-esm/src/config/config_helpers.js.map +1 -0
- package/dist-esm/src/helpers/stage_director.js +2 -2
- package/dist-esm/src/i18n/errors.js +8 -3
- package/dist-esm/src/i18n/errors.js.map +1 -1
- package/dist-esm/src/index.js +2 -1
- package/dist-esm/src/index.js.map +1 -1
- package/dist-esm/src/runtime/handler_runtime.js +38 -2
- package/dist-esm/src/runtime/handler_runtime.js.map +1 -1
- package/dist-esm/src/utils/patch.js +2 -2
- package/package.json +1 -1
- package/template/README.md +5 -8
- package/template/config/config.yaml +12 -0
- package/template/config/wfe-config.yaml +12 -0
- package/template/package-lock.json +1908 -0
- package/template/package.json +4 -2
- package/template/src/connect.ts +35 -37
- package/template/src/provider.ts +4 -4
- package/template/src/samples/event-source/echo-handler.ts +2 -2
- package/template/src/samples/event-source/event-processor.ts +1 -1
- package/template/src/samples/event-source/event-source.ts +1 -1
- package/template/src/samples/event-source/stream.ts +11 -11
- package/template/src/samples/hello/flow.ts +36 -36
- package/template/src/samples/hello/handlers.ts +2 -2
- package/template/src/samples/hello/transaction.ts +4 -4
- package/template/src/samples/http-invoke/flow.ts +29 -29
- package/template/src/samples/http-invoke/handlers.ts +88 -39
- package/template/src/samples/http-invoke/transaction.ts +3 -3
- package/template/src/samples/snap/event-source.ts +1 -1
- package/template/src/samples/snap/flow.ts +40 -40
- package/template/src/samples/snap/snap-handler.ts +2 -2
- package/template/src/samples/snap/transaction.ts +6 -6
- package/template/.env.sample +0 -14
package/README.md
CHANGED
|
@@ -23,43 +23,48 @@ This will create a new project in a directory named for project-name, and in a f
|
|
|
23
23
|
### Integrating into an existing project
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
|
-
import {
|
|
27
|
-
WorkflowEngineClient,
|
|
26
|
+
import {
|
|
27
|
+
WorkflowEngineClient,
|
|
28
28
|
ConfigLoader,
|
|
29
29
|
WorkflowEngineConfig,
|
|
30
30
|
newDirectedTransactionHandler,
|
|
31
31
|
InvocationMode,
|
|
32
|
-
EvalResult
|
|
33
|
-
} from
|
|
34
|
-
import * as fs from
|
|
35
|
-
import * as yaml from
|
|
32
|
+
EvalResult,
|
|
33
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
34
|
+
import * as fs from "fs";
|
|
35
|
+
import * as yaml from "js-yaml";
|
|
36
36
|
|
|
37
37
|
// 1. Load configuration (your application handles file loading)
|
|
38
|
-
const configFile = fs.readFileSync(
|
|
39
|
-
const config: WorkflowEngineConfig = yaml.load(
|
|
38
|
+
const configFile = fs.readFileSync("./config.yaml", "utf8");
|
|
39
|
+
const config: WorkflowEngineConfig = yaml.load(
|
|
40
|
+
configFile,
|
|
41
|
+
) as WorkflowEngineConfig;
|
|
40
42
|
|
|
41
43
|
// 2. Use SDK's ConfigLoader to create client config with your provider name
|
|
42
44
|
// The SDK handles authentication header setup and URL conversion automatically
|
|
43
|
-
const clientConfig = ConfigLoader.createClientConfig(config,
|
|
45
|
+
const clientConfig = ConfigLoader.createClientConfig(config, "my-service");
|
|
44
46
|
|
|
45
47
|
// 3. Create client
|
|
46
48
|
const client = new WorkflowEngineClient(clientConfig);
|
|
47
49
|
|
|
48
50
|
// 4. Create and register transaction handler
|
|
49
51
|
const actionMap = new Map([
|
|
50
|
-
[
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
[
|
|
53
|
+
"myAction",
|
|
54
|
+
{
|
|
55
|
+
invocationMode: InvocationMode.PARALLEL,
|
|
56
|
+
handler: async (transaction, input) => {
|
|
57
|
+
return {
|
|
58
|
+
result: EvalResult.COMPLETE,
|
|
59
|
+
output: { success: true },
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
59
64
|
]);
|
|
60
65
|
|
|
61
|
-
const handler = newDirectedTransactionHandler(
|
|
62
|
-
client.registerTransactionHandler(
|
|
66
|
+
const handler = newDirectedTransactionHandler("my-handler", actionMap);
|
|
67
|
+
client.registerTransactionHandler("my-handler", handler);
|
|
63
68
|
|
|
64
69
|
// 5. Connect
|
|
65
70
|
await client.connect();
|
|
@@ -70,6 +75,7 @@ await client.connect();
|
|
|
70
75
|
### WorkflowEngineClient
|
|
71
76
|
|
|
72
77
|
The main entry point that manages:
|
|
78
|
+
|
|
73
79
|
- Handler registration (transaction handlers and event sources)
|
|
74
80
|
- WebSocket connection lifecycle
|
|
75
81
|
- Automatic reconnection and re-registration
|
|
@@ -77,24 +83,24 @@ The main entry point that manages:
|
|
|
77
83
|
|
|
78
84
|
```typescript
|
|
79
85
|
const client = new WorkflowEngineClient({
|
|
80
|
-
url:
|
|
81
|
-
providerName:
|
|
82
|
-
authToken:
|
|
83
|
-
authHeaderName:
|
|
84
|
-
reconnectDelay: 2000,
|
|
85
|
-
maxAttempts: undefined
|
|
86
|
+
url: "ws://localhost:5503/ws",
|
|
87
|
+
providerName: "my-service",
|
|
88
|
+
authToken: "your-token",
|
|
89
|
+
authHeaderName: "X-Kld-Authz", // Optional, defaults to X-Kld-Authz
|
|
90
|
+
reconnectDelay: 2000, // Optional, ms between reconnect attempts
|
|
91
|
+
maxAttempts: undefined, // Optional, undefined = infinite retries (recommended)
|
|
86
92
|
});
|
|
87
93
|
|
|
88
94
|
// Register handlers
|
|
89
|
-
client.registerTransactionHandler(
|
|
90
|
-
client.registerEventSource(
|
|
95
|
+
client.registerTransactionHandler("handler-name", transactionHandler);
|
|
96
|
+
client.registerEventSource("source-name", eventSource);
|
|
91
97
|
|
|
92
98
|
// Connect
|
|
93
99
|
await client.connect();
|
|
94
100
|
|
|
95
101
|
// Check connection status
|
|
96
102
|
if (client.isConnected()) {
|
|
97
|
-
console.log(
|
|
103
|
+
console.log("Connected!");
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
// Disconnect
|
|
@@ -103,57 +109,75 @@ client.disconnect();
|
|
|
103
109
|
|
|
104
110
|
### Configuration file format
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
The SDK only accepts the root key **`workflow-engine`** in config files (not `workflowEngine`). Use one of two modes:
|
|
113
|
+
|
|
114
|
+
- **Outbound:** Provide `url` and `auth`. The app (SDK client) connects to the workflow engine at that URL.
|
|
115
|
+
- **Inbound:** Provide `server` with `address` and `port`. The app creates a WebSocket server on that address/port; the workflow engine connects to the app. Auth is not used. Optional `server.tls` (`enabled`, `caFile`, `certFile`, `keyFile`, `clientAuth`) enables TLS for the server.
|
|
116
|
+
|
|
117
|
+
Delay fields (`retryDelay`, etc.) use time strings: `ms`, `s`, `m`, `h` (e.g. `"2s"`, `"30s"`, `"100ms"`, `"1m"`).
|
|
118
|
+
|
|
119
|
+
**Example — outbound (local dev) with basic auth:**
|
|
107
120
|
|
|
108
121
|
```yaml
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
workflow-engine:
|
|
123
|
+
providerName: my-service
|
|
111
124
|
url: http://localhost:5503
|
|
112
125
|
auth:
|
|
113
126
|
type: basic
|
|
114
127
|
username: my-user
|
|
115
128
|
password: my-password
|
|
116
129
|
# maxRetries: undefined = infinite reconnection (recommended)
|
|
117
|
-
#
|
|
130
|
+
# retryDelay: "2s" (time string: ms, s, m, h)
|
|
118
131
|
retryDelay: 2s
|
|
119
|
-
timeout: 30s
|
|
120
|
-
batchSize: 10
|
|
121
|
-
batchTimeout: 500ms
|
|
122
|
-
pollDuration: 2s
|
|
123
132
|
```
|
|
124
133
|
|
|
125
|
-
**
|
|
134
|
+
**Example — outbound with token auth:**
|
|
126
135
|
|
|
127
136
|
```yaml
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
workflow-engine:
|
|
138
|
+
providerName: my-service
|
|
130
139
|
url: http://localhost:5503
|
|
131
140
|
auth:
|
|
132
141
|
type: token
|
|
133
142
|
token: dev-token-123
|
|
134
|
-
header: X-Kld-Authz
|
|
135
|
-
scheme: ""
|
|
136
|
-
# maxRetries: undefined = infinite reconnection (recommended for long-running services)
|
|
143
|
+
header: X-Kld-Authz # optional, defaults to Authorization
|
|
144
|
+
scheme: "" # optional, e.g. "Bearer" for "Bearer <token>"
|
|
137
145
|
retryDelay: 2s
|
|
138
146
|
```
|
|
139
147
|
|
|
148
|
+
**Example — inbound (app creates WebSocket server):**
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
workflow-engine:
|
|
152
|
+
providerName: my-service
|
|
153
|
+
providerMetadata: {}
|
|
154
|
+
server:
|
|
155
|
+
address: "0.0.0.0"
|
|
156
|
+
port: 6000
|
|
157
|
+
heartbeatInterval: "30s"
|
|
158
|
+
requestsPerSecond: 100
|
|
159
|
+
burst: 200
|
|
160
|
+
```
|
|
161
|
+
|
|
140
162
|
Load and use configuration:
|
|
141
163
|
|
|
142
164
|
```typescript
|
|
143
|
-
import {
|
|
144
|
-
ConfigLoader,
|
|
145
|
-
WorkflowEngineConfig
|
|
146
|
-
} from
|
|
147
|
-
import * as fs from
|
|
148
|
-
import * as yaml from
|
|
165
|
+
import {
|
|
166
|
+
ConfigLoader,
|
|
167
|
+
WorkflowEngineConfig,
|
|
168
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
169
|
+
import * as fs from "fs";
|
|
170
|
+
import * as yaml from "js-yaml";
|
|
149
171
|
|
|
150
172
|
// Your application loads configuration (the SDK doesn't load files)
|
|
151
|
-
const configFile = fs.readFileSync(
|
|
152
|
-
const config: WorkflowEngineConfig = yaml.load(
|
|
173
|
+
const configFile = fs.readFileSync("./config.yaml", "utf8");
|
|
174
|
+
const config: WorkflowEngineConfig = yaml.load(
|
|
175
|
+
configFile,
|
|
176
|
+
) as WorkflowEngineConfig;
|
|
153
177
|
|
|
154
178
|
// Use SDK's ConfigLoader to create client config with provider name (REQUIRED)
|
|
155
179
|
// Note: SDK automatically converts http:// to ws:// and adds /ws path
|
|
156
|
-
const clientConfig = ConfigLoader.createClientConfig(config,
|
|
180
|
+
const clientConfig = ConfigLoader.createClientConfig(config, "my-service");
|
|
157
181
|
|
|
158
182
|
// Optionally log summary (without sensitive data)
|
|
159
183
|
ConfigLoader.logConfigSummary(config);
|
|
@@ -163,25 +187,69 @@ const client = new WorkflowEngineClient(clientConfig);
|
|
|
163
187
|
```
|
|
164
188
|
|
|
165
189
|
**URL Handling:**
|
|
190
|
+
|
|
166
191
|
- Config file uses HTTP URL: `http://localhost:5503` or `https://example.com`
|
|
167
192
|
- SDK automatically converts to WebSocket: `ws://localhost:5503/ws` or `wss://example.com/ws`
|
|
168
193
|
- `/ws` path is automatically added if not present
|
|
169
194
|
|
|
195
|
+
### Use configuration files setup
|
|
196
|
+
|
|
197
|
+
Two config files are required for this setup:
|
|
198
|
+
|
|
199
|
+
1. **WFE config (SDK contract)** — Workflow engine connection and identity. Path from `WFE_CONFIG_FILE` or pass `configFile` to `NewWorkflowEngineClient`. Root key in YAML must be **`workflow-engine`**. **Outbound:** `providerName`, `url`, and `auth`. **Inbound:** `providerName` and `server` (address, port); the app creates a WebSocket server and the engine connects to it. Optional `server.tls` for TLS. Delay fields use time strings (e.g. `retryDelay: "2s"`).
|
|
200
|
+
2. **Provider config (application-owned)** — App-specific settings. Path from `CONFIG_FILE` or `-f`; your app loads and uses it (e.g. to build handlers). The SDK does not read or define its schema.
|
|
201
|
+
|
|
202
|
+
Example run:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
CONFIG_FILE=./config.yaml WFE_CONFIG_FILE=./wfe-config.yaml node connect.js
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### New workflow engine client using config file
|
|
209
|
+
|
|
210
|
+
To initialize a Workflow Engine Client with using single function, use `NewWorkflowEngineClient` and `HandlerSetFor`:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import {
|
|
214
|
+
NewWorkflowEngineClient,
|
|
215
|
+
HandlerSetFor,
|
|
216
|
+
WFE_CONFIG_FILE,
|
|
217
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
218
|
+
|
|
219
|
+
// Build your handlers (e.g. from provider config in app code)
|
|
220
|
+
const handler = newEventProcessorFromConfig(providerConfig);
|
|
221
|
+
const txHandler = newDirectedTransactionHandler("my-handler", actionMap);
|
|
222
|
+
|
|
223
|
+
// Create and start runtime (loads WFE config from file, registers handlers, connects)
|
|
224
|
+
const runtime = await NewWorkflowEngineClient(
|
|
225
|
+
HandlerSetFor(handler, txHandler),
|
|
226
|
+
process.env[WFE_CONFIG_FILE] ?? "./wfe-config.yaml",
|
|
227
|
+
);
|
|
228
|
+
// runtime is already connected
|
|
229
|
+
// On shutdown:
|
|
230
|
+
runtime.disconnect();
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- **HandlerSetFor(...handlers)**: Builds a handler set from one or more transaction handlers, event sources, or event processors.
|
|
234
|
+
- **NewWorkflowEngineClient(handlerSet, configFile?)**: Loads WFE config from file (when `configFile` or `WFE_CONFIG_FILE` env is set), creates the client, registers all handlers, connects, and returns the client. Uses `ConfigLoader.loadClientConfigFromFile` under the hood.
|
|
235
|
+
|
|
236
|
+
To load client config from a WFE config file without using `NewWorkflowEngineClient` (e.g. for custom startup), use `ConfigLoader.loadClientConfigFromFile(configFilePath?)`. If `configFilePath` is omitted, `process.env[WFE_CONFIG_FILE]` is used. The file must use the root key **`workflow-engine`** only. **Outbound:** include `providerName`, `url`, and `auth`. **Inbound:** include `providerName` and `server` (address, port); the app will create a WebSocket server and auth is not used.
|
|
237
|
+
|
|
170
238
|
### Configuration Schema
|
|
171
239
|
|
|
172
240
|
```typescript
|
|
173
241
|
interface WorkflowEngineConfig {
|
|
174
242
|
workflowEngine: {
|
|
175
|
-
mode?: HandlerRuntimeMode;
|
|
176
|
-
port?: number;
|
|
177
|
-
url?: string;
|
|
178
|
-
auth?: AuthConfig;
|
|
179
|
-
timeout?: string;
|
|
180
|
-
maxRetries?: number;
|
|
181
|
-
retryDelay?: string;
|
|
182
|
-
batchSize?: number;
|
|
183
|
-
batchTimeout?: string;
|
|
184
|
-
pollDuration?: string;
|
|
243
|
+
mode?: HandlerRuntimeMode; // Defaults to outbound
|
|
244
|
+
port?: number; // port used for the web socket server in inbound mode
|
|
245
|
+
url?: string; // Workflow engine URL
|
|
246
|
+
auth?: AuthConfig; // Authentication (see below)
|
|
247
|
+
timeout?: string; // Request timeout (e.g. "30s")
|
|
248
|
+
maxRetries?: number; // Max reconnection attempts (undefined = infinite)
|
|
249
|
+
retryDelay?: string; // Delay between retries (e.g. "2s")
|
|
250
|
+
batchSize?: number; // Batch size for handlers
|
|
251
|
+
batchTimeout?: string; // Batch timeout (e.g. "500ms")
|
|
252
|
+
pollDuration?: string; // Event source poll duration
|
|
185
253
|
};
|
|
186
254
|
}
|
|
187
255
|
|
|
@@ -189,24 +257,26 @@ interface WorkflowEngineConfig {
|
|
|
189
257
|
type AuthConfig = BasicAuth | TokenAuth;
|
|
190
258
|
|
|
191
259
|
interface BasicAuth {
|
|
192
|
-
type:
|
|
193
|
-
username: string;
|
|
194
|
-
password: string;
|
|
260
|
+
type: "basic"; // Must be 'basic'
|
|
261
|
+
username: string; // Username
|
|
262
|
+
password: string; // Password
|
|
195
263
|
}
|
|
196
264
|
|
|
197
265
|
interface TokenAuth {
|
|
198
|
-
type:
|
|
199
|
-
token: string;
|
|
200
|
-
header?: string;
|
|
201
|
-
scheme?: string;
|
|
266
|
+
type: "token"; // Must be 'token'
|
|
267
|
+
token: string; // API token
|
|
268
|
+
header?: string; // Header name (default: 'Authorization')
|
|
269
|
+
scheme?: string; // Scheme (e.g. 'Bearer', default: '')
|
|
202
270
|
}
|
|
203
271
|
```
|
|
204
272
|
|
|
205
|
-
### Configuration examples
|
|
273
|
+
### Configuration examples (config file: root key `workflow-engine` only)
|
|
274
|
+
|
|
275
|
+
**Outbound — basic auth:**
|
|
206
276
|
|
|
207
|
-
**Outbound, basic auth:**
|
|
208
277
|
```yaml
|
|
209
|
-
|
|
278
|
+
workflow-engine:
|
|
279
|
+
providerName: my-service
|
|
210
280
|
url: http://localhost:5503
|
|
211
281
|
auth:
|
|
212
282
|
type: basic
|
|
@@ -214,45 +284,60 @@ workflowEngine:
|
|
|
214
284
|
password: secret123
|
|
215
285
|
```
|
|
216
286
|
|
|
217
|
-
**Outbound
|
|
287
|
+
**Outbound — token auth (raw token):**
|
|
288
|
+
|
|
218
289
|
```yaml
|
|
219
|
-
|
|
290
|
+
workflow-engine:
|
|
291
|
+
providerName: my-service
|
|
220
292
|
url: http://localhost:5503
|
|
221
293
|
auth:
|
|
222
294
|
type: token
|
|
223
295
|
token: dev-token-123
|
|
224
296
|
header: X-Kld-Authz
|
|
225
|
-
scheme: ""
|
|
297
|
+
scheme: "" # Empty string = raw token
|
|
226
298
|
```
|
|
227
299
|
|
|
228
|
-
**Outbound
|
|
300
|
+
**Outbound — token auth (bearer):**
|
|
301
|
+
|
|
229
302
|
```yaml
|
|
230
|
-
|
|
303
|
+
workflow-engine:
|
|
304
|
+
providerName: my-service
|
|
231
305
|
url: http://localhost:5503
|
|
232
306
|
auth:
|
|
233
307
|
type: token
|
|
234
308
|
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
235
|
-
scheme: Bearer
|
|
309
|
+
scheme: Bearer
|
|
236
310
|
```
|
|
237
311
|
|
|
238
|
-
**
|
|
312
|
+
**Hosted (server; auth not needed):**
|
|
239
313
|
|
|
240
|
-
The client will wait for an inbound connection from the workflow engine
|
|
241
314
|
```yaml
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
315
|
+
workflow-engine:
|
|
316
|
+
providerName: my-service
|
|
317
|
+
providerMetadata: {}
|
|
318
|
+
server:
|
|
319
|
+
address: "0.0.0.0"
|
|
320
|
+
port: 6000
|
|
321
|
+
heartbeatInterval: "30s"
|
|
322
|
+
requestsPerSecond: 100
|
|
323
|
+
burst: 200
|
|
245
324
|
```
|
|
246
325
|
|
|
247
326
|
**With environment variable overrides:**
|
|
327
|
+
|
|
248
328
|
```typescript
|
|
249
|
-
import * as fs from
|
|
250
|
-
import * as yaml from
|
|
251
|
-
import {
|
|
329
|
+
import * as fs from "fs";
|
|
330
|
+
import * as yaml from "js-yaml";
|
|
331
|
+
import {
|
|
332
|
+
ConfigLoader,
|
|
333
|
+
WorkflowEngineConfig,
|
|
334
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
252
335
|
|
|
253
336
|
// Your application loads and merges config with env vars
|
|
254
|
-
const configFile = fs.readFileSync(
|
|
255
|
-
const config: WorkflowEngineConfig = yaml.load(
|
|
337
|
+
const configFile = fs.readFileSync("./config.yaml", "utf8");
|
|
338
|
+
const config: WorkflowEngineConfig = yaml.load(
|
|
339
|
+
configFile,
|
|
340
|
+
) as WorkflowEngineConfig;
|
|
256
341
|
|
|
257
342
|
// Override URL from environment
|
|
258
343
|
if (process.env.WORKFLOW_ENGINE_URL) {
|
|
@@ -260,13 +345,15 @@ if (process.env.WORKFLOW_ENGINE_URL) {
|
|
|
260
345
|
}
|
|
261
346
|
|
|
262
347
|
// Override token from environment
|
|
263
|
-
if (
|
|
264
|
-
|
|
348
|
+
if (
|
|
349
|
+
process.env.WORKFLOW_ENGINE_TOKEN &&
|
|
350
|
+
config.workflowEngine.auth.type === "token"
|
|
351
|
+
) {
|
|
265
352
|
config.workflowEngine.auth.token = process.env.WORKFLOW_ENGINE_TOKEN;
|
|
266
353
|
}
|
|
267
354
|
|
|
268
355
|
// SDK transforms config into client config
|
|
269
|
-
const clientConfig = ConfigLoader.createClientConfig(config,
|
|
356
|
+
const clientConfig = ConfigLoader.createClientConfig(config, "my-service");
|
|
270
357
|
```
|
|
271
358
|
|
|
272
359
|
## Transaction handlers
|
|
@@ -276,12 +363,12 @@ const clientConfig = ConfigLoader.createClientConfig(config, 'my-service');
|
|
|
276
363
|
The recommended approach for building transaction handlers:
|
|
277
364
|
|
|
278
365
|
```typescript
|
|
279
|
-
import {
|
|
366
|
+
import {
|
|
280
367
|
newDirectedTransactionHandler,
|
|
281
368
|
InvocationMode,
|
|
282
369
|
EvalResult,
|
|
283
|
-
Patch
|
|
284
|
-
} from
|
|
370
|
+
Patch,
|
|
371
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
285
372
|
|
|
286
373
|
// Define your input type
|
|
287
374
|
interface MyInput {
|
|
@@ -291,53 +378,58 @@ interface MyInput {
|
|
|
291
378
|
|
|
292
379
|
// Create action map
|
|
293
380
|
const actionMap = new Map([
|
|
294
|
-
[
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
381
|
+
[
|
|
382
|
+
"processData",
|
|
383
|
+
{
|
|
384
|
+
invocationMode: InvocationMode.PARALLEL,
|
|
385
|
+
handler: async (transaction, input: MyInput) => {
|
|
386
|
+
// Process the data
|
|
387
|
+
const result = processData(input.data);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
result: EvalResult.COMPLETE,
|
|
391
|
+
output: { processed: result },
|
|
392
|
+
extraUpdates: [Patch.add("/processedData", result)],
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
|
|
398
|
+
[
|
|
399
|
+
"batchProcess",
|
|
400
|
+
{
|
|
401
|
+
invocationMode: InvocationMode.BATCH,
|
|
402
|
+
batchHandler: async (transactions) => {
|
|
403
|
+
// Process all transactions together
|
|
404
|
+
const results = await processBatch(transactions.map((r) => r.value));
|
|
405
|
+
|
|
406
|
+
return results.map((result) => ({
|
|
407
|
+
result: EvalResult.COMPLETE,
|
|
408
|
+
output: result,
|
|
409
|
+
}));
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
],
|
|
322
413
|
]);
|
|
323
414
|
|
|
324
415
|
// Create handler
|
|
325
|
-
const handler = newDirectedTransactionHandler(
|
|
416
|
+
const handler = newDirectedTransactionHandler("my-handler", actionMap)
|
|
326
417
|
.withInitFn(async (engAPI) => {
|
|
327
418
|
// Initialize resources
|
|
328
|
-
console.log(
|
|
419
|
+
console.log("Handler initialized");
|
|
329
420
|
})
|
|
330
421
|
.withCloseFn(() => {
|
|
331
422
|
// Cleanup resources
|
|
332
|
-
console.log(
|
|
423
|
+
console.log("Handler closed");
|
|
333
424
|
});
|
|
334
425
|
|
|
335
|
-
client.registerTransactionHandler(
|
|
426
|
+
client.registerTransactionHandler("my-handler", handler);
|
|
336
427
|
```
|
|
337
428
|
|
|
338
429
|
### Invocation modes
|
|
339
430
|
|
|
340
431
|
**PARALLEL**: Each transaction processed independently in parallel
|
|
432
|
+
|
|
341
433
|
```typescript
|
|
342
434
|
{
|
|
343
435
|
invocationMode: InvocationMode.PARALLEL,
|
|
@@ -349,6 +441,7 @@ client.registerTransactionHandler('my-handler', handler);
|
|
|
349
441
|
```
|
|
350
442
|
|
|
351
443
|
**BATCH**: All transactions in batch processed together
|
|
444
|
+
|
|
352
445
|
```typescript
|
|
353
446
|
{
|
|
354
447
|
invocationMode: InvocationMode.BATCH,
|
|
@@ -375,16 +468,16 @@ Return appropriate result based on outcome:
|
|
|
375
468
|
Use JSON Patch operations to update workflow state:
|
|
376
469
|
|
|
377
470
|
```typescript
|
|
378
|
-
import { Patch } from
|
|
471
|
+
import { Patch } from "@kaleido-io/workflow-engine-sdk";
|
|
379
472
|
|
|
380
473
|
return {
|
|
381
474
|
result: EvalResult.COMPLETE,
|
|
382
475
|
stateUpdates: [
|
|
383
|
-
Patch.add(
|
|
384
|
-
Patch.replace(
|
|
385
|
-
Patch.remove(
|
|
386
|
-
Patch.add(
|
|
387
|
-
]
|
|
476
|
+
Patch.add("/newField", "value"),
|
|
477
|
+
Patch.replace("/existingField", "newValue"),
|
|
478
|
+
Patch.remove("/oldField"),
|
|
479
|
+
Patch.add("/array/-", "append to array"),
|
|
480
|
+
],
|
|
388
481
|
};
|
|
389
482
|
```
|
|
390
483
|
|
|
@@ -395,8 +488,8 @@ Override the default next stage:
|
|
|
395
488
|
```typescript
|
|
396
489
|
return {
|
|
397
490
|
result: EvalResult.COMPLETE,
|
|
398
|
-
customStage:
|
|
399
|
-
output: { data:
|
|
491
|
+
customStage: "custom-next-stage", // Override default nextStage
|
|
492
|
+
output: { data: "result" },
|
|
400
493
|
};
|
|
401
494
|
```
|
|
402
495
|
|
|
@@ -408,9 +501,9 @@ Emit events to trigger other workflows:
|
|
|
408
501
|
return {
|
|
409
502
|
result: EvalResult.COMPLETE,
|
|
410
503
|
triggers: [
|
|
411
|
-
{ topic:
|
|
412
|
-
{ topic:
|
|
413
|
-
]
|
|
504
|
+
{ topic: "user.created" },
|
|
505
|
+
{ topic: "notification.send", ephemeral: true },
|
|
506
|
+
],
|
|
414
507
|
};
|
|
415
508
|
```
|
|
416
509
|
|
|
@@ -421,9 +514,7 @@ Emit events directly from handlers:
|
|
|
421
514
|
```typescript
|
|
422
515
|
return {
|
|
423
516
|
result: EvalResult.COMPLETE,
|
|
424
|
-
events: [
|
|
425
|
-
{ topic: 'something-happened', data: {} }
|
|
426
|
-
]
|
|
517
|
+
events: [{ topic: "something-happened", data: {} }],
|
|
427
518
|
};
|
|
428
519
|
```
|
|
429
520
|
|
|
@@ -434,7 +525,7 @@ Event sources poll external systems and emit events to the workflow engine.
|
|
|
434
525
|
### Creating an Event Source
|
|
435
526
|
|
|
436
527
|
```typescript
|
|
437
|
-
import { newEventSource } from
|
|
528
|
+
import { newEventSource } from "@kaleido-io/workflow-engine-sdk";
|
|
438
529
|
|
|
439
530
|
// Define your types
|
|
440
531
|
interface MyCheckpoint {
|
|
@@ -453,54 +544,54 @@ interface MyEventData {
|
|
|
453
544
|
|
|
454
545
|
// Create event source
|
|
455
546
|
const eventSource = newEventSource<MyCheckpoint, MyConfig, MyEventData>(
|
|
456
|
-
|
|
547
|
+
"my-event-source",
|
|
457
548
|
async (config, checkpointIn) => {
|
|
458
549
|
// Poll for events
|
|
459
550
|
const events = await fetchNewEvents(
|
|
460
551
|
config.config.topic,
|
|
461
|
-
checkpointIn?.lastId || 0
|
|
552
|
+
checkpointIn?.lastId || 0,
|
|
462
553
|
);
|
|
463
|
-
|
|
554
|
+
|
|
464
555
|
// Return checkpoint and events
|
|
465
556
|
return {
|
|
466
|
-
checkpointOut: {
|
|
467
|
-
lastId: events[events.length - 1]?.id || checkpointIn?.lastId || 0
|
|
557
|
+
checkpointOut: {
|
|
558
|
+
lastId: events[events.length - 1]?.id || checkpointIn?.lastId || 0,
|
|
468
559
|
},
|
|
469
|
-
events: events.map(e => ({
|
|
560
|
+
events: events.map((e) => ({
|
|
470
561
|
idempotencyKey: `event-${e.id}`,
|
|
471
562
|
topic: config.config.topic,
|
|
472
|
-
data: e
|
|
473
|
-
}))
|
|
563
|
+
data: e,
|
|
564
|
+
})),
|
|
474
565
|
};
|
|
475
|
-
}
|
|
566
|
+
},
|
|
476
567
|
)
|
|
477
|
-
.withInitialCheckpoint(async (config) => {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
})
|
|
481
|
-
.withConfigParser(async (info, configData) => {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
})
|
|
489
|
-
.withDeleteFn(async (info) => {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
})
|
|
493
|
-
.withInitFn(async (engAPI) => {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
})
|
|
497
|
-
.withCloseFn(() => {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
});
|
|
568
|
+
.withInitialCheckpoint(async (config) => {
|
|
569
|
+
// Build initial checkpoint
|
|
570
|
+
return { lastId: 0 };
|
|
571
|
+
})
|
|
572
|
+
.withConfigParser(async (info, configData) => {
|
|
573
|
+
// Parse and validate config
|
|
574
|
+
const config = configData as MyConfig;
|
|
575
|
+
if (!config.topic) {
|
|
576
|
+
throw new Error("topic is required");
|
|
577
|
+
}
|
|
578
|
+
return config;
|
|
579
|
+
})
|
|
580
|
+
.withDeleteFn(async (info) => {
|
|
581
|
+
// Cleanup on deletion
|
|
582
|
+
console.log(`Deleting event source: ${info.streamName}`);
|
|
583
|
+
})
|
|
584
|
+
.withInitFn(async (engAPI) => {
|
|
585
|
+
// Initialize resources
|
|
586
|
+
console.log("Event source initialized");
|
|
587
|
+
})
|
|
588
|
+
.withCloseFn(() => {
|
|
589
|
+
// Cleanup resources
|
|
590
|
+
console.log("Event source closed");
|
|
591
|
+
});
|
|
501
592
|
|
|
502
593
|
// Register event source
|
|
503
|
-
client.registerEventSource(
|
|
594
|
+
client.registerEventSource("my-event-source", eventSource);
|
|
504
595
|
```
|
|
505
596
|
|
|
506
597
|
### Event source lifecycle
|
|
@@ -534,52 +625,52 @@ const stellarBlocks = newEventSource<
|
|
|
534
625
|
StellarBlockCheckpoint,
|
|
535
626
|
StellarBlockConfig,
|
|
536
627
|
MinimalLedger
|
|
537
|
-
>(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
628
|
+
>("stellarBlocks", async (config, checkpointIn) => {
|
|
629
|
+
const startLedger = checkpointIn
|
|
630
|
+
? checkpointIn.lastLedger + 1
|
|
631
|
+
: await getLatestLedger();
|
|
632
|
+
const batchSize = config.config.batchSize || 10;
|
|
633
|
+
|
|
634
|
+
const events = [];
|
|
635
|
+
let newCheckpoint = startLedger - 1;
|
|
636
|
+
|
|
637
|
+
for (let i = 0; i < batchSize; i++) {
|
|
638
|
+
try {
|
|
639
|
+
const ledger = await fetchLedger(startLedger + i);
|
|
640
|
+
events.push({
|
|
641
|
+
idempotencyKey: ledger.hash,
|
|
642
|
+
topic: config.config.topic,
|
|
643
|
+
data: {
|
|
644
|
+
sequence: ledger.sequence,
|
|
645
|
+
hash: ledger.hash,
|
|
646
|
+
closedAt: ledger.closed_at,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
newCheckpoint = ledger.sequence;
|
|
650
|
+
} catch (error) {
|
|
651
|
+
break; // Ledger not yet available
|
|
562
652
|
}
|
|
563
|
-
|
|
564
|
-
return {
|
|
565
|
-
checkpointOut: { lastLedger: newCheckpoint },
|
|
566
|
-
events
|
|
567
|
-
};
|
|
568
653
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
return { lastLedger: ledgerNum };
|
|
654
|
+
|
|
655
|
+
return {
|
|
656
|
+
checkpointOut: { lastLedger: newCheckpoint },
|
|
657
|
+
events,
|
|
658
|
+
};
|
|
575
659
|
})
|
|
576
|
-
.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
})
|
|
660
|
+
.withInitialCheckpoint(async (config) => {
|
|
661
|
+
const ledgerNum =
|
|
662
|
+
config.fromLedger === "latest"
|
|
663
|
+
? await getLatestLedger()
|
|
664
|
+
: parseInt(config.fromLedger || "0", 10);
|
|
665
|
+
return { lastLedger: ledgerNum };
|
|
666
|
+
})
|
|
667
|
+
.withConfigParser(async (info, configData) => {
|
|
668
|
+
const config = configData as StellarBlockConfig;
|
|
669
|
+
if (!config.topic) {
|
|
670
|
+
throw new Error("topic is required");
|
|
671
|
+
}
|
|
672
|
+
return config;
|
|
673
|
+
});
|
|
583
674
|
```
|
|
584
675
|
|
|
585
676
|
### Creating event streams
|
|
@@ -612,20 +703,17 @@ The `EngineAPI` interface allows handlers to make synchronous API calls back to
|
|
|
612
703
|
```typescript
|
|
613
704
|
async function myHandler(transaction, input, engAPI: EngineAPI) {
|
|
614
705
|
// Submit transactions to the engine
|
|
615
|
-
const results = await engAPI.submitAsyncTransactions(
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
]
|
|
624
|
-
);
|
|
625
|
-
|
|
706
|
+
const results = await engAPI.submitAsyncTransactions(transaction.authRef, [
|
|
707
|
+
{
|
|
708
|
+
workflowId: "flw:abc123",
|
|
709
|
+
operation: "process",
|
|
710
|
+
input: { data: "value" },
|
|
711
|
+
},
|
|
712
|
+
]);
|
|
713
|
+
|
|
626
714
|
return {
|
|
627
715
|
result: EvalResult.COMPLETE,
|
|
628
|
-
output: { submittedTxs: results }
|
|
716
|
+
output: { submittedTxs: results },
|
|
629
717
|
};
|
|
630
718
|
}
|
|
631
719
|
```
|
|
@@ -635,7 +723,10 @@ async function myHandler(transaction, input, engAPI: EngineAPI) {
|
|
|
635
723
|
For workflows with action-based routing and automatic stage transitions:
|
|
636
724
|
|
|
637
725
|
```typescript
|
|
638
|
-
import {
|
|
726
|
+
import {
|
|
727
|
+
BasicStageDirector,
|
|
728
|
+
WithStageDirector,
|
|
729
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
639
730
|
|
|
640
731
|
interface MyInput extends WithStageDirector {
|
|
641
732
|
data: string;
|
|
@@ -647,10 +738,10 @@ class MyInputImpl implements MyInput {
|
|
|
647
738
|
|
|
648
739
|
constructor(input: any) {
|
|
649
740
|
this.stageDirector = new BasicStageDirector(
|
|
650
|
-
input.action,
|
|
651
|
-
input.outputPath,
|
|
652
|
-
input.nextStage,
|
|
653
|
-
input.failureStage
|
|
741
|
+
input.action, // Action to execute
|
|
742
|
+
input.outputPath, // Where to store output
|
|
743
|
+
input.nextStage, // Stage on success
|
|
744
|
+
input.failureStage, // Stage on failure
|
|
654
745
|
);
|
|
655
746
|
this.data = input.data;
|
|
656
747
|
}
|
|
@@ -663,16 +754,19 @@ class MyInputImpl implements MyInput {
|
|
|
663
754
|
// The SDK automatically wraps plain JSON objects from the engine
|
|
664
755
|
// with a getStageDirector() method, so you can also use plain objects:
|
|
665
756
|
const actionMap = new Map([
|
|
666
|
-
[
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
757
|
+
[
|
|
758
|
+
"myAction",
|
|
759
|
+
{
|
|
760
|
+
invocationMode: InvocationMode.PARALLEL,
|
|
761
|
+
handler: async (transaction, input: any) => {
|
|
762
|
+
// input.action, input.outputPath, input.nextStage are available
|
|
763
|
+
return {
|
|
764
|
+
result: EvalResult.COMPLETE,
|
|
765
|
+
output: { processed: input.data },
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
],
|
|
676
770
|
]);
|
|
677
771
|
```
|
|
678
772
|
|
|
@@ -688,27 +782,28 @@ handler: async (transaction, input) => {
|
|
|
688
782
|
const result = await riskyOperation(input);
|
|
689
783
|
return {
|
|
690
784
|
result: EvalResult.COMPLETE,
|
|
691
|
-
output: result
|
|
785
|
+
output: result,
|
|
692
786
|
};
|
|
693
787
|
} catch (error) {
|
|
694
788
|
if (isTransient(error)) {
|
|
695
789
|
return {
|
|
696
790
|
result: EvalResult.TRANSIENT_ERROR,
|
|
697
|
-
error: error as Error
|
|
791
|
+
error: error as Error,
|
|
698
792
|
};
|
|
699
793
|
} else {
|
|
700
794
|
return {
|
|
701
795
|
result: EvalResult.HARD_FAILURE,
|
|
702
|
-
error: error as Error
|
|
796
|
+
error: error as Error,
|
|
703
797
|
};
|
|
704
798
|
}
|
|
705
799
|
}
|
|
706
|
-
}
|
|
800
|
+
};
|
|
707
801
|
```
|
|
708
802
|
|
|
709
803
|
### Connection errors
|
|
710
804
|
|
|
711
805
|
The client automatically handles:
|
|
806
|
+
|
|
712
807
|
- WebSocket disconnections
|
|
713
808
|
- Automatic reconnection with exponential backoff
|
|
714
809
|
- Handler re-registration on reconnect
|
|
@@ -720,7 +815,7 @@ Monitor connection events:
|
|
|
720
815
|
// The SDK logs connection events automatically
|
|
721
816
|
// Check connection status programmatically:
|
|
722
817
|
if (!client.isConnected()) {
|
|
723
|
-
console.warn(
|
|
818
|
+
console.warn("Client disconnected, will auto-reconnect");
|
|
724
819
|
}
|
|
725
820
|
```
|
|
726
821
|
|
|
@@ -729,14 +824,14 @@ if (!client.isConnected()) {
|
|
|
729
824
|
The SDK uses a structured logger:
|
|
730
825
|
|
|
731
826
|
```typescript
|
|
732
|
-
import { newLogger } from
|
|
827
|
+
import { newLogger } from "@kaleido-io/workflow-engine-sdk";
|
|
733
828
|
|
|
734
|
-
const log = newLogger(
|
|
829
|
+
const log = newLogger("my-component");
|
|
735
830
|
|
|
736
|
-
log.debug(
|
|
737
|
-
log.info(
|
|
738
|
-
log.warn(
|
|
739
|
-
log.error(
|
|
831
|
+
log.debug("Debug message", { metadata: "value" });
|
|
832
|
+
log.info("Info message", { userId: 123 });
|
|
833
|
+
log.warn("Warning message", { reason: "low memory" });
|
|
834
|
+
log.error("Error message", { error: err.message });
|
|
740
835
|
```
|
|
741
836
|
|
|
742
837
|
## Testing
|
|
@@ -746,18 +841,18 @@ log.error('Error message', { error: err.message });
|
|
|
746
841
|
Mock the EngineAPI and test handlers in isolation:
|
|
747
842
|
|
|
748
843
|
```typescript
|
|
749
|
-
import { jest } from
|
|
844
|
+
import { jest } from "@jest/globals";
|
|
750
845
|
|
|
751
|
-
describe(
|
|
752
|
-
it(
|
|
846
|
+
describe("MyHandler", () => {
|
|
847
|
+
it("should process data correctly", async () => {
|
|
753
848
|
const mockEngAPI = {
|
|
754
|
-
submitAsyncTransactions: jest.fn().mockResolvedValue([])
|
|
849
|
+
submitAsyncTransactions: jest.fn().mockResolvedValue([]),
|
|
755
850
|
};
|
|
756
851
|
|
|
757
852
|
const transaction = {
|
|
758
|
-
transactionId:
|
|
759
|
-
workflowId:
|
|
760
|
-
input: { action:
|
|
853
|
+
transactionId: "ftx:test123",
|
|
854
|
+
workflowId: "flw:test",
|
|
855
|
+
input: { action: "process", data: "test" },
|
|
761
856
|
};
|
|
762
857
|
|
|
763
858
|
const result = await myHandler(transaction, transaction.input, mockEngAPI);
|
|
@@ -773,37 +868,42 @@ describe('MyHandler', () => {
|
|
|
773
868
|
Test with a running workflow engine:
|
|
774
869
|
|
|
775
870
|
```typescript
|
|
776
|
-
import {
|
|
777
|
-
WorkflowEngineClient,
|
|
871
|
+
import {
|
|
872
|
+
WorkflowEngineClient,
|
|
778
873
|
ConfigLoader,
|
|
779
|
-
WorkflowEngineConfig
|
|
780
|
-
} from
|
|
781
|
-
import * as fs from
|
|
782
|
-
import * as yaml from
|
|
874
|
+
WorkflowEngineConfig,
|
|
875
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
876
|
+
import * as fs from "fs";
|
|
877
|
+
import * as yaml from "js-yaml";
|
|
783
878
|
|
|
784
879
|
// Helper to load test config (your test infrastructure)
|
|
785
880
|
function loadTestConfig(): WorkflowEngineConfig {
|
|
786
|
-
const configFile = fs.readFileSync(
|
|
787
|
-
const config: WorkflowEngineConfig = yaml.load(
|
|
788
|
-
|
|
881
|
+
const configFile = fs.readFileSync("./test-config.yaml", "utf8");
|
|
882
|
+
const config: WorkflowEngineConfig = yaml.load(
|
|
883
|
+
configFile,
|
|
884
|
+
) as WorkflowEngineConfig;
|
|
885
|
+
|
|
789
886
|
// Override with environment variables if present
|
|
790
887
|
if (process.env.WORKFLOW_ENGINE_URL) {
|
|
791
888
|
config.workflowEngine.url = process.env.WORKFLOW_ENGINE_URL;
|
|
792
889
|
}
|
|
793
|
-
|
|
890
|
+
|
|
794
891
|
return config;
|
|
795
892
|
}
|
|
796
893
|
|
|
797
|
-
describe(
|
|
894
|
+
describe("Component Test", () => {
|
|
798
895
|
let client: WorkflowEngineClient;
|
|
799
896
|
const testConfig = loadTestConfig();
|
|
800
897
|
|
|
801
898
|
beforeAll(async () => {
|
|
802
899
|
// Use SDK's ConfigLoader to transform config
|
|
803
|
-
const clientConfig = ConfigLoader.createClientConfig(
|
|
900
|
+
const clientConfig = ConfigLoader.createClientConfig(
|
|
901
|
+
testConfig,
|
|
902
|
+
"test-provider",
|
|
903
|
+
);
|
|
804
904
|
client = new WorkflowEngineClient(clientConfig);
|
|
805
905
|
|
|
806
|
-
client.registerTransactionHandler(
|
|
906
|
+
client.registerTransactionHandler("my-handler", handler);
|
|
807
907
|
await client.connect();
|
|
808
908
|
});
|
|
809
909
|
|
|
@@ -811,25 +911,31 @@ describe('Component Test', () => {
|
|
|
811
911
|
client.disconnect();
|
|
812
912
|
});
|
|
813
913
|
|
|
814
|
-
it(
|
|
914
|
+
it("should process workflow end-to-end", async () => {
|
|
815
915
|
// For REST API calls, extract auth headers from SDK config
|
|
816
916
|
function getAuthHeaders(): Record<string, string> {
|
|
817
|
-
const clientConfig = ConfigLoader.createClientConfig(
|
|
917
|
+
const clientConfig = ConfigLoader.createClientConfig(
|
|
918
|
+
testConfig,
|
|
919
|
+
"test-client",
|
|
920
|
+
);
|
|
818
921
|
return clientConfig.options?.headers || {};
|
|
819
922
|
}
|
|
820
923
|
|
|
821
924
|
const authHeaders = getAuthHeaders();
|
|
822
|
-
|
|
925
|
+
|
|
823
926
|
// Create workflow
|
|
824
|
-
const workflowResponse = await fetch(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
927
|
+
const workflowResponse = await fetch(
|
|
928
|
+
"http://localhost:5503/api/v1/workflows",
|
|
929
|
+
{
|
|
930
|
+
method: "POST",
|
|
931
|
+
headers: {
|
|
932
|
+
"Content-Type": "application/x-yaml",
|
|
933
|
+
...authHeaders, // SDK handles auth automatically
|
|
934
|
+
},
|
|
935
|
+
body: workflowYAML,
|
|
829
936
|
},
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
937
|
+
);
|
|
938
|
+
|
|
833
939
|
// Wait for completion and verify results
|
|
834
940
|
});
|
|
835
941
|
});
|
|
@@ -847,10 +953,10 @@ import {
|
|
|
847
953
|
InvocationMode,
|
|
848
954
|
EvalResult,
|
|
849
955
|
Patch,
|
|
850
|
-
ConfigLoader
|
|
851
|
-
} from
|
|
852
|
-
import * as fs from
|
|
853
|
-
import * as yaml from
|
|
956
|
+
ConfigLoader,
|
|
957
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
958
|
+
import * as fs from "fs";
|
|
959
|
+
import * as yaml from "js-yaml";
|
|
854
960
|
|
|
855
961
|
interface ProcessInput {
|
|
856
962
|
action: string;
|
|
@@ -860,67 +966,79 @@ interface ProcessInput {
|
|
|
860
966
|
|
|
861
967
|
async function main() {
|
|
862
968
|
// Load config (your application handles file loading)
|
|
863
|
-
const configFile = fs.readFileSync(
|
|
864
|
-
const config: WorkflowEngineConfig = yaml.load(
|
|
865
|
-
|
|
969
|
+
const configFile = fs.readFileSync("./config.yaml", "utf8");
|
|
970
|
+
const config: WorkflowEngineConfig = yaml.load(
|
|
971
|
+
configFile,
|
|
972
|
+
) as WorkflowEngineConfig;
|
|
973
|
+
|
|
866
974
|
// SDK transforms config
|
|
867
|
-
const clientConfig = ConfigLoader.createClientConfig(
|
|
975
|
+
const clientConfig = ConfigLoader.createClientConfig(
|
|
976
|
+
config,
|
|
977
|
+
"payment-service",
|
|
978
|
+
);
|
|
868
979
|
|
|
869
980
|
// Create client
|
|
870
981
|
const client = new WorkflowEngineClient(clientConfig);
|
|
871
982
|
|
|
872
983
|
// Define actions
|
|
873
984
|
const actionMap = new Map([
|
|
874
|
-
[
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
985
|
+
[
|
|
986
|
+
"validatePayment",
|
|
987
|
+
{
|
|
988
|
+
invocationMode: InvocationMode.PARALLEL,
|
|
989
|
+
handler: async (transaction, input: ProcessInput) => {
|
|
990
|
+
if (input.amount <= 0) {
|
|
991
|
+
return {
|
|
992
|
+
result: EvalResult.HARD_FAILURE,
|
|
993
|
+
error: new Error("Invalid amount"),
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
878
997
|
return {
|
|
879
|
-
result: EvalResult.
|
|
880
|
-
|
|
998
|
+
result: EvalResult.COMPLETE,
|
|
999
|
+
output: { validated: true },
|
|
1000
|
+
extraUpdates: [
|
|
1001
|
+
Patch.add("/validation", { valid: true, timestamp: new Date() }),
|
|
1002
|
+
],
|
|
881
1003
|
};
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
result: EvalResult.COMPLETE,
|
|
886
|
-
output: { validated: true },
|
|
887
|
-
extraUpdates: [
|
|
888
|
-
Patch.add('/validation', { valid: true, timestamp: new Date() })
|
|
889
|
-
]
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
}],
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
],
|
|
893
1007
|
|
|
894
|
-
[
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1008
|
+
[
|
|
1009
|
+
"processPayment",
|
|
1010
|
+
{
|
|
1011
|
+
invocationMode: InvocationMode.PARALLEL,
|
|
1012
|
+
handler: async (transaction, input: ProcessInput) => {
|
|
1013
|
+
const paymentResult = await processPayment(
|
|
1014
|
+
input.userId,
|
|
1015
|
+
input.amount,
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
return {
|
|
1019
|
+
result: EvalResult.COMPLETE,
|
|
1020
|
+
output: paymentResult,
|
|
1021
|
+
triggers: [{ topic: "payment.completed" }],
|
|
1022
|
+
};
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
],
|
|
908
1026
|
]);
|
|
909
1027
|
|
|
910
1028
|
// Create handler
|
|
911
|
-
const handler = newDirectedTransactionHandler(
|
|
1029
|
+
const handler = newDirectedTransactionHandler("payment-handler", actionMap)
|
|
912
1030
|
.withInitFn(async (engAPI) => {
|
|
913
|
-
console.log(
|
|
1031
|
+
console.log("Payment handler initialized");
|
|
914
1032
|
})
|
|
915
1033
|
.withCloseFn(() => {
|
|
916
|
-
console.log(
|
|
1034
|
+
console.log("Payment handler closed");
|
|
917
1035
|
});
|
|
918
1036
|
|
|
919
1037
|
// Register and connect
|
|
920
|
-
client.registerTransactionHandler(
|
|
1038
|
+
client.registerTransactionHandler("payment-handler", handler);
|
|
921
1039
|
await client.connect();
|
|
922
1040
|
|
|
923
|
-
console.log(
|
|
1041
|
+
console.log("Payment service ready");
|
|
924
1042
|
}
|
|
925
1043
|
|
|
926
1044
|
main().catch(console.error);
|
|
@@ -973,13 +1091,13 @@ Workflow Engine
|
|
|
973
1091
|
|
|
974
1092
|
```typescript
|
|
975
1093
|
const client = new WorkflowEngineClient({
|
|
976
|
-
url:
|
|
977
|
-
providerName:
|
|
1094
|
+
url: "ws://localhost:5503/ws",
|
|
1095
|
+
providerName: "my-service",
|
|
978
1096
|
options: {
|
|
979
1097
|
headers: {
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
}
|
|
1098
|
+
Authorization: `Bearer ${process.env.AUTH_TOKEN}`,
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
983
1101
|
});
|
|
984
1102
|
```
|
|
985
1103
|
|
|
@@ -987,10 +1105,10 @@ const client = new WorkflowEngineClient({
|
|
|
987
1105
|
|
|
988
1106
|
```typescript
|
|
989
1107
|
// Register multiple handlers
|
|
990
|
-
client.registerTransactionHandler(
|
|
991
|
-
client.registerTransactionHandler(
|
|
992
|
-
client.registerEventSource(
|
|
993
|
-
client.registerEventSource(
|
|
1108
|
+
client.registerTransactionHandler("handler1", handler1);
|
|
1109
|
+
client.registerTransactionHandler("handler2", handler2);
|
|
1110
|
+
client.registerEventSource("source1", source1);
|
|
1111
|
+
client.registerEventSource("source2", source2);
|
|
994
1112
|
|
|
995
1113
|
// All handlers use the same WebSocket connection
|
|
996
1114
|
await client.connect();
|
|
@@ -999,30 +1117,35 @@ await client.connect();
|
|
|
999
1117
|
### Configuration validation
|
|
1000
1118
|
|
|
1001
1119
|
```typescript
|
|
1002
|
-
import {
|
|
1003
|
-
|
|
1004
|
-
|
|
1120
|
+
import {
|
|
1121
|
+
ConfigLoader,
|
|
1122
|
+
WorkflowEngineConfig,
|
|
1123
|
+
} from "@kaleido-io/workflow-engine-sdk";
|
|
1124
|
+
import * as fs from "fs";
|
|
1125
|
+
import * as yaml from "js-yaml";
|
|
1005
1126
|
|
|
1006
1127
|
try {
|
|
1007
1128
|
// Your application loads config
|
|
1008
|
-
const configFile = fs.readFileSync(
|
|
1009
|
-
const config: WorkflowEngineConfig = yaml.load(
|
|
1010
|
-
|
|
1129
|
+
const configFile = fs.readFileSync("./config.yaml", "utf8");
|
|
1130
|
+
const config: WorkflowEngineConfig = yaml.load(
|
|
1131
|
+
configFile,
|
|
1132
|
+
) as WorkflowEngineConfig;
|
|
1133
|
+
|
|
1011
1134
|
// Validate required fields
|
|
1012
1135
|
if (!config.workflowEngine) {
|
|
1013
|
-
throw new Error(
|
|
1136
|
+
throw new Error("Missing workflowEngine configuration");
|
|
1014
1137
|
}
|
|
1015
1138
|
if (!config.workflowEngine.url) {
|
|
1016
|
-
throw new Error(
|
|
1139
|
+
throw new Error("Missing workflowEngine.url");
|
|
1017
1140
|
}
|
|
1018
1141
|
if (!config.workflowEngine.auth) {
|
|
1019
|
-
throw new Error(
|
|
1142
|
+
throw new Error("Missing workflowEngine.auth");
|
|
1020
1143
|
}
|
|
1021
|
-
|
|
1144
|
+
|
|
1022
1145
|
// SDK logs summary (without sensitive data)
|
|
1023
1146
|
ConfigLoader.logConfigSummary(config);
|
|
1024
1147
|
} catch (error) {
|
|
1025
|
-
console.error(
|
|
1148
|
+
console.error("Invalid configuration:", error.message);
|
|
1026
1149
|
process.exit(1);
|
|
1027
1150
|
}
|
|
1028
1151
|
```
|
|
@@ -1048,7 +1171,7 @@ try {
|
|
|
1048
1171
|
|
|
1049
1172
|
```typescript
|
|
1050
1173
|
// Register BEFORE submitting workflows
|
|
1051
|
-
client.registerTransactionHandler(
|
|
1174
|
+
client.registerTransactionHandler("my-handler", handler);
|
|
1052
1175
|
await client.connect();
|
|
1053
1176
|
// Now workflows can use this handler
|
|
1054
1177
|
```
|
|
@@ -1061,18 +1184,19 @@ await client.connect();
|
|
|
1061
1184
|
|
|
1062
1185
|
```typescript
|
|
1063
1186
|
// Verify URL format (should include ws:// or wss://)
|
|
1064
|
-
url:
|
|
1065
|
-
url:
|
|
1187
|
+
url: "ws://localhost:5503/ws"; // ✓ Correct
|
|
1188
|
+
url: "localhost:5503"; // ✗ Wrong
|
|
1066
1189
|
|
|
1067
1190
|
// Check authentication
|
|
1068
|
-
authToken: process.env.AUTH_TOKEN
|
|
1191
|
+
authToken: process.env.AUTH_TOKEN; // Ensure token is valid
|
|
1069
1192
|
```
|
|
1070
1193
|
|
|
1071
1194
|
### Event source not polling
|
|
1072
1195
|
|
|
1073
1196
|
**Problem**: Event stream created but no events emitted
|
|
1074
1197
|
|
|
1075
|
-
**Solution**:
|
|
1198
|
+
**Solution**:
|
|
1199
|
+
|
|
1076
1200
|
1. Check stream is started: `"started": true`
|
|
1077
1201
|
2. Verify handler name matches: `listenerHandler: 'my-event-source'`
|
|
1078
1202
|
3. Check provider name matches: `listenerHandlerProvider: 'my-service'`
|
|
@@ -1112,10 +1236,10 @@ See the TypeScript type definitions for complete API documentation:
|
|
|
1112
1236
|
|
|
1113
1237
|
The `ConfigLoader` class provides utilities for transforming configuration:
|
|
1114
1238
|
|
|
1115
|
-
- `
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1239
|
+
- `loadClientConfigFromFile(configFilePath?)` - Loads WFE config from a YAML file. Only the root key **`workflow-engine`** is supported. **Outbound:** `url` + `auth`. **Inbound:** `server` (address, port); app creates WebSocket server, optional `server.tls`. Uses `process.env[WFE_CONFIG_FILE]` when path is omitted.
|
|
1240
|
+
- `createClientConfig(config, providerName)` - Transforms `WorkflowEngineConfig` into `WorkflowEngineClientConfig` (converts HTTP to WebSocket URL, sets auth headers, parses time strings for delays).
|
|
1241
|
+
- `logConfigSummary(config)` - Logs configuration summary (without sensitive data).
|
|
1242
|
+
|
|
1243
|
+
Time strings for delay fields (e.g. `retryDelay`) are parsed by `parseTimeStringToMs`: units `ms`, `s`, `m`, `h` (e.g. `"2s"`, `"30s"`, `"100ms"`, `"1m"`).
|
|
1120
1244
|
|
|
1121
|
-
**Note:**
|
|
1245
|
+
**Note:** For file-based config, use `loadClientConfigFromFile` so the SDK handles the `workflow-engine` file format. Your application can still load YAML and call `createClientConfig` when using the in-memory `WorkflowEngineConfig` shape (e.g. with a `workflowEngine` property).
|