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