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
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
# 📊 Event Accumulation & Explicit Control Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to handle **multiple events** that accumulate data and **explicitly control transitions** based on business logic.
|
|
4
|
+
|
|
5
|
+
## 🎯 Use Case
|
|
6
|
+
|
|
7
|
+
**Trading Order Example:**
|
|
8
|
+
- Order for 1000 shares receives multiple execution notifications
|
|
9
|
+
- Each notification has a partial quantity (e.g., 100, 250, 150...)
|
|
10
|
+
- Order should transition to "Fully Executed" **only when** `executedQuantity >= totalQuantity`
|
|
11
|
+
|
|
12
|
+
**Key Challenge:** How to accumulate data from multiple events and explicitly decide when to transition?
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 💡 Solution: Explicit Control with sender.sendToSelf()
|
|
17
|
+
|
|
18
|
+
xcomponent-ai provides **explicit control** through triggered methods:
|
|
19
|
+
|
|
20
|
+
1. **Context** - Stores accumulated state
|
|
21
|
+
2. **Triggered Methods** - Accumulate data from events and decide when to transition
|
|
22
|
+
3. **sender.sendToSelf()** - Explicitly trigger state transitions from code
|
|
23
|
+
|
|
24
|
+
**Philosophy:** Business logic should be in code (triggered methods), not in YAML configuration.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🔧 Implementation
|
|
29
|
+
|
|
30
|
+
### Step 1: Define Context Schema
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
stateMachines:
|
|
34
|
+
- name: TradingOrder
|
|
35
|
+
initialState: Created
|
|
36
|
+
|
|
37
|
+
contextSchema:
|
|
38
|
+
orderId:
|
|
39
|
+
type: text
|
|
40
|
+
required: true
|
|
41
|
+
totalQuantity:
|
|
42
|
+
type: number
|
|
43
|
+
required: true
|
|
44
|
+
min: 1
|
|
45
|
+
# These will be populated by triggered methods
|
|
46
|
+
executedQuantity:
|
|
47
|
+
type: number
|
|
48
|
+
default: 0
|
|
49
|
+
executions:
|
|
50
|
+
type: array
|
|
51
|
+
default: []
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Step 2: Create Accumulation Method with Explicit Control
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
triggeredMethods:
|
|
58
|
+
accumulateExecution: |
|
|
59
|
+
async function(event, context, sender) {
|
|
60
|
+
// Initialize counters
|
|
61
|
+
if (!context.executedQuantity) {
|
|
62
|
+
context.executedQuantity = 0;
|
|
63
|
+
}
|
|
64
|
+
if (!context.executions) {
|
|
65
|
+
context.executions = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Accumulate quantity from event
|
|
69
|
+
const qty = event.payload.quantity || 0;
|
|
70
|
+
context.executedQuantity += qty;
|
|
71
|
+
|
|
72
|
+
// Track individual executions
|
|
73
|
+
context.executions.push({
|
|
74
|
+
quantity: qty,
|
|
75
|
+
price: event.payload.price,
|
|
76
|
+
executionId: event.payload.executionId,
|
|
77
|
+
timestamp: event.timestamp
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
console.log(`Executed: ${context.executedQuantity}/${context.totalQuantity}`);
|
|
81
|
+
|
|
82
|
+
// EXPLICIT CONTROL: Decide when to transition
|
|
83
|
+
if (context.executedQuantity >= context.totalQuantity) {
|
|
84
|
+
// Send event to self to trigger completion
|
|
85
|
+
await sender.sendToSelf({
|
|
86
|
+
type: 'FULLY_EXECUTED',
|
|
87
|
+
payload: {
|
|
88
|
+
totalExecuted: context.executedQuantity,
|
|
89
|
+
executionCount: context.executions.length,
|
|
90
|
+
averagePrice: context.executions.reduce((sum, e) => sum + e.price, 0) / context.executions.length
|
|
91
|
+
},
|
|
92
|
+
timestamp: Date.now()
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Key Points:**
|
|
99
|
+
- Method accumulates data from the event
|
|
100
|
+
- Method **decides** when to transition based on business logic
|
|
101
|
+
- Uses `sender.sendToSelf()` to **explicitly** trigger the transition
|
|
102
|
+
- Event is queued asynchronously to avoid race conditions
|
|
103
|
+
|
|
104
|
+
### Step 3: Define Transitions (No Guards!)
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
transitions:
|
|
108
|
+
# SELF-LOOP: Stay in PartiallyExecuted
|
|
109
|
+
# Triggered method decides when to send FULLY_EXECUTED event
|
|
110
|
+
- from: PartiallyExecuted
|
|
111
|
+
to: PartiallyExecuted
|
|
112
|
+
event: EXECUTION_NOTIFICATION
|
|
113
|
+
type: triggerable
|
|
114
|
+
triggeredMethod: accumulateExecution
|
|
115
|
+
|
|
116
|
+
# EXPLICIT TRANSITION: Triggered by sender.sendToSelf()
|
|
117
|
+
# No guards - logic is in the triggered method
|
|
118
|
+
- from: PartiallyExecuted
|
|
119
|
+
to: FullyExecuted
|
|
120
|
+
event: FULLY_EXECUTED
|
|
121
|
+
type: triggerable
|
|
122
|
+
triggeredMethod: handleCompletion
|
|
123
|
+
|
|
124
|
+
# Also handle direct full execution from Submitted
|
|
125
|
+
- from: Submitted
|
|
126
|
+
to: FullyExecuted
|
|
127
|
+
event: FULLY_EXECUTED
|
|
128
|
+
type: triggerable
|
|
129
|
+
triggeredMethod: handleCompletion
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**How it works:**
|
|
133
|
+
1. Event `EXECUTION_NOTIFICATION` arrives
|
|
134
|
+
2. `accumulateExecution` method executes, updates context
|
|
135
|
+
3. Method checks if `executedQuantity >= totalQuantity`
|
|
136
|
+
4. If true, method calls `sender.sendToSelf()` with `FULLY_EXECUTED` event
|
|
137
|
+
5. `FULLY_EXECUTED` event triggers transition to `FullyExecuted` state
|
|
138
|
+
|
|
139
|
+
**Benefits:**
|
|
140
|
+
- ✅ All business logic in one place (triggered method)
|
|
141
|
+
- ✅ Easy to test and debug
|
|
142
|
+
- ✅ Can set event payload properties dynamically
|
|
143
|
+
- ✅ Full control over transition timing
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 📝 Complete Example
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
name: TradingComponent
|
|
151
|
+
version: 1.0.0
|
|
152
|
+
|
|
153
|
+
triggeredMethods:
|
|
154
|
+
accumulateExecution: |
|
|
155
|
+
async function(event, context, sender) {
|
|
156
|
+
if (!context.executedQuantity) context.executedQuantity = 0;
|
|
157
|
+
if (!context.executions) context.executions = [];
|
|
158
|
+
|
|
159
|
+
const qty = event.payload.quantity || 0;
|
|
160
|
+
context.executedQuantity += qty;
|
|
161
|
+
context.executions.push({
|
|
162
|
+
quantity: qty,
|
|
163
|
+
price: event.payload.price,
|
|
164
|
+
executionId: event.payload.executionId,
|
|
165
|
+
timestamp: event.timestamp
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
console.log(`Executed: ${context.executedQuantity}/${context.totalQuantity}`);
|
|
169
|
+
|
|
170
|
+
// EXPLICIT CONTROL
|
|
171
|
+
if (context.executedQuantity >= context.totalQuantity) {
|
|
172
|
+
await sender.sendToSelf({
|
|
173
|
+
type: 'FULLY_EXECUTED',
|
|
174
|
+
payload: {
|
|
175
|
+
totalExecuted: context.executedQuantity,
|
|
176
|
+
executionCount: context.executions.length
|
|
177
|
+
},
|
|
178
|
+
timestamp: Date.now()
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
handleCompletion: |
|
|
184
|
+
async function(event, context, sender) {
|
|
185
|
+
console.log(`Order completed!`);
|
|
186
|
+
console.log(` Total: ${event.payload.totalExecuted}`);
|
|
187
|
+
console.log(` Executions: ${event.payload.executionCount}`);
|
|
188
|
+
context.stats = event.payload;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
stateMachines:
|
|
192
|
+
- name: TradingOrder
|
|
193
|
+
initialState: Created
|
|
194
|
+
|
|
195
|
+
states:
|
|
196
|
+
- name: Created
|
|
197
|
+
type: entry
|
|
198
|
+
- name: Submitted
|
|
199
|
+
type: regular
|
|
200
|
+
- name: PartiallyExecuted
|
|
201
|
+
type: regular
|
|
202
|
+
- name: FullyExecuted
|
|
203
|
+
type: regular
|
|
204
|
+
- name: Completed
|
|
205
|
+
type: final
|
|
206
|
+
|
|
207
|
+
transitions:
|
|
208
|
+
- from: Created
|
|
209
|
+
to: Submitted
|
|
210
|
+
event: SUBMIT
|
|
211
|
+
type: triggerable
|
|
212
|
+
|
|
213
|
+
- from: Submitted
|
|
214
|
+
to: PartiallyExecuted
|
|
215
|
+
event: EXECUTION_NOTIFICATION
|
|
216
|
+
type: triggerable
|
|
217
|
+
triggeredMethod: accumulateExecution
|
|
218
|
+
|
|
219
|
+
# SELF-LOOP
|
|
220
|
+
- from: PartiallyExecuted
|
|
221
|
+
to: PartiallyExecuted
|
|
222
|
+
event: EXECUTION_NOTIFICATION
|
|
223
|
+
type: triggerable
|
|
224
|
+
triggeredMethod: accumulateExecution
|
|
225
|
+
|
|
226
|
+
# EXPLICIT TRANSITION
|
|
227
|
+
- from: PartiallyExecuted
|
|
228
|
+
to: FullyExecuted
|
|
229
|
+
event: FULLY_EXECUTED
|
|
230
|
+
type: triggerable
|
|
231
|
+
triggeredMethod: handleCompletion
|
|
232
|
+
|
|
233
|
+
- from: Submitted
|
|
234
|
+
to: FullyExecuted
|
|
235
|
+
event: FULLY_EXECUTED
|
|
236
|
+
type: triggerable
|
|
237
|
+
triggeredMethod: handleCompletion
|
|
238
|
+
|
|
239
|
+
- from: FullyExecuted
|
|
240
|
+
to: Completed
|
|
241
|
+
event: SETTLE
|
|
242
|
+
type: triggerable
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## 🧪 Testing
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# Start the server
|
|
251
|
+
xcomponent-ai serve examples/event-accumulation-demo.yaml
|
|
252
|
+
|
|
253
|
+
# Create order for 1000 shares
|
|
254
|
+
curl -X POST http://localhost:3000/api/instances \
|
|
255
|
+
-H "Content-Type: application/json" \
|
|
256
|
+
-d '{
|
|
257
|
+
"machineName": "TradingOrder",
|
|
258
|
+
"context": {
|
|
259
|
+
"orderId": "ORD-001",
|
|
260
|
+
"symbol": "AAPL",
|
|
261
|
+
"totalQuantity": 1000,
|
|
262
|
+
"side": "BUY"
|
|
263
|
+
}
|
|
264
|
+
}'
|
|
265
|
+
|
|
266
|
+
# Submit order
|
|
267
|
+
curl -X POST http://localhost:3000/api/instances/{instanceId}/events \
|
|
268
|
+
-H "Content-Type: application/json" \
|
|
269
|
+
-d '{"type": "SUBMIT"}'
|
|
270
|
+
|
|
271
|
+
# Send execution notification #1: 300 shares
|
|
272
|
+
curl -X POST http://localhost:3000/api/instances/{instanceId}/events \
|
|
273
|
+
-H "Content-Type: application/json" \
|
|
274
|
+
-d '{
|
|
275
|
+
"type": "EXECUTION_NOTIFICATION",
|
|
276
|
+
"payload": {
|
|
277
|
+
"quantity": 300,
|
|
278
|
+
"price": 150.50,
|
|
279
|
+
"executionId": "EXEC-001"
|
|
280
|
+
}
|
|
281
|
+
}'
|
|
282
|
+
# → State: PartiallyExecuted
|
|
283
|
+
|
|
284
|
+
# Send execution notification #2: 400 shares
|
|
285
|
+
curl -X POST http://localhost:3000/api/instances/{instanceId}/events \
|
|
286
|
+
-H "Content-Type: application/json" \
|
|
287
|
+
-d '{
|
|
288
|
+
"type": "EXECUTION_NOTIFICATION",
|
|
289
|
+
"payload": {
|
|
290
|
+
"quantity": 400,
|
|
291
|
+
"price": 150.45,
|
|
292
|
+
"executionId": "EXEC-002"
|
|
293
|
+
}
|
|
294
|
+
}'
|
|
295
|
+
# → State: PartiallyExecuted (700/1000)
|
|
296
|
+
|
|
297
|
+
# Send execution notification #3: 300 shares
|
|
298
|
+
curl -X POST http://localhost:3000/api/instances/{instanceId}/events \
|
|
299
|
+
-H "Content-Type: application/json" \
|
|
300
|
+
-d '{
|
|
301
|
+
"type": "EXECUTION_NOTIFICATION",
|
|
302
|
+
"payload": {
|
|
303
|
+
"quantity": 300,
|
|
304
|
+
"price": 150.40,
|
|
305
|
+
"executionId": "EXEC-003"
|
|
306
|
+
}
|
|
307
|
+
}'
|
|
308
|
+
# → State: FullyExecuted (1000/1000) ✅
|
|
309
|
+
|
|
310
|
+
# Check final state
|
|
311
|
+
curl http://localhost:3000/api/instances/{instanceId}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Expected context:**
|
|
315
|
+
```json
|
|
316
|
+
{
|
|
317
|
+
"orderId": "ORD-001",
|
|
318
|
+
"symbol": "AAPL",
|
|
319
|
+
"totalQuantity": 1000,
|
|
320
|
+
"side": "BUY",
|
|
321
|
+
"executedQuantity": 1000,
|
|
322
|
+
"executions": [
|
|
323
|
+
{"quantity": 300, "price": 150.50, "executionId": "EXEC-001"},
|
|
324
|
+
{"quantity": 400, "price": 150.45, "executionId": "EXEC-002"},
|
|
325
|
+
{"quantity": 300, "price": 150.40, "executionId": "EXEC-003"}
|
|
326
|
+
],
|
|
327
|
+
"stats": {
|
|
328
|
+
"totalExecuted": 1000,
|
|
329
|
+
"executionCount": 3
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 📤 Sender Methods Reference
|
|
337
|
+
|
|
338
|
+
Triggered methods receive a `sender` parameter with methods for cross-instance communication.
|
|
339
|
+
|
|
340
|
+
### sendToSelf(event)
|
|
341
|
+
|
|
342
|
+
Send event to current instance (explicit control):
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
await sender.sendToSelf({
|
|
346
|
+
type: 'FULLY_EXECUTED',
|
|
347
|
+
payload: { totalExecuted: context.executedQuantity },
|
|
348
|
+
timestamp: Date.now()
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### sendTo(instanceId, event)
|
|
353
|
+
|
|
354
|
+
Send event to specific instance:
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
await sender.sendTo(context.paymentInstanceId, {
|
|
358
|
+
type: 'ORDER_CONFIRMED',
|
|
359
|
+
payload: { orderId: context.orderId },
|
|
360
|
+
timestamp: Date.now()
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### broadcast(machineName, event, currentState?, componentName?)
|
|
365
|
+
|
|
366
|
+
Broadcast to all matching instances. Filtering is done via **matchingRules in YAML**, not in code:
|
|
367
|
+
|
|
368
|
+
```javascript
|
|
369
|
+
// Broadcast to all Orders (any state)
|
|
370
|
+
await sender.broadcast('Order', {
|
|
371
|
+
type: 'SYSTEM_ALERT',
|
|
372
|
+
payload: {},
|
|
373
|
+
timestamp: Date.now()
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Broadcast to Orders in Pending state only
|
|
377
|
+
await sender.broadcast('Order', {
|
|
378
|
+
type: 'TIMEOUT',
|
|
379
|
+
payload: {},
|
|
380
|
+
timestamp: Date.now()
|
|
381
|
+
}, 'Pending');
|
|
382
|
+
|
|
383
|
+
// Cross-component broadcast
|
|
384
|
+
await sender.broadcast('Payment', {
|
|
385
|
+
type: 'ORDER_COMPLETED',
|
|
386
|
+
payload: { orderId: context.orderId },
|
|
387
|
+
timestamp: Date.now()
|
|
388
|
+
}, undefined, 'PaymentComponent');
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Filtering via matchingRules in YAML:**
|
|
392
|
+
|
|
393
|
+
```yaml
|
|
394
|
+
# In the receiving machine's transition
|
|
395
|
+
transitions:
|
|
396
|
+
- from: Monitoring
|
|
397
|
+
to: Monitoring
|
|
398
|
+
event: ORDER_UPDATE
|
|
399
|
+
type: triggerable
|
|
400
|
+
matchingRules:
|
|
401
|
+
# Only instances with matching customerId receive event
|
|
402
|
+
- eventProperty: payload.customerId
|
|
403
|
+
instanceProperty: customerId
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### createInstance(machineName, initialContext)
|
|
407
|
+
|
|
408
|
+
Create new instance:
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
const newInstanceId = sender.createInstance('Order', {
|
|
412
|
+
orderId: 'ORD-001',
|
|
413
|
+
totalQuantity: 1000
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### createInstanceInComponent(componentName, machineName, initialContext)
|
|
418
|
+
|
|
419
|
+
Create instance in another component:
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
sender.createInstanceInComponent('PaymentComponent', 'Payment', {
|
|
423
|
+
orderId: context.orderId,
|
|
424
|
+
amount: context.totalAmount
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 🎯 Other Use Cases
|
|
431
|
+
|
|
432
|
+
### 1. Payment Installments
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
async function(event, context, sender) {
|
|
436
|
+
if (!context.paidInstallments) context.paidInstallments = 0;
|
|
437
|
+
context.paidInstallments += 1;
|
|
438
|
+
|
|
439
|
+
if (context.paidInstallments >= context.totalInstallments) {
|
|
440
|
+
await sender.sendToSelf({
|
|
441
|
+
type: 'FULLY_PAID',
|
|
442
|
+
payload: { installments: context.paidInstallments },
|
|
443
|
+
timestamp: Date.now()
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### 2. Multi-Step Approval
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
async function(event, context, sender) {
|
|
453
|
+
if (!context.approvals) context.approvals = [];
|
|
454
|
+
context.approvals.push({
|
|
455
|
+
approver: event.payload.approver,
|
|
456
|
+
timestamp: event.timestamp
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (context.approvals.length >= 3) {
|
|
460
|
+
await sender.sendToSelf({
|
|
461
|
+
type: 'APPROVED',
|
|
462
|
+
payload: { approvers: context.approvals.map(a => a.approver) },
|
|
463
|
+
timestamp: Date.now()
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 3. Time-based Accumulation
|
|
470
|
+
|
|
471
|
+
```javascript
|
|
472
|
+
async function(event, context, sender) {
|
|
473
|
+
const elapsed = Date.now() - context.createdAt;
|
|
474
|
+
|
|
475
|
+
if (elapsed > 86400000) { // 24 hours
|
|
476
|
+
await sender.sendToSelf({
|
|
477
|
+
type: 'EXPIRED',
|
|
478
|
+
payload: { duration: elapsed },
|
|
479
|
+
timestamp: Date.now()
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## ⚠️ Best Practices
|
|
488
|
+
|
|
489
|
+
1. **Initialize accumulators** in triggered methods (handle first event)
|
|
490
|
+
2. **Use sender.sendToSelf()** for explicit control instead of guards
|
|
491
|
+
3. **Log state changes** for debugging (`console.log` in triggered methods)
|
|
492
|
+
4. **Test edge cases** (exact match, overfill, underfill)
|
|
493
|
+
5. **Keep business logic in code** (triggered methods), not YAML
|
|
494
|
+
6. **Document transitions** with clear comments in YAML
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 🔗 See Also
|
|
499
|
+
|
|
500
|
+
- [LLM-GUIDE.md](./LLM-GUIDE.md) - Complete YAML patterns
|
|
501
|
+
- [examples/event-accumulation-demo.yaml](./examples/event-accumulation-demo.yaml) - Full example
|
|
502
|
+
- [examples/explicit-transitions-demo.yaml](./examples/explicit-transitions-demo.yaml) - Explicit control demo
|
|
503
|
+
- [QUICKSTART.md](./QUICKSTART.md) - Getting started
|
|
504
|
+
|
|
505
|
+
**Built for explicit control.** 🚀
|