@cnstra/core 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +242 -557
  2. package/dist/CNS.d.ts +76 -0
  3. package/dist/CNS.d.ts.map +1 -0
  4. package/dist/CNSCollateral.d.ts +9 -0
  5. package/dist/CNSCollateral.d.ts.map +1 -0
  6. package/dist/CNSFunctionalQueue.d.ts +26 -0
  7. package/dist/CNSFunctionalQueue.d.ts.map +1 -0
  8. package/dist/CNSStimulation.d.ts +52 -0
  9. package/dist/CNSStimulation.d.ts.map +1 -0
  10. package/dist/CNSStimulationContextStore.d.ts +11 -0
  11. package/dist/CNSStimulationContextStore.d.ts.map +1 -0
  12. package/dist/factory/index.d.ts +16 -0
  13. package/dist/factory/index.d.ts.map +1 -0
  14. package/dist/index.cjs +490 -214
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +8 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +490 -214
  19. package/dist/index.js.map +1 -1
  20. package/dist/interfaces/ICNS.d.ts +14 -0
  21. package/dist/interfaces/ICNS.d.ts.map +1 -0
  22. package/dist/interfaces/ICNSCollateral.d.ts +6 -0
  23. package/dist/interfaces/ICNSCollateral.d.ts.map +1 -0
  24. package/dist/interfaces/ICNSStimulationContextStore.d.ts +8 -0
  25. package/dist/interfaces/ICNSStimulationContextStore.d.ts.map +1 -0
  26. package/dist/types/TCNSAxon.d.ts +3 -0
  27. package/dist/types/TCNSAxon.d.ts.map +1 -0
  28. package/dist/types/TCNSDendrite.d.ts +9 -0
  29. package/dist/types/TCNSDendrite.d.ts.map +1 -0
  30. package/dist/types/TCNSLocalContextValueStore.d.ts +5 -0
  31. package/dist/types/TCNSLocalContextValueStore.d.ts.map +1 -0
  32. package/dist/types/TCNSNeuron.d.ts +8 -0
  33. package/dist/types/TCNSNeuron.d.ts.map +1 -0
  34. package/dist/types/TCNSNeuronResponseReturn.d.ts +3 -0
  35. package/dist/types/TCNSNeuronResponseReturn.d.ts.map +1 -0
  36. package/dist/types/TCNSOptions.d.ts +11 -0
  37. package/dist/types/TCNSOptions.d.ts.map +1 -0
  38. package/dist/types/TCNSSignal.d.ts +6 -0
  39. package/dist/types/TCNSSignal.d.ts.map +1 -0
  40. package/dist/types/TCNSStimulationOptions.d.ts +13 -0
  41. package/dist/types/TCNSStimulationOptions.d.ts.map +1 -0
  42. package/dist/types/TCNSStimulationQueueItem.d.ts +10 -0
  43. package/dist/types/TCNSStimulationQueueItem.d.ts.map +1 -0
  44. package/dist/types/TCNSStimulationResponse.d.ts +11 -0
  45. package/dist/types/TCNSStimulationResponse.d.ts.map +1 -0
  46. package/dist/types/TCNSSubscriber.d.ts +7 -0
  47. package/dist/types/TCNSSubscriber.d.ts.map +1 -0
  48. package/package.json +4 -1
package/dist/index.cjs CHANGED
@@ -19,52 +19,337 @@ var CNSStimulationContextStore = class {
19
19
  this.ctx.set(key, value);
20
20
  }
21
21
  }
22
+ delete(key) {
23
+ this.ctx.delete(key);
24
+ }
22
25
  };
23
26
 
24
- // src/CNS.ts
25
- var asArray = (x) => Array.isArray(x) ? x : [x];
26
- var isPromise = (v) => !!v && typeof v.then === "function";
27
- var Deque = class {
28
- constructor() {
29
- this.a = [];
27
+ // src/CNSFunctionalQueue.ts
28
+ var CNSFunctionalQueue = class {
29
+ constructor(processor, concurrency, abortSignal, initialCapacity) {
30
+ this.processor = processor;
31
+ this.concurrency = concurrency;
32
+ this.abortSignal = abortSignal;
33
+ this.items = [];
30
34
  this.head = 0;
35
+ this.tail = 0;
36
+ this.size = 0;
37
+ this.capacity = 16;
38
+ this.activeOperations = 0;
39
+ // NEW: prevent re-entrant pump; defer extra runs
40
+ this.pumping = false;
41
+ this.needsPump = false;
42
+ if (initialCapacity && initialCapacity > 0) {
43
+ this.capacity = initialCapacity;
44
+ }
45
+ this.items = new Array(this.capacity);
31
46
  }
32
- push(x) {
33
- this.a.push(x);
47
+ get canStartOperation() {
48
+ const limit = this.concurrency ?? Infinity;
49
+ return this.activeOperations < limit && !this.abortSignal?.aborted;
50
+ }
51
+ resize() {
52
+ const oldCapacity = this.capacity;
53
+ this.capacity = oldCapacity * 2;
54
+ const newItems = new Array(this.capacity);
55
+ let oldIndex = this.head;
56
+ for (let i = 0; i < this.size; i++) {
57
+ newItems[i] = this.items[oldIndex];
58
+ oldIndex = (oldIndex + 1) % oldCapacity;
59
+ }
60
+ this.items = newItems;
61
+ this.head = 0;
62
+ this.tail = this.size;
63
+ }
64
+ dequeue() {
65
+ if (this.size === 0) return void 0;
66
+ const item = this.items[this.head];
67
+ this.items[this.head] = void 0;
68
+ this.head = (this.head + 1) % this.capacity;
69
+ this.size--;
70
+ return item;
71
+ }
72
+ enqueueItem(item) {
73
+ if (this.size === this.capacity) {
74
+ this.resize();
75
+ }
76
+ this.items[this.tail] = item;
77
+ this.tail = (this.tail + 1) % this.capacity;
78
+ this.size++;
34
79
  }
35
- shift() {
36
- if (this.head >= this.a.length) return void 0;
37
- const v = this.a[this.head++];
38
- if (this.head > 1024 && this.head * 2 > this.a.length) {
39
- this.a = this.a.slice(this.head);
40
- this.head = 0;
80
+ pump() {
81
+ if (this.pumping) {
82
+ this.needsPump = true;
83
+ return;
84
+ }
85
+ this.pumping = true;
86
+ while (this.canStartOperation && this.size > 0) {
87
+ const item = this.dequeue();
88
+ this.activeOperations++;
89
+ const ret = this.processor(item);
90
+ if (ret && typeof ret.then === "function") {
91
+ ret.then(
92
+ (cb) => {
93
+ this.activeOperations--;
94
+ if (typeof cb === "function") cb();
95
+ if (this.size > 0 && this.canStartOperation) {
96
+ if (this.pumping) this.needsPump = true;
97
+ else this.pump();
98
+ }
99
+ },
100
+ (err) => {
101
+ this.activeOperations--;
102
+ if (this.size > 0 && this.canStartOperation) {
103
+ if (this.pumping) this.needsPump = true;
104
+ else this.pump();
105
+ }
106
+ throw err;
107
+ }
108
+ );
109
+ break;
110
+ }
111
+ this.activeOperations--;
112
+ if (typeof ret === "function") ret();
113
+ }
114
+ this.pumping = false;
115
+ if (this.needsPump) {
116
+ this.needsPump = false;
117
+ this.pump();
41
118
  }
42
- return v;
119
+ }
120
+ enqueue(x) {
121
+ this.enqueueItem(x);
122
+ if (!this.pumping) this.pump();
123
+ else this.needsPump = true;
43
124
  }
44
125
  get length() {
45
- return this.a.length - this.head;
126
+ return this.size;
127
+ }
128
+ getActiveOperationsCount() {
129
+ return this.activeOperations;
130
+ }
131
+ };
132
+
133
+ // src/CNSStimulation.ts
134
+ var CNSStimulation = class {
135
+ constructor(cns, options) {
136
+ this.cns = cns;
137
+ this.options = options;
138
+ this.scheduledCount = 0;
139
+ /**
140
+ * Track active SCCs: SCC index -> count of active neurons in that SCC
141
+ */
142
+ this.activeSccCounts = /* @__PURE__ */ new Map();
143
+ this.ctx = options?.ctx ?? options?.createContextStore?.() ?? new CNSStimulationContextStore();
144
+ this.id = options?.stimulationId || Math.random().toString(36).slice(2);
145
+ this.queue = new CNSFunctionalQueue(
146
+ (item) => this.processQueueItem(item),
147
+ options?.concurrency,
148
+ options?.abortSignal
149
+ );
150
+ if (this.options?.maxNeuronHops) {
151
+ this.nueronVisitMap = /* @__PURE__ */ new Map();
152
+ }
153
+ }
154
+ get concurrencyEnabled() {
155
+ return this.options?.concurrency !== void 0 && this.options?.concurrency > 0;
156
+ }
157
+ get autoCleanupContextsEnabled() {
158
+ return this.cns?.options?.autoCleanupContexts ?? false;
159
+ }
160
+ /**
161
+ * Mark a neuron as active (being processed)
162
+ */
163
+ markNeuronActive(neuronId) {
164
+ if (!this.autoCleanupContextsEnabled) return;
165
+ this.incrementSccCount(neuronId);
166
+ }
167
+ /**
168
+ * Mark a neuron as inactive (finished processing)
169
+ */
170
+ markNeuronInactive(neuronId) {
171
+ if (!this.autoCleanupContextsEnabled) return;
172
+ this.decrementSccCount(neuronId);
173
+ }
174
+ /**
175
+ * Increment the active count for the SCC containing this neuron
176
+ */
177
+ incrementSccCount(neuronId) {
178
+ const sccIndex = this.cns.getSccIndexByNeuronId(neuronId);
179
+ if (sccIndex === void 0) return;
180
+ const currentCount = this.activeSccCounts.get(sccIndex) || 0;
181
+ this.activeSccCounts.set(sccIndex, currentCount + 1);
182
+ }
183
+ /**
184
+ * Decrement the active count for the SCC containing this neuron
185
+ */
186
+ decrementSccCount(neuronId) {
187
+ const sccIndex = this.cns.getSccIndexByNeuronId(neuronId);
188
+ if (sccIndex === void 0) return;
189
+ const currentCount = this.activeSccCounts.get(sccIndex) || 0;
190
+ const nextCount = Math.max(0, currentCount - 1);
191
+ this.activeSccCounts.set(sccIndex, nextCount);
192
+ if (currentCount === 0) {
193
+ console.warn(
194
+ `[CNSStimulation] Attempting to decrement SCC count below 0 for neuron ${neuronId}`
195
+ );
196
+ }
197
+ }
198
+ /**
199
+ * Check if a neuron can be guaranteed not to be visited again
200
+ */
201
+ canNeuronBeGuaranteedDone(neuronId) {
202
+ if (!this.autoCleanupContextsEnabled) return false;
203
+ return this.cns.canNeuronBeGuaranteedDone(
204
+ neuronId,
205
+ this.activeSccCounts
206
+ );
207
+ }
208
+ cleanupCtxIfNeeded(neuron2) {
209
+ if (this.autoCleanupContextsEnabled && this.canNeuronBeGuaranteedDone(neuron2.id)) {
210
+ this.ctx.delete(neuron2.id);
211
+ }
212
+ }
213
+ createSubscriberQueueItem(subscriber, inputSignal) {
214
+ if (this.options?.maxNeuronHops) {
215
+ if (this.nueronVisitMap?.get(subscriber.neuron.id) ?? 0 >= this.options?.maxNeuronHops) {
216
+ throw new Error(
217
+ `Max neuron hops reached for neuron "${subscriber.neuron.id}" when trying to enqueue subscriber "${subscriber.dendrite.collateral.id}" in stimulation "${this.id}"`
218
+ );
219
+ } else {
220
+ this.nueronVisitMap?.set(
221
+ subscriber.neuron.id,
222
+ (this.nueronVisitMap?.get(subscriber.neuron.id) ?? 0) + 1
223
+ );
224
+ }
225
+ }
226
+ return {
227
+ neuronId: subscriber.neuron.id,
228
+ subscriber,
229
+ inputSignal
230
+ };
231
+ }
232
+ processQueueItem(item) {
233
+ const { subscriber, inputSignal } = item;
234
+ const { neuron: neuron2, dendrite } = subscriber;
235
+ this.markNeuronActive(neuron2.id);
236
+ const response = dendrite.response(
237
+ inputSignal?.payload,
238
+ neuron2.axon,
239
+ {
240
+ get: () => this.ctx.get(neuron2.id),
241
+ set: (value) => this.ctx.set(neuron2.id, value)
242
+ },
243
+ this.options?.abortSignal
244
+ );
245
+ if (response instanceof Promise) {
246
+ return response.then(
247
+ (signal) => {
248
+ return () => {
249
+ this.markNeuronInactive(neuron2.id);
250
+ return this.processResponse(
251
+ inputSignal,
252
+ signal
253
+ );
254
+ };
255
+ },
256
+ (error) => {
257
+ return () => {
258
+ this.markNeuronInactive(neuron2.id);
259
+ return this.processResponse(
260
+ inputSignal,
261
+ void 0,
262
+ error
263
+ );
264
+ };
265
+ }
266
+ );
267
+ } else {
268
+ return () => {
269
+ this.markNeuronInactive(neuron2.id);
270
+ return this.processResponse(
271
+ inputSignal,
272
+ response
273
+ );
274
+ };
275
+ }
276
+ }
277
+ processResponse(inputSignal, outputSignal, error) {
278
+ const collateral2 = outputSignal?.collateral;
279
+ const ownerNeuron = collateral2 ? this.cns.getParentNeuronByCollateralId(collateral2.id) : void 0;
280
+ const subscribers = collateral2 ? this.cns.getSubscribers(collateral2.id) : [];
281
+ const subscriberQueueItems = [];
282
+ if (collateral2 && !error) {
283
+ if (ownerNeuron) {
284
+ this.cleanupCtxIfNeeded(ownerNeuron);
285
+ }
286
+ for (let i = 0; i < subscribers.length; i++) {
287
+ subscriberQueueItems.push(
288
+ this.createSubscriberQueueItem(subscribers[i], outputSignal)
289
+ );
290
+ }
291
+ }
292
+ this.scheduledCount += subscriberQueueItems.length;
293
+ this.options?.onResponse?.({
294
+ inputSignal,
295
+ outputSignal,
296
+ ctx: this.ctx,
297
+ queueLength: this.queue.length + this.scheduledCount,
298
+ hops: this.options?.maxNeuronHops && ownerNeuron ? this.nueronVisitMap?.get(ownerNeuron.id) ?? 0 : void 0,
299
+ error
300
+ });
301
+ for (let i = 0; i < subscriberQueueItems.length; i++) {
302
+ this.scheduledCount--;
303
+ this.queue.enqueue(subscriberQueueItems[i]);
304
+ }
305
+ }
306
+ responseToSignal(signal) {
307
+ const ownerNeuron = this.cns.getParentNeuronByCollateralId(
308
+ signal.collateral.id
309
+ );
310
+ if (ownerNeuron) {
311
+ this.markNeuronActive(ownerNeuron.id);
312
+ }
313
+ this.processResponse(void 0, signal);
46
314
  }
47
- // Optional convenience if you need to inspect without removing:
48
- // peek(): T | undefined { return this.a[this.head]; }
49
315
  };
316
+
317
+ // src/CNS.ts
50
318
  var CNS = class {
51
- constructor(neurons) {
319
+ constructor(neurons, options) {
52
320
  this.neurons = neurons;
321
+ this.options = options;
53
322
  /**
54
323
  * Fast lookup: collateralId -> list of (neuron, dendrite) subscribers.
55
324
  * Built once at construction time.
56
325
  */
57
326
  this.subIndex = /* @__PURE__ */ new Map();
327
+ this.parentNeuronByCollateralId = /* @__PURE__ */ new Map();
58
328
  /**
59
- * Cached hashes for stable IDs to optimize dedup key generation.
329
+ * Strongly Connected Components of the neuron graph.
330
+ * Each component is a set of neuron IDs that can reach each other.
60
331
  */
61
- this.neuronHashes = /* @__PURE__ */ new Map();
62
- this.collateralHashes = /* @__PURE__ */ new Map();
63
- this.buildIndex();
64
- this.precomputeHashes();
332
+ this.stronglyConnectedComponents = [];
333
+ /**
334
+ * Quick lookup: neuron ID -> SCC index for fast reachability checks.
335
+ */
336
+ this.neuronToSCC = /* @__PURE__ */ new Map();
337
+ /**
338
+ * DAG of SCCs: SCC index -> set of SCC indices that can reach this SCC
339
+ */
340
+ this.sccDag = /* @__PURE__ */ new Map();
341
+ /**
342
+ * Precomputed ancestor sets for each SCC for fast reachability checks
343
+ */
344
+ this.sccAncestors = /* @__PURE__ */ new Map();
345
+ this.buildIndexes();
346
+ if (this.options?.autoCleanupContexts) {
347
+ this.buildSCC();
348
+ }
65
349
  }
66
- buildIndex() {
350
+ buildIndexes() {
67
351
  this.subIndex.clear();
352
+ this.parentNeuronByCollateralId.clear();
68
353
  for (const neuron2 of this.neurons) {
69
354
  for (const dendrite of neuron2.dendrites) {
70
355
  const key = dendrite.collateral.id;
@@ -75,218 +360,210 @@ var CNS = class {
75
360
  arr
76
361
  );
77
362
  }
363
+ Object.values(neuron2.axon).forEach((collateral2) => {
364
+ this.parentNeuronByCollateralId.set(
365
+ collateral2.id,
366
+ neuron2
367
+ );
368
+ });
78
369
  }
79
370
  }
80
371
  /**
81
- * Precompute hashes for stable neuron and collateral IDs to optimize
82
- * dedup key generation during stimulation.
372
+ * Build the neuron graph where neurons are connected if one can stimulate the other.
83
373
  */
84
- precomputeHashes() {
374
+ buildNeuronGraph() {
375
+ const graph = /* @__PURE__ */ new Map();
376
+ const neuronIds = this.neurons.map((n) => n.id);
377
+ for (const neuronId of neuronIds) {
378
+ graph.set(neuronId, /* @__PURE__ */ new Set());
379
+ }
85
380
  for (const neuron2 of this.neurons) {
86
- this.getHash(neuron2.id, this.neuronHashes);
87
- for (const dendrite of neuron2.dendrites) {
88
- this.getHash(dendrite.collateral.id, this.collateralHashes);
381
+ const reachableNeurons = /* @__PURE__ */ new Set();
382
+ for (const axonKey in neuron2.axon) {
383
+ const collateral2 = neuron2.axon[axonKey];
384
+ const collateralId = collateral2.id;
385
+ const subscribers = this.subIndex.get(collateralId);
386
+ if (subscribers) {
387
+ for (const { neuron: targetNeuron } of subscribers) {
388
+ reachableNeurons.add(targetNeuron.id);
389
+ }
390
+ }
89
391
  }
392
+ graph.set(neuron2.id, reachableNeurons);
90
393
  }
394
+ return graph;
91
395
  }
92
396
  /**
93
- * Simple but effective hash function for string keys.
397
+ * Build strongly connected components using Tarjan's algorithm.
94
398
  */
95
- simpleHash(str) {
96
- let hash = 0;
97
- for (let i = 0; i < str.length; i++) {
98
- const char = str.charCodeAt(i);
99
- hash = (hash << 5) - hash + char;
100
- hash = hash & hash;
399
+ buildSCC() {
400
+ const graph = this.buildNeuronGraph();
401
+ const neuronIds = this.neurons.map((n) => n.id);
402
+ const index = /* @__PURE__ */ new Map();
403
+ const lowlink = /* @__PURE__ */ new Map();
404
+ const onStack = /* @__PURE__ */ new Set();
405
+ const stack = [];
406
+ const components = [];
407
+ let currentIndex = 0;
408
+ const strongConnect = (neuronId) => {
409
+ index.set(neuronId, currentIndex);
410
+ lowlink.set(neuronId, currentIndex);
411
+ currentIndex++;
412
+ stack.push(neuronId);
413
+ onStack.add(neuronId);
414
+ const neighbors = graph.get(neuronId) || /* @__PURE__ */ new Set();
415
+ for (const neighbor of neighbors) {
416
+ if (!index.has(neighbor)) {
417
+ strongConnect(neighbor);
418
+ lowlink.set(
419
+ neuronId,
420
+ Math.min(lowlink.get(neuronId), lowlink.get(neighbor))
421
+ );
422
+ } else if (onStack.has(neighbor)) {
423
+ lowlink.set(
424
+ neuronId,
425
+ Math.min(lowlink.get(neuronId), index.get(neighbor))
426
+ );
427
+ }
428
+ }
429
+ if (lowlink.get(neuronId) === index.get(neuronId)) {
430
+ const component = /* @__PURE__ */ new Set();
431
+ let w;
432
+ do {
433
+ w = stack.pop();
434
+ onStack.delete(w);
435
+ component.add(w);
436
+ } while (w !== neuronId);
437
+ components.push(component);
438
+ }
439
+ };
440
+ for (const neuronId of neuronIds) {
441
+ if (!index.has(neuronId)) {
442
+ strongConnect(neuronId);
443
+ }
101
444
  }
102
- return hash;
445
+ this.stronglyConnectedComponents = components;
446
+ for (let i = 0; i < components.length; i++) {
447
+ for (const neuronId of components[i]) {
448
+ this.neuronToSCC.set(neuronId, i);
449
+ }
450
+ }
451
+ this.buildSCCDAG(graph);
452
+ this.buildSCCAncestors();
103
453
  }
104
454
  /**
105
- * Get or compute hash for a string, using cache for stable IDs.
455
+ * Build DAG between SCCs based on the original neuron graph
106
456
  */
107
- getHash(str, cache) {
108
- let hash = cache.get(str);
109
- if (hash === void 0) {
110
- hash = this.simpleHash(str);
111
- if (cache.size > 1e3) {
112
- const oldestKey = cache.keys().next().value;
113
- if (oldestKey !== void 0) {
114
- cache.delete(oldestKey);
457
+ buildSCCDAG(neuronGraph) {
458
+ this.sccDag.clear();
459
+ for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
460
+ this.sccDag.set(i, /* @__PURE__ */ new Set());
461
+ }
462
+ for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
463
+ const scc = this.stronglyConnectedComponents[i];
464
+ for (const neuronId of scc) {
465
+ const neighbors = neuronGraph.get(neuronId) || /* @__PURE__ */ new Set();
466
+ for (const neighborId of neighbors) {
467
+ const neighborSccIndex = this.neuronToSCC.get(neighborId);
468
+ if (neighborSccIndex !== void 0 && neighborSccIndex !== i) {
469
+ this.sccDag.get(neighborSccIndex).add(i);
470
+ }
115
471
  }
116
472
  }
117
- cache.set(str, hash);
118
- } else {
119
- cache.delete(str);
120
- cache.set(str, hash);
121
473
  }
122
- return hash;
123
474
  }
124
475
  /**
125
- * Create optimized dedup key using composite numeric hash.
126
- * This replaces string concatenation with faster numeric operations.
127
- * Includes hops to allow signals to pass through the same neuron at different levels.
476
+ * Precompute ancestor sets for each SCC for fast reachability checks
477
+ * Uses topological sort for efficient computation
128
478
  */
129
- createDedupKey(neuronId, collateralId, spikeId, hops) {
130
- const nHash = this.getHash(neuronId, this.neuronHashes);
131
- const cHash = this.getHash(collateralId, this.collateralHashes);
132
- const sHash = this.simpleHash(spikeId);
133
- const hHash = this.simpleHash(hops.toString());
134
- return `${nHash}:${cHash}:${sHash}:${hHash}`;
135
- }
136
- /**
137
- * Fire-and-forget fan-out for a single queue item.
138
- *
139
- * Behavior:
140
- * - Synchronous dendrite responses enqueue their outgoing signals immediately.
141
- * - Asynchronous responses:
142
- * • We DO NOT await them (no per-item barrier).
143
- * • We attach `.then` to enqueue outgoing signals when ready.
144
- * • We notify the scheduler when the first async for this item starts,
145
- * and when the last async for this item settles.
146
- * - No error handling: we do not try/catch. Rejections bubble via wireRejectionToRun().
147
- *
148
- * Returns:
149
- * boolean — whether this item launched any async work (>= 1 promises).
150
- */
151
- fanOutFireAndForget(incoming, allowType, seen, queue, ctx, enableDedup, onItemFirstAsyncStart, onItemAllAsyncSettled, onAnyAsyncSettled, onTrace) {
152
- const subscribers = this.subIndex.get(incoming.collateralId);
153
- if (!subscribers || subscribers.length === 0) return false;
154
- const nextHop = incoming.hops + 1;
155
- const spikeId = incoming.spikeId;
156
- const enqueueOut = (out) => {
157
- if (out == null) return;
158
- for (const spike of asArray(out)) {
159
- const t = spike.type;
160
- if (allowType && !allowType(t)) continue;
161
- queue.push({
162
- collateralId: t,
163
- payload: spike.payload,
164
- hops: nextHop,
165
- spikeId
166
- });
479
+ buildSCCAncestors() {
480
+ this.sccAncestors.clear();
481
+ for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
482
+ this.sccAncestors.set(i, /* @__PURE__ */ new Set());
483
+ }
484
+ const inDegree = /* @__PURE__ */ new Map();
485
+ const queue = [];
486
+ for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
487
+ const incomingEdges = this.sccDag.get(i) || /* @__PURE__ */ new Set();
488
+ inDegree.set(i, incomingEdges.size);
489
+ if (incomingEdges.size === 0) {
490
+ queue.push(i);
167
491
  }
168
- };
169
- let openAsync = 0;
170
- let itemMarkedAsync = false;
171
- for (const { neuron: neuron2, dendrite } of subscribers) {
172
- const dedupKey = enableDedup ? this.createDedupKey(
173
- neuron2.id,
174
- incoming.collateralId,
175
- incoming.spikeId,
176
- incoming.hops
177
- ) : void 0;
178
- if (dedupKey && seen.has(dedupKey)) continue;
179
- if (dedupKey) seen.add(dedupKey);
180
- const res = dendrite.response(incoming.payload, neuron2.axon, {
181
- get: () => ctx.get(neuron2.id),
182
- set: (value) => ctx.set(neuron2.id, value)
183
- });
184
- if (isPromise(res)) {
185
- if (!itemMarkedAsync) {
186
- itemMarkedAsync = true;
187
- onItemFirstAsyncStart();
492
+ }
493
+ while (queue.length > 0) {
494
+ const current = queue.shift();
495
+ const outgoingEdges = this.getOutgoingEdges(current);
496
+ for (const neighbor of outgoingEdges) {
497
+ const neighborAncestors = this.sccAncestors.get(neighbor);
498
+ neighborAncestors.add(current);
499
+ const currentAncestors = this.sccAncestors.get(current);
500
+ for (const ancestor of currentAncestors) {
501
+ neighborAncestors.add(ancestor);
502
+ }
503
+ const newInDegree = (inDegree.get(neighbor) || 0) - 1;
504
+ inDegree.set(neighbor, newInDegree);
505
+ if (newInDegree === 0) {
506
+ queue.push(neighbor);
188
507
  }
189
- openAsync++;
190
- res.then(
191
- (out) => {
192
- enqueueOut(out);
193
- openAsync--;
194
- onAnyAsyncSettled();
195
- if (openAsync === 0) {
196
- onItemAllAsyncSettled();
197
- }
198
- },
199
- (err) => {
200
- onTrace?.({
201
- collateralId: incoming.collateralId,
202
- hops: incoming.hops,
203
- payload: incoming.payload,
204
- queueLength: queue.length,
205
- error: err,
206
- contextStore: ctx
207
- // Pass the context store interface
208
- });
209
- openAsync--;
210
- onAnyAsyncSettled();
211
- if (openAsync === 0) {
212
- onItemAllAsyncSettled();
213
- }
214
- }
215
- );
216
- } else {
217
- enqueueOut(res);
218
508
  }
219
509
  }
220
- return itemMarkedAsync;
221
- }
222
- stimulate(collateral2, payload, opts) {
223
- const ctx = opts?.ctx ?? opts?.createContextStore?.() ?? new CNSStimulationContextStore();
224
- const spikeId = opts?.spikeId || Math.random().toString(36).slice(2);
225
- const maxHops = opts?.maxHops;
226
- const limit = (opts?.concurrency ?? 0) > 0 ? opts.concurrency : Infinity;
227
- const queue = new Deque();
228
- queue.push({
229
- collateralId: collateral2.id,
230
- payload,
231
- hops: 0,
232
- spikeId
233
- });
234
- const seen = /* @__PURE__ */ new Set();
235
- let activeOperations = 0;
236
- const startOperation = () => {
237
- activeOperations++;
238
- };
239
- const finishOperation = () => {
240
- activeOperations--;
241
- tryProcessMore();
242
- };
243
- const canStartOperation = () => {
244
- return activeOperations < limit && !opts?.abortSignal?.aborted;
245
- };
246
- const processItem = (item) => {
247
- opts?.onTrace?.({
248
- collateralId: item.collateralId,
249
- hops: item.hops,
250
- payload: item.payload,
251
- queueLength: queue.length,
252
- contextStore: ctx
253
- // Pass the context store
254
- });
255
- if (maxHops && item.hops >= maxHops) {
256
- finishOperation();
257
- return;
258
- }
259
- const onOperationComplete = () => {
260
- finishOperation();
261
- };
262
- const onAsyncStart = () => {
263
- };
264
- const launchedAsync = this.fanOutFireAndForget(
265
- item,
266
- opts?.allowType,
267
- seen,
268
- queue,
269
- ctx,
270
- maxHops !== void 0,
271
- // Enable dedup only if maxHops is set
272
- onAsyncStart,
273
- onOperationComplete,
274
- tryProcessMore,
275
- // Continue streaming on any completion
276
- opts?.onTrace
277
- );
278
- if (!launchedAsync) {
279
- finishOperation();
510
+ }
511
+ /**
512
+ * Get outgoing edges for a given SCC index
513
+ */
514
+ getOutgoingEdges(sccIndex) {
515
+ const outgoing = /* @__PURE__ */ new Set();
516
+ for (const [targetScc, incomingEdges] of this.sccDag) {
517
+ if (incomingEdges.has(sccIndex)) {
518
+ outgoing.add(targetScc);
280
519
  }
281
- };
282
- const tryProcessMore = () => {
283
- while (queue.length && canStartOperation()) {
284
- const item = queue.shift();
285
- startOperation();
286
- processItem(item);
520
+ }
521
+ return outgoing;
522
+ }
523
+ /**
524
+ * Check if a neuron can be reached again (including self-calling).
525
+ * Returns true if the neuron is in a strongly connected component with more than one member,
526
+ * or if it's in a single-member SCC that can reach itself.
527
+ */
528
+ getSCCSetByNeuronId(neuronId) {
529
+ const sccIndex = this.neuronToSCC.get(neuronId);
530
+ if (sccIndex === void 0) return;
531
+ return this.stronglyConnectedComponents[sccIndex];
532
+ }
533
+ /**
534
+ * Get the SCC index for a given neuron ID
535
+ */
536
+ getSccIndexByNeuronId(neuronId) {
537
+ return this.neuronToSCC.get(neuronId);
538
+ }
539
+ /**
540
+ * Check if a neuron can be guaranteed not to be visited again during the current propagation.
541
+ * This is the core logic for safe context cleanup.
542
+ */
543
+ canNeuronBeGuaranteedDone(neuronId, activeSccCounts) {
544
+ const sccIndex = this.neuronToSCC.get(neuronId);
545
+ if (sccIndex === void 0) return true;
546
+ if (activeSccCounts.get(sccIndex) && activeSccCounts.get(sccIndex) > 0) {
547
+ return false;
548
+ }
549
+ const ancestors = this.sccAncestors.get(sccIndex);
550
+ if (!ancestors) return true;
551
+ for (const ancestorSccIndex of ancestors) {
552
+ if (activeSccCounts.get(ancestorSccIndex) && activeSccCounts.get(ancestorSccIndex) > 0) {
553
+ return false;
287
554
  }
288
- };
289
- tryProcessMore();
555
+ }
556
+ return true;
557
+ }
558
+ getParentNeuronByCollateralId(collateralId) {
559
+ return this.parentNeuronByCollateralId.get(collateralId);
560
+ }
561
+ getSubscribers(collateralId) {
562
+ return this.subIndex.get(collateralId) ?? [];
563
+ }
564
+ stimulate(signal, options) {
565
+ const stimulation = new CNSStimulation(this, options);
566
+ stimulation.responseToSignal(signal);
290
567
  }
291
568
  };
292
569
 
@@ -295,9 +572,8 @@ var CNSCollateral = class {
295
572
  constructor(id) {
296
573
  this.id = id;
297
574
  }
298
- createSignal(...args) {
299
- const payload = args[0];
300
- return { type: this.id, payload };
575
+ createSignal(payload) {
576
+ return { collateral: this, payload };
301
577
  }
302
578
  };
303
579