@cnstra/core 1.1.0 → 1.2.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/README.md +567 -10
- package/dist/index.cjs +233 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +233 -37
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
**CNStra (Central Nervous System Orchestrator)** models your app as a **typed neuron graph**.
|
|
8
8
|
You explicitly start a run with `cns.stimulate(...)`; CNStra then performs a **deterministic, hop-bounded traversal** from **collateral → dendrite → returned signal**, step by step.
|
|
9
9
|
|
|
10
|
+
> **Zero dependencies:** CNS has no third-party dependencies, making it suitable for any JavaScript/TypeScript environment - browsers, Node.js, serverless, edge functions, React Native, or embedded systems.
|
|
11
|
+
|
|
10
12
|
> **Not pub/sub:** there are no ambient listeners or global `emit`. Only the **signal you return** from a dendrite continues the traversal; returning `null`/`undefined` ends that branch. `maxHops` guards against cycles.
|
|
11
13
|
|
|
12
14
|
## 🏗️ Core Model
|
|
@@ -44,8 +46,8 @@ const userService = neuron('user-service', {
|
|
|
44
46
|
})
|
|
45
47
|
.dendrite({
|
|
46
48
|
collateral: userCreated,
|
|
47
|
-
response:
|
|
48
|
-
const userData = payload
|
|
49
|
+
response: (payload, axon) => {
|
|
50
|
+
const userData = payload;
|
|
49
51
|
|
|
50
52
|
// Process the user creation
|
|
51
53
|
console.log(`Processing user: ${userData.name}`);
|
|
@@ -119,7 +121,7 @@ new CNS(neurons)
|
|
|
119
121
|
|
|
120
122
|
#### `stimulate()` Method
|
|
121
123
|
```typescript
|
|
122
|
-
|
|
124
|
+
cns.stimulate(collateral, payload, options?)
|
|
123
125
|
```
|
|
124
126
|
|
|
125
127
|
**Parameters:**
|
|
@@ -128,12 +130,333 @@ await cns.stimulate(collateral, payload, options?)
|
|
|
128
130
|
- `options`: Optional configuration object
|
|
129
131
|
|
|
130
132
|
**Options:**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
|
|
134
|
+
##### `maxHops?: number` (default: 1000)
|
|
135
|
+
Prevents infinite loops by limiting signal traversal depth. Each signal hop increments the counter.
|
|
136
|
+
```typescript
|
|
137
|
+
// Prevent runaway recursion
|
|
138
|
+
cns.stimulate(input, data, {
|
|
139
|
+
maxHops: 50 // Stop after 50 signal hops
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
⚠️ **Memory Impact**: Higher values allow deeper graphs but increase memory usage.
|
|
143
|
+
|
|
144
|
+
##### `onTrace?: (trace) => void`
|
|
145
|
+
Real-time callback for monitoring signal flow, errors, and completion. Essential for fire-and-forget pattern.
|
|
146
|
+
```typescript
|
|
147
|
+
cns.stimulate(input, data, {
|
|
148
|
+
onTrace: (trace) => {
|
|
149
|
+
console.log(`Signal: ${trace.collateralId}, Hops: ${trace.hops}`);
|
|
150
|
+
|
|
151
|
+
if (trace.error) {
|
|
152
|
+
console.error('Processing failed:', trace.error);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (trace.queueLength === 0) {
|
|
156
|
+
console.log('Stimulation completed');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
##### `abortSignal?: AbortSignal`
|
|
163
|
+
Gracefully stop stimulation using AbortController. Already-running operations complete, but no new work starts.
|
|
164
|
+
```typescript
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
|
|
167
|
+
cns.stimulate(input, data, {
|
|
168
|
+
abortSignal: controller.signal
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Cancel after 5 seconds
|
|
172
|
+
setTimeout(() => controller.abort(), 5000);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
##### `ctx?: ICNSStimulationContextStore`
|
|
176
|
+
Provide existing context store for recovery/retry scenarios. Restores neuron state from previous stimulation.
|
|
177
|
+
```typescript
|
|
178
|
+
// Retry with preserved context
|
|
179
|
+
cns.stimulate(failedCollateral, payload, {
|
|
180
|
+
ctx: savedContextStore, // Restore previous state
|
|
181
|
+
onTrace: (trace) => { /* monitor retry */ }
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
##### `createContextStore?: () => ICNSStimulationContextStore`
|
|
186
|
+
Factory for custom context store implementations. Useful for persistence, encryption, or specialized storage.
|
|
187
|
+
```typescript
|
|
188
|
+
cns.stimulate(input, data, {
|
|
189
|
+
createContextStore: () => new RedisContextStore('session-123'),
|
|
190
|
+
onTrace: (trace) => {
|
|
191
|
+
// trace.contextStore is your custom RedisContextStore
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
##### `spikeId?: string`
|
|
197
|
+
Custom identifier for this stimulation cascade. Auto-generated if not provided. Useful for debugging and correlation.
|
|
198
|
+
```typescript
|
|
199
|
+
cns.stimulate(input, data, {
|
|
200
|
+
spikeId: 'user-action-' + Date.now(),
|
|
201
|
+
onTrace: (trace) => {
|
|
202
|
+
console.log('Spike ID:', trace.spikeId);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
##### `allowType?: (collateralId: string) => boolean`
|
|
208
|
+
Filter which collateral types can be processed. Useful for selective stimulation or debugging specific paths.
|
|
209
|
+
```typescript
|
|
210
|
+
cns.stimulate(input, data, {
|
|
211
|
+
allowType: (type) => type.startsWith('user:'), // Only process user-related signals
|
|
212
|
+
onTrace: (trace) => {
|
|
213
|
+
// Only user: collaterals will appear in traces
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
##### `concurrency?: number` (default: unlimited)
|
|
219
|
+
Limit concurrent operations to prevent resource exhaustion. Both sync and async operations count toward this limit.
|
|
220
|
+
```typescript
|
|
221
|
+
cns.stimulate(input, data, {
|
|
222
|
+
concurrency: 10, // Max 10 operations at once
|
|
223
|
+
onTrace: (trace) => {
|
|
224
|
+
// Processing respects concurrency limit
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Why These Options Matter:**
|
|
230
|
+
- **Control**: Fine-tune behavior for your specific use case
|
|
231
|
+
- **Performance**: Manage memory and concurrency based on system constraints
|
|
232
|
+
- **Reliability**: Implement timeouts, retries, and error recovery
|
|
233
|
+
- **Debugging**: Filter signals, add correlation IDs, and monitor execution
|
|
234
|
+
- **Integration**: Work with existing systems via custom context stores
|
|
235
|
+
|
|
236
|
+
## 🧠 Memory Policy & Error Handling
|
|
237
|
+
|
|
238
|
+
### Memory-First Design
|
|
239
|
+
CNS is designed with strict memory efficiency and universal compatibility in mind:
|
|
240
|
+
|
|
241
|
+
- **Zero dependencies**: No third-party packages - runs in any JavaScript/TypeScript environment
|
|
242
|
+
- **No error storage**: Errors are immediately passed to your `onTrace` callback and not stored in memory
|
|
243
|
+
- **Streaming traces**: Signal traces are delivered via callbacks, not accumulated in arrays
|
|
244
|
+
- **Context on-demand**: Context stores are created only when needed and passed via traces
|
|
245
|
+
- **No global state**: The system maintains no persistent state between stimulations
|
|
246
|
+
- **Context scope**: Context stores exist only for the duration of a single stimulation - they are automatically cleaned up when stimulation completes
|
|
247
|
+
- **Context content warning**: ⚠️ Context values dictionary is kept in memory during stimulation - store minimal data only (IDs, counters, flags) rather than large objects or payloads
|
|
248
|
+
- **Context mutation**: CNS modifies context dictionaries in-place - create new context objects if you need versioning or rollback capabilities
|
|
249
|
+
|
|
250
|
+
### maxHops Memory Impact
|
|
251
|
+
⚠️ **Warning**: Using `maxHops` parameter increases memory consumption proportionally:
|
|
252
|
+
- Default hop limit: 1000
|
|
253
|
+
- Higher `maxHops` = more potential concurrent signals = more memory usage
|
|
254
|
+
- For infinite recursion scenarios, keep `maxHops` reasonable (100-1000 range)
|
|
255
|
+
|
|
256
|
+
### Error Handling Philosophy
|
|
257
|
+
CNS handles errors through immediate callback delivery rather than storage because:
|
|
258
|
+
1. **Memory efficiency**: No error accumulation in library memory
|
|
259
|
+
2. **Real-time feedback**: Immediate error notification to your application
|
|
260
|
+
3. **Recovery control**: You decide how to handle and store errors
|
|
261
|
+
4. **Context preservation**: Error traces include full context for recovery scenarios
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// Error handling example
|
|
265
|
+
cns.stimulate(input, payload, {
|
|
266
|
+
onTrace: (trace) => {
|
|
267
|
+
if (trace.error) {
|
|
268
|
+
// Handle immediately - error won't be stored by CNS
|
|
269
|
+
logError(trace.error);
|
|
270
|
+
saveErrorContext(trace.contextStore);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ✅ Good: Minimal context data
|
|
276
|
+
const goodContext = {
|
|
277
|
+
retryCount: 3,
|
|
278
|
+
userId: "123",
|
|
279
|
+
lastAttempt: Date.now()
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// ❌ Bad: Large objects in context
|
|
283
|
+
const badContext = {
|
|
284
|
+
fullUserProfile: largeUserObject,
|
|
285
|
+
historicalData: massiveArray,
|
|
286
|
+
fileBuffer: binaryData
|
|
287
|
+
};
|
|
288
|
+
```
|
|
134
289
|
|
|
135
290
|
## 🔄 Key Behavior
|
|
136
291
|
|
|
292
|
+
### Sync-First Processing with Async Support
|
|
293
|
+
CNS processes signals synchronously by default, avoiding microtask scheduling until absolutely necessary. This provides predictable, immediate execution for synchronous operations while seamlessly handling async when needed.
|
|
294
|
+
|
|
295
|
+
**Synchronous Path** - No async/await overhead:
|
|
296
|
+
```typescript
|
|
297
|
+
const syncNeuron = neuron('sync-processor', { output })
|
|
298
|
+
.dendrite({
|
|
299
|
+
collateral: input,
|
|
300
|
+
response: (payload, axon) => {
|
|
301
|
+
// Pure synchronous processing - runs immediately
|
|
302
|
+
// No Promise creation, no microtask scheduling
|
|
303
|
+
const result = processSync(payload);
|
|
304
|
+
return axon.output.createSignal({ result });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Chain of sync neurons executes immediately in single tick
|
|
309
|
+
const chainStart = collateral<{ value: number }>('start');
|
|
310
|
+
const chainMiddle = collateral<{ doubled: number }>('middle');
|
|
311
|
+
const chainEnd = collateral<{ final: string }>('end');
|
|
312
|
+
|
|
313
|
+
const step1 = neuron('step1', { chainMiddle })
|
|
314
|
+
.dendrite({
|
|
315
|
+
collateral: chainStart,
|
|
316
|
+
response: (payload, axon) => {
|
|
317
|
+
return axon.chainMiddle.createSignal({ doubled: payload.value * 2 });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const step2 = neuron('step2', { chainEnd })
|
|
322
|
+
.dendrite({
|
|
323
|
+
collateral: chainMiddle,
|
|
324
|
+
response: (payload, axon) => {
|
|
325
|
+
return axon.chainEnd.createSignal({ final: `Result: ${payload.doubled}` });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// This entire chain executes synchronously in one tick
|
|
330
|
+
cns.stimulate(chainStart, { value: 5 });
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Mixed Sync/Async** - Async only where needed:
|
|
334
|
+
```typescript
|
|
335
|
+
const mixedNeuron = neuron('mixed-processor', { output })
|
|
336
|
+
.dendrite({
|
|
337
|
+
collateral: input,
|
|
338
|
+
response: async (payload, axon) => {
|
|
339
|
+
// Synchronous preprocessing - runs immediately
|
|
340
|
+
const validated = validateInput(payload);
|
|
341
|
+
const transformed = transformData(validated);
|
|
342
|
+
|
|
343
|
+
// Only here do we enter async context
|
|
344
|
+
const result = await fetchFromAPI(transformed);
|
|
345
|
+
|
|
346
|
+
// Synchronous postprocessing
|
|
347
|
+
const formatted = formatResult(result);
|
|
348
|
+
return axon.output.createSignal({ formatted });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Key Performance Benefits:**
|
|
354
|
+
- **Sync chains execute in single tick**: No microtask delays between neurons
|
|
355
|
+
- **Minimal Promise overhead**: Async context created only when needed
|
|
356
|
+
- **Predictable execution**: Sync operations complete before any async work starts
|
|
357
|
+
- **No unnecessary await**: Sync neurons don't use async/await syntax
|
|
358
|
+
|
|
359
|
+
### Isolated Stimulation Scope
|
|
360
|
+
Each `cns.stimulate()` call creates an isolated signal cascade. You never need to worry about "messageId" or signal correlation - all signals in your trace belong exclusively to your stimulation.
|
|
361
|
+
|
|
362
|
+
#### Complex Orchestration Example: Card & Deck Creation
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// Define collaterals for the orchestration
|
|
366
|
+
const httpRequest = collateral<{ cardText: string }>('http:request');
|
|
367
|
+
const requestProcessed = collateral<{ cardText: string }>('request:processed');
|
|
368
|
+
const deckCreated = collateral<{ deckId: string; cardText: string }>('deck:created');
|
|
369
|
+
const cardCreated = collateral<{ cardId: string; deckId: string }>('card:created');
|
|
370
|
+
|
|
371
|
+
// Controller neuron: orchestrates the entire process
|
|
372
|
+
const controllerNeuron = withCtx<{
|
|
373
|
+
cardText?: string;
|
|
374
|
+
deckId?: string;
|
|
375
|
+
cardId?: string;
|
|
376
|
+
}>().neuron('controller', { requestProcessed, cardCreated })
|
|
377
|
+
.dendrite({
|
|
378
|
+
collateral: httpRequest,
|
|
379
|
+
response: async (payload, axon, ctx) => {
|
|
380
|
+
// Step 1: Store card text in context and signal processing started
|
|
381
|
+
ctx.set({ cardText: payload.cardText });
|
|
382
|
+
return axon.requestProcessed.createSignal({ cardText: payload.cardText });
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
.dendrite({
|
|
386
|
+
collateral: deckCreated,
|
|
387
|
+
response: async (payload, axon, ctx) => {
|
|
388
|
+
// Step 3: Deck created, now create the card
|
|
389
|
+
const context = ctx.get()!;
|
|
390
|
+
ctx.set({ ...context, deckId: payload.deckId });
|
|
391
|
+
return axon.cardCreated.createSignal({
|
|
392
|
+
deckId: payload.deckId,
|
|
393
|
+
cardText: context.cardText!
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Deck service: creates deck when request is processed
|
|
399
|
+
const deckNeuron = neuron('deck-service', { deckCreated })
|
|
400
|
+
.dendrite({
|
|
401
|
+
collateral: requestProcessed,
|
|
402
|
+
response: async (payload, axon) => {
|
|
403
|
+
// Step 2: Create deck (async operation)
|
|
404
|
+
const deckId = await createDeck();
|
|
405
|
+
return axon.deckCreated.createSignal({
|
|
406
|
+
deckId,
|
|
407
|
+
cardText: payload.cardText
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Card service: creates card when deck is ready
|
|
413
|
+
const cardNeuron = neuron('card-service', { cardCreated })
|
|
414
|
+
.dendrite({
|
|
415
|
+
collateral: cardCreated,
|
|
416
|
+
response: async (payload, axon) => {
|
|
417
|
+
// Step 4: Create card (async operation)
|
|
418
|
+
const cardId = await createCard(payload.deckId, payload.cardText);
|
|
419
|
+
return axon.cardCreated.createSignal({
|
|
420
|
+
cardId,
|
|
421
|
+
deckId: payload.deckId
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// HTTP handler using CNS orchestration
|
|
427
|
+
app.post('/create-card', async (req, res) => {
|
|
428
|
+
const cns = new CNS([controllerNeuron, deckNeuron, cardNeuron]);
|
|
429
|
+
|
|
430
|
+
let deckId: string | undefined;
|
|
431
|
+
let cardId: string | undefined;
|
|
432
|
+
|
|
433
|
+
cns.stimulate(httpRequest, { cardText: req.body.text }, {
|
|
434
|
+
onTrace: (trace) => {
|
|
435
|
+
// Collect results as they become available
|
|
436
|
+
if (trace.collateralId === 'deck:created') {
|
|
437
|
+
deckId = (trace.payload as any).deckId;
|
|
438
|
+
}
|
|
439
|
+
if (trace.collateralId === 'card:created') {
|
|
440
|
+
cardId = (trace.payload as any).cardId;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Process complete when queue is empty
|
|
444
|
+
if (trace.queueLength === 0) {
|
|
445
|
+
res.json({ deckId, cardId, success: true });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Key Benefits of Isolated Stimulation:**
|
|
453
|
+
|
|
454
|
+
1. **No Signal Pollution**: Every signal in your trace belongs to your request - no cross-contamination
|
|
455
|
+
2. **No Correlation IDs**: No need to track "messageId" or "requestId" through the system
|
|
456
|
+
3. **Guaranteed Ordering**: Signals process in deterministic order within your stimulation
|
|
457
|
+
4. **Clean Scoping**: Each HTTP request gets its own isolated signal cascade
|
|
458
|
+
5. **Context Safety**: Context store is private to your stimulation - no shared state issues
|
|
459
|
+
|
|
137
460
|
### Only Returned Signals Are Processed
|
|
138
461
|
**Important**: In CNS, only the signal returned from the `reaction` function is processed and propagated to other neurons. Signals created with `axon.collateral.createSignal()` but not returned are NOT processed by the system.
|
|
139
462
|
|
|
@@ -160,7 +483,7 @@ Multiple neurons can listen to the same collateral:
|
|
|
160
483
|
const emailService = neuron('email-service', { emailSent })
|
|
161
484
|
.dendrite({
|
|
162
485
|
collateral: userCreated,
|
|
163
|
-
response:
|
|
486
|
+
response: (payload, axon) => {
|
|
164
487
|
return axon.emailSent.createSignal({ to: 'user@example.com' });
|
|
165
488
|
}
|
|
166
489
|
});
|
|
@@ -168,7 +491,7 @@ const emailService = neuron('email-service', { emailSent })
|
|
|
168
491
|
const notificationService = neuron('notification-service', { notificationSent })
|
|
169
492
|
.dendrite({
|
|
170
493
|
collateral: userCreated,
|
|
171
|
-
response:
|
|
494
|
+
response: (payload, axon) => {
|
|
172
495
|
return axon.notificationSent.createSignal({ message: 'User created' });
|
|
173
496
|
}
|
|
174
497
|
});
|
|
@@ -201,10 +524,10 @@ const router = neuron('router', {
|
|
|
201
524
|
Neurons can maintain local state using the context parameter:
|
|
202
525
|
|
|
203
526
|
```typescript
|
|
204
|
-
const statefulNeuron = neuron('stateful', { output: outputCollateral })
|
|
527
|
+
const statefulNeuron = withCtx().neuron('stateful', { output: outputCollateral })
|
|
205
528
|
.dendrite({
|
|
206
529
|
collateral: inputCollateral,
|
|
207
|
-
response:
|
|
530
|
+
response: (payload, axon, ctx) => {
|
|
208
531
|
// Get current state
|
|
209
532
|
const currentState = ctx.get();
|
|
210
533
|
|
|
@@ -216,6 +539,240 @@ const statefulNeuron = neuron('stateful', { output: outputCollateral })
|
|
|
216
539
|
});
|
|
217
540
|
```
|
|
218
541
|
|
|
542
|
+
### Context-Based Retry Mechanisms
|
|
543
|
+
|
|
544
|
+
CNS supports robust retry mechanisms using context stores to maintain state across stimulation attempts. This is particularly useful for handling transient failures:
|
|
545
|
+
|
|
546
|
+
#### Stateful Retry Pattern
|
|
547
|
+
```typescript
|
|
548
|
+
type RetryContext = {
|
|
549
|
+
tryNumber: number;
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const retryableNeuron = withCtx<RetryContext>().neuron('retryable', { output })
|
|
553
|
+
.dendrite({
|
|
554
|
+
collateral: input,
|
|
555
|
+
response: async (payload, axon, ctx) => {
|
|
556
|
+
const current = ctx.get() || { tryNumber: 0 };
|
|
557
|
+
const tryNumber = current.tryNumber + 1;
|
|
558
|
+
|
|
559
|
+
// Update context with incremented try number
|
|
560
|
+
ctx.set({ tryNumber });
|
|
561
|
+
|
|
562
|
+
if (tryNumber === 1) {
|
|
563
|
+
// First attempt: simulate failure
|
|
564
|
+
throw new Error('Transient failure');
|
|
565
|
+
} else {
|
|
566
|
+
// Second attempt: succeed
|
|
567
|
+
return axon.output.createSignal({
|
|
568
|
+
result: `Success on try ${tryNumber}`,
|
|
569
|
+
payload
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
#### Retry Orchestration
|
|
577
|
+
```typescript
|
|
578
|
+
let contextStore: ICNSStimulationContextStore | undefined;
|
|
579
|
+
|
|
580
|
+
const handleStimulation = (isRetry = false) => {
|
|
581
|
+
cns.stimulate(input, payload, {
|
|
582
|
+
ctx: contextStore, // Use preserved context for retries
|
|
583
|
+
onTrace: (trace) => {
|
|
584
|
+
// Detect stimulation end
|
|
585
|
+
if (trace.queueLength === 0) {
|
|
586
|
+
if (trace.error && !isRetry) {
|
|
587
|
+
// Capture context and retry
|
|
588
|
+
contextStore = trace.contextStore;
|
|
589
|
+
setTimeout(() => handleStimulation(true), 100);
|
|
590
|
+
} else if (!trace.error && trace.collateralId === 'output') {
|
|
591
|
+
// Success - process result
|
|
592
|
+
console.log('Retry succeeded:', trace.payload);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
handleStimulation();
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
#### Context Versioning for Selective Recovery
|
|
603
|
+
Since CNS modifies the context dictionary in-place, you can create new context objects for version control and selective recovery:
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// Create context snapshots for version control
|
|
607
|
+
const createContextSnapshot = (store: ICNSStimulationContextStore) => {
|
|
608
|
+
const snapshot = new CNSStimulationContextStore();
|
|
609
|
+
snapshot.setAll(store.getAll());
|
|
610
|
+
return snapshot;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const retryWithVersionControl = () => {
|
|
614
|
+
let preFailureContext: ICNSStimulationContextStore | undefined;
|
|
615
|
+
|
|
616
|
+
cns.stimulate(input, payload, {
|
|
617
|
+
onTrace: (trace) => {
|
|
618
|
+
// Save context before potential failure points
|
|
619
|
+
if (trace.collateralId === 'risky-operation') {
|
|
620
|
+
preFailureContext = createContextSnapshot(trace.contextStore);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (trace.queueLength === 0 && trace.error) {
|
|
624
|
+
// Option 1: Retry with context from just before failure
|
|
625
|
+
// (excludes the failed step's context changes)
|
|
626
|
+
cns.stimulate(failedCollateral, payload, {
|
|
627
|
+
ctx: preFailureContext,
|
|
628
|
+
onTrace: (retryTrace) => { /* handle retry */ }
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Option 2: Retry with full context including failed step
|
|
632
|
+
// (useful when failure was transient, not logic error)
|
|
633
|
+
cns.stimulate(failedCollateral, payload, {
|
|
634
|
+
ctx: trace.contextStore,
|
|
635
|
+
onTrace: (retryTrace) => { /* handle retry */ }
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
};
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### State Recovery and Restart
|
|
644
|
+
|
|
645
|
+
CNS provides built-in support for recovering from failures and restarting operations. The `onTrace` callback includes the current context state, allowing you to:
|
|
646
|
+
|
|
647
|
+
1. **Capture context on failure**: When an error occurs, the trace contains the complete context state
|
|
648
|
+
2. **Restart with preserved context**: Use the saved context to restore neuron states
|
|
649
|
+
3. **Resume from failure point**: Continue processing from where it left off
|
|
650
|
+
4. **Implement retry logic**: Context preserves attempt counts and other stateful retry data
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
// Example: Advanced recovery with persistence
|
|
654
|
+
let savedContextStore: ICNSStimulationContextStore | undefined;
|
|
655
|
+
|
|
656
|
+
cns.stimulate(input, payload, {
|
|
657
|
+
onTrace: (trace) => {
|
|
658
|
+
if (trace.queueLength === 0) {
|
|
659
|
+
if (trace.error) {
|
|
660
|
+
// Failure: save context for recovery
|
|
661
|
+
savedContextStore = trace.contextStore;
|
|
662
|
+
await persistContextToStorage(trace.contextStore.getAll());
|
|
663
|
+
console.log('Context saved for recovery');
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Recovery from persistent storage
|
|
670
|
+
const restoreAndRetry = async () => {
|
|
671
|
+
const persistedState = await loadContextFromStorage();
|
|
672
|
+
const contextStore = createContextStore();
|
|
673
|
+
contextStore.setAll(persistedState);
|
|
674
|
+
|
|
675
|
+
cns.stimulate(failedCollateral, originalPayload, {
|
|
676
|
+
ctx: contextStore, // Restore full state
|
|
677
|
+
onTrace: (trace) => {
|
|
678
|
+
// Monitor recovery attempt
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
**Custom Context Store Implementations**: You can create your own context store implementations by implementing the `ICNSStimulationContextStore` interface:
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
import { ICNSStimulationContextStore } from '@cnstra/core';
|
|
688
|
+
|
|
689
|
+
// Example: Redis-backed context store for distributed systems
|
|
690
|
+
class RedisContextStore implements ICNSStimulationContextStore {
|
|
691
|
+
constructor(private redis: RedisClient, private keyPrefix: string) {}
|
|
692
|
+
|
|
693
|
+
get(key: string): unknown {
|
|
694
|
+
// Synchronous get - consider caching for performance
|
|
695
|
+
return this.redis.getSync(`${this.keyPrefix}:${key}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
set(key: string, value: unknown): void {
|
|
699
|
+
this.redis.set(`${this.keyPrefix}:${key}`, JSON.stringify(value));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
getAll(): Record<string, unknown> {
|
|
703
|
+
const keys = this.redis.keys(`${this.keyPrefix}:*`);
|
|
704
|
+
const result: Record<string, unknown> = {};
|
|
705
|
+
for (const key of keys) {
|
|
706
|
+
const shortKey = key.replace(`${this.keyPrefix}:`, '');
|
|
707
|
+
result[shortKey] = JSON.parse(this.redis.getSync(key));
|
|
708
|
+
}
|
|
709
|
+
return result;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
setAll(values: Record<string, unknown>): void {
|
|
713
|
+
for (const [key, value] of Object.entries(values)) {
|
|
714
|
+
this.set(key, value);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Example: Immutable context store for versioning
|
|
720
|
+
class ImmutableContextStore implements ICNSStimulationContextStore {
|
|
721
|
+
private data: Record<string, unknown> = {};
|
|
722
|
+
private version = 0;
|
|
723
|
+
|
|
724
|
+
get(key: string): unknown {
|
|
725
|
+
return this.data[key];
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
set(key: string, value: unknown): void {
|
|
729
|
+
// Create new instance instead of mutating
|
|
730
|
+
this.data = { ...this.data, [key]: value };
|
|
731
|
+
this.version++;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
getAll(): Record<string, unknown> {
|
|
735
|
+
return { ...this.data }; // Return copy
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
setAll(values: Record<string, unknown>): void {
|
|
739
|
+
this.data = { ...values };
|
|
740
|
+
this.version++;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
getVersion(): number {
|
|
744
|
+
return this.version;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Usage with custom context store
|
|
749
|
+
cns.stimulate(input, payload, {
|
|
750
|
+
createContextStore: () => new RedisContextStore(redisClient, 'session-123'),
|
|
751
|
+
onTrace: (trace) => {
|
|
752
|
+
// trace.contextStore is your custom RedisContextStore
|
|
753
|
+
console.log('Using Redis-backed context');
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Context Store Interface**:
|
|
759
|
+
```typescript
|
|
760
|
+
interface ICNSStimulationContextStore {
|
|
761
|
+
get(key: string): unknown;
|
|
762
|
+
set(key: string, value: unknown): void;
|
|
763
|
+
getAll(): Record<string, unknown>;
|
|
764
|
+
setAll(values: Record<string, unknown>): void;
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**Custom Store Use Cases**:
|
|
769
|
+
- **Persistence**: Redis, database, or file-based storage
|
|
770
|
+
- **Distribution**: Share context across multiple CNS instances
|
|
771
|
+
- **Immutability**: Version-controlled context for advanced debugging
|
|
772
|
+
- **Encryption**: Secure sensitive context data
|
|
773
|
+
- **Compression**: Optimize memory usage for large context objects
|
|
774
|
+
- **Auditing**: Log all context changes for compliance
|
|
775
|
+
|
|
219
776
|
## 🧪 Testing
|
|
220
777
|
|
|
221
778
|
```bash
|