xcomponent-ai 0.2.2 β 0.3.0
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/EVENT-ACCUMULATION-GUIDE.md +505 -0
- package/EXTERNAL-API.md +719 -0
- package/LLM-GUIDE.md +575 -0
- package/QUICKSTART.md +1 -1
- package/SCALABILITY.md +455 -0
- package/dist/cli.js +170 -63
- package/dist/cli.js.map +1 -1
- package/dist/component-registry.d.ts +35 -4
- package/dist/component-registry.d.ts.map +1 -1
- package/dist/component-registry.js +194 -9
- package/dist/component-registry.js.map +1 -1
- package/dist/external-broker-api.d.ts +205 -0
- package/dist/external-broker-api.d.ts.map +1 -0
- package/dist/external-broker-api.js +222 -0
- package/dist/external-broker-api.js.map +1 -0
- package/dist/fsm-runtime.d.ts +47 -17
- package/dist/fsm-runtime.d.ts.map +1 -1
- package/dist/fsm-runtime.js +333 -136
- package/dist/fsm-runtime.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/mermaid-generator.d.ts.map +1 -1
- package/dist/mermaid-generator.js +1 -17
- package/dist/mermaid-generator.js.map +1 -1
- package/dist/message-broker.d.ts +116 -0
- package/dist/message-broker.d.ts.map +1 -0
- package/dist/message-broker.js +225 -0
- package/dist/message-broker.js.map +1 -0
- package/dist/types.d.ts +92 -19
- package/dist/types.d.ts.map +1 -1
- package/examples/advanced-patterns-demo.yaml +339 -0
- package/examples/cross-component-demo.yaml +68 -0
- package/examples/distributed-demo/README.md +234 -0
- package/examples/distributed-demo/order.yaml +71 -0
- package/examples/distributed-demo/payment.yaml +60 -0
- package/examples/event-accumulation-demo.yaml +172 -0
- package/examples/explicit-transitions-demo.yaml +236 -0
- package/examples/payment-receiver.yaml +56 -0
- package/package.json +8 -1
- package/public/dashboard.html +647 -110
package/EXTERNAL-API.md
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
# π External Broker API Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to interact with xcomponent-ai from **any programming language** using the message broker (Redis), without needing HTTP API access.
|
|
4
|
+
|
|
5
|
+
## π Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Sending Events to FSMs](#sending-events-to-fsms)
|
|
9
|
+
- [Subscribing to FSM Events](#subscribing-to-fsm-events)
|
|
10
|
+
- [Channel Reference](#channel-reference)
|
|
11
|
+
- [Examples by Language](#examples-by-language)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
The External Broker API allows you to:
|
|
18
|
+
1. **Send events to FSM instances** from any system via message broker
|
|
19
|
+
2. **Subscribe to FSM state changes** in real-time from external applications
|
|
20
|
+
3. **Language-agnostic integration** (Python, Go, Java, Ruby, Rust, etc.)
|
|
21
|
+
|
|
22
|
+
### Architecture
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
βββββββββββββββββββ Redis Pub/Sub βββββββββββββββββββ
|
|
26
|
+
β Your App β β xcomponent-ai β
|
|
27
|
+
β (Python/Go/ ββββββββββββββββββββββββββββββββΆβ Runtime β
|
|
28
|
+
β Java/etc.) β Send events via broker β β
|
|
29
|
+
β β β β
|
|
30
|
+
β βββββββββββββββββββββββββββββββββ Publish state β
|
|
31
|
+
β β Subscribe to FSM events β changes β
|
|
32
|
+
βββββββββββββββββββ βββββββββββββββββββ
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**No HTTP needed!** Just publish/subscribe to Redis channels.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Sending Events to FSMs
|
|
40
|
+
|
|
41
|
+
### Enable External Commands
|
|
42
|
+
|
|
43
|
+
Start xcomponent-ai with the `--external-api` flag:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
xcomponent-ai serve order.yaml \
|
|
47
|
+
--port 3000 \
|
|
48
|
+
--broker redis://localhost:6379 \
|
|
49
|
+
--external-api
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Send Event to Specific Instance
|
|
53
|
+
|
|
54
|
+
**Channel:** `xcomponent:external:commands`
|
|
55
|
+
|
|
56
|
+
**Message format:**
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"componentName": "OrderComponent",
|
|
60
|
+
"instanceId": "abc-123-def-456",
|
|
61
|
+
"event": {
|
|
62
|
+
"type": "VALIDATE",
|
|
63
|
+
"payload": {
|
|
64
|
+
"approvedBy": "user@example.com"
|
|
65
|
+
},
|
|
66
|
+
"timestamp": 1706280000000
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Broadcast Event to Instances in a State
|
|
72
|
+
|
|
73
|
+
**Channel:** `xcomponent:external:broadcasts`
|
|
74
|
+
|
|
75
|
+
#### Broadcast to ALL instances (no filters)
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"componentName": "OrderComponent",
|
|
80
|
+
"machineName": "Order",
|
|
81
|
+
"currentState": "Pending",
|
|
82
|
+
"event": {
|
|
83
|
+
"type": "TIMEOUT",
|
|
84
|
+
"payload": {},
|
|
85
|
+
"timestamp": 1706280000000
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This sends `TIMEOUT` to **ALL** Order instances in `Pending` state.
|
|
91
|
+
|
|
92
|
+
#### Broadcast to specific instances (with property filters)
|
|
93
|
+
|
|
94
|
+
**Target a single customer:**
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"componentName": "OrderComponent",
|
|
98
|
+
"machineName": "Order",
|
|
99
|
+
"currentState": "Pending",
|
|
100
|
+
"filters": [
|
|
101
|
+
{
|
|
102
|
+
"property": "customerId",
|
|
103
|
+
"operator": "===",
|
|
104
|
+
"value": "CUST-001"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"event": {
|
|
108
|
+
"type": "TIMEOUT"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Sends `TIMEOUT` only to Orders with `customerId === "CUST-001"`.
|
|
114
|
+
|
|
115
|
+
**Multiple filters (AND logic):**
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"componentName": "OrderComponent",
|
|
119
|
+
"machineName": "Order",
|
|
120
|
+
"currentState": "Pending",
|
|
121
|
+
"filters": [
|
|
122
|
+
{
|
|
123
|
+
"property": "customerId",
|
|
124
|
+
"value": "CUST-001"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"property": "amount",
|
|
128
|
+
"operator": ">",
|
|
129
|
+
"value": 1000
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"event": {
|
|
133
|
+
"type": "URGENT_REVIEW"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Sends `URGENT_REVIEW` to Orders with `customerId === "CUST-001"` **AND** `amount > 1000`.
|
|
139
|
+
|
|
140
|
+
**Nested properties:**
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"filters": [
|
|
144
|
+
{
|
|
145
|
+
"property": "customer.tier",
|
|
146
|
+
"value": "premium"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**β
Can target a single instance** if filters are specific enough!
|
|
153
|
+
|
|
154
|
+
**Supported operators:**
|
|
155
|
+
- `===` (equal, default)
|
|
156
|
+
- `!==` (not equal)
|
|
157
|
+
- `>` (greater than)
|
|
158
|
+
- `<` (less than)
|
|
159
|
+
- `>=` (greater or equal)
|
|
160
|
+
- `<=` (less or equal)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Subscribing to FSM Events
|
|
165
|
+
|
|
166
|
+
### Enable Event Publishing
|
|
167
|
+
|
|
168
|
+
Start xcomponent-ai with event publishing enabled:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
xcomponent-ai serve order.yaml \
|
|
172
|
+
--port 3000 \
|
|
173
|
+
--broker redis://localhost:6379 \
|
|
174
|
+
--publish-events
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Available Event Channels
|
|
178
|
+
|
|
179
|
+
| Channel | Description |
|
|
180
|
+
|---------|-------------|
|
|
181
|
+
| `xcomponent:events:state_change` | State transitions (e.g., Pending β Validated) |
|
|
182
|
+
| `xcomponent:events:instance_created` | New FSM instances created |
|
|
183
|
+
| `xcomponent:events:instance_disposed` | FSM instances disposed |
|
|
184
|
+
| `xcomponent:events:instance_error` | Errors in FSM instances |
|
|
185
|
+
| `xcomponent:events:cross_component_cascade` | Cross-component cascades triggered |
|
|
186
|
+
|
|
187
|
+
### Event Message Format
|
|
188
|
+
|
|
189
|
+
#### state_change Event
|
|
190
|
+
|
|
191
|
+
Emitted when an instance transitions between states.
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"type": "state_change",
|
|
196
|
+
"componentName": "OrderComponent",
|
|
197
|
+
"data": {
|
|
198
|
+
"instanceId": "abc-123",
|
|
199
|
+
"machineName": "Order",
|
|
200
|
+
"previousState": "Pending",
|
|
201
|
+
"newState": "Validated",
|
|
202
|
+
"event": {
|
|
203
|
+
"type": "VALIDATE",
|
|
204
|
+
"payload": {
|
|
205
|
+
"approvedBy": "user@example.com"
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"eventId": "evt-456",
|
|
209
|
+
"timestamp": 1706280000000,
|
|
210
|
+
"instance": {
|
|
211
|
+
"id": "abc-123",
|
|
212
|
+
"machineName": "Order",
|
|
213
|
+
"currentState": "Validated",
|
|
214
|
+
"context": {
|
|
215
|
+
"orderId": "ORD-001",
|
|
216
|
+
"amount": 1000,
|
|
217
|
+
"customerId": "CUST-001",
|
|
218
|
+
"approvedBy": "user@example.com"
|
|
219
|
+
},
|
|
220
|
+
"publicMember": {
|
|
221
|
+
"status": "validated",
|
|
222
|
+
"totalAmount": 1000
|
|
223
|
+
},
|
|
224
|
+
"status": "active",
|
|
225
|
+
"createdAt": 1706279000000,
|
|
226
|
+
"updatedAt": 1706280000000
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
"timestamp": 1706280000000
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Key fields:**
|
|
234
|
+
- β
`componentName` - Component name (e.g., "OrderComponent")
|
|
235
|
+
- β
`data.machineName` - State machine name (e.g., "Order")
|
|
236
|
+
- β
`data.instanceId` - Instance ID
|
|
237
|
+
- β
`data.instance` - **Complete instance object** with:
|
|
238
|
+
- `context` - Full instance context (business data)
|
|
239
|
+
- `publicMember` - Public member data (if defined in YAML)
|
|
240
|
+
- `currentState` - Current state after transition
|
|
241
|
+
- `status` - Instance status (active, completed, error)
|
|
242
|
+
- `createdAt`, `updatedAt` - Timestamps
|
|
243
|
+
|
|
244
|
+
#### instance_created Event
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"type": "instance_created",
|
|
249
|
+
"componentName": "OrderComponent",
|
|
250
|
+
"data": {
|
|
251
|
+
"id": "abc-123",
|
|
252
|
+
"machineName": "Order",
|
|
253
|
+
"currentState": "Created",
|
|
254
|
+
"context": {
|
|
255
|
+
"orderId": "ORD-001",
|
|
256
|
+
"amount": 1000
|
|
257
|
+
},
|
|
258
|
+
"publicMember": {},
|
|
259
|
+
"status": "active",
|
|
260
|
+
"createdAt": 1706279000000,
|
|
261
|
+
"updatedAt": 1706279000000
|
|
262
|
+
},
|
|
263
|
+
"timestamp": 1706279000000
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### instance_disposed Event
|
|
268
|
+
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"type": "instance_disposed",
|
|
272
|
+
"componentName": "OrderComponent",
|
|
273
|
+
"data": {
|
|
274
|
+
"id": "abc-123",
|
|
275
|
+
"machineName": "Order",
|
|
276
|
+
"currentState": "Completed",
|
|
277
|
+
"context": {
|
|
278
|
+
"orderId": "ORD-001",
|
|
279
|
+
"amount": 1000
|
|
280
|
+
},
|
|
281
|
+
"status": "completed",
|
|
282
|
+
"createdAt": 1706279000000,
|
|
283
|
+
"updatedAt": 1706280000000
|
|
284
|
+
},
|
|
285
|
+
"timestamp": 1706280000000
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### instance_error Event
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"type": "instance_error",
|
|
294
|
+
"componentName": "OrderComponent",
|
|
295
|
+
"data": {
|
|
296
|
+
"instanceId": "abc-123",
|
|
297
|
+
"machineName": "Order",
|
|
298
|
+
"error": "Guard validation failed",
|
|
299
|
+
"instance": {
|
|
300
|
+
"id": "abc-123",
|
|
301
|
+
"machineName": "Order",
|
|
302
|
+
"currentState": "Pending",
|
|
303
|
+
"context": {
|
|
304
|
+
"orderId": "ORD-001"
|
|
305
|
+
},
|
|
306
|
+
"status": "error"
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
"timestamp": 1706280000000
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Channel Reference
|
|
316
|
+
|
|
317
|
+
### Commands (Publish to these)
|
|
318
|
+
|
|
319
|
+
| Channel | Purpose | Message Type |
|
|
320
|
+
|---------|---------|--------------|
|
|
321
|
+
| `xcomponent:external:commands` | Send event to specific instance | `ExternalCommand` |
|
|
322
|
+
| `xcomponent:external:broadcasts` | Broadcast event to instances in state | `ExternalBroadcastCommand` |
|
|
323
|
+
|
|
324
|
+
### Events (Subscribe to these)
|
|
325
|
+
|
|
326
|
+
| Channel | Purpose | Message Type |
|
|
327
|
+
|---------|---------|--------------|
|
|
328
|
+
| `xcomponent:events:state_change` | State transitions | `PublishedFSMEvent` |
|
|
329
|
+
| `xcomponent:events:instance_created` | Instance creations | `PublishedFSMEvent` |
|
|
330
|
+
| `xcomponent:events:instance_disposed` | Instance disposals | `PublishedFSMEvent` |
|
|
331
|
+
| `xcomponent:events:instance_error` | Instance errors | `PublishedFSMEvent` |
|
|
332
|
+
| `xcomponent:events:cross_component_cascade` | Cross-component cascades | `PublishedFSMEvent` |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Examples by Language
|
|
337
|
+
|
|
338
|
+
### Node.js / TypeScript
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
import { createClient } from 'redis';
|
|
342
|
+
|
|
343
|
+
// Connect to Redis
|
|
344
|
+
const redis = createClient({ url: 'redis://localhost:6379' });
|
|
345
|
+
await redis.connect();
|
|
346
|
+
|
|
347
|
+
// Send event to instance
|
|
348
|
+
await redis.publish('xcomponent:external:commands', JSON.stringify({
|
|
349
|
+
componentName: 'OrderComponent',
|
|
350
|
+
instanceId: 'order-123',
|
|
351
|
+
event: {
|
|
352
|
+
type: 'VALIDATE',
|
|
353
|
+
payload: { approvedBy: 'alice@example.com' },
|
|
354
|
+
timestamp: Date.now()
|
|
355
|
+
}
|
|
356
|
+
}));
|
|
357
|
+
|
|
358
|
+
// Broadcast to all instances in a state
|
|
359
|
+
await redis.publish('xcomponent:external:broadcasts', JSON.stringify({
|
|
360
|
+
componentName: 'OrderComponent',
|
|
361
|
+
machineName: 'Order',
|
|
362
|
+
currentState: 'Pending',
|
|
363
|
+
event: { type: 'TIMEOUT', timestamp: Date.now() }
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
// Broadcast with property filters (target specific customer)
|
|
367
|
+
await redis.publish('xcomponent:external:broadcasts', JSON.stringify({
|
|
368
|
+
componentName: 'OrderComponent',
|
|
369
|
+
machineName: 'Order',
|
|
370
|
+
currentState: 'Pending',
|
|
371
|
+
filters: [
|
|
372
|
+
{ property: 'customerId', value: 'CUST-001' },
|
|
373
|
+
{ property: 'amount', operator: '>', value: 1000 }
|
|
374
|
+
],
|
|
375
|
+
event: { type: 'URGENT_REVIEW', timestamp: Date.now() }
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
// Subscribe to state changes
|
|
379
|
+
const subscriber = redis.duplicate();
|
|
380
|
+
await subscriber.connect();
|
|
381
|
+
|
|
382
|
+
await subscriber.subscribe('xcomponent:events:state_change', (message) => {
|
|
383
|
+
const event = JSON.parse(message);
|
|
384
|
+
console.log(`State changed: ${event.data.previousState} β ${event.data.newState}`);
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Python
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
import redis
|
|
392
|
+
import json
|
|
393
|
+
import time
|
|
394
|
+
|
|
395
|
+
# Connect to Redis
|
|
396
|
+
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
|
|
397
|
+
|
|
398
|
+
# Send event to instance
|
|
399
|
+
command = {
|
|
400
|
+
'componentName': 'OrderComponent',
|
|
401
|
+
'instanceId': 'order-123',
|
|
402
|
+
'event': {
|
|
403
|
+
'type': 'VALIDATE',
|
|
404
|
+
'payload': {'approvedBy': 'alice@example.com'},
|
|
405
|
+
'timestamp': int(time.time() * 1000)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
r.publish('xcomponent:external:commands', json.dumps(command))
|
|
409
|
+
|
|
410
|
+
# Broadcast with property filters
|
|
411
|
+
broadcast = {
|
|
412
|
+
'componentName': 'OrderComponent',
|
|
413
|
+
'machineName': 'Order',
|
|
414
|
+
'currentState': 'Pending',
|
|
415
|
+
'filters': [
|
|
416
|
+
{'property': 'customerId', 'value': 'CUST-001'},
|
|
417
|
+
{'property': 'amount', 'operator': '>', 'value': 1000}
|
|
418
|
+
],
|
|
419
|
+
'event': {'type': 'URGENT_REVIEW', 'timestamp': int(time.time() * 1000)}
|
|
420
|
+
}
|
|
421
|
+
r.publish('xcomponent:external:broadcasts', json.dumps(broadcast))
|
|
422
|
+
|
|
423
|
+
# Subscribe to state changes
|
|
424
|
+
pubsub = r.pubsub()
|
|
425
|
+
pubsub.subscribe('xcomponent:events:state_change')
|
|
426
|
+
|
|
427
|
+
for message in pubsub.listen():
|
|
428
|
+
if message['type'] == 'message':
|
|
429
|
+
event = json.loads(message['data'])
|
|
430
|
+
prev = event['data']['previousState']
|
|
431
|
+
new = event['data']['newState']
|
|
432
|
+
print(f"State changed: {prev} β {new}")
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Go
|
|
436
|
+
|
|
437
|
+
```go
|
|
438
|
+
package main
|
|
439
|
+
|
|
440
|
+
import (
|
|
441
|
+
"context"
|
|
442
|
+
"encoding/json"
|
|
443
|
+
"fmt"
|
|
444
|
+
"time"
|
|
445
|
+
|
|
446
|
+
"github.com/redis/go-redis/v9"
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
func main() {
|
|
450
|
+
ctx := context.Background()
|
|
451
|
+
client := redis.NewClient(&redis.Options{
|
|
452
|
+
Addr: "localhost:6379",
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// Send event to instance
|
|
456
|
+
command := map[string]interface{}{
|
|
457
|
+
"componentName": "OrderComponent",
|
|
458
|
+
"instanceId": "order-123",
|
|
459
|
+
"event": map[string]interface{}{
|
|
460
|
+
"type": "VALIDATE",
|
|
461
|
+
"payload": map[string]string{"approvedBy": "alice@example.com"},
|
|
462
|
+
"timestamp": time.Now().UnixMilli(),
|
|
463
|
+
},
|
|
464
|
+
}
|
|
465
|
+
commandJSON, _ := json.Marshal(command)
|
|
466
|
+
client.Publish(ctx, "xcomponent:external:commands", commandJSON)
|
|
467
|
+
|
|
468
|
+
// Subscribe to state changes
|
|
469
|
+
pubsub := client.Subscribe(ctx, "xcomponent:events:state_change")
|
|
470
|
+
ch := pubsub.Channel()
|
|
471
|
+
|
|
472
|
+
for msg := range ch {
|
|
473
|
+
var event map[string]interface{}
|
|
474
|
+
json.Unmarshal([]byte(msg.Payload), &event)
|
|
475
|
+
|
|
476
|
+
data := event["data"].(map[string]interface{})
|
|
477
|
+
prev := data["previousState"].(string)
|
|
478
|
+
new := data["newState"].(string)
|
|
479
|
+
fmt.Printf("State changed: %s β %s\n", prev, new)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Java
|
|
485
|
+
|
|
486
|
+
```java
|
|
487
|
+
import redis.clients.jedis.Jedis;
|
|
488
|
+
import redis.clients.jedis.JedisPubSub;
|
|
489
|
+
import com.google.gson.Gson;
|
|
490
|
+
import java.util.HashMap;
|
|
491
|
+
import java.util.Map;
|
|
492
|
+
|
|
493
|
+
public class XComponentClient {
|
|
494
|
+
public static void main(String[] args) {
|
|
495
|
+
Jedis jedis = new Jedis("localhost", 6379);
|
|
496
|
+
Gson gson = new Gson();
|
|
497
|
+
|
|
498
|
+
// Send event to instance
|
|
499
|
+
Map<String, Object> command = new HashMap<>();
|
|
500
|
+
command.put("componentName", "OrderComponent");
|
|
501
|
+
command.put("instanceId", "order-123");
|
|
502
|
+
|
|
503
|
+
Map<String, Object> event = new HashMap<>();
|
|
504
|
+
event.put("type", "VALIDATE");
|
|
505
|
+
event.put("payload", Map.of("approvedBy", "alice@example.com"));
|
|
506
|
+
event.put("timestamp", System.currentTimeMillis());
|
|
507
|
+
command.put("event", event);
|
|
508
|
+
|
|
509
|
+
jedis.publish("xcomponent:external:commands", gson.toJson(command));
|
|
510
|
+
|
|
511
|
+
// Subscribe to state changes
|
|
512
|
+
Jedis subscriber = new Jedis("localhost", 6379);
|
|
513
|
+
subscriber.subscribe(new JedisPubSub() {
|
|
514
|
+
@Override
|
|
515
|
+
public void onMessage(String channel, String message) {
|
|
516
|
+
Map<String, Object> event = gson.fromJson(message, Map.class);
|
|
517
|
+
Map<String, Object> data = (Map<String, Object>) event.get("data");
|
|
518
|
+
String prev = (String) data.get("previousState");
|
|
519
|
+
String newState = (String) data.get("newState");
|
|
520
|
+
System.out.printf("State changed: %s β %s\n", prev, newState);
|
|
521
|
+
}
|
|
522
|
+
}, "xcomponent:events:state_change");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Ruby
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
require 'redis'
|
|
531
|
+
require 'json'
|
|
532
|
+
|
|
533
|
+
# Connect to Redis
|
|
534
|
+
redis = Redis.new(host: 'localhost', port: 6379)
|
|
535
|
+
|
|
536
|
+
# Send event to instance
|
|
537
|
+
command = {
|
|
538
|
+
componentName: 'OrderComponent',
|
|
539
|
+
instanceId: 'order-123',
|
|
540
|
+
event: {
|
|
541
|
+
type: 'VALIDATE',
|
|
542
|
+
payload: { approvedBy: 'alice@example.com' },
|
|
543
|
+
timestamp: (Time.now.to_f * 1000).to_i
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
redis.publish('xcomponent:external:commands', command.to_json)
|
|
547
|
+
|
|
548
|
+
# Subscribe to state changes
|
|
549
|
+
redis.subscribe('xcomponent:events:state_change') do |on|
|
|
550
|
+
on.message do |channel, message|
|
|
551
|
+
event = JSON.parse(message)
|
|
552
|
+
prev = event['data']['previousState']
|
|
553
|
+
new_state = event['data']['newState']
|
|
554
|
+
puts "State changed: #{prev} β #{new_state}"
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Rust
|
|
560
|
+
|
|
561
|
+
```rust
|
|
562
|
+
use redis::{Client, Commands, PubSubCommands};
|
|
563
|
+
use serde_json::json;
|
|
564
|
+
|
|
565
|
+
fn main() -> redis::RedisResult<()> {
|
|
566
|
+
let client = Client::open("redis://127.0.0.1:6379")?;
|
|
567
|
+
let mut con = client.get_connection()?;
|
|
568
|
+
|
|
569
|
+
// Send event to instance
|
|
570
|
+
let command = json!({
|
|
571
|
+
"componentName": "OrderComponent",
|
|
572
|
+
"instanceId": "order-123",
|
|
573
|
+
"event": {
|
|
574
|
+
"type": "VALIDATE",
|
|
575
|
+
"payload": { "approvedBy": "alice@example.com" },
|
|
576
|
+
"timestamp": chrono::Utc::now().timestamp_millis()
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
con.publish("xcomponent:external:commands", command.to_string())?;
|
|
580
|
+
|
|
581
|
+
// Subscribe to state changes
|
|
582
|
+
let mut pubsub = con.as_pubsub();
|
|
583
|
+
pubsub.subscribe("xcomponent:events:state_change")?;
|
|
584
|
+
|
|
585
|
+
loop {
|
|
586
|
+
let msg = pubsub.get_message()?;
|
|
587
|
+
let payload: String = msg.get_payload()?;
|
|
588
|
+
let event: serde_json::Value = serde_json::from_str(&payload)?;
|
|
589
|
+
|
|
590
|
+
let prev = event["data"]["previousState"].as_str().unwrap();
|
|
591
|
+
let new = event["data"]["newState"].as_str().unwrap();
|
|
592
|
+
println!("State changed: {} β {}", prev, new);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Authentication & Security
|
|
600
|
+
|
|
601
|
+
### Redis with Password
|
|
602
|
+
|
|
603
|
+
All examples support Redis URLs with authentication:
|
|
604
|
+
|
|
605
|
+
```
|
|
606
|
+
redis://:password@localhost:6379
|
|
607
|
+
redis://username:password@localhost:6379
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
**Example (Python):**
|
|
611
|
+
```python
|
|
612
|
+
r = redis.Redis.from_url('redis://:mypassword@prod-redis:6379')
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Redis with TLS/SSL
|
|
616
|
+
|
|
617
|
+
Use `rediss://` protocol for encrypted connections:
|
|
618
|
+
|
|
619
|
+
```
|
|
620
|
+
rediss://username:password@prod-redis:6380
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### Access Control
|
|
624
|
+
|
|
625
|
+
Use Redis ACLs to restrict access:
|
|
626
|
+
|
|
627
|
+
```bash
|
|
628
|
+
# Create user that can only publish commands and subscribe to events
|
|
629
|
+
redis-cli ACL SETUSER external-app on \
|
|
630
|
+
>password \
|
|
631
|
+
+publish|xcomponent:external:* \
|
|
632
|
+
+subscribe|xcomponent:events:* \
|
|
633
|
+
-@all
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## Use Cases
|
|
639
|
+
|
|
640
|
+
### 1. Python Data Pipeline β FSM
|
|
641
|
+
|
|
642
|
+
```python
|
|
643
|
+
# data_pipeline.py
|
|
644
|
+
import redis, json
|
|
645
|
+
|
|
646
|
+
r = redis.Redis(host='localhost', port=6379)
|
|
647
|
+
|
|
648
|
+
# When data processing completes, trigger FSM
|
|
649
|
+
def on_data_processed(order_id, result):
|
|
650
|
+
r.publish('xcomponent:external:commands', json.dumps({
|
|
651
|
+
'componentName': 'OrderComponent',
|
|
652
|
+
'instanceId': order_id,
|
|
653
|
+
'event': {'type': 'DATA_PROCESSED', 'payload': result}
|
|
654
|
+
}))
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### 2. Monitoring Dashboard (React + Go backend)
|
|
658
|
+
|
|
659
|
+
```go
|
|
660
|
+
// Go backend streams FSM events to frontend via WebSocket
|
|
661
|
+
func streamFSMEvents(w http.ResponseWriter, r *http.Request) {
|
|
662
|
+
upgrader.Upgrade(w, r, nil) // WebSocket upgrade
|
|
663
|
+
|
|
664
|
+
redisClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
|
|
665
|
+
pubsub := redisClient.Subscribe(ctx, "xcomponent:events:state_change")
|
|
666
|
+
|
|
667
|
+
for msg := range pubsub.Channel() {
|
|
668
|
+
// Forward to frontend WebSocket
|
|
669
|
+
wsConn.WriteMessage(websocket.TextMessage, []byte(msg.Payload))
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### 3. Java Microservice Integration
|
|
675
|
+
|
|
676
|
+
```java
|
|
677
|
+
// OrderService.java - triggers FSM when order validated
|
|
678
|
+
public void onOrderValidated(String orderId) {
|
|
679
|
+
jedis.publish("xcomponent:external:commands", gson.toJson(
|
|
680
|
+
Map.of(
|
|
681
|
+
"componentName", "PaymentComponent",
|
|
682
|
+
"instanceId", "payment-" + orderId,
|
|
683
|
+
"event", Map.of("type", "START_PAYMENT")
|
|
684
|
+
)
|
|
685
|
+
));
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## Troubleshooting
|
|
692
|
+
|
|
693
|
+
### Commands not received
|
|
694
|
+
|
|
695
|
+
1. Check xcomponent-ai started with `--external-api` flag
|
|
696
|
+
2. Verify Redis connection: `redis-cli ping`
|
|
697
|
+
3. Check channel name exactly matches `xcomponent:external:commands`
|
|
698
|
+
|
|
699
|
+
### Events not published
|
|
700
|
+
|
|
701
|
+
1. Check xcomponent-ai started with `--publish-events` flag
|
|
702
|
+
2. Verify subscription to correct channel (e.g., `xcomponent:events:state_change`)
|
|
703
|
+
3. Check Redis authentication if using password
|
|
704
|
+
|
|
705
|
+
### Performance considerations
|
|
706
|
+
|
|
707
|
+
- Use connection pooling for high-throughput systems
|
|
708
|
+
- Consider Redis Cluster for horizontal scaling
|
|
709
|
+
- Monitor Redis memory usage (`INFO memory`)
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## Next Steps
|
|
714
|
+
|
|
715
|
+
- See [SCALABILITY.md](./SCALABILITY.md) for production deployment
|
|
716
|
+
- See [LLM-GUIDE.md](./LLM-GUIDE.md) for YAML FSM design patterns
|
|
717
|
+
- See `examples/distributed-demo/` for working examples
|
|
718
|
+
|
|
719
|
+
**Built for interoperability.** Any language, any platform. π
|