@cnstra/core 1.0.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 +226 -0
- package/dist/index.cjs +140 -0
- package/dist/index.d.cts +56 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +110 -0
- package/package.json +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# @cnstra/core
|
|
2
|
+
|
|
3
|
+
**Graph-routed, type-safe orchestration for reactive apps β no global event bus.**
|
|
4
|
+
|
|
5
|
+
## π§ What is CNStra?
|
|
6
|
+
|
|
7
|
+
**CNStra (Central Nervous System Orchestrator)** models your app as a **typed neuron graph**.
|
|
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
|
+
|
|
10
|
+
> **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
|
+
|
|
12
|
+
## ποΈ Core Model
|
|
13
|
+
|
|
14
|
+
### Neurons
|
|
15
|
+
Units of logic with clear DI and sharp boundaries:
|
|
16
|
+
- **ID** β unique name
|
|
17
|
+
- **Axon** β the neuronβs **output channels** (its collaterals)
|
|
18
|
+
- **Dendrites** β **input receptors** (typed reactions bound to specific collaterals)
|
|
19
|
+
|
|
20
|
+
### Collaterals
|
|
21
|
+
Typed **output channels** that mint signals:
|
|
22
|
+
- **ID** β string identifier (e.g., `'user:created'`)
|
|
23
|
+
- **Payload** β the shape carried by the signal
|
|
24
|
+
- `createSignal(payload)` β `{ type, payload }`
|
|
25
|
+
|
|
26
|
+
> **Afferent axon:** the object of collaterals you expose publicly. Its **keys** (e.g., `userCreated`) are what you pass to `cns.stimulate(...)`, not the string IDs.
|
|
27
|
+
|
|
28
|
+
## π Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @cnstra/core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { CNS, collateral, neuron } from '@cnstra/core';
|
|
36
|
+
|
|
37
|
+
// Define collaterals (communication channels)
|
|
38
|
+
const userCreated = collateral<{ id: string; name: string }>('user:created');
|
|
39
|
+
const userRegistered = collateral<{ userId: string; status: string }>('user:registered');
|
|
40
|
+
|
|
41
|
+
// Create a neuron
|
|
42
|
+
const userService = neuron('user-service', {
|
|
43
|
+
userRegistered
|
|
44
|
+
})
|
|
45
|
+
.dendrite({
|
|
46
|
+
collateral: userCreated,
|
|
47
|
+
reaction: async (payload, axon) => {
|
|
48
|
+
const userData = payload as { id: string; name: string };
|
|
49
|
+
|
|
50
|
+
// Process the user creation
|
|
51
|
+
console.log(`Processing user: ${userData.name}`);
|
|
52
|
+
|
|
53
|
+
// Return the signal that will be processed by CNS
|
|
54
|
+
return axon.userRegistered.createSignal({
|
|
55
|
+
userId: userData.id,
|
|
56
|
+
status: 'completed'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Create the CNS system
|
|
62
|
+
const cns = new CNS({ userCreated }, [userService]);
|
|
63
|
+
|
|
64
|
+
// Stimulate the system
|
|
65
|
+
await cns.stimulate('userCreated', {
|
|
66
|
+
id: '123',
|
|
67
|
+
name: 'John Doe'
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## π API Reference
|
|
72
|
+
|
|
73
|
+
### `collateral<T>(id: string)`
|
|
74
|
+
|
|
75
|
+
Creates a new collateral (communication channel).
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const userEvent = collateral<{ userId: string }>('user:event');
|
|
79
|
+
const simpleEvent = collateral('simple:event'); // No payload type
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `neuron(id: string, axon: Axon)`
|
|
83
|
+
|
|
84
|
+
Creates a new neuron with the specified axon (output channels).
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const myNeuron = neuron('my-neuron', {
|
|
88
|
+
output: myCollateral
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `neuron.dendrite(dendrite: Dendrite)`
|
|
93
|
+
|
|
94
|
+
Adds a dendrite (input receptor) to a neuron. Returns the neuron for chaining.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
myNeuron
|
|
98
|
+
.dendrite({
|
|
99
|
+
collateral: inputCollateral,
|
|
100
|
+
reaction: async (payload, axon) => {
|
|
101
|
+
// Process input and return output signal
|
|
102
|
+
return axon.output.createSignal(result);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `CNS` Class
|
|
108
|
+
|
|
109
|
+
The main orchestrator that manages signal flow between neurons.
|
|
110
|
+
|
|
111
|
+
#### Constructor
|
|
112
|
+
```typescript
|
|
113
|
+
new CNS(afferentAxon, neurons)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Parameters:**
|
|
117
|
+
- `afferentAxon`: Object containing collaterals that can be used as input triggers
|
|
118
|
+
- `neurons`: Array of neurons that process signals
|
|
119
|
+
|
|
120
|
+
#### `stimulate()` Method
|
|
121
|
+
```typescript
|
|
122
|
+
await cns.stimulate(axonKey, payload, options?)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Parameters:**
|
|
126
|
+
- `axonKey`: Key from the afferent axon to trigger
|
|
127
|
+
- `payload`: Signal payload with type and data
|
|
128
|
+
- `options`: Optional configuration object
|
|
129
|
+
|
|
130
|
+
**Options:**
|
|
131
|
+
- `maxHops`: Maximum number of signal hops (default: 1000)
|
|
132
|
+
- `onTrace`: Callback for tracing signal flow
|
|
133
|
+
- `abortSignal`: AbortController signal for cancellation
|
|
134
|
+
|
|
135
|
+
## π Key Behavior
|
|
136
|
+
|
|
137
|
+
### Only Returned Signals Are Processed
|
|
138
|
+
**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
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const processor = neuron('processor', {
|
|
142
|
+
output: outputCollateral
|
|
143
|
+
})
|
|
144
|
+
.dendrite({
|
|
145
|
+
collateral: inputCollateral,
|
|
146
|
+
reaction: async (payload, axon) => {
|
|
147
|
+
// This signal is created but NOT processed
|
|
148
|
+
axon.output.createSignal({ message: 'Hello' });
|
|
149
|
+
|
|
150
|
+
// Only this returned signal is processed
|
|
151
|
+
return axon.output.createSignal({ message: 'World' });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Multiple Neurons on Same Collateral
|
|
157
|
+
Multiple neurons can listen to the same collateral:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const emailService = neuron('email-service', { emailSent })
|
|
161
|
+
.dendrite({
|
|
162
|
+
collateral: userCreated,
|
|
163
|
+
reaction: async (payload, axon) => {
|
|
164
|
+
return axon.emailSent.createSignal({ to: 'user@example.com' });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const notificationService = neuron('notification-service', { notificationSent })
|
|
169
|
+
.dendrite({
|
|
170
|
+
collateral: userCreated,
|
|
171
|
+
reaction: async (payload, axon) => {
|
|
172
|
+
return axon.notificationSent.createSignal({ message: 'User created' });
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Both neurons will process the userCreated signal
|
|
177
|
+
const cns = new CNS({ userCreated }, [emailService, notificationService]);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Conditional Logic
|
|
181
|
+
```typescript
|
|
182
|
+
const router = neuron('router', {
|
|
183
|
+
success: successCollateral,
|
|
184
|
+
error: errorCollateral
|
|
185
|
+
})
|
|
186
|
+
.dendrite({
|
|
187
|
+
collateral: requestCollateral,
|
|
188
|
+
reaction: async (payload, axon) => {
|
|
189
|
+
try {
|
|
190
|
+
const result = await processRequest(payload);
|
|
191
|
+
return axon.success.createSignal(result);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return axon.error.createSignal({ error: error.message });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## π§ͺ Testing
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm test
|
|
203
|
+
npm run test:types
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## π¦ Build
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npm run build
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## π Examples
|
|
213
|
+
|
|
214
|
+
Run the examples to see CNS in action:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npm run examples
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## π€ Contributing
|
|
221
|
+
|
|
222
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
223
|
+
|
|
224
|
+
## π License
|
|
225
|
+
|
|
226
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CNS: () => CNS,
|
|
24
|
+
CNSCollateral: () => CNSCollateral,
|
|
25
|
+
collateral: () => collateral,
|
|
26
|
+
neuron: () => neuron
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/CNS.ts
|
|
31
|
+
var asArray = (x) => Array.isArray(x) ? x : [x];
|
|
32
|
+
var CNS = class {
|
|
33
|
+
constructor(afferentLayerAsAxon, neurons) {
|
|
34
|
+
this.afferentLayerAsAxon = afferentLayerAsAxon;
|
|
35
|
+
this.neurons = neurons;
|
|
36
|
+
// index by collateral *type* => list of subscribers
|
|
37
|
+
this.subIndex = /* @__PURE__ */ new Map();
|
|
38
|
+
this.buildIndex();
|
|
39
|
+
}
|
|
40
|
+
buildIndex() {
|
|
41
|
+
this.subIndex.clear();
|
|
42
|
+
for (const neuron2 of this.neurons) {
|
|
43
|
+
for (const dendrite of neuron2.dendrites) {
|
|
44
|
+
const key = dendrite.collateral.id;
|
|
45
|
+
const arr = this.subIndex.get(key) ?? [];
|
|
46
|
+
arr.push({ neuron: neuron2, dendrite });
|
|
47
|
+
this.subIndex.set(
|
|
48
|
+
key,
|
|
49
|
+
arr
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async fanOut(incoming, allowType, seen, queue) {
|
|
55
|
+
const subscribers = this.subIndex.get(incoming.edgeId);
|
|
56
|
+
if (!subscribers || subscribers.length === 0) return;
|
|
57
|
+
for (const { neuron: neuron2, dendrite } of subscribers) {
|
|
58
|
+
const k = `${neuron2.id}::${incoming.edgeId}::${incoming.spikeId}`;
|
|
59
|
+
if (seen.has(k)) continue;
|
|
60
|
+
seen.add(k);
|
|
61
|
+
const out = await dendrite.reaction(incoming.payload, neuron2.axon);
|
|
62
|
+
if (out === void 0) continue;
|
|
63
|
+
for (const spike of asArray(out)) {
|
|
64
|
+
if (allowType && !allowType(spike.type))
|
|
65
|
+
continue;
|
|
66
|
+
queue.push({
|
|
67
|
+
edgeId: spike.type,
|
|
68
|
+
payload: spike.payload,
|
|
69
|
+
hops: incoming.hops + 1,
|
|
70
|
+
spikeId: incoming.spikeId
|
|
71
|
+
// keep the same spike id across the cascade
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async stimulate(axonKey, payload, opts) {
|
|
77
|
+
const afferent = this.afferentLayerAsAxon[axonKey];
|
|
78
|
+
const spikeId = opts?.spikeId || Math.random().toString(36).slice(2);
|
|
79
|
+
const maxHops = opts?.maxHops ?? 1e3;
|
|
80
|
+
const queue = [
|
|
81
|
+
{
|
|
82
|
+
edgeId: afferent.id,
|
|
83
|
+
payload,
|
|
84
|
+
hops: 0,
|
|
85
|
+
spikeId
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
const seen = /* @__PURE__ */ new Set();
|
|
89
|
+
while (queue.length) {
|
|
90
|
+
if (opts?.abortSignal?.aborted) break;
|
|
91
|
+
const item = queue.shift();
|
|
92
|
+
opts?.onTrace?.({
|
|
93
|
+
edgeId: item.edgeId,
|
|
94
|
+
hops: item.hops,
|
|
95
|
+
payload: item.payload
|
|
96
|
+
});
|
|
97
|
+
if (item.hops >= maxHops) continue;
|
|
98
|
+
await this.fanOut(
|
|
99
|
+
item,
|
|
100
|
+
opts?.allowType,
|
|
101
|
+
seen,
|
|
102
|
+
queue
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/CNSCollateral.ts
|
|
109
|
+
var CNSCollateral = class {
|
|
110
|
+
constructor(id) {
|
|
111
|
+
this.id = id;
|
|
112
|
+
}
|
|
113
|
+
createSignal(...args) {
|
|
114
|
+
const payload = args[0];
|
|
115
|
+
return { type: this.id, payload };
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/factory/index.ts
|
|
120
|
+
var collateral = (id) => new CNSCollateral(id);
|
|
121
|
+
var neuron = (id, axon) => {
|
|
122
|
+
const dendrites = [];
|
|
123
|
+
const api = {
|
|
124
|
+
axon,
|
|
125
|
+
id,
|
|
126
|
+
dendrites,
|
|
127
|
+
dendrite(s) {
|
|
128
|
+
dendrites.push(s);
|
|
129
|
+
return api;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
return api;
|
|
133
|
+
};
|
|
134
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
135
|
+
0 && (module.exports = {
|
|
136
|
+
CNS,
|
|
137
|
+
CNSCollateral,
|
|
138
|
+
collateral,
|
|
139
|
+
neuron
|
|
140
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
type TCNSSignal<TType extends string, TPayload> = {
|
|
2
|
+
type: TType;
|
|
3
|
+
payload?: TPayload;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
declare class CNSCollateral<TId extends string, TPayload> {
|
|
7
|
+
readonly id: TId;
|
|
8
|
+
constructor(id: TId);
|
|
9
|
+
createSignal(...args: [TPayload] extends [undefined] ? [] : [payload: TPayload]): TCNSSignal<TId, TPayload>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type TCNSAxon<TCId extends string, TInputPayload = unknown> = Record<TCId, CNSCollateral<TCId, TInputPayload>>;
|
|
13
|
+
|
|
14
|
+
type TCNDendrite<TSenderCollateralIdType extends string, TReceiverAxonCollateralPayload, TReceiverCollateralIdType extends string, TReceiverAxon extends TCNSAxon<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>> = {
|
|
15
|
+
collateral: CNSCollateral<TSenderCollateralIdType, TReceiverAxonCollateralPayload>;
|
|
16
|
+
reaction: (payload: TReceiverAxonCollateralPayload, axon: TReceiverAxon) => Promise<ReturnType<CNSCollateral<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>['createSignal']>> | ReturnType<CNSCollateral<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>['createSignal']> | Promise<void> | void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TCNSNeuron<TId extends string, TCollateralId extends string, TAxon extends TCNSAxon<TCollateralId, unknown>, TSenderCollateralId extends string, TDendrites extends TCNDendrite<TSenderCollateralId, unknown, TCollateralId, TAxon>[]> = {
|
|
20
|
+
id: TId;
|
|
21
|
+
axon: TAxon;
|
|
22
|
+
dendrites: TDendrites;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
declare class CNS<TAfferentCollateralId extends string, TAfferentAxon extends TCNSAxon<TAfferentCollateralId, unknown>, TNeuronId extends string, TCollateralId extends string, TAxon extends TCNSAxon<TCollateralId, any>> {
|
|
26
|
+
protected readonly afferentLayerAsAxon: TAfferentAxon;
|
|
27
|
+
protected readonly neurons: TCNSNeuron<TNeuronId, TCollateralId, TAxon, TCollateralId, TCNDendrite<TCollateralId, any, TCollateralId, TAxon>[]>[];
|
|
28
|
+
private subIndex;
|
|
29
|
+
constructor(afferentLayerAsAxon: TAfferentAxon, neurons: TCNSNeuron<TNeuronId, TCollateralId, TAxon, TCollateralId, TCNDendrite<TCollateralId, any, TCollateralId, TAxon>[]>[]);
|
|
30
|
+
private buildIndex;
|
|
31
|
+
private fanOut;
|
|
32
|
+
stimulate<K extends keyof TAfferentAxon, TACollateralId extends string>(axonKey: K, payload: ReturnType<TAfferentAxon[K]['createSignal']> & {
|
|
33
|
+
type: TACollateralId;
|
|
34
|
+
}, opts?: {
|
|
35
|
+
maxHops?: number;
|
|
36
|
+
allowType?: (t: TCollateralId | TAfferentCollateralId) => boolean;
|
|
37
|
+
onTrace?: (e: {
|
|
38
|
+
edgeId: TCollateralId | TAfferentCollateralId;
|
|
39
|
+
hops: number;
|
|
40
|
+
payload: unknown;
|
|
41
|
+
}) => void;
|
|
42
|
+
abortSignal?: AbortSignal;
|
|
43
|
+
spikeId?: string;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare const collateral: <TPayload = undefined, TId extends string = string>(id: TId) => CNSCollateral<TId, TPayload>;
|
|
48
|
+
type InterNeuronAPI<TIdType extends string, TSenderCollateralIdType extends string, TReceiverAxonCollateralPayload, TReceiverCollateralIdType extends string, TReceiverAxon extends TCNSAxon<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>> = {
|
|
49
|
+
id: TIdType;
|
|
50
|
+
axon: TReceiverAxon;
|
|
51
|
+
dendrites: TCNDendrite<TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>[];
|
|
52
|
+
dendrite: (s: TCNDendrite<TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>) => InterNeuronAPI<TIdType, TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>;
|
|
53
|
+
};
|
|
54
|
+
declare const neuron: <TIdType extends string, TSenderCollateralIdType extends string, TReceiverAxonCollateralPayload, TReceiverCollateralIdType extends string, TReceiverAxon extends TCNSAxon<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>>(id: TIdType, axon: TReceiverAxon) => InterNeuronAPI<TIdType, TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>;
|
|
55
|
+
|
|
56
|
+
export { CNS, CNSCollateral, type TCNDendrite, type TCNSAxon, type TCNSNeuron, type TCNSSignal, collateral, neuron };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
type TCNSSignal<TType extends string, TPayload> = {
|
|
2
|
+
type: TType;
|
|
3
|
+
payload?: TPayload;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
declare class CNSCollateral<TId extends string, TPayload> {
|
|
7
|
+
readonly id: TId;
|
|
8
|
+
constructor(id: TId);
|
|
9
|
+
createSignal(...args: [TPayload] extends [undefined] ? [] : [payload: TPayload]): TCNSSignal<TId, TPayload>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type TCNSAxon<TCId extends string, TInputPayload = unknown> = Record<TCId, CNSCollateral<TCId, TInputPayload>>;
|
|
13
|
+
|
|
14
|
+
type TCNDendrite<TSenderCollateralIdType extends string, TReceiverAxonCollateralPayload, TReceiverCollateralIdType extends string, TReceiverAxon extends TCNSAxon<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>> = {
|
|
15
|
+
collateral: CNSCollateral<TSenderCollateralIdType, TReceiverAxonCollateralPayload>;
|
|
16
|
+
reaction: (payload: TReceiverAxonCollateralPayload, axon: TReceiverAxon) => Promise<ReturnType<CNSCollateral<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>['createSignal']>> | ReturnType<CNSCollateral<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>['createSignal']> | Promise<void> | void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TCNSNeuron<TId extends string, TCollateralId extends string, TAxon extends TCNSAxon<TCollateralId, unknown>, TSenderCollateralId extends string, TDendrites extends TCNDendrite<TSenderCollateralId, unknown, TCollateralId, TAxon>[]> = {
|
|
20
|
+
id: TId;
|
|
21
|
+
axon: TAxon;
|
|
22
|
+
dendrites: TDendrites;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
declare class CNS<TAfferentCollateralId extends string, TAfferentAxon extends TCNSAxon<TAfferentCollateralId, unknown>, TNeuronId extends string, TCollateralId extends string, TAxon extends TCNSAxon<TCollateralId, any>> {
|
|
26
|
+
protected readonly afferentLayerAsAxon: TAfferentAxon;
|
|
27
|
+
protected readonly neurons: TCNSNeuron<TNeuronId, TCollateralId, TAxon, TCollateralId, TCNDendrite<TCollateralId, any, TCollateralId, TAxon>[]>[];
|
|
28
|
+
private subIndex;
|
|
29
|
+
constructor(afferentLayerAsAxon: TAfferentAxon, neurons: TCNSNeuron<TNeuronId, TCollateralId, TAxon, TCollateralId, TCNDendrite<TCollateralId, any, TCollateralId, TAxon>[]>[]);
|
|
30
|
+
private buildIndex;
|
|
31
|
+
private fanOut;
|
|
32
|
+
stimulate<K extends keyof TAfferentAxon, TACollateralId extends string>(axonKey: K, payload: ReturnType<TAfferentAxon[K]['createSignal']> & {
|
|
33
|
+
type: TACollateralId;
|
|
34
|
+
}, opts?: {
|
|
35
|
+
maxHops?: number;
|
|
36
|
+
allowType?: (t: TCollateralId | TAfferentCollateralId) => boolean;
|
|
37
|
+
onTrace?: (e: {
|
|
38
|
+
edgeId: TCollateralId | TAfferentCollateralId;
|
|
39
|
+
hops: number;
|
|
40
|
+
payload: unknown;
|
|
41
|
+
}) => void;
|
|
42
|
+
abortSignal?: AbortSignal;
|
|
43
|
+
spikeId?: string;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare const collateral: <TPayload = undefined, TId extends string = string>(id: TId) => CNSCollateral<TId, TPayload>;
|
|
48
|
+
type InterNeuronAPI<TIdType extends string, TSenderCollateralIdType extends string, TReceiverAxonCollateralPayload, TReceiverCollateralIdType extends string, TReceiverAxon extends TCNSAxon<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>> = {
|
|
49
|
+
id: TIdType;
|
|
50
|
+
axon: TReceiverAxon;
|
|
51
|
+
dendrites: TCNDendrite<TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>[];
|
|
52
|
+
dendrite: (s: TCNDendrite<TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>) => InterNeuronAPI<TIdType, TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>;
|
|
53
|
+
};
|
|
54
|
+
declare const neuron: <TIdType extends string, TSenderCollateralIdType extends string, TReceiverAxonCollateralPayload, TReceiverCollateralIdType extends string, TReceiverAxon extends TCNSAxon<TReceiverCollateralIdType, TReceiverAxonCollateralPayload>>(id: TIdType, axon: TReceiverAxon) => InterNeuronAPI<TIdType, TSenderCollateralIdType, TReceiverAxonCollateralPayload, TReceiverCollateralIdType, TReceiverAxon>;
|
|
55
|
+
|
|
56
|
+
export { CNS, CNSCollateral, type TCNDendrite, type TCNSAxon, type TCNSNeuron, type TCNSSignal, collateral, neuron };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/CNS.ts
|
|
2
|
+
var asArray = (x) => Array.isArray(x) ? x : [x];
|
|
3
|
+
var CNS = class {
|
|
4
|
+
constructor(afferentLayerAsAxon, neurons) {
|
|
5
|
+
this.afferentLayerAsAxon = afferentLayerAsAxon;
|
|
6
|
+
this.neurons = neurons;
|
|
7
|
+
// index by collateral *type* => list of subscribers
|
|
8
|
+
this.subIndex = /* @__PURE__ */ new Map();
|
|
9
|
+
this.buildIndex();
|
|
10
|
+
}
|
|
11
|
+
buildIndex() {
|
|
12
|
+
this.subIndex.clear();
|
|
13
|
+
for (const neuron2 of this.neurons) {
|
|
14
|
+
for (const dendrite of neuron2.dendrites) {
|
|
15
|
+
const key = dendrite.collateral.id;
|
|
16
|
+
const arr = this.subIndex.get(key) ?? [];
|
|
17
|
+
arr.push({ neuron: neuron2, dendrite });
|
|
18
|
+
this.subIndex.set(
|
|
19
|
+
key,
|
|
20
|
+
arr
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async fanOut(incoming, allowType, seen, queue) {
|
|
26
|
+
const subscribers = this.subIndex.get(incoming.edgeId);
|
|
27
|
+
if (!subscribers || subscribers.length === 0) return;
|
|
28
|
+
for (const { neuron: neuron2, dendrite } of subscribers) {
|
|
29
|
+
const k = `${neuron2.id}::${incoming.edgeId}::${incoming.spikeId}`;
|
|
30
|
+
if (seen.has(k)) continue;
|
|
31
|
+
seen.add(k);
|
|
32
|
+
const out = await dendrite.reaction(incoming.payload, neuron2.axon);
|
|
33
|
+
if (out === void 0) continue;
|
|
34
|
+
for (const spike of asArray(out)) {
|
|
35
|
+
if (allowType && !allowType(spike.type))
|
|
36
|
+
continue;
|
|
37
|
+
queue.push({
|
|
38
|
+
edgeId: spike.type,
|
|
39
|
+
payload: spike.payload,
|
|
40
|
+
hops: incoming.hops + 1,
|
|
41
|
+
spikeId: incoming.spikeId
|
|
42
|
+
// keep the same spike id across the cascade
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async stimulate(axonKey, payload, opts) {
|
|
48
|
+
const afferent = this.afferentLayerAsAxon[axonKey];
|
|
49
|
+
const spikeId = opts?.spikeId || Math.random().toString(36).slice(2);
|
|
50
|
+
const maxHops = opts?.maxHops ?? 1e3;
|
|
51
|
+
const queue = [
|
|
52
|
+
{
|
|
53
|
+
edgeId: afferent.id,
|
|
54
|
+
payload,
|
|
55
|
+
hops: 0,
|
|
56
|
+
spikeId
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
const seen = /* @__PURE__ */ new Set();
|
|
60
|
+
while (queue.length) {
|
|
61
|
+
if (opts?.abortSignal?.aborted) break;
|
|
62
|
+
const item = queue.shift();
|
|
63
|
+
opts?.onTrace?.({
|
|
64
|
+
edgeId: item.edgeId,
|
|
65
|
+
hops: item.hops,
|
|
66
|
+
payload: item.payload
|
|
67
|
+
});
|
|
68
|
+
if (item.hops >= maxHops) continue;
|
|
69
|
+
await this.fanOut(
|
|
70
|
+
item,
|
|
71
|
+
opts?.allowType,
|
|
72
|
+
seen,
|
|
73
|
+
queue
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/CNSCollateral.ts
|
|
80
|
+
var CNSCollateral = class {
|
|
81
|
+
constructor(id) {
|
|
82
|
+
this.id = id;
|
|
83
|
+
}
|
|
84
|
+
createSignal(...args) {
|
|
85
|
+
const payload = args[0];
|
|
86
|
+
return { type: this.id, payload };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/factory/index.ts
|
|
91
|
+
var collateral = (id) => new CNSCollateral(id);
|
|
92
|
+
var neuron = (id, axon) => {
|
|
93
|
+
const dendrites = [];
|
|
94
|
+
const api = {
|
|
95
|
+
axon,
|
|
96
|
+
id,
|
|
97
|
+
dendrites,
|
|
98
|
+
dendrite(s) {
|
|
99
|
+
dendrites.push(s);
|
|
100
|
+
return api;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return api;
|
|
104
|
+
};
|
|
105
|
+
export {
|
|
106
|
+
CNS,
|
|
107
|
+
CNSCollateral,
|
|
108
|
+
collateral,
|
|
109
|
+
neuron
|
|
110
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cnstra/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Neural network-inspired event flow system for building reactive applications with dependency-injected neurons and synapses",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"neural-network",
|
|
7
|
+
"event-flow",
|
|
8
|
+
"reactive",
|
|
9
|
+
"dependency-injection",
|
|
10
|
+
"neuron",
|
|
11
|
+
"synapse",
|
|
12
|
+
"axon",
|
|
13
|
+
"dendrite",
|
|
14
|
+
"typescript",
|
|
15
|
+
"node",
|
|
16
|
+
"browser"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/abaikov/cnstra.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/abaikov/cnstra/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/abaikov/cnstra#readme",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js",
|
|
35
|
+
"require": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts --dts --format esm,cjs --tsconfig tsconfig.build.json",
|
|
40
|
+
"prepublishOnly": "npm run build",
|
|
41
|
+
"test": "jest",
|
|
42
|
+
"test:types": "tsd",
|
|
43
|
+
"examples": "tsx examples/run-examples.ts"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"author": "Andrei Baikov <abaikov23@gmail.com>",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/jest": "^29.5.12",
|
|
52
|
+
"jest": "^29.7.0",
|
|
53
|
+
"ts-jest": "^29.1.1",
|
|
54
|
+
"tsup": "^8.5.0",
|
|
55
|
+
"tsx": "^4.20.5",
|
|
56
|
+
"typescript": "^5.8.3"
|
|
57
|
+
}
|
|
58
|
+
}
|