@cnstra/core 2.2.0 → 2.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.
package/dist/index.cjs CHANGED
@@ -2,80 +2,155 @@
2
2
 
3
3
  // src/CNSStimulationContextStore.ts
4
4
  var CNSStimulationContextStore = class {
5
- constructor(ctx = /* @__PURE__ */ new Map()) {
5
+ constructor(ctx) {
6
6
  this.ctx = ctx;
7
7
  }
8
8
  get(key) {
9
- return this.ctx.get(key);
9
+ return this.ctx?.get(key);
10
10
  }
11
11
  set(key, value) {
12
- this.ctx.set(key, value);
12
+ (this.ctx ?? (this.ctx = /* @__PURE__ */ new Map())).set(key, value);
13
13
  }
14
14
  getAll() {
15
- return new Map(this.ctx);
15
+ return this.ctx ? new Map(this.ctx) : /* @__PURE__ */ new Map();
16
16
  }
17
17
  setAll(values) {
18
- this.ctx.clear();
19
- for (const [key, value] of values) this.ctx.set(key, value);
18
+ const ctx = this.ctx ?? (this.ctx = /* @__PURE__ */ new Map());
19
+ ctx.clear();
20
+ for (const [key, value] of values) ctx.set(key, value);
20
21
  }
21
22
  delete(key) {
22
- this.ctx.delete(key);
23
+ this.ctx?.delete(key);
23
24
  }
24
25
  };
25
26
 
26
- // src/CNSNeuronActivationPump.ts
27
- var CNSNeuronActivationPump = class {
28
- constructor(processor, concurrency, abortSignal, initialCapacity) {
29
- this.processor = processor;
30
- this.concurrency = concurrency;
27
+ // src/CNSStimulation.ts
28
+ var TASK_DENDRITE = /* @__PURE__ */ Symbol("cnstra.taskDendrite");
29
+ var CNSDendriteContext = class {
30
+ constructor(stimulation, neuron2, abortSignal, cns) {
31
+ this.stimulation = stimulation;
32
+ this.neuron = neuron2;
31
33
  this.abortSignal = abortSignal;
32
- this.items = [];
33
- this.head = 0;
34
- this.tail = 0;
35
- this.size = 0;
36
- this.capacity = 16;
34
+ this.cns = cns;
35
+ }
36
+ get() {
37
+ return this.stimulation.getContext().get(this.neuron);
38
+ }
39
+ set(value) {
40
+ this.stimulation.getContext().set(this.neuron, value);
41
+ }
42
+ delete() {
43
+ this.stimulation.getContext().delete(this.neuron);
44
+ }
45
+ };
46
+ var CNSStimulationResponseImpl = class {
47
+ constructor(stimulation, store, inputSignal, outputSignal, queueLength, hops, error) {
48
+ this.stimulation = stimulation;
49
+ this.store = store;
50
+ this.inputSignal = inputSignal;
51
+ this.outputSignal = outputSignal;
52
+ this.queueLength = queueLength;
53
+ this.hops = hops;
54
+ this.error = error;
55
+ }
56
+ get contextValue() {
57
+ return this.store ? this.store.getAll() : /* @__PURE__ */ new Map();
58
+ }
59
+ };
60
+ var CNSStimulation = class {
61
+ constructor(cns, instanceNeuronQueue, options, onResponse) {
62
+ this.cns = cns;
63
+ this.options = options;
64
+ this.qHead = 0;
65
+ this.qTail = 0;
66
+ this.qSize = 0;
67
+ this.qCapacity = 4;
37
68
  this.activeOperations = 0;
38
- this.activeTasks = /* @__PURE__ */ new Set();
39
- // NEW: prevent re-entrant pump; defer extra runs
69
+ // Re-entrancy guard for the pump loop.
40
70
  this.pumping = false;
41
71
  this.needsPump = false;
42
- if (initialCapacity && initialCapacity > 0) {
43
- this.capacity = initialCapacity;
72
+ this.scheduledCount = 0;
73
+ this.isCompleted = false;
74
+ this.completionRejected = false;
75
+ this.ctxStore = options?.ctx;
76
+ this.instanceNeuronQueue = instanceNeuronQueue;
77
+ this.onResponse = onResponse;
78
+ if (this.options?.maxNeuronHops) {
79
+ this.neuronVisitMap = /* @__PURE__ */ new Map();
80
+ }
81
+ if (this.autoCleanupContextsEnabled) {
82
+ this.activeSccCounts = /* @__PURE__ */ new Map();
44
83
  }
45
- this.items = new Array(this.capacity);
46
84
  }
85
+ pushFailedTask(failure) {
86
+ (this.failedTasks ?? (this.failedTasks = [])).push(failure);
87
+ }
88
+ finalizeResolve() {
89
+ this.completionRejected = false;
90
+ this.resolveCompleted?.();
91
+ }
92
+ finalizeReject(error) {
93
+ this.completionRejected = true;
94
+ this.completionError = error;
95
+ this.rejectCompleted?.(error);
96
+ }
97
+ /** Lazily-created neuron context store (see {@link ctxStore}). */
98
+ get ctx() {
99
+ return this.ctxStore ?? (this.ctxStore = new CNSStimulationContextStore());
100
+ }
101
+ // ----- Inlined activation queue / pump -----
47
102
  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];
103
+ const limit = this.options?.concurrency ?? Infinity;
104
+ return this.activeOperations < limit && !this.options?.abortSignal?.aborted;
105
+ }
106
+ qResize() {
107
+ const oldCapacity = this.qCapacity;
108
+ const oldItems = this.qItems;
109
+ this.qCapacity = oldCapacity * 2;
110
+ const newItems = new Array(this.qCapacity);
111
+ let oldIndex = this.qHead;
112
+ for (let i = 0; i < this.qSize; i++) {
113
+ newItems[i] = oldItems[oldIndex];
58
114
  oldIndex = (oldIndex + 1) % oldCapacity;
59
115
  }
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 neuronActivationTask = this.items[this.head];
67
- this.items[this.head] = void 0;
68
- this.head = (this.head + 1) % this.capacity;
69
- this.size--;
70
- return neuronActivationTask;
116
+ this.qItems = newItems;
117
+ this.qHead = 0;
118
+ this.qTail = this.qSize;
119
+ }
120
+ qDequeue() {
121
+ if (this.qSize === 0) return void 0;
122
+ const items = this.qItems;
123
+ const task = items[this.qHead];
124
+ items[this.qHead] = void 0;
125
+ this.qHead = (this.qHead + 1) % this.qCapacity;
126
+ this.qSize--;
127
+ return task;
128
+ }
129
+ qEnqueueItem(task) {
130
+ if (!this.qItems) {
131
+ this.qItems = new Array(this.qCapacity);
132
+ } else if (this.qSize === this.qCapacity) {
133
+ this.qResize();
134
+ }
135
+ this.qItems[this.qTail] = task;
136
+ this.qTail = (this.qTail + 1) % this.qCapacity;
137
+ this.qSize++;
138
+ }
139
+ /** Enqueue a task and drive the pump unless one is already running. */
140
+ qEnqueue(task) {
141
+ this.qEnqueueItem(task);
142
+ if (!this.pumping) this.pump();
143
+ else this.needsPump = true;
71
144
  }
72
- enqueueItem(neuronActivationTask) {
73
- if (this.size === this.capacity) {
74
- this.resize();
145
+ qGetQueuedTasks() {
146
+ const result = [];
147
+ if (!this.qItems) return result;
148
+ let index = this.qHead;
149
+ for (let i = 0; i < this.qSize; i++) {
150
+ result.push(this.qItems[index]);
151
+ index = (index + 1) % this.qCapacity;
75
152
  }
76
- this.items[this.tail] = neuronActivationTask;
77
- this.tail = (this.tail + 1) % this.capacity;
78
- this.size++;
153
+ return result;
79
154
  }
80
155
  pump() {
81
156
  if (this.pumping) {
@@ -83,26 +158,26 @@ var CNSNeuronActivationPump = class {
83
158
  return;
84
159
  }
85
160
  this.pumping = true;
86
- while (this.canStartOperation && this.size > 0) {
87
- const neuronActivationTask = this.dequeue();
161
+ while (this.canStartOperation && this.qSize > 0) {
162
+ const task = this.qDequeue();
88
163
  this.activeOperations++;
89
- this.activeTasks.add(neuronActivationTask);
90
- const ret = this.processor(neuronActivationTask);
164
+ const ret = this.executeActivationTask(task);
91
165
  if (ret && typeof ret.then === "function") {
166
+ (this.activeTasks ?? (this.activeTasks = /* @__PURE__ */ new Set())).add(task);
92
167
  ret.then(
93
168
  (cb) => {
94
169
  this.activeOperations--;
95
- this.activeTasks.delete(neuronActivationTask);
170
+ this.activeTasks.delete(task);
96
171
  if (typeof cb === "function") cb();
97
- if (this.size > 0 && this.canStartOperation) {
172
+ if (this.qSize > 0 && this.canStartOperation) {
98
173
  if (this.pumping) this.needsPump = true;
99
174
  else this.pump();
100
175
  }
101
176
  },
102
177
  (err) => {
103
178
  this.activeOperations--;
104
- this.activeTasks.delete(neuronActivationTask);
105
- if (this.size > 0 && this.canStartOperation) {
179
+ this.activeTasks.delete(task);
180
+ if (this.qSize > 0 && this.canStartOperation) {
106
181
  if (this.pumping) this.needsPump = true;
107
182
  else this.pump();
108
183
  }
@@ -112,7 +187,6 @@ var CNSNeuronActivationPump = class {
112
187
  continue;
113
188
  }
114
189
  this.activeOperations--;
115
- this.activeTasks.delete(neuronActivationTask);
116
190
  if (typeof ret === "function") ret();
117
191
  }
118
192
  this.pumping = false;
@@ -121,73 +195,20 @@ var CNSNeuronActivationPump = class {
121
195
  this.pump();
122
196
  }
123
197
  }
124
- enqueue(neuronActivationTask) {
125
- this.enqueueItem(neuronActivationTask);
126
- if (!this.pumping) this.pump();
127
- else this.needsPump = true;
128
- }
129
- get length() {
130
- return this.size;
131
- }
132
- getActiveOperationsCount() {
133
- return this.activeOperations;
134
- }
135
- getQueuedTasks() {
136
- const result = [];
137
- let index = this.head;
138
- for (let i = 0; i < this.size; i++) {
139
- result.push(this.items[index]);
140
- index = (index + 1) % this.capacity;
141
- }
142
- return result;
143
- }
144
- getActiveTasks() {
145
- return Array.from(this.activeTasks);
146
- }
147
- };
148
-
149
- // src/CNSStimulation.ts
150
- var CNSStimulation = class {
151
- constructor(cns, instanceNeuronQueue, options) {
152
- this.cns = cns;
153
- this.options = options;
154
- this.scheduledCount = 0;
155
- this.pendingTasks = /* @__PURE__ */ new Set();
156
- this.failedTasks = [];
157
- this.completed = new Promise((resolve, reject) => {
158
- this.resolveCompleted = resolve;
159
- this.rejectCompleted = reject;
160
- });
161
- this.isCompleted = false;
162
- /**
163
- * Track active SCCs: SCC index -> count of active neurons in that SCC
164
- */
165
- this.activeSccCounts = /* @__PURE__ */ new Map();
166
- this.ctx = options?.ctx ?? new CNSStimulationContextStore();
167
- this.queue = new CNSNeuronActivationPump(
168
- (neuronActivationTask) => this.executeActivationTask(neuronActivationTask),
169
- options?.concurrency,
170
- options?.abortSignal
171
- );
172
- this.instanceNeuronQueue = instanceNeuronQueue;
173
- if (this.options?.maxNeuronHops) {
174
- this.nueronVisitMap = /* @__PURE__ */ new Map();
175
- }
176
- }
177
198
  tryResolveCompleted() {
178
199
  if (this.isCompleted) return;
179
- const noActive = this.queue.getActiveOperationsCount() === 0;
180
- const noPending = this.queue.length + this.scheduledCount === 0;
200
+ const noActive = this.activeOperations === 0;
201
+ const noPending = this.qSize + this.scheduledCount === 0;
181
202
  const aborted = !!this.options?.abortSignal?.aborted;
182
203
  if (noPending && noActive || aborted && noActive && !noPending) {
183
204
  if (aborted) {
184
- const queuedTasks = this.queue.getQueuedTasks();
205
+ const queuedTasks = this.qGetQueuedTasks();
185
206
  for (const task of [...queuedTasks]) {
186
- const alreadyTracked = this.failedTasks.some(
207
+ const alreadyTracked = this.failedTasks?.some(
187
208
  (ft) => ft.task === task
188
209
  );
189
210
  if (!alreadyTracked) {
190
- this.failedTasks.push({
211
+ this.pushFailedTask({
191
212
  task,
192
213
  error: new Error(
193
214
  "Task aborted - not started due to abort signal"
@@ -197,37 +218,47 @@ var CNSStimulation = class {
197
218
  }
198
219
  }
199
220
  this.isCompleted = true;
200
- this.rejectCompleted(new Error("Stimulation aborted"));
221
+ this.finalizeReject(new Error("Stimulation aborted"));
201
222
  return;
202
223
  }
203
224
  this.isCompleted = true;
204
- if (this.failedTasks.length > 0 || this.onResponseError) {
225
+ const failedCount = this.failedTasks?.length ?? 0;
226
+ if (failedCount > 0 || this.onResponseError) {
205
227
  const error = this.onResponseError || new Error(
206
- `Stimulation completed with ${this.failedTasks.length} failed task(s)`
228
+ `Stimulation completed with ${failedCount} failed task(s)`
207
229
  );
208
- this.rejectCompleted(error);
230
+ this.finalizeReject(error);
209
231
  } else {
210
- this.resolveCompleted();
232
+ this.finalizeResolve();
211
233
  }
212
234
  }
213
235
  }
214
236
  waitUntilComplete() {
237
+ if (this.completed) return this.completed;
238
+ if (this.isCompleted) {
239
+ this.completed = this.completionRejected ? Promise.reject(this.completionError) : Promise.resolve();
240
+ return this.completed;
241
+ }
242
+ this.completed = new Promise((resolve, reject) => {
243
+ this.resolveCompleted = resolve;
244
+ this.rejectCompleted = reject;
245
+ });
215
246
  return this.completed;
216
247
  }
217
248
  /**
218
249
  * Returns all current activation tasks: queued, active, and pending (scheduled but not yet enqueued)
219
250
  */
220
251
  getAllActivationTasks() {
221
- const queuedTasks = this.queue.getQueuedTasks();
222
- const activeTasks = this.queue.getActiveTasks();
223
- const pendingTasks = Array.from(this.pendingTasks);
252
+ const queuedTasks = this.qGetQueuedTasks();
253
+ const activeTasks = this.activeTasks ? Array.from(this.activeTasks) : [];
254
+ const pendingTasks = this.pendingTasks ? Array.from(this.pendingTasks) : [];
224
255
  return [...queuedTasks, ...activeTasks, ...pendingTasks];
225
256
  }
226
257
  /**
227
258
  * Returns all tasks that failed or were aborted
228
259
  */
229
260
  getFailedTasks() {
230
- return [...this.failedTasks];
261
+ return this.failedTasks ? [...this.failedTasks] : [];
231
262
  }
232
263
  /**
233
264
  * Get the context store for this stimulation
@@ -240,7 +271,7 @@ var CNSStimulation = class {
240
271
  */
241
272
  enqueueTasks(tasks) {
242
273
  for (const task of tasks) {
243
- this.queue.enqueue(task);
274
+ this.qEnqueue(task);
244
275
  }
245
276
  this.tryResolveCompleted();
246
277
  }
@@ -268,20 +299,24 @@ var CNSStimulation = class {
268
299
  * Increment the active count for the SCC containing this neuron
269
300
  */
270
301
  incrementSccCount(neuron2) {
302
+ const counts = this.activeSccCounts;
303
+ if (!counts) return;
271
304
  const sccIndex = this.cns.network.getSccIndexByNeuron(neuron2);
272
305
  if (sccIndex === void 0) return;
273
- const currentCount = this.activeSccCounts.get(sccIndex) || 0;
274
- this.activeSccCounts.set(sccIndex, currentCount + 1);
306
+ const currentCount = counts.get(sccIndex) || 0;
307
+ counts.set(sccIndex, currentCount + 1);
275
308
  }
276
309
  /**
277
310
  * Decrement the active count for the SCC containing this neuron
278
311
  */
279
312
  decrementSccCount(neuron2) {
313
+ const counts = this.activeSccCounts;
314
+ if (!counts) return;
280
315
  const sccIndex = this.cns.network.getSccIndexByNeuron(neuron2);
281
316
  if (sccIndex === void 0) return;
282
- const currentCount = this.activeSccCounts.get(sccIndex) || 0;
317
+ const currentCount = counts.get(sccIndex) || 0;
283
318
  const nextCount = Math.max(0, currentCount - 1);
284
- this.activeSccCounts.set(sccIndex, nextCount);
319
+ counts.set(sccIndex, nextCount);
285
320
  if (currentCount === 0) {
286
321
  console.warn(
287
322
  `[CNSStimulation] Attempting to decrement SCC count below 0 for neuron`
@@ -292,7 +327,8 @@ var CNSStimulation = class {
292
327
  * Check if a neuron can be guaranteed not to be visited again
293
328
  */
294
329
  canNeuronBeGuaranteedDone(neuron2) {
295
- if (!this.autoCleanupContextsEnabled) return false;
330
+ if (!this.autoCleanupContextsEnabled || !this.activeSccCounts)
331
+ return false;
296
332
  return this.cns.network.canNeuronBeGuaranteedDone(
297
333
  neuron2,
298
334
  this.activeSccCounts
@@ -300,19 +336,19 @@ var CNSStimulation = class {
300
336
  }
301
337
  cleanupCtxIfNeeded(neuron2) {
302
338
  if (this.autoCleanupContextsEnabled && this.canNeuronBeGuaranteedDone(neuron2)) {
303
- this.ctx.delete(neuron2);
339
+ this.ctxStore?.delete(neuron2);
304
340
  }
305
341
  }
306
342
  createSubscriberQueueItem(subscriber, inputSignal) {
307
343
  if (this.options?.maxNeuronHops) {
308
- if ((this.nueronVisitMap?.get(subscriber.neuron) ?? 0) >= this.options?.maxNeuronHops) {
344
+ if ((this.neuronVisitMap?.get(subscriber.neuron) ?? 0) >= this.options?.maxNeuronHops) {
309
345
  throw new Error(
310
346
  `Max neuron hops reached when trying to enqueue subscriber`
311
347
  );
312
348
  } else {
313
- this.nueronVisitMap?.set(
349
+ this.neuronVisitMap?.set(
314
350
  subscriber.neuron,
315
- (this.nueronVisitMap?.get(subscriber.neuron) ?? 0) + 1
351
+ (this.neuronVisitMap?.get(subscriber.neuron) ?? 0) + 1
316
352
  );
317
353
  }
318
354
  }
@@ -321,17 +357,24 @@ var CNSStimulation = class {
321
357
  dendriteCollateral: subscriber.dendrite.collateral,
322
358
  input: inputSignal
323
359
  };
360
+ neuronActivationTask[TASK_DENDRITE] = subscriber;
324
361
  return neuronActivationTask;
325
362
  }
326
363
  executeActivationTask(neuronActivationTask) {
327
- const subscribers = this.cns.network.getSubscribers(
328
- neuronActivationTask.dendriteCollateral
329
- );
330
- const subscriber = subscribers.find(
331
- (s) => s.neuron === neuronActivationTask.neuron
332
- );
364
+ let subscriber = neuronActivationTask[TASK_DENDRITE];
333
365
  if (!subscriber) {
334
- this.failedTasks.push({
366
+ const subscribers = this.cns.network.getSubscribers(
367
+ neuronActivationTask.dendriteCollateral
368
+ );
369
+ for (let i = 0; i < subscribers.length; i++) {
370
+ if (subscribers[i].neuron === neuronActivationTask.neuron) {
371
+ subscriber = subscribers[i];
372
+ break;
373
+ }
374
+ }
375
+ }
376
+ if (!subscriber) {
377
+ this.pushFailedTask({
335
378
  task: neuronActivationTask,
336
379
  error: new Error(
337
380
  `Subscriber not found for activation task`
@@ -344,40 +387,58 @@ var CNSStimulation = class {
344
387
  const neuron2 = subscriber.neuron;
345
388
  const dendrite = subscriber.dendrite;
346
389
  const inputSignal = neuronActivationTask.input;
347
- const starter = () => {
348
- this.markNeuronActive(neuron2);
349
- let response;
350
- try {
351
- response = dendrite.response(
352
- inputSignal?.payload,
353
- neuron2.axon,
354
- {
355
- get: () => this.ctx.get(neuron2),
356
- set: (value) => this.ctx.set(neuron2, value),
357
- delete: () => this.ctx.delete(neuron2),
358
- abortSignal: this.options?.abortSignal,
359
- cns: this.cns,
360
- stimulation: this
361
- }
362
- );
363
- } catch (error) {
364
- this.markNeuronInactive(neuron2);
365
- const isAborted = this.options?.abortSignal?.aborted ?? false;
366
- this.failedTasks.push({
367
- task: neuronActivationTask,
368
- error: error instanceof Error ? error : new Error(String(error)),
369
- aborted: isAborted
370
- });
371
- this.processResponseOrResponses(
390
+ const concurrency = neuron2.concurrency;
391
+ if (concurrency === void 0 || concurrency <= 0) {
392
+ return this.runStarter(
393
+ neuron2,
394
+ dendrite,
395
+ inputSignal,
396
+ neuronActivationTask
397
+ );
398
+ }
399
+ return this.instanceNeuronQueue.run(
400
+ neuron2,
401
+ () => this.runStarter(
402
+ neuron2,
403
+ dendrite,
404
+ inputSignal,
405
+ neuronActivationTask
406
+ )
407
+ );
408
+ }
409
+ runStarter(neuron2, dendrite, inputSignal, neuronActivationTask) {
410
+ this.markNeuronActive(neuron2);
411
+ let response;
412
+ try {
413
+ response = dendrite.response(
414
+ inputSignal?.payload,
415
+ neuron2.axon,
416
+ new CNSDendriteContext(
417
+ this,
372
418
  neuron2,
373
- inputSignal,
374
- void 0,
375
- error
376
- );
377
- return () => {
378
- };
379
- }
380
- const maxDuration = neuron2.maxDuration;
419
+ this.options?.abortSignal,
420
+ this.cns
421
+ )
422
+ );
423
+ } catch (error) {
424
+ this.markNeuronInactive(neuron2);
425
+ const isAborted = this.options?.abortSignal?.aborted ?? false;
426
+ this.pushFailedTask({
427
+ task: neuronActivationTask,
428
+ error: error instanceof Error ? error : new Error(String(error)),
429
+ aborted: isAborted
430
+ });
431
+ this.processResponseOrResponses(
432
+ neuron2,
433
+ inputSignal,
434
+ void 0,
435
+ error
436
+ );
437
+ return () => {
438
+ };
439
+ }
440
+ const maxDuration = neuron2.maxDuration;
441
+ if (response instanceof Promise || maxDuration) {
381
442
  const asPromise = response instanceof Promise ? response : Promise.resolve(response);
382
443
  const timedPromise = maxDuration && maxDuration > 0 ? new Promise((resolve, reject) => {
383
444
  const t = setTimeout(() => {
@@ -397,48 +458,45 @@ var CNSStimulation = class {
397
458
  }
398
459
  );
399
460
  }) : asPromise;
400
- if (response instanceof Promise || maxDuration) {
401
- return timedPromise.then(
402
- (signal) => {
403
- return () => {
404
- this.markNeuronInactive(neuron2);
405
- return this.processResponseOrResponses(
406
- neuron2,
407
- inputSignal,
408
- signal
409
- );
410
- };
411
- },
412
- (error) => {
413
- return () => {
414
- this.markNeuronInactive(neuron2);
415
- const isAborted = this.options?.abortSignal?.aborted ?? false;
416
- this.failedTasks.push({
417
- task: neuronActivationTask,
418
- error: error instanceof Error ? error : new Error(String(error)),
419
- aborted: isAborted
420
- });
421
- return this.processResponseOrResponses(
422
- neuron2,
423
- inputSignal,
424
- void 0,
425
- error
426
- );
427
- };
428
- }
461
+ return timedPromise.then(
462
+ (signal) => {
463
+ return () => {
464
+ this.markNeuronInactive(neuron2);
465
+ return this.processResponseOrResponses(
466
+ neuron2,
467
+ inputSignal,
468
+ signal
469
+ );
470
+ };
471
+ },
472
+ (error) => {
473
+ return () => {
474
+ this.markNeuronInactive(neuron2);
475
+ const isAborted = this.options?.abortSignal?.aborted ?? false;
476
+ this.pushFailedTask({
477
+ task: neuronActivationTask,
478
+ error: error instanceof Error ? error : new Error(String(error)),
479
+ aborted: isAborted
480
+ });
481
+ return this.processResponseOrResponses(
482
+ neuron2,
483
+ inputSignal,
484
+ void 0,
485
+ error
486
+ );
487
+ };
488
+ }
489
+ );
490
+ } else {
491
+ return () => {
492
+ this.markNeuronInactive(neuron2);
493
+ return this.processResponseOrResponses(
494
+ neuron2,
495
+ inputSignal,
496
+ response
429
497
  );
430
- } else {
431
- return () => {
432
- this.markNeuronInactive(neuron2);
433
- return this.processResponseOrResponses(
434
- neuron2,
435
- inputSignal,
436
- response
437
- );
438
- };
439
- }
440
- };
441
- return this.instanceNeuronQueue.run(neuron2, starter);
498
+ };
499
+ }
442
500
  }
443
501
  processResponseOrResponses(emitter, inputSignal, outputSignalOrSignals, error) {
444
502
  if (Array.isArray(outputSignalOrSignals)) {
@@ -456,36 +514,62 @@ var CNSStimulation = class {
456
514
  processResponse(emitter, inputSignal, outputSignal, error) {
457
515
  const collateral2 = outputSignal?.collateral;
458
516
  const subscribers = collateral2 ? this.cns.network.getSubscribers(collateral2) : [];
517
+ const onResponse = this.onResponse;
518
+ if (!onResponse) {
519
+ if (collateral2 && !error) {
520
+ if (emitter) this.cleanupCtxIfNeeded(emitter);
521
+ const n = subscribers.length;
522
+ if (n > 0) {
523
+ const tasks = new Array(n);
524
+ for (let i = 0; i < n; i++) {
525
+ tasks[i] = this.createSubscriberQueueItem(
526
+ subscribers[i],
527
+ outputSignal
528
+ );
529
+ }
530
+ this.scheduledCount += n;
531
+ for (let i = 0; i < n; i++) {
532
+ this.scheduledCount--;
533
+ this.qEnqueue(tasks[i]);
534
+ }
535
+ }
536
+ }
537
+ this.tryResolveCompleted();
538
+ return;
539
+ }
459
540
  const subscriberActivationTasks = [];
460
541
  if (collateral2 && !error) {
461
542
  if (emitter) this.cleanupCtxIfNeeded(emitter);
543
+ const pending = this.pendingTasks ?? (this.pendingTasks = /* @__PURE__ */ new Set());
462
544
  for (let i = 0; i < subscribers.length; i++) {
463
545
  const task = this.createSubscriberQueueItem(
464
546
  subscribers[i],
465
547
  outputSignal
466
548
  );
467
549
  subscriberActivationTasks.push(task);
468
- this.pendingTasks.add(task);
550
+ pending.add(task);
469
551
  }
470
552
  }
471
553
  this.scheduledCount += subscriberActivationTasks.length;
472
554
  let maybePromise;
473
555
  try {
474
- maybePromise = this.options?.onResponse?.({
475
- inputSignal,
476
- outputSignal,
477
- contextValue: this.ctx.getAll(),
478
- queueLength: this.queue.length + this.scheduledCount,
479
- stimulation: this,
480
- hops: this.options?.maxNeuronHops && emitter ? this.nueronVisitMap?.get(emitter) ?? 0 : void 0,
481
- error
482
- });
556
+ maybePromise = onResponse(
557
+ new CNSStimulationResponseImpl(
558
+ this,
559
+ this.ctxStore,
560
+ inputSignal,
561
+ outputSignal,
562
+ this.qSize + this.scheduledCount,
563
+ this.options?.maxNeuronHops && emitter ? this.neuronVisitMap?.get(emitter) ?? 0 : void 0,
564
+ error
565
+ )
566
+ );
483
567
  } catch (e) {
484
568
  this.onResponseError = e instanceof Error ? e : new Error(String(e));
485
569
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
486
570
  this.scheduledCount--;
487
- this.pendingTasks.delete(subscriberActivationTasks[i]);
488
- this.queue.enqueue(subscriberActivationTasks[i]);
571
+ this.pendingTasks?.delete(subscriberActivationTasks[i]);
572
+ this.qEnqueue(subscriberActivationTasks[i]);
489
573
  }
490
574
  this.tryResolveCompleted();
491
575
  return;
@@ -495,8 +579,8 @@ var CNSStimulation = class {
495
579
  () => {
496
580
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
497
581
  this.scheduledCount--;
498
- this.pendingTasks.delete(subscriberActivationTasks[i]);
499
- this.queue.enqueue(subscriberActivationTasks[i]);
582
+ this.pendingTasks?.delete(subscriberActivationTasks[i]);
583
+ this.qEnqueue(subscriberActivationTasks[i]);
500
584
  }
501
585
  this.tryResolveCompleted();
502
586
  },
@@ -504,8 +588,8 @@ var CNSStimulation = class {
504
588
  this.onResponseError = error2 instanceof Error ? error2 : new Error(String(error2));
505
589
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
506
590
  this.scheduledCount--;
507
- this.pendingTasks.delete(subscriberActivationTasks[i]);
508
- this.queue.enqueue(subscriberActivationTasks[i]);
591
+ this.pendingTasks?.delete(subscriberActivationTasks[i]);
592
+ this.qEnqueue(subscriberActivationTasks[i]);
509
593
  }
510
594
  this.tryResolveCompleted();
511
595
  }
@@ -514,8 +598,8 @@ var CNSStimulation = class {
514
598
  }
515
599
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
516
600
  this.scheduledCount--;
517
- this.pendingTasks.delete(subscriberActivationTasks[i]);
518
- this.queue.enqueue(subscriberActivationTasks[i]);
601
+ this.pendingTasks?.delete(subscriberActivationTasks[i]);
602
+ this.qEnqueue(subscriberActivationTasks[i]);
519
603
  }
520
604
  this.tryResolveCompleted();
521
605
  }
@@ -591,8 +675,12 @@ var CNSNetwork = class {
591
675
  /**
592
676
  * Strongly Connected Components of the neuron graph.
593
677
  * Each component is a set of neuron objects that can reach each other.
678
+ *
679
+ * Computed lazily on first access: the SCC machinery is only needed for
680
+ * `autoCleanupContexts` (off by default), so most networks never pay for it.
594
681
  */
595
- this.stronglyConnectedComponents = [];
682
+ this._stronglyConnectedComponents = [];
683
+ this.sccBuilt = false;
596
684
  /**
597
685
  * Quick lookup: neuron -> SCC index for fast reachability checks.
598
686
  */
@@ -610,21 +698,18 @@ var CNSNetwork = class {
610
698
  * Built once at construction time.
611
699
  */
612
700
  this.subIndex = /* @__PURE__ */ new Map();
613
- this.dendriteIndex = /* @__PURE__ */ new Map();
614
- this.collateralIndex = /* @__PURE__ */ new Set();
615
701
  this.parentNeuronByCollateral = /* @__PURE__ */ new Map();
616
- this.validateUniqueIdentifiers();
617
702
  this.buildIndexes();
618
- this.buildSCC();
619
703
  }
620
- /**
621
- * Validate uniqueness constraints:
622
- * - (legacy) collateral instances used to be single-owner; in object-identity routing
623
- * a collateral can be emitted by multiple neurons (fan-in), so we no longer
624
- * enforce single ownership here.
625
- * Throws aggregated error if violations are found.
626
- */
627
- validateUniqueIdentifiers() {
704
+ get stronglyConnectedComponents() {
705
+ this.ensureSCC();
706
+ return this._stronglyConnectedComponents;
707
+ }
708
+ /** Build the SCC analysis the first time anything needs it. */
709
+ ensureSCC() {
710
+ if (this.sccBuilt) return;
711
+ this.sccBuilt = true;
712
+ this.buildSCC();
628
713
  }
629
714
  /**
630
715
  * Build the neuron graph where neurons are connected if one can stimulate the other.
@@ -699,7 +784,7 @@ var CNSNetwork = class {
699
784
  strongConnect(neuron2);
700
785
  }
701
786
  }
702
- this.stronglyConnectedComponents = components;
787
+ this._stronglyConnectedComponents = components;
703
788
  for (let i = 0; i < components.length; i++) {
704
789
  for (const neuronId of Array.from(components[i])) {
705
790
  this.neuronToSCC.set(neuronId, i);
@@ -713,11 +798,11 @@ var CNSNetwork = class {
713
798
  */
714
799
  buildSCCDAG(neuronGraph) {
715
800
  this.sccDag.clear();
716
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
801
+ for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
717
802
  this.sccDag.set(i, /* @__PURE__ */ new Set());
718
803
  }
719
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
720
- const scc = this.stronglyConnectedComponents[i];
804
+ for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
805
+ const scc = this._stronglyConnectedComponents[i];
721
806
  for (const neuron2 of Array.from(scc)) {
722
807
  const neighbors = neuronGraph.get(neuron2) || /* @__PURE__ */ new Set();
723
808
  for (const neighbor of Array.from(neighbors)) {
@@ -735,25 +820,27 @@ var CNSNetwork = class {
735
820
  */
736
821
  buildSCCAncestors() {
737
822
  this.sccAncestors.clear();
738
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
823
+ for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
739
824
  this.sccAncestors.set(i, /* @__PURE__ */ new Set());
740
825
  }
826
+ const n = this._stronglyConnectedComponents.length;
827
+ const forward = Array.from({ length: n }, () => []);
741
828
  const inDegree = /* @__PURE__ */ new Map();
742
829
  const queue = [];
743
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
744
- const incomingEdges = this.sccDag.get(i) || /* @__PURE__ */ new Set();
745
- inDegree.set(i, incomingEdges.size);
746
- if (incomingEdges.size === 0) {
747
- queue.push(i);
830
+ for (let target = 0; target < n; target++) {
831
+ const sources = this.sccDag.get(target) || /* @__PURE__ */ new Set();
832
+ inDegree.set(target, sources.size);
833
+ if (sources.size === 0) queue.push(target);
834
+ for (const source of Array.from(sources)) {
835
+ forward[source].push(target);
748
836
  }
749
837
  }
750
838
  while (queue.length > 0) {
751
839
  const current = queue.shift();
752
- const outgoingEdges = this.getOutgoingEdges(current);
753
- for (const neighbor of Array.from(outgoingEdges)) {
840
+ const currentAncestors = this.sccAncestors.get(current);
841
+ for (const neighbor of forward[current]) {
754
842
  const neighborAncestors = this.sccAncestors.get(neighbor);
755
843
  neighborAncestors.add(current);
756
- const currentAncestors = this.sccAncestors.get(current);
757
844
  for (const ancestor of Array.from(currentAncestors)) {
758
845
  neighborAncestors.add(ancestor);
759
846
  }
@@ -765,32 +852,22 @@ var CNSNetwork = class {
765
852
  }
766
853
  }
767
854
  }
768
- /**
769
- * Get outgoing edges for a given SCC index
770
- */
771
- getOutgoingEdges(sccIndex) {
772
- const outgoing = /* @__PURE__ */ new Set();
773
- for (const [targetScc, incomingEdges] of Array.from(this.sccDag)) {
774
- if (incomingEdges.has(sccIndex)) {
775
- outgoing.add(targetScc);
776
- }
777
- }
778
- return outgoing;
779
- }
780
855
  /**
781
856
  * Check if a neuron can be reached again (including self-calling).
782
857
  * Returns true if the neuron is in a strongly connected component with more than one member,
783
858
  * or if it's in a single-member SCC that can reach itself.
784
859
  */
785
860
  getSCCSetByNeuron(neuron2) {
861
+ this.ensureSCC();
786
862
  const sccIndex = this.neuronToSCC.get(neuron2);
787
863
  if (sccIndex === void 0) return;
788
- return this.stronglyConnectedComponents[sccIndex];
864
+ return this._stronglyConnectedComponents[sccIndex];
789
865
  }
790
866
  /**
791
867
  * Get the SCC index for a given neuron
792
868
  */
793
869
  getSccIndexByNeuron(neuron2) {
870
+ this.ensureSCC();
794
871
  return this.neuronToSCC.get(neuron2);
795
872
  }
796
873
  /**
@@ -798,6 +875,7 @@ var CNSNetwork = class {
798
875
  * This is the core logic for safe context cleanup.
799
876
  */
800
877
  canNeuronBeGuaranteedDone(neuron2, activeSccCounts) {
878
+ this.ensureSCC();
801
879
  const sccIndex = this.neuronToSCC.get(neuron2);
802
880
  if (sccIndex === void 0) return true;
803
881
  if (activeSccCounts.get(sccIndex) && activeSccCounts.get(sccIndex) > 0) {
@@ -815,8 +893,6 @@ var CNSNetwork = class {
815
893
  buildIndexes() {
816
894
  this.subIndex.clear();
817
895
  this.parentNeuronByCollateral.clear();
818
- this.dendriteIndex.clear();
819
- this.collateralIndex.clear();
820
896
  for (const neuron2 of this.neurons) {
821
897
  for (const dendrite of neuron2.dendrites) {
822
898
  const key = dendrite.collateral;
@@ -826,25 +902,40 @@ var CNSNetwork = class {
826
902
  key,
827
903
  arr
828
904
  );
829
- this.dendriteIndex.set(key, dendrite);
830
905
  }
831
906
  Object.values(neuron2.axon).forEach((collateral2) => {
832
907
  this.parentNeuronByCollateral.set(
833
908
  collateral2,
834
909
  neuron2
835
910
  );
836
- this.collateralIndex.add(collateral2);
837
911
  });
838
912
  }
839
913
  }
840
914
  getParentNeuronByCollateral(collateral2) {
841
915
  return this.parentNeuronByCollateral.get(collateral2);
842
916
  }
917
+ // Computed on demand (rarely used): one dendrite per collateral, last wins —
918
+ // preserving the previous index semantics without building it every construct.
843
919
  getDendrites() {
844
- return Array.from(this.dendriteIndex.values());
920
+ const byCollateral = /* @__PURE__ */ new Map();
921
+ for (const neuron2 of this.neurons) {
922
+ for (const dendrite of neuron2.dendrites) {
923
+ byCollateral.set(
924
+ dendrite.collateral,
925
+ dendrite
926
+ );
927
+ }
928
+ }
929
+ return Array.from(byCollateral.values());
845
930
  }
846
931
  getCollaterals() {
847
- return Array.from(this.collateralIndex.values());
932
+ const collaterals = /* @__PURE__ */ new Set();
933
+ for (const neuron2 of this.neurons) {
934
+ for (const collateral2 of Object.values(neuron2.axon)) {
935
+ collaterals.add(collateral2);
936
+ }
937
+ }
938
+ return Array.from(collaterals);
848
939
  }
849
940
  getSubscribers(collateral2) {
850
941
  return this.subIndex.get(collateral2) ?? [];
@@ -879,8 +970,7 @@ var CNS = class {
879
970
  }
880
971
  wrapOnResponse(local) {
881
972
  if (this.globalResponseListeners.length === 0 && !local) {
882
- return () => {
883
- };
973
+ return void 0;
884
974
  }
885
975
  return (r) => {
886
976
  let anyPromise = false;
@@ -924,10 +1014,8 @@ var CNS = class {
924
1014
  const stimulation = new CNSStimulation(
925
1015
  this,
926
1016
  this.instanceNeuronQueue,
927
- {
928
- ...options,
929
- onResponse: wrapped
930
- }
1017
+ options,
1018
+ wrapped
931
1019
  );
932
1020
  stimulation.responseToSignal(signalOrSignals);
933
1021
  return stimulation;
@@ -940,10 +1028,8 @@ var CNS = class {
940
1028
  const stimulation = new CNSStimulation(
941
1029
  this,
942
1030
  this.instanceNeuronQueue,
943
- {
944
- ...options,
945
- onResponse: wrapped
946
- }
1031
+ options,
1032
+ wrapped
947
1033
  );
948
1034
  stimulation.enqueueTasks(tasks);
949
1035
  return stimulation;
@@ -1123,27 +1209,26 @@ var neuron = (axon) => {
1123
1209
  modality: singleModality,
1124
1210
  modalities: multipleModalities,
1125
1211
  afferentPaths: singleAfferentPaths,
1126
- default: singleDefaultHandler,
1127
- default: globalDefaultHandler,
1212
+ default: defaultHandler,
1128
1213
  output
1129
1214
  } = s;
1130
1215
  const modalityConfigs = multipleModalities ? multipleModalities : singleModality ? [
1131
1216
  {
1132
1217
  modality: singleModality,
1133
1218
  afferentPaths: singleAfferentPaths,
1134
- default: singleDefaultHandler
1219
+ default: defaultHandler
1135
1220
  }
1136
1221
  ] : [];
1137
1222
  const response = (payload, axon2, ctx) => {
1138
1223
  const stimulation = ctx.stimulation;
1139
1224
  const stimOptions = stimulation?.options;
1140
1225
  const runGlobalDefault = () => {
1141
- if (!globalDefaultHandler) {
1226
+ if (!defaultHandler) {
1142
1227
  throw new Error(
1143
1228
  `modalityDendrite: No handler found for modality and no default handler provided`
1144
1229
  );
1145
1230
  }
1146
- const res = globalDefaultHandler(payload, axon2, ctx);
1231
+ const res = defaultHandler(payload, axon2, ctx);
1147
1232
  if (res && typeof res.then === "function") {
1148
1233
  return res.then(
1149
1234
  (result) => output(result, axon2, ctx)
@@ -1162,7 +1247,7 @@ var neuron = (axon) => {
1162
1247
  }
1163
1248
  const stimAfferentPath = stimOptions.afferentPath;
1164
1249
  const handler = stimAfferentPath && matchingConfig.afferentPaths ? matchingConfig.afferentPaths.get(stimAfferentPath) : void 0;
1165
- const effectiveHandler = handler ?? matchingConfig.default ?? globalDefaultHandler;
1250
+ const effectiveHandler = handler ?? matchingConfig.default ?? defaultHandler;
1166
1251
  if (!effectiveHandler) {
1167
1252
  throw new Error(
1168
1253
  `modalityDendrite: No handler found for afferent path in modality and no default handler provided`