@cnstra/core 1.2.2 โ 1.3.1
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 +242 -557
- package/dist/CNS.d.ts +54 -61
- package/dist/CNS.d.ts.map +1 -1
- package/dist/CNSCollateral.d.ts +4 -2
- package/dist/CNSCollateral.d.ts.map +1 -1
- package/dist/CNSFunctionalQueue.d.ts +26 -0
- package/dist/CNSFunctionalQueue.d.ts.map +1 -0
- package/dist/CNSStimulation.d.ts +52 -0
- package/dist/CNSStimulation.d.ts.map +1 -0
- package/dist/CNSStimulationContextStore.d.ts +1 -0
- package/dist/CNSStimulationContextStore.d.ts.map +1 -1
- package/dist/factory/index.d.ts.map +1 -1
- package/dist/index.cjs +490 -214
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +490 -214
- package/dist/index.js.map +1 -1
- package/dist/interfaces/ICNS.d.ts +14 -0
- package/dist/interfaces/ICNS.d.ts.map +1 -0
- package/dist/interfaces/ICNSCollateral.d.ts +6 -0
- package/dist/interfaces/ICNSCollateral.d.ts.map +1 -0
- package/dist/interfaces/ICNSStimulationContextStore.d.ts +1 -0
- package/dist/interfaces/ICNSStimulationContextStore.d.ts.map +1 -1
- package/dist/types/TCNSDendrite.d.ts +2 -1
- package/dist/types/TCNSDendrite.d.ts.map +1 -1
- package/dist/types/TCNSNeuronResponseReturn.d.ts +3 -0
- package/dist/types/TCNSNeuronResponseReturn.d.ts.map +1 -0
- package/dist/types/TCNSOptions.d.ts +11 -0
- package/dist/types/TCNSOptions.d.ts.map +1 -0
- package/dist/types/TCNSSignal.d.ts +3 -2
- package/dist/types/TCNSSignal.d.ts.map +1 -1
- package/dist/types/TCNSStimulationOptions.d.ts +13 -0
- package/dist/types/TCNSStimulationOptions.d.ts.map +1 -0
- package/dist/types/TCNSStimulationQueueItem.d.ts +10 -0
- package/dist/types/TCNSStimulationQueueItem.d.ts.map +1 -0
- package/dist/types/TCNSStimulationResponse.d.ts +11 -0
- package/dist/types/TCNSStimulationResponse.d.ts.map +1 -0
- package/dist/types/TCNSSubscriber.d.ts +7 -0
- package/dist/types/TCNSSubscriber.d.ts.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,23 +9,26 @@ You explicitly start a run with `cns.stimulate(...)`; CNStra then performs a **d
|
|
|
9
9
|
|
|
10
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
11
|
|
|
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.
|
|
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. Hop limits guard against cycles.
|
|
13
13
|
|
|
14
14
|
## ๐๏ธ Core Model
|
|
15
15
|
|
|
16
16
|
### Neurons
|
|
17
17
|
Units of logic with clear DI and sharp boundaries:
|
|
18
18
|
- **ID** โ unique name
|
|
19
|
-
- **Axon** โ the neuron
|
|
19
|
+
- **Axon** โ the neuron's **output channels** (its collaterals)
|
|
20
20
|
- **Dendrites** โ **input receptors** (typed reactions bound to specific collaterals)
|
|
21
21
|
|
|
22
22
|
### Collaterals
|
|
23
23
|
Typed **output channels** that mint signals:
|
|
24
24
|
- **ID** โ string identifier (e.g., `'user:created'`)
|
|
25
25
|
- **Payload** โ the shape carried by the signal
|
|
26
|
-
- `createSignal(payload)` โ `{
|
|
26
|
+
- `createSignal(payload)` โ `{ collateral, payload }`
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
### Signals
|
|
29
|
+
The data structures that flow through the system:
|
|
30
|
+
- **collateral** โ reference to the collateral that created this signal
|
|
31
|
+
- **payload** โ the typed data being transmitted
|
|
29
32
|
|
|
30
33
|
## ๐ Quick Start
|
|
31
34
|
|
|
@@ -64,10 +67,10 @@ const userService = neuron('user-service', {
|
|
|
64
67
|
const cns = new CNS([userService]);
|
|
65
68
|
|
|
66
69
|
// Stimulate the system
|
|
67
|
-
await cns.stimulate(userCreated
|
|
70
|
+
await cns.stimulate(userCreated.createSignal({
|
|
68
71
|
id: '123',
|
|
69
72
|
name: 'John Doe'
|
|
70
|
-
});
|
|
73
|
+
}));
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
## ๐ API Reference
|
|
@@ -113,58 +116,76 @@ The main orchestrator that manages signal flow between neurons.
|
|
|
113
116
|
|
|
114
117
|
#### Constructor
|
|
115
118
|
```typescript
|
|
116
|
-
new CNS(neurons)
|
|
119
|
+
new CNS(neurons, options?)
|
|
117
120
|
```
|
|
118
121
|
|
|
119
122
|
**Parameters:**
|
|
120
123
|
- `neurons`: Array of neurons that process signals
|
|
124
|
+
- `options`: Optional CNS configuration
|
|
121
125
|
|
|
122
126
|
#### `stimulate()` Method
|
|
123
127
|
```typescript
|
|
124
|
-
cns.stimulate(
|
|
128
|
+
cns.stimulate(signal, options?)
|
|
125
129
|
```
|
|
126
130
|
|
|
127
131
|
**Parameters:**
|
|
128
|
-
- `
|
|
129
|
-
- `
|
|
130
|
-
|
|
132
|
+
- `signal`: A signal created by `collateral.createSignal(payload)`
|
|
133
|
+
- `options`: Optional stimulation configuration
|
|
134
|
+
|
|
135
|
+
**Returns:** `Promise<void>` that resolves when stimulation completes
|
|
136
|
+
|
|
137
|
+
**Example:**
|
|
138
|
+
```typescript
|
|
139
|
+
await cns.stimulate(
|
|
140
|
+
userCreated.createSignal({ id: '123', name: 'John' })
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## โ๏ธ Stimulation Options
|
|
131
145
|
|
|
132
|
-
|
|
146
|
+
### `maxNeuronHops?: number` (default: 1000)
|
|
147
|
+
Prevents infinite loops by limiting signal traversal depth.
|
|
133
148
|
|
|
134
|
-
##### `maxHops?: number` (default: 1000)
|
|
135
|
-
Prevents infinite loops by limiting signal traversal depth. Each signal hop increments the counter.
|
|
136
149
|
```typescript
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
maxHops: 50 // Stop after 50 signal hops
|
|
150
|
+
await cns.stimulate(signal, {
|
|
151
|
+
maxNeuronHops: 50 // Stop after 50 neuron hops
|
|
140
152
|
});
|
|
141
153
|
```
|
|
142
|
-
โ ๏ธ **Memory Impact**: Higher values allow deeper graphs but increase memory usage.
|
|
143
154
|
|
|
144
|
-
|
|
145
|
-
Real-time callback for monitoring signal flow
|
|
155
|
+
### `onResponse?: (response) => void`
|
|
156
|
+
Real-time callback for monitoring signal flow and completion.
|
|
157
|
+
|
|
146
158
|
```typescript
|
|
147
|
-
cns.stimulate(
|
|
148
|
-
|
|
149
|
-
console.log(`Signal: ${
|
|
159
|
+
await cns.stimulate(signal, {
|
|
160
|
+
onResponse: (response) => {
|
|
161
|
+
console.log(`Signal: ${response.outputSignal?.collateral.id}`);
|
|
162
|
+
console.log(`Hops: ${response.hops}`);
|
|
150
163
|
|
|
151
|
-
if (
|
|
152
|
-
console.error('Processing failed:',
|
|
164
|
+
if (response.error) {
|
|
165
|
+
console.error('Processing failed:', response.error);
|
|
153
166
|
}
|
|
154
167
|
|
|
155
|
-
if (
|
|
168
|
+
if (response.queueLength === 0) {
|
|
156
169
|
console.log('Stimulation completed');
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
172
|
});
|
|
160
173
|
```
|
|
161
174
|
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
**Response Object:**
|
|
176
|
+
- `outputSignal` โ The signal being processed (if any)
|
|
177
|
+
- `hops` โ Number of neuron hops taken so far
|
|
178
|
+
- `queueLength` โ Remaining signals in processing queue (0 = complete)
|
|
179
|
+
- `error` โ Any error that occurred during processing
|
|
180
|
+
- `stimulationId` โ Unique identifier for this stimulation
|
|
181
|
+
|
|
182
|
+
### `abortSignal?: AbortSignal`
|
|
183
|
+
Gracefully stop stimulation using AbortController.
|
|
184
|
+
|
|
164
185
|
```typescript
|
|
165
186
|
const controller = new AbortController();
|
|
166
187
|
|
|
167
|
-
cns.stimulate(
|
|
188
|
+
cns.stimulate(signal, {
|
|
168
189
|
abortSignal: controller.signal
|
|
169
190
|
});
|
|
170
191
|
|
|
@@ -172,632 +193,296 @@ cns.stimulate(input, data, {
|
|
|
172
193
|
setTimeout(() => controller.abort(), 5000);
|
|
173
194
|
```
|
|
174
195
|
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
### `stimulationId?: string`
|
|
197
|
+
Custom identifier for this stimulation cascade. Auto-generated if not provided.
|
|
198
|
+
|
|
177
199
|
```typescript
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
ctx: savedContextStore, // Restore previous state
|
|
181
|
-
onTrace: (trace) => { /* monitor retry */ }
|
|
200
|
+
await cns.stimulate(signal, {
|
|
201
|
+
stimulationId: 'user-action-' + Date.now()
|
|
182
202
|
});
|
|
183
203
|
```
|
|
184
204
|
|
|
185
|
-
|
|
186
|
-
|
|
205
|
+
### `allowType?: (collateralId: string) => boolean`
|
|
206
|
+
Filter which collateral types can be processed.
|
|
207
|
+
|
|
187
208
|
```typescript
|
|
188
|
-
cns.stimulate(
|
|
189
|
-
|
|
190
|
-
onTrace: (trace) => {
|
|
191
|
-
// trace.contextStore is your custom RedisContextStore
|
|
192
|
-
}
|
|
209
|
+
await cns.stimulate(signal, {
|
|
210
|
+
allowType: (type) => type.startsWith('user:') // Only process user-related signals
|
|
193
211
|
});
|
|
194
212
|
```
|
|
195
213
|
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
### `concurrency?: number` (default: unlimited)
|
|
215
|
+
Limit concurrent operations to prevent resource exhaustion.
|
|
216
|
+
|
|
198
217
|
```typescript
|
|
199
|
-
cns.stimulate(
|
|
200
|
-
|
|
201
|
-
onTrace: (trace) => {
|
|
202
|
-
console.log('Spike ID:', trace.spikeId);
|
|
203
|
-
}
|
|
218
|
+
await cns.stimulate(signal, {
|
|
219
|
+
concurrency: 10 // Max 10 operations at once
|
|
204
220
|
});
|
|
205
221
|
```
|
|
206
222
|
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
### `ctx?: ICNSStimulationContextStore`
|
|
224
|
+
Provide existing context store for recovery/retry scenarios.
|
|
225
|
+
|
|
209
226
|
```typescript
|
|
210
|
-
cns.stimulate(
|
|
211
|
-
|
|
212
|
-
onTrace: (trace) => {
|
|
213
|
-
// Only user: collaterals will appear in traces
|
|
214
|
-
}
|
|
227
|
+
await cns.stimulate(signal, {
|
|
228
|
+
ctx: savedContextStore // Restore previous state
|
|
215
229
|
});
|
|
216
230
|
```
|
|
217
231
|
|
|
218
|
-
|
|
219
|
-
|
|
232
|
+
### `createContextStore?: () => ICNSStimulationContextStore`
|
|
233
|
+
Factory for custom context store implementations.
|
|
234
|
+
|
|
220
235
|
```typescript
|
|
221
|
-
cns.stimulate(
|
|
222
|
-
|
|
223
|
-
onTrace: (trace) => {
|
|
224
|
-
// Processing respects concurrency limit
|
|
225
|
-
}
|
|
236
|
+
await cns.stimulate(signal, {
|
|
237
|
+
createContextStore: () => new CustomContextStore()
|
|
226
238
|
});
|
|
227
239
|
```
|
|
228
240
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
241
|
+
## ๐ Signal Flow Patterns
|
|
242
|
+
|
|
243
|
+
### Basic Chain Processing
|
|
262
244
|
|
|
263
245
|
```typescript
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
246
|
+
const input = collateral<{ value: number }>('input');
|
|
247
|
+
const middle = collateral<{ doubled: number }>('middle');
|
|
248
|
+
const output = collateral<{ result: string }>('output');
|
|
249
|
+
|
|
250
|
+
const step1 = neuron('step1', { middle }).dendrite({
|
|
251
|
+
collateral: input,
|
|
252
|
+
response: (payload, axon) => {
|
|
253
|
+
return axon.middle.createSignal({ doubled: payload.value * 2 });
|
|
272
254
|
}
|
|
273
255
|
});
|
|
274
256
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const badContext = {
|
|
284
|
-
fullUserProfile: largeUserObject,
|
|
285
|
-
historicalData: massiveArray,
|
|
286
|
-
fileBuffer: binaryData
|
|
287
|
-
};
|
|
288
|
-
```
|
|
257
|
+
const step2 = neuron('step2', { output }).dendrite({
|
|
258
|
+
collateral: middle,
|
|
259
|
+
response: (payload, axon) => {
|
|
260
|
+
return axon.output.createSignal({ result: `Final: ${payload.doubled}` });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const cns = new CNS([step1, step2]);
|
|
289
265
|
|
|
290
|
-
|
|
266
|
+
await cns.stimulate(input.createSignal({ value: 5 }));
|
|
267
|
+
// Flows: input(5) โ middle(10) โ output("Final: 10")
|
|
268
|
+
```
|
|
291
269
|
|
|
292
|
-
###
|
|
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.
|
|
270
|
+
### Fan-out Processing
|
|
294
271
|
|
|
295
|
-
**Synchronous Path** - No async/await overhead:
|
|
296
272
|
```typescript
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
});
|
|
273
|
+
const trigger = collateral<{ data: string }>('trigger');
|
|
274
|
+
const branch1 = collateral<{ result: string }>('branch1');
|
|
275
|
+
const branch2 = collateral<{ result: string }>('branch2');
|
|
307
276
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
277
|
+
const processor1 = neuron('proc1', { branch1 }).dendrite({
|
|
278
|
+
collateral: trigger,
|
|
279
|
+
response: (payload, axon) => {
|
|
280
|
+
return axon.branch1.createSignal({ result: `A-${payload.data}` });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
312
283
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
});
|
|
284
|
+
const processor2 = neuron('proc2', { branch2 }).dendrite({
|
|
285
|
+
collateral: trigger,
|
|
286
|
+
response: (payload, axon) => {
|
|
287
|
+
return axon.branch2.createSignal({ result: `B-${payload.data}` });
|
|
288
|
+
}
|
|
289
|
+
});
|
|
320
290
|
|
|
321
|
-
const
|
|
322
|
-
.dendrite({
|
|
323
|
-
collateral: chainMiddle,
|
|
324
|
-
response: (payload, axon) => {
|
|
325
|
-
return axon.chainEnd.createSignal({ final: `Result: ${payload.doubled}` });
|
|
326
|
-
}
|
|
327
|
-
});
|
|
291
|
+
const cns = new CNS([processor1, processor2]);
|
|
328
292
|
|
|
329
|
-
|
|
330
|
-
|
|
293
|
+
await cns.stimulate(trigger.createSignal({ data: 'test' }));
|
|
294
|
+
// Flows: trigger("test") โ [branch1("A-test"), branch2("B-test")]
|
|
331
295
|
```
|
|
332
296
|
|
|
333
|
-
|
|
297
|
+
### Context-Aware Processing
|
|
298
|
+
|
|
334
299
|
```typescript
|
|
335
|
-
|
|
300
|
+
import { withCtx } from '@cnstra/core';
|
|
301
|
+
|
|
302
|
+
const input = collateral<{ increment: number }>('input');
|
|
303
|
+
const output = collateral<{ count: number }>('output');
|
|
304
|
+
|
|
305
|
+
const counter = withCtx<{ total: number }>()
|
|
306
|
+
.neuron('counter', { output })
|
|
336
307
|
.dendrite({
|
|
337
308
|
collateral: input,
|
|
338
|
-
response: async (payload, axon) => {
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
const transformed = transformData(validated);
|
|
309
|
+
response: async (payload, axon, ctx) => {
|
|
310
|
+
const current = ctx.get()?.total || 0;
|
|
311
|
+
const newTotal = current + payload.increment;
|
|
342
312
|
|
|
343
|
-
|
|
344
|
-
const result = await fetchFromAPI(transformed);
|
|
313
|
+
ctx.set({ total: newTotal });
|
|
345
314
|
|
|
346
|
-
|
|
347
|
-
const formatted = formatResult(result);
|
|
348
|
-
return axon.output.createSignal({ formatted });
|
|
315
|
+
return axon.output.createSignal({ count: newTotal });
|
|
349
316
|
}
|
|
350
317
|
});
|
|
318
|
+
|
|
319
|
+
const cns = new CNS([counter]);
|
|
320
|
+
|
|
321
|
+
await cns.stimulate(input.createSignal({ increment: 5 })); // count: 5
|
|
322
|
+
await cns.stimulate(input.createSignal({ increment: 3 })); // count: 8 (separate context)
|
|
351
323
|
```
|
|
352
324
|
|
|
353
|
-
|
|
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
|
|
325
|
+
## ๐ง Memory & Performance
|
|
358
326
|
|
|
359
|
-
###
|
|
360
|
-
|
|
327
|
+
### Memory-Efficient Design
|
|
328
|
+
- **Zero dependencies**: No third-party packages
|
|
329
|
+
- **No error storage**: Errors delivered via callbacks, not stored
|
|
330
|
+
- **Streaming responses**: Signal traces delivered via callbacks
|
|
331
|
+
- **Context on-demand**: Context stores created only when needed
|
|
332
|
+
- **No global state**: Clean slate between stimulations
|
|
361
333
|
|
|
362
|
-
|
|
334
|
+
### Performance Characteristics
|
|
335
|
+
- **Sync-first**: Synchronous chains execute in single tick
|
|
336
|
+
- **Minimal async overhead**: Promises created only when needed
|
|
337
|
+
- **Stack-safe**: Handles deep chains without stack overflow
|
|
338
|
+
- **Bounded execution**: `maxNeuronHops` prevents runaway processing
|
|
363
339
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
});
|
|
340
|
+
### Best Practices
|
|
341
|
+
- Keep context data minimal (IDs, counters, flags)
|
|
342
|
+
- Use synchronous responses when possible
|
|
343
|
+
- Set reasonable `maxNeuronHops` limits
|
|
344
|
+
- Implement proper error handling in `onResponse`
|
|
397
345
|
|
|
398
|
-
|
|
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
|
-
});
|
|
346
|
+
## ๐ฏ Common Use Cases
|
|
411
347
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
});
|
|
348
|
+
### HTTP Request Processing
|
|
349
|
+
```typescript
|
|
350
|
+
const httpRequest = collateral<{ method: string; url: string }>('http:request');
|
|
351
|
+
const requestValidated = collateral<{ method: string; url: string }>('request:validated');
|
|
352
|
+
const responseReady = collateral<{ status: number; body: any }>('response:ready');
|
|
425
353
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
}
|
|
354
|
+
const validator = neuron('validator', { requestValidated }).dendrite({
|
|
355
|
+
collateral: httpRequest,
|
|
356
|
+
response: (payload, axon) => {
|
|
357
|
+
if (!payload.url.startsWith('https://')) {
|
|
358
|
+
throw new Error('Only HTTPS URLs allowed');
|
|
447
359
|
}
|
|
448
|
-
|
|
360
|
+
return axon.requestValidated.createSignal(payload);
|
|
361
|
+
}
|
|
449
362
|
});
|
|
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
363
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
```typescript
|
|
464
|
-
const processor = neuron('processor', {
|
|
465
|
-
output: outputCollateral
|
|
466
|
-
})
|
|
467
|
-
.dendrite({
|
|
468
|
-
collateral: inputCollateral,
|
|
364
|
+
const handler = neuron('handler', { responseReady }).dendrite({
|
|
365
|
+
collateral: requestValidated,
|
|
469
366
|
response: async (payload, axon) => {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// Only this returned signal is processed
|
|
474
|
-
return axon.output.createSignal({ message: 'World' });
|
|
367
|
+
const response = await fetch(payload.url, { method: payload.method });
|
|
368
|
+
const body = await response.json();
|
|
369
|
+
return axon.responseReady.createSignal({ status: response.status, body });
|
|
475
370
|
}
|
|
476
371
|
});
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### Multiple Neurons on Same Collateral
|
|
480
|
-
Multiple neurons can listen to the same collateral:
|
|
481
372
|
|
|
482
|
-
|
|
483
|
-
const emailService = neuron('email-service', { emailSent })
|
|
484
|
-
.dendrite({
|
|
485
|
-
collateral: userCreated,
|
|
486
|
-
response: (payload, axon) => {
|
|
487
|
-
return axon.emailSent.createSignal({ to: 'user@example.com' });
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
const notificationService = neuron('notification-service', { notificationSent })
|
|
492
|
-
.dendrite({
|
|
493
|
-
collateral: userCreated,
|
|
494
|
-
response: (payload, axon) => {
|
|
495
|
-
return axon.notificationSent.createSignal({ message: 'User created' });
|
|
496
|
-
}
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// Both neurons will process the userCreated signal
|
|
500
|
-
const cns = new CNS([emailService, notificationService]);
|
|
373
|
+
const cns = new CNS([validator, handler]);
|
|
501
374
|
```
|
|
502
375
|
|
|
503
|
-
###
|
|
376
|
+
### Event Sourcing
|
|
504
377
|
```typescript
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.dendrite({
|
|
510
|
-
collateral:
|
|
378
|
+
const eventReceived = collateral<{ type: string; data: any }>('event:received');
|
|
379
|
+
const eventStored = collateral<{ eventId: string }>('event:stored');
|
|
380
|
+
const stateUpdated = collateral<{ aggregateId: string }>('state:updated');
|
|
381
|
+
|
|
382
|
+
const eventStore = neuron('event-store', { eventStored }).dendrite({
|
|
383
|
+
collateral: eventReceived,
|
|
511
384
|
response: async (payload, axon) => {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return axon.success.createSignal(result);
|
|
515
|
-
} catch (error) {
|
|
516
|
-
return axon.error.createSignal({ error: error.message });
|
|
517
|
-
}
|
|
385
|
+
const eventId = await saveEvent(payload);
|
|
386
|
+
return axon.eventStored.createSignal({ eventId });
|
|
518
387
|
}
|
|
519
388
|
});
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
### Context Management
|
|
523
389
|
|
|
524
|
-
|
|
390
|
+
const stateManager = neuron('state-manager', { stateUpdated }).dendrite({
|
|
391
|
+
collateral: eventStored,
|
|
392
|
+
response: async (payload, axon) => {
|
|
393
|
+
const aggregateId = await updateState(payload.eventId);
|
|
394
|
+
return axon.stateUpdated.createSignal({ aggregateId });
|
|
395
|
+
}
|
|
396
|
+
});
|
|
525
397
|
|
|
526
|
-
|
|
527
|
-
const statefulNeuron = withCtx().neuron('stateful', { output: outputCollateral })
|
|
528
|
-
.dendrite({
|
|
529
|
-
collateral: inputCollateral,
|
|
530
|
-
response: (payload, axon, ctx) => {
|
|
531
|
-
// Get current state
|
|
532
|
-
const currentState = ctx.get();
|
|
533
|
-
|
|
534
|
-
// Update state
|
|
535
|
-
ctx.set({ count: (currentState?.count || 0) + 1 });
|
|
536
|
-
|
|
537
|
-
return axon.output.createSignal({ count: ctx.get()?.count });
|
|
538
|
-
}
|
|
539
|
-
});
|
|
398
|
+
const cns = new CNS([eventStore, stateManager]);
|
|
540
399
|
```
|
|
541
400
|
|
|
542
|
-
|
|
401
|
+
## ๐จ Error Handling
|
|
543
402
|
|
|
544
|
-
|
|
403
|
+
Errors are delivered immediately via `onResponse` callbacks:
|
|
545
404
|
|
|
546
|
-
#### Stateful Retry Pattern
|
|
547
405
|
```typescript
|
|
548
|
-
|
|
549
|
-
|
|
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;
|
|
406
|
+
await cns.stimulate(signal, {
|
|
407
|
+
onResponse: (response) => {
|
|
408
|
+
if (response.error) {
|
|
409
|
+
console.error(`Error in neuron processing:`, response.error);
|
|
558
410
|
|
|
559
|
-
//
|
|
560
|
-
|
|
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
|
-
}
|
|
411
|
+
// Log error details
|
|
412
|
+
console.error(`Signal: ${response.outputSignal?.collateral.id}`);
|
|
413
|
+
console.error(`Stimulation: ${response.stimulationId}`);
|
|
622
414
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
});
|
|
415
|
+
// Handle specific error types
|
|
416
|
+
if (response.error instanceof ValidationError) {
|
|
417
|
+
handleValidationError(response.error);
|
|
637
418
|
}
|
|
638
419
|
}
|
|
639
|
-
}
|
|
640
|
-
};
|
|
420
|
+
}
|
|
421
|
+
});
|
|
641
422
|
```
|
|
642
423
|
|
|
643
|
-
|
|
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
|
-
|
|
424
|
+
**Error Recovery with Context:**
|
|
652
425
|
```typescript
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
// Failure: save context for recovery
|
|
661
|
-
savedContextStore = trace.contextStore;
|
|
662
|
-
await persistContextToStorage(trace.contextStore.getAll());
|
|
663
|
-
console.log('Context saved for recovery');
|
|
664
|
-
}
|
|
426
|
+
let savedContext: ICNSStimulationContextStore | undefined;
|
|
427
|
+
|
|
428
|
+
await cns.stimulate(signal, {
|
|
429
|
+
onResponse: (response) => {
|
|
430
|
+
if (response.error) {
|
|
431
|
+
// Save context for retry
|
|
432
|
+
savedContext = response.contextStore;
|
|
665
433
|
}
|
|
666
434
|
}
|
|
667
435
|
});
|
|
668
436
|
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
contextStore.setAll(persistedState);
|
|
674
|
-
|
|
675
|
-
cns.stimulate(failedCollateral, originalPayload, {
|
|
676
|
-
ctx: contextStore, // Restore full state
|
|
677
|
-
onTrace: (trace) => {
|
|
678
|
-
// Monitor recovery attempt
|
|
679
|
-
}
|
|
437
|
+
// Retry with preserved context
|
|
438
|
+
if (savedContext) {
|
|
439
|
+
await cns.stimulate(retrySignal, {
|
|
440
|
+
ctx: savedContext
|
|
680
441
|
});
|
|
681
|
-
}
|
|
442
|
+
}
|
|
682
443
|
```
|
|
683
444
|
|
|
684
|
-
|
|
445
|
+
## ๐ง Advanced Configuration
|
|
685
446
|
|
|
447
|
+
### Custom Context Store
|
|
686
448
|
```typescript
|
|
687
|
-
import { ICNSStimulationContextStore } from '@cnstra/core';
|
|
688
|
-
|
|
689
|
-
// Example: Redis-backed context store for distributed systems
|
|
690
449
|
class RedisContextStore implements ICNSStimulationContextStore {
|
|
691
|
-
constructor(private
|
|
692
|
-
|
|
693
|
-
get(
|
|
694
|
-
//
|
|
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++;
|
|
450
|
+
constructor(private client: RedisClient, private sessionId: string) {}
|
|
451
|
+
|
|
452
|
+
get<T>(): T | undefined {
|
|
453
|
+
// Implement Redis-backed context retrieval
|
|
741
454
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
455
|
+
|
|
456
|
+
set<T>(value: T): void {
|
|
457
|
+
// Implement Redis-backed context storage
|
|
745
458
|
}
|
|
746
459
|
}
|
|
747
460
|
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
}
|
|
461
|
+
await cns.stimulate(signal, {
|
|
462
|
+
createContextStore: () => new RedisContextStore(redisClient, 'session-123')
|
|
755
463
|
});
|
|
756
464
|
```
|
|
757
465
|
|
|
758
|
-
|
|
466
|
+
### CNS Configuration
|
|
759
467
|
```typescript
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
776
|
-
## ๐งช Testing
|
|
777
|
-
|
|
778
|
-
```bash
|
|
779
|
-
npm test
|
|
780
|
-
npm run test:types
|
|
781
|
-
```
|
|
782
|
-
|
|
783
|
-
## ๐ฆ Build
|
|
784
|
-
|
|
785
|
-
```bash
|
|
786
|
-
npm run build
|
|
468
|
+
const cns = new CNS(neurons, {
|
|
469
|
+
autoCleanupContexts: true, // Auto-cleanup unused contexts
|
|
470
|
+
defaultConcurrency: 50 // Default concurrency limit
|
|
471
|
+
});
|
|
787
472
|
```
|
|
788
473
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
```bash
|
|
794
|
-
npm run examples
|
|
795
|
-
```
|
|
474
|
+
โ ๏ธ **Performance Warning**: `autoCleanupContexts` adds computational overhead due to:
|
|
475
|
+
- **O(Vยฒ) initialization cost** - building SCC (Strongly Connected Components) structures
|
|
476
|
+
- **O(1 + A) runtime cost** per cleanup check (where A = number of SCC ancestors)
|
|
477
|
+
- **Memory overhead** for storing SCC graphs and ancestor relationships
|
|
796
478
|
|
|
797
|
-
|
|
479
|
+
**Use only when:**
|
|
480
|
+
- Memory leaks are a critical issue
|
|
481
|
+
- You have a small to medium-sized neuron graph (< 1000 neurons)
|
|
482
|
+
- Performance is less critical than memory management
|
|
798
483
|
|
|
799
|
-
|
|
484
|
+
**For production systems**, consider manual context cleanup or custom cleanup strategies instead.
|
|
800
485
|
|
|
801
|
-
|
|
486
|
+
---
|
|
802
487
|
|
|
803
|
-
|
|
488
|
+
*CNStra provides deterministic, type-safe orchestration without the complexity of traditional event systems. Build reliable, maintainable reactive applications with clear data flow and predictable behavior.*
|