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