@cnstra/core 2.6.0 → 3.2.1

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
@@ -1,41 +1,19 @@
1
1
  'use strict';
2
2
 
3
- // src/CNSStimulationContextStore.ts
4
- var CNSStimulationContextStore = class {
5
- constructor(ctx) {
6
- this.ctx = ctx;
7
- }
8
- get(key) {
9
- return this.ctx?.get(key);
10
- }
11
- set(key, value) {
12
- (this.ctx ?? (this.ctx = /* @__PURE__ */ new Map())).set(key, value);
13
- }
14
- getAll() {
15
- return this.ctx ? new Map(this.ctx) : /* @__PURE__ */ new Map();
16
- }
17
- setAll(values) {
18
- const ctx = this.ctx ?? (this.ctx = /* @__PURE__ */ new Map());
19
- ctx.clear();
20
- for (const [key, value] of values) ctx.set(key, value);
21
- }
22
- delete(key) {
23
- this.ctx?.delete(key);
24
- }
25
- };
26
-
27
3
  // src/CNSStimulation.ts
28
4
  var TASK_DENDRITE = /* @__PURE__ */ Symbol("cnstra.taskDendrite");
5
+ var TASK_ACTIVE_POSITION = /* @__PURE__ */ Symbol("cnstra.taskActivePosition");
6
+ var TASK_PENDING_POSITION = /* @__PURE__ */ Symbol("cnstra.taskPendingPosition");
29
7
  var CNSDendriteContext = class {
30
- constructor(stimulation, neuron2, abortSignal, cns) {
8
+ constructor(stimulation, abortSignal, cns, task) {
31
9
  this.stimulation = stimulation;
32
- this.neuron = neuron2;
33
10
  this.abortSignal = abortSignal;
34
11
  this.cns = cns;
12
+ this.task = task;
35
13
  }
36
- // Prototype getter, not a constructor field: the per-activation allocation is
37
- // unchanged (still 4 fields) and you only pay a single `cns.global` read on the
38
- // dendrites that actually reach for it. Value getter, so `{ global }` destructuring is safe.
14
+ // Prototype getter, not a constructor field: you only pay a single `cns.global`
15
+ // read on the dendrites that actually reach for it. Value getter, so
16
+ // `{ global }` destructuring is safe.
39
17
  get global() {
40
18
  return this.cns.global;
41
19
  }
@@ -46,20 +24,16 @@ var CNSDendriteContext = class {
46
24
  get stimulationContext() {
47
25
  return this.stimulation.options?.stimulationContext;
48
26
  }
49
- get() {
50
- return this.stimulation.getContext().get(this.neuron);
51
- }
52
- set(value) {
53
- this.stimulation.getContext().set(this.neuron, value);
54
- }
55
- delete() {
56
- this.stimulation.getContext().delete(this.neuron);
27
+ // This activation's own store - written by a response listener before the
28
+ // activation ran. Prototype getter like the two above: nothing is allocated
29
+ // for a handler that never reaches for it.
30
+ get activationContext() {
31
+ return this.task?.activationContext;
57
32
  }
58
33
  };
59
34
  var CNSStimulationResponseImpl = class {
60
- constructor(stimulation, store, inputSignal, outputSignal, queueLength, pendingActivations, activeActivations, hops, error, neuron2) {
35
+ constructor(stimulation, inputSignal, outputSignal, queueLength, pendingActivations, activeActivations, hops, error, neuron, startedAt, duration, scheduledActivationTasks = EMPTY_TASKS, activationContext = void 0) {
61
36
  this.stimulation = stimulation;
62
- this.store = store;
63
37
  this.inputSignal = inputSignal;
64
38
  this.outputSignal = outputSignal;
65
39
  this.queueLength = queueLength;
@@ -67,12 +41,15 @@ var CNSStimulationResponseImpl = class {
67
41
  this.activeActivations = activeActivations;
68
42
  this.hops = hops;
69
43
  this.error = error;
70
- this.neuron = neuron2;
71
- }
72
- get contextValue() {
73
- return this.store ? this.store.getAll() : /* @__PURE__ */ new Map();
44
+ this.neuron = neuron;
45
+ this.startedAt = startedAt;
46
+ this.duration = duration;
47
+ this.scheduledActivationTasks = scheduledActivationTasks;
48
+ this.activationContext = activationContext;
74
49
  }
75
50
  };
51
+ var EMPTY_TASKS = [];
52
+ var EMPTY_SUBSCRIBERS = [];
76
53
  var CNSStimulation = class {
77
54
  constructor(cns, instanceNeuronQueue, options, onResponse, onDrain) {
78
55
  this.cns = cns;
@@ -99,14 +76,16 @@ var CNSStimulation = class {
99
76
  this.isCompleted = false;
100
77
  this.completionRejected = false;
101
78
  this.onDrain = onDrain;
102
- this.ctxStore = options?.ctx;
103
79
  this.instanceNeuronQueue = instanceNeuronQueue;
104
80
  this.onResponse = onResponse;
81
+ this.concurrencyLimit = this.options?.concurrency ?? Infinity;
82
+ this.abortSignal = this.options?.abortSignal;
83
+ this.subscribersByCollateralIndex = cns.network.getSubscriberTable(
84
+ this.options?.modality,
85
+ this.options?.afferentPath
86
+ );
105
87
  if (this.options?.maxNeuronHops) {
106
- this.neuronVisitMap = /* @__PURE__ */ new Map();
107
- }
108
- if (this.autoCleanupContextsEnabled) {
109
- this.activeSccCounts = /* @__PURE__ */ new Map();
88
+ this.neuronVisitCounts = [];
110
89
  }
111
90
  }
112
91
  pushFailedTask(failure) {
@@ -121,14 +100,9 @@ var CNSStimulation = class {
121
100
  this.completionError = error;
122
101
  this.rejectCompleted?.(error);
123
102
  }
124
- /** Lazily-created neuron context store (see {@link ctxStore}). */
125
- get ctx() {
126
- return this.ctxStore ?? (this.ctxStore = new CNSStimulationContextStore());
127
- }
128
103
  // ----- Inlined activation queue / pump -----
129
104
  get canStartOperation() {
130
- const limit = this.options?.concurrency ?? Infinity;
131
- return this.activeOperations < limit && !this.options?.abortSignal?.aborted;
105
+ return this.activeOperations < this.concurrencyLimit && !this.abortSignal?.aborted;
132
106
  }
133
107
  qResize() {
134
108
  const oldCapacity = this.qCapacity;
@@ -185,48 +159,56 @@ var CNSStimulation = class {
185
159
  return;
186
160
  }
187
161
  this.pumping = true;
188
- while (this.canStartOperation && this.qSize > 0) {
189
- const task = this.qDequeue();
190
- this.activeOperations++;
191
- const ret = this.executeActivationTask(task);
192
- if (ret && typeof ret.then === "function") {
193
- (this.activeTasks ?? (this.activeTasks = /* @__PURE__ */ new Set())).add(task);
194
- ret.then(
195
- (cb) => {
196
- this.activeOperations--;
197
- this.activeTasks.delete(task);
198
- this.beginTurn();
199
- try {
200
- if (typeof cb === "function") cb();
201
- if (this.qSize > 0 && this.canStartOperation) {
202
- if (this.pumping) this.needsPump = true;
203
- else this.pump();
162
+ try {
163
+ while (this.canStartOperation && this.qSize > 0) {
164
+ const task = this.qDequeue();
165
+ this.activeOperations++;
166
+ const ret = this.executeActivationTask(task);
167
+ if (ret && typeof ret.then === "function") {
168
+ this.addPositionedTask(this.activeTasks ?? (this.activeTasks = []), task, TASK_ACTIVE_POSITION);
169
+ ret.then(
170
+ (cb) => {
171
+ this.activeOperations--;
172
+ this.removeActiveTask(task);
173
+ this.beginTurn();
174
+ try {
175
+ if (typeof cb === "function") cb();
176
+ if (this.qSize > 0 && this.canStartOperation) {
177
+ if (this.pumping) this.needsPump = true;
178
+ else this.pump();
179
+ }
180
+ } catch (error) {
181
+ this.endRunWithError(error);
182
+ } finally {
183
+ this.endTurn();
204
184
  }
205
- } finally {
206
- this.endTurn();
207
- }
208
- },
209
- (err) => {
210
- this.activeOperations--;
211
- this.activeTasks.delete(task);
212
- this.beginTurn();
213
- try {
214
- if (this.qSize > 0 && this.canStartOperation) {
215
- if (this.pumping) this.needsPump = true;
216
- else this.pump();
185
+ },
186
+ (err) => {
187
+ this.activeOperations--;
188
+ this.removeActiveTask(task);
189
+ this.beginTurn();
190
+ try {
191
+ if (this.qSize > 0 && this.canStartOperation) {
192
+ if (this.pumping) this.needsPump = true;
193
+ else this.pump();
194
+ }
195
+ } finally {
196
+ this.endTurn();
217
197
  }
218
- } finally {
219
- this.endTurn();
198
+ throw err;
220
199
  }
221
- throw err;
222
- }
223
- );
224
- continue;
200
+ );
201
+ continue;
202
+ }
203
+ this.activeOperations--;
204
+ if (typeof ret === "function") ret();
225
205
  }
226
- this.activeOperations--;
227
- if (typeof ret === "function") ret();
206
+ } catch (error) {
207
+ this.endRunWithError(error);
208
+ throw error;
209
+ } finally {
210
+ this.pumping = false;
228
211
  }
229
- this.pumping = false;
230
212
  if (this.needsPump) {
231
213
  this.needsPump = false;
232
214
  this.pump();
@@ -268,14 +250,30 @@ var CNSStimulation = class {
268
250
  }
269
251
  }
270
252
  /**
271
- * Activations that exist (or are reserved) but whose dendrite body has not
272
- * been invoked yet: sitting in the ring buffer, created and held in
253
+ * Activations that exist (or are reserved) but whose dendrite response has
254
+ * not been invoked yet: sitting in the ring buffer, created and held in
273
255
  * pendingTasks, or reserved by an array fan-out mid-dispatch. Subscribers
274
256
  * parked behind an async onResponse land here too - their body has not run.
275
257
  */
276
258
  get pendingActivations() {
277
259
  return this.qSize + this.scheduledCount + this.reservedActivations;
278
260
  }
261
+ /**
262
+ * End the run because the machinery itself threw. Dendrite bodies never get
263
+ * here - runStarter catches those and records them as failed tasks; what
264
+ * lands here is a throw from dispatch, today the hop-limit guard in
265
+ * createSubscriberQueueItem. Nothing settles the counters after such a
266
+ * throw, so completion has to be forced: without it the stimulation neither
267
+ * resolves nor rejects, and a worker holds a job that never finishes and
268
+ * never fails.
269
+ */
270
+ endRunWithError(error) {
271
+ if (this.isCompleted) return;
272
+ this.isCompleted = true;
273
+ this.finalizeReject(
274
+ error instanceof Error ? error : new Error(String(error))
275
+ );
276
+ }
279
277
  tryResolveCompleted() {
280
278
  if (this.isCompleted) return;
281
279
  const noActive = this.activeOperations === 0;
@@ -299,6 +297,7 @@ var CNSStimulation = class {
299
297
  }
300
298
  }
301
299
  this.isCompleted = true;
300
+ if (!noPending) this.emitAbortedResponse();
302
301
  this.finalizeReject(new Error("Stimulation aborted"));
303
302
  return;
304
303
  }
@@ -331,10 +330,34 @@ var CNSStimulation = class {
331
330
  */
332
331
  getAllActivationTasks() {
333
332
  const queuedTasks = this.qGetQueuedTasks();
334
- const activeTasks = this.activeTasks ? Array.from(this.activeTasks) : [];
335
- const pendingTasks = this.pendingTasks ? Array.from(this.pendingTasks) : [];
333
+ const activeTasks = this.activeTasks ?? EMPTY_TASKS;
334
+ const pendingTasks = this.pendingTasks ?? EMPTY_TASKS;
336
335
  return [...queuedTasks, ...activeTasks, ...pendingTasks];
337
336
  }
337
+ getSubscribers(collateral) {
338
+ return this.subscribersByCollateralIndex[collateral.index] ?? EMPTY_SUBSCRIBERS;
339
+ }
340
+ addPositionedTask(tasks, task, positionKey) {
341
+ task[positionKey] = tasks.length;
342
+ tasks.push(task);
343
+ }
344
+ removePositionedTask(tasks, task, positionKey) {
345
+ if (tasks === void 0) return;
346
+ const position = task[positionKey];
347
+ if (position === void 0 || tasks[position] !== task) return;
348
+ const lastTask = tasks.pop();
349
+ if (lastTask !== task) {
350
+ tasks[position] = lastTask;
351
+ lastTask[positionKey] = position;
352
+ }
353
+ task[positionKey] = void 0;
354
+ }
355
+ removeActiveTask(task) {
356
+ this.removePositionedTask(this.activeTasks, task, TASK_ACTIVE_POSITION);
357
+ }
358
+ removePendingTask(task) {
359
+ this.removePositionedTask(this.pendingTasks, task, TASK_PENDING_POSITION);
360
+ }
338
361
  /**
339
362
  * Returns all tasks that failed or were aborted
340
363
  */
@@ -347,18 +370,25 @@ var CNSStimulation = class {
347
370
  * failed/aborted tasks. This is the set to persist for resume: re-activating
348
371
  * it re-runs exactly the not-yet-done work. Safe to call at any point during
349
372
  * a run (for progress) or after it fails.
373
+ *
374
+ * Each task appears exactly once. The two sources overlap: aborting marks
375
+ * every still-queued task as failed without dequeuing it, so a task sits in
376
+ * the ring buffer AND in failedTasks. Concatenating them blindly handed the
377
+ * broker a checkpoint with the whole queued frontier listed twice, and the
378
+ * resumed run re-executed every one of those tasks a second time - duplicate
379
+ * charges, emails and POSTs on every graceful shutdown.
350
380
  */
351
381
  getOutstandingTasks() {
352
- return [
353
- ...this.getAllActivationTasks(),
354
- ...this.getFailedTasks().map((f) => f.task)
355
- ];
356
- }
357
- /**
358
- * Get the context store for this stimulation
359
- */
360
- getContext() {
361
- return this.ctx;
382
+ const seen = /* @__PURE__ */ new Set();
383
+ const outstanding = [];
384
+ const add = (task) => {
385
+ if (seen.has(task)) return;
386
+ seen.add(task);
387
+ outstanding.push(task);
388
+ };
389
+ for (const task of this.getAllActivationTasks()) add(task);
390
+ for (const failure of this.getFailedTasks()) add(failure.task);
391
+ return outstanding;
362
392
  }
363
393
  /**
364
394
  * Enqueue activation tasks directly into the stimulation queue
@@ -377,79 +407,17 @@ var CNSStimulation = class {
377
407
  get concurrencyEnabled() {
378
408
  return this.options?.concurrency !== void 0 && this.options?.concurrency > 0;
379
409
  }
380
- get autoCleanupContextsEnabled() {
381
- return this.cns?.options?.autoCleanupContexts ?? false;
382
- }
383
- /**
384
- * Mark a neuron as active (being processed)
385
- */
386
- markNeuronActive(neuron2) {
387
- if (!this.autoCleanupContextsEnabled) return;
388
- this.incrementSccCount(neuron2);
389
- }
390
- /**
391
- * Mark a neuron as inactive (finished processing)
392
- */
393
- markNeuronInactive(neuron2) {
394
- if (!this.autoCleanupContextsEnabled) return;
395
- this.decrementSccCount(neuron2);
396
- }
397
- /**
398
- * Increment the active count for the SCC containing this neuron
399
- */
400
- incrementSccCount(neuron2) {
401
- const counts = this.activeSccCounts;
402
- if (!counts) return;
403
- const sccIndex = this.cns.network.getSccIndexByNeuron(neuron2);
404
- if (sccIndex === void 0) return;
405
- const currentCount = counts.get(sccIndex) || 0;
406
- counts.set(sccIndex, currentCount + 1);
407
- }
408
- /**
409
- * Decrement the active count for the SCC containing this neuron
410
- */
411
- decrementSccCount(neuron2) {
412
- const counts = this.activeSccCounts;
413
- if (!counts) return;
414
- const sccIndex = this.cns.network.getSccIndexByNeuron(neuron2);
415
- if (sccIndex === void 0) return;
416
- const currentCount = counts.get(sccIndex) || 0;
417
- const nextCount = Math.max(0, currentCount - 1);
418
- counts.set(sccIndex, nextCount);
419
- if (currentCount === 0) {
420
- console.warn(
421
- `[CNSStimulation] Attempting to decrement SCC count below 0 for neuron`
422
- );
423
- }
424
- }
425
- /**
426
- * Check if a neuron can be guaranteed not to be visited again
427
- */
428
- canNeuronBeGuaranteedDone(neuron2) {
429
- if (!this.autoCleanupContextsEnabled || !this.activeSccCounts)
430
- return false;
431
- return this.cns.network.canNeuronBeGuaranteedDone(
432
- neuron2,
433
- this.activeSccCounts
434
- );
435
- }
436
- cleanupCtxIfNeeded(neuron2) {
437
- if (this.autoCleanupContextsEnabled && this.canNeuronBeGuaranteedDone(neuron2)) {
438
- this.ctxStore?.delete(neuron2);
439
- }
440
- }
441
410
  createSubscriberQueueItem(subscriber, inputSignal) {
442
- if (this.options?.maxNeuronHops) {
443
- if ((this.neuronVisitMap?.get(subscriber.neuron) ?? 0) >= this.options?.maxNeuronHops) {
411
+ const neuronVisitCounts = this.neuronVisitCounts;
412
+ if (neuronVisitCounts !== void 0) {
413
+ const neuronIndex = subscriber.neuron.index;
414
+ const neuronVisitCount = neuronVisitCounts[neuronIndex] ?? 0;
415
+ if (neuronVisitCount >= this.options.maxNeuronHops) {
444
416
  throw new Error(
445
417
  `Max neuron hops reached when trying to enqueue subscriber`
446
418
  );
447
- } else {
448
- this.neuronVisitMap?.set(
449
- subscriber.neuron,
450
- (this.neuronVisitMap?.get(subscriber.neuron) ?? 0) + 1
451
- );
452
419
  }
420
+ neuronVisitCounts[neuronIndex] = neuronVisitCount + 1;
453
421
  }
454
422
  const neuronActivationTask = {
455
423
  neuron: subscriber.neuron,
@@ -457,12 +425,14 @@ var CNSStimulation = class {
457
425
  input: inputSignal
458
426
  };
459
427
  neuronActivationTask[TASK_DENDRITE] = subscriber;
428
+ neuronActivationTask[TASK_ACTIVE_POSITION] = void 0;
429
+ neuronActivationTask[TASK_PENDING_POSITION] = void 0;
460
430
  return neuronActivationTask;
461
431
  }
462
432
  executeActivationTask(neuronActivationTask) {
463
433
  let subscriber = neuronActivationTask[TASK_DENDRITE];
464
434
  if (!subscriber) {
465
- const subscribers = this.cns.network.getSubscribers(
435
+ const subscribers = this.getSubscribers(
466
436
  neuronActivationTask.dendriteCollateral
467
437
  );
468
438
  for (let i = 0; i < subscribers.length; i++) {
@@ -484,48 +454,49 @@ var CNSStimulation = class {
484
454
  void 0,
485
455
  neuronActivationTask.input,
486
456
  void 0,
487
- error
457
+ error,
458
+ void 0,
459
+ neuronActivationTask.activationContext
488
460
  );
489
461
  };
490
462
  }
491
- const neuron2 = subscriber.neuron;
463
+ const neuron = subscriber.neuron;
492
464
  const dendrite = subscriber.dendrite;
493
465
  const inputSignal = neuronActivationTask.input;
494
- const concurrency = neuron2.concurrency;
466
+ const concurrency = neuron.concurrency;
495
467
  if (concurrency === void 0 || concurrency <= 0) {
496
468
  return this.runStarter(
497
- neuron2,
469
+ neuron,
498
470
  dendrite,
499
471
  inputSignal,
500
472
  neuronActivationTask
501
473
  );
502
474
  }
503
475
  return this.instanceNeuronQueue.run(
504
- neuron2,
476
+ neuron,
505
477
  () => this.runStarter(
506
- neuron2,
478
+ neuron,
507
479
  dendrite,
508
480
  inputSignal,
509
481
  neuronActivationTask
510
482
  )
511
483
  );
512
484
  }
513
- runStarter(neuron2, dendrite, inputSignal, neuronActivationTask) {
514
- this.markNeuronActive(neuron2);
485
+ runStarter(neuron, dendrite, inputSignal, neuronActivationTask) {
515
486
  let response;
487
+ const startedAt = this.onResponse === void 0 ? void 0 : Date.now();
516
488
  try {
517
489
  response = dendrite.response(
518
490
  inputSignal?.payload,
519
- neuron2.axon,
491
+ neuron.axon,
520
492
  new CNSDendriteContext(
521
493
  this,
522
- neuron2,
523
494
  this.options?.abortSignal,
524
- this.cns
495
+ this.cns,
496
+ neuronActivationTask
525
497
  )
526
498
  );
527
499
  } catch (error) {
528
- this.markNeuronInactive(neuron2);
529
500
  const isAborted = this.options?.abortSignal?.aborted ?? false;
530
501
  this.pushFailedTask({
531
502
  task: neuronActivationTask,
@@ -534,14 +505,16 @@ var CNSStimulation = class {
534
505
  });
535
506
  return () => {
536
507
  this.processResponseOrResponses(
537
- neuron2,
508
+ neuron,
538
509
  inputSignal,
539
510
  void 0,
540
- error
511
+ error,
512
+ startedAt,
513
+ neuronActivationTask.activationContext
541
514
  );
542
515
  };
543
516
  }
544
- const maxDuration = neuron2.maxDuration;
517
+ const maxDuration = neuron.maxDuration;
545
518
  if (response instanceof Promise || maxDuration) {
546
519
  const asPromise = response instanceof Promise ? response : Promise.resolve(response);
547
520
  const timedPromise = maxDuration && maxDuration > 0 ? new Promise((resolve, reject) => {
@@ -565,17 +538,18 @@ var CNSStimulation = class {
565
538
  return timedPromise.then(
566
539
  (signal) => {
567
540
  return () => {
568
- this.markNeuronInactive(neuron2);
569
541
  return this.processResponseOrResponses(
570
- neuron2,
542
+ neuron,
571
543
  inputSignal,
572
- signal
544
+ signal,
545
+ void 0,
546
+ startedAt,
547
+ neuronActivationTask.activationContext
573
548
  );
574
549
  };
575
550
  },
576
551
  (error) => {
577
552
  return () => {
578
- this.markNeuronInactive(neuron2);
579
553
  const isAborted = this.options?.abortSignal?.aborted ?? false;
580
554
  this.pushFailedTask({
581
555
  task: neuronActivationTask,
@@ -583,29 +557,42 @@ var CNSStimulation = class {
583
557
  aborted: isAborted
584
558
  });
585
559
  return this.processResponseOrResponses(
586
- neuron2,
560
+ neuron,
587
561
  inputSignal,
588
562
  void 0,
589
- error
563
+ error,
564
+ startedAt,
565
+ neuronActivationTask.activationContext
590
566
  );
591
567
  };
592
568
  }
593
569
  );
594
570
  } else {
595
571
  return () => {
596
- this.markNeuronInactive(neuron2);
597
572
  return this.processResponseOrResponses(
598
- neuron2,
573
+ neuron,
599
574
  inputSignal,
600
- response
575
+ response,
576
+ void 0,
577
+ startedAt,
578
+ neuronActivationTask.activationContext
601
579
  );
602
580
  };
603
581
  }
604
582
  }
605
- processResponseOrResponses(emitter, inputSignal, outputSignalOrSignals, error) {
583
+ processResponseOrResponses(emitter, inputSignal, outputSignalOrSignals, error, startedAt, activationContext) {
584
+ const duration = startedAt === void 0 ? void 0 : Date.now() - startedAt;
606
585
  if (Array.isArray(outputSignalOrSignals)) {
607
586
  if (outputSignalOrSignals.length === 0) {
608
- this.processResponse(emitter, inputSignal, void 0, error);
587
+ this.processResponse(
588
+ emitter,
589
+ inputSignal,
590
+ void 0,
591
+ error,
592
+ startedAt,
593
+ duration,
594
+ activationContext
595
+ );
609
596
  return;
610
597
  }
611
598
  let perSignal;
@@ -614,32 +601,82 @@ var CNSStimulation = class {
614
601
  let reserved = 0;
615
602
  for (let i = 0; i < outputSignalOrSignals.length; i++) {
616
603
  const c = outputSignalOrSignals[i]?.collateral;
617
- const n = c ? this.cns.network.getSubscribers(c).length : 0;
604
+ const n = c ? this.getSubscribers(c).length : 0;
618
605
  perSignal[i] = n;
619
606
  reserved += n;
620
607
  }
621
608
  this.reservedActivations += reserved;
622
609
  }
623
- for (let i = 0; i < outputSignalOrSignals.length; i++) {
624
- if (perSignal) this.reservedActivations -= perSignal[i];
625
- this.processResponse(
626
- emitter,
627
- inputSignal,
628
- outputSignalOrSignals[i],
629
- error
630
- );
610
+ let dispatchIndex = 0;
611
+ try {
612
+ for (; dispatchIndex < outputSignalOrSignals.length; dispatchIndex++) {
613
+ if (perSignal)
614
+ this.reservedActivations -= perSignal[dispatchIndex];
615
+ this.processResponse(
616
+ emitter,
617
+ inputSignal,
618
+ outputSignalOrSignals[dispatchIndex],
619
+ error,
620
+ startedAt,
621
+ duration,
622
+ activationContext
623
+ );
624
+ }
625
+ } finally {
626
+ if (perSignal) {
627
+ for (let j = dispatchIndex + 1; j < perSignal.length; j++) {
628
+ this.reservedActivations -= perSignal[j];
629
+ }
630
+ }
631
631
  }
632
632
  return;
633
633
  }
634
- this.processResponse(emitter, inputSignal, outputSignalOrSignals, error);
634
+ this.processResponse(
635
+ emitter,
636
+ inputSignal,
637
+ outputSignalOrSignals,
638
+ error,
639
+ startedAt,
640
+ duration,
641
+ activationContext
642
+ );
635
643
  }
636
- processResponse(emitter, inputSignal, outputSignal, error) {
637
- const collateral2 = outputSignal?.collateral;
638
- const subscribers = collateral2 ? this.cns.network.getSubscribers(collateral2) : [];
644
+ /**
645
+ * The one response an aborted run emits: no neuron, no signals, an empty
646
+ * queue. It exists so `queueLength === 0` — the completion condition every
647
+ * listener uses — is reached on the abort path too. Listener errors are
648
+ * swallowed: the run is already over and rejecting, and there is nothing
649
+ * left to fail.
650
+ */
651
+ emitAbortedResponse() {
652
+ const onResponse = this.onResponse;
653
+ if (!onResponse) return;
654
+ try {
655
+ void onResponse(
656
+ new CNSStimulationResponseImpl(
657
+ this,
658
+ void 0,
659
+ void 0,
660
+ 0,
661
+ 0,
662
+ 0,
663
+ void 0,
664
+ new Error("Stimulation aborted"),
665
+ void 0,
666
+ void 0,
667
+ void 0
668
+ )
669
+ );
670
+ } catch (e) {
671
+ this.onResponseError = e instanceof Error ? e : new Error(String(e));
672
+ }
673
+ }
674
+ processResponse(emitter, inputSignal, outputSignal, error, startedAt, duration, activationContext) {
675
+ const collateral = outputSignal?.collateral;
676
+ const subscribers = collateral ? this.getSubscribers(collateral) : EMPTY_SUBSCRIBERS;
639
677
  const onResponse = this.onResponse;
640
678
  if (!onResponse) {
641
- if (collateral2 && !error) {
642
- if (emitter) this.cleanupCtxIfNeeded(emitter);
679
+ if (collateral && !error) {
643
680
  const n = subscribers.length;
644
681
  if (n > 0) {
645
682
  const tasks = new Array(n);
@@ -659,17 +696,17 @@ var CNSStimulation = class {
659
696
  this.tryResolveCompleted();
660
697
  return;
661
698
  }
662
- const subscriberActivationTasks = [];
663
- if (collateral2 && !error) {
664
- if (emitter) this.cleanupCtxIfNeeded(emitter);
665
- const pending = this.pendingTasks ?? (this.pendingTasks = /* @__PURE__ */ new Set());
699
+ let subscriberActivationTasks = EMPTY_TASKS;
700
+ if (collateral && !error && subscribers.length > 0) {
701
+ const pending = this.pendingTasks ?? (this.pendingTasks = []);
702
+ subscriberActivationTasks = new Array(subscribers.length);
666
703
  for (let i = 0; i < subscribers.length; i++) {
667
704
  const task = this.createSubscriberQueueItem(
668
705
  subscribers[i],
669
706
  outputSignal
670
707
  );
671
- subscriberActivationTasks.push(task);
672
- pending.add(task);
708
+ subscriberActivationTasks[i] = task;
709
+ this.addPositionedTask(pending, task, TASK_PENDING_POSITION);
673
710
  }
674
711
  }
675
712
  this.scheduledCount += subscriberActivationTasks.length;
@@ -678,22 +715,25 @@ var CNSStimulation = class {
678
715
  maybePromise = onResponse(
679
716
  new CNSStimulationResponseImpl(
680
717
  this,
681
- this.ctxStore,
682
718
  inputSignal,
683
719
  outputSignal,
684
720
  this.pendingActivations + this.activeOperations,
685
721
  this.pendingActivations,
686
722
  this.activeOperations,
687
- this.options?.maxNeuronHops && emitter ? this.neuronVisitMap?.get(emitter) ?? 0 : void 0,
723
+ this.options?.maxNeuronHops && emitter ? this.neuronVisitCounts?.[emitter.index] ?? 0 : void 0,
688
724
  error,
689
- emitter
725
+ emitter,
726
+ startedAt,
727
+ duration,
728
+ subscriberActivationTasks,
729
+ activationContext
690
730
  )
691
731
  );
692
732
  } catch (e) {
693
733
  this.onResponseError = e instanceof Error ? e : new Error(String(e));
694
734
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
695
735
  this.scheduledCount--;
696
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
736
+ this.removePendingTask(subscriberActivationTasks[i]);
697
737
  this.qEnqueue(subscriberActivationTasks[i]);
698
738
  }
699
739
  this.tryResolveCompleted();
@@ -706,7 +746,7 @@ var CNSStimulation = class {
706
746
  try {
707
747
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
708
748
  this.scheduledCount--;
709
- this.pendingTasks?.delete(
749
+ this.removePendingTask(
710
750
  subscriberActivationTasks[i]
711
751
  );
712
752
  this.qEnqueue(subscriberActivationTasks[i]);
@@ -722,7 +762,7 @@ var CNSStimulation = class {
722
762
  try {
723
763
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
724
764
  this.scheduledCount--;
725
- this.pendingTasks?.delete(
765
+ this.removePendingTask(
726
766
  subscriberActivationTasks[i]
727
767
  );
728
768
  this.qEnqueue(subscriberActivationTasks[i]);
@@ -737,7 +777,7 @@ var CNSStimulation = class {
737
777
  }
738
778
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
739
779
  this.scheduledCount--;
740
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
780
+ this.removePendingTask(subscriberActivationTasks[i]);
741
781
  this.qEnqueue(subscriberActivationTasks[i]);
742
782
  }
743
783
  this.tryResolveCompleted();
@@ -764,36 +804,36 @@ var CNSStimulation = class {
764
804
  // src/CNSInstanceNeuronQueue.ts
765
805
  var CNSInstanceNeuronQueue = class {
766
806
  constructor() {
767
- this.gates = /* @__PURE__ */ new Map();
807
+ this.gatesByNeuronIndex = [];
768
808
  }
769
- run(neuron2, fn) {
770
- const limit = neuron2?.concurrency;
809
+ run(neuron, fn) {
810
+ const limit = neuron?.concurrency;
771
811
  if (limit === void 0 || limit <= 0) {
772
812
  return fn();
773
813
  }
774
- let gate = this.gates.get(neuron2);
814
+ let gate = this.gatesByNeuronIndex[neuron.index];
775
815
  if (!gate) {
776
816
  gate = { limit, active: 0, waiters: [] };
777
- this.gates.set(neuron2, gate);
817
+ this.gatesByNeuronIndex[neuron.index] = gate;
778
818
  } else {
779
819
  gate.limit = limit;
780
820
  }
821
+ const ownGate = gate;
781
822
  const wrap = (cb) => {
782
823
  return () => {
783
824
  try {
784
825
  cb();
785
826
  } finally {
786
- const g = this.gates.get(neuron2);
787
- g.active = Math.max(0, g.active - 1);
788
- if (g.waiters.length > 0) {
789
- const next = g.waiters.shift();
827
+ ownGate.active = Math.max(0, ownGate.active - 1);
828
+ if (ownGate.waiters.length > 0) {
829
+ const next = ownGate.waiters.shift();
790
830
  next();
791
831
  }
792
832
  }
793
833
  };
794
834
  };
795
- if (gate.active < gate.limit) {
796
- gate.active++;
835
+ if (ownGate.active < ownGate.limit) {
836
+ ownGate.active++;
797
837
  const ret = fn();
798
838
  if (ret && typeof ret.then === "function") {
799
839
  return ret.then((cb) => wrap(cb));
@@ -802,8 +842,7 @@ var CNSInstanceNeuronQueue = class {
802
842
  }
803
843
  return new Promise((resolve) => {
804
844
  const starter = () => {
805
- const g = this.gates.get(neuron2);
806
- g.active++;
845
+ ownGate.active++;
807
846
  const ret = fn();
808
847
  if (ret && typeof ret.then === "function") {
809
848
  ret.then((cb) => resolve(wrap(cb)));
@@ -811,12 +850,52 @@ var CNSInstanceNeuronQueue = class {
811
850
  resolve(wrap(ret));
812
851
  }
813
852
  };
814
- gate.waiters.push(starter);
853
+ ownGate.waiters.push(starter);
815
854
  });
816
855
  }
817
856
  };
818
857
 
858
+ // src/utils/cnsResolveModalityHandler.ts
859
+ function cnsResolveModalityHandler(modalitySelection, modality, afferentPath) {
860
+ if (modality !== void 0) {
861
+ const modalityConfigs = modalitySelection.modalityConfigs;
862
+ for (let i = 0; i < modalityConfigs.length; i++) {
863
+ const modalityConfig = modalityConfigs[i];
864
+ if (modalityConfig.modality !== modality) continue;
865
+ for (let path = afferentPath; path !== void 0; path = path.parentAfferentPath) {
866
+ const handler = modalityConfig.afferentPaths?.get(path);
867
+ if (handler !== void 0) return handler;
868
+ }
869
+ if (modalityConfig.default !== void 0) return modalityConfig.default;
870
+ break;
871
+ }
872
+ }
873
+ return modalitySelection.defaultHandler;
874
+ }
875
+
876
+ // src/utils/cnsComposeModalityResponse.ts
877
+ function cnsComposeModalityResponse(handler, output) {
878
+ return (payload, axon, ctx) => {
879
+ const maybeResult = handler(payload, axon, ctx);
880
+ if (maybeResult && typeof maybeResult.then === "function") {
881
+ return maybeResult.then((result) => output(result, axon, ctx));
882
+ }
883
+ try {
884
+ return output(maybeResult, axon, ctx);
885
+ } catch (error) {
886
+ if (error instanceof Error && error.message.includes("Cannot read properties of undefined")) {
887
+ const axonKeys = axon && typeof axon === "object" ? Object.keys(axon) : "not an object";
888
+ throw new Error(
889
+ `modalityDendrite: axon.output is undefined. Axon type: ${typeof axon}, Axon keys: ${Array.isArray(axonKeys) ? axonKeys.join(", ") : axonKeys}. Make sure the neuron was created with the correct axon structure. Original error: ${error.message}`
890
+ );
891
+ }
892
+ throw error;
893
+ }
894
+ };
895
+ }
896
+
819
897
  // src/CNSNetwork.ts
898
+ var NO_SUBSCRIBERS = [];
820
899
  var CNSNetwork = class {
821
900
  constructor(neurons) {
822
901
  this.neurons = neurons;
@@ -824,8 +903,9 @@ var CNSNetwork = class {
824
903
  * Strongly Connected Components of the neuron graph.
825
904
  * Each component is a set of neuron objects that can reach each other.
826
905
  *
827
- * Computed lazily on first access: the SCC machinery is only needed for
828
- * `autoCleanupContexts` (off by default), so most networks never pay for it.
906
+ * Computed lazily on first access: the SCC machinery only drives graph
907
+ * introspection (`getSCCSetByNeuron`, `canNeuronBeGuaranteedDone`), so a
908
+ * network that never inspects reachability never pays for it.
829
909
  */
830
910
  this._stronglyConnectedComponents = [];
831
911
  this.sccBuilt = false;
@@ -845,16 +925,19 @@ var CNSNetwork = class {
845
925
  * Fast lookup: collateral -> list of (neuron, dendrite) subscribers.
846
926
  * Built once at construction time.
847
927
  */
848
- this.subIndex = /* @__PURE__ */ new Map();
928
+ this.subscribersByCollateralIndex = [];
929
+ this.allSubscribersByCollateralIndex = [];
930
+ this.modalityCollateralIndices = [];
931
+ this.subscriberTablesByModalityIndex = [];
849
932
  this.parentNeuronByCollateral = /* @__PURE__ */ new Map();
850
933
  this.buildIndexes();
851
934
  }
852
935
  get stronglyConnectedComponents() {
853
- this.ensureSCC();
936
+ this.buildSccIndex();
854
937
  return this._stronglyConnectedComponents;
855
938
  }
856
939
  /** Build the SCC analysis the first time anything needs it. */
857
- ensureSCC() {
940
+ buildSccIndex() {
858
941
  if (this.sccBuilt) return;
859
942
  this.sccBuilt = true;
860
943
  this.buildSCC();
@@ -864,23 +947,21 @@ var CNSNetwork = class {
864
947
  */
865
948
  buildNeuronGraph() {
866
949
  const graph = /* @__PURE__ */ new Map();
867
- for (const neuron2 of this.neurons) {
868
- graph.set(neuron2, /* @__PURE__ */ new Set());
950
+ for (const neuron of this.neurons) {
951
+ graph.set(neuron, /* @__PURE__ */ new Set());
869
952
  }
870
- for (const neuron2 of this.neurons) {
953
+ for (const neuron of this.neurons) {
871
954
  const reachableNeurons = /* @__PURE__ */ new Set();
872
- for (const axonKey in neuron2.axon) {
873
- const collateral2 = neuron2.axon[axonKey];
874
- const subscribers = this.subIndex.get(
875
- collateral2
876
- );
955
+ for (const axonKey in neuron.axon) {
956
+ const collateral = neuron.axon[axonKey];
957
+ const subscribers = this.allSubscribersByCollateralIndex[collateral.index];
877
958
  if (subscribers) {
878
959
  for (const { neuron: targetNeuron } of subscribers) {
879
960
  reachableNeurons.add(targetNeuron);
880
961
  }
881
962
  }
882
963
  }
883
- graph.set(neuron2, reachableNeurons);
964
+ graph.set(neuron, reachableNeurons);
884
965
  }
885
966
  return graph;
886
967
  }
@@ -895,41 +976,41 @@ var CNSNetwork = class {
895
976
  const stack = [];
896
977
  const components = [];
897
978
  let currentIndex = 0;
898
- const strongConnect = (neuron2) => {
899
- index.set(neuron2, currentIndex);
900
- lowlink.set(neuron2, currentIndex);
979
+ const strongConnect = (neuron) => {
980
+ index.set(neuron, currentIndex);
981
+ lowlink.set(neuron, currentIndex);
901
982
  currentIndex++;
902
- stack.push(neuron2);
903
- onStack.add(neuron2);
904
- const neighbors = graph.get(neuron2) || /* @__PURE__ */ new Set();
983
+ stack.push(neuron);
984
+ onStack.add(neuron);
985
+ const neighbors = graph.get(neuron) || /* @__PURE__ */ new Set();
905
986
  for (const neighbor of Array.from(neighbors)) {
906
987
  if (!index.has(neighbor)) {
907
988
  strongConnect(neighbor);
908
989
  lowlink.set(
909
- neuron2,
910
- Math.min(lowlink.get(neuron2), lowlink.get(neighbor))
990
+ neuron,
991
+ Math.min(lowlink.get(neuron), lowlink.get(neighbor))
911
992
  );
912
993
  } else if (onStack.has(neighbor)) {
913
994
  lowlink.set(
914
- neuron2,
915
- Math.min(lowlink.get(neuron2), index.get(neighbor))
995
+ neuron,
996
+ Math.min(lowlink.get(neuron), index.get(neighbor))
916
997
  );
917
998
  }
918
999
  }
919
- if (lowlink.get(neuron2) === index.get(neuron2)) {
1000
+ if (lowlink.get(neuron) === index.get(neuron)) {
920
1001
  const component = /* @__PURE__ */ new Set();
921
1002
  let w;
922
1003
  do {
923
1004
  w = stack.pop();
924
1005
  onStack.delete(w);
925
1006
  component.add(w);
926
- } while (w !== neuron2);
1007
+ } while (w !== neuron);
927
1008
  components.push(component);
928
1009
  }
929
1010
  };
930
- for (const neuron2 of this.neurons) {
931
- if (!index.has(neuron2)) {
932
- strongConnect(neuron2);
1011
+ for (const neuron of this.neurons) {
1012
+ if (!index.has(neuron)) {
1013
+ strongConnect(neuron);
933
1014
  }
934
1015
  }
935
1016
  this._stronglyConnectedComponents = components;
@@ -951,8 +1032,8 @@ var CNSNetwork = class {
951
1032
  }
952
1033
  for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
953
1034
  const scc = this._stronglyConnectedComponents[i];
954
- for (const neuron2 of Array.from(scc)) {
955
- const neighbors = neuronGraph.get(neuron2) || /* @__PURE__ */ new Set();
1035
+ for (const neuron of Array.from(scc)) {
1036
+ const neighbors = neuronGraph.get(neuron) || /* @__PURE__ */ new Set();
956
1037
  for (const neighbor of Array.from(neighbors)) {
957
1038
  const neighborSccIndex = this.neuronToSCC.get(neighbor);
958
1039
  if (neighborSccIndex !== void 0 && neighborSccIndex !== i) {
@@ -1005,26 +1086,26 @@ var CNSNetwork = class {
1005
1086
  * Returns true if the neuron is in a strongly connected component with more than one member,
1006
1087
  * or if it's in a single-member SCC that can reach itself.
1007
1088
  */
1008
- getSCCSetByNeuron(neuron2) {
1009
- this.ensureSCC();
1010
- const sccIndex = this.neuronToSCC.get(neuron2);
1089
+ getSCCSetByNeuron(neuron) {
1090
+ this.buildSccIndex();
1091
+ const sccIndex = this.neuronToSCC.get(neuron);
1011
1092
  if (sccIndex === void 0) return;
1012
1093
  return this._stronglyConnectedComponents[sccIndex];
1013
1094
  }
1014
1095
  /**
1015
1096
  * Get the SCC index for a given neuron
1016
1097
  */
1017
- getSccIndexByNeuron(neuron2) {
1018
- this.ensureSCC();
1019
- return this.neuronToSCC.get(neuron2);
1098
+ getSccIndexByNeuron(neuron) {
1099
+ this.buildSccIndex();
1100
+ return this.neuronToSCC.get(neuron);
1020
1101
  }
1021
1102
  /**
1022
1103
  * Check if a neuron can be guaranteed not to be visited again during the current propagation.
1023
1104
  * This is the core logic for safe context cleanup.
1024
1105
  */
1025
- canNeuronBeGuaranteedDone(neuron2, activeSccCounts) {
1026
- this.ensureSCC();
1027
- const sccIndex = this.neuronToSCC.get(neuron2);
1106
+ canNeuronBeGuaranteedDone(neuron, activeSccCounts) {
1107
+ this.buildSccIndex();
1108
+ const sccIndex = this.neuronToSCC.get(neuron);
1028
1109
  if (sccIndex === void 0) return true;
1029
1110
  if (activeSccCounts.get(sccIndex) && activeSccCounts.get(sccIndex) > 0) {
1030
1111
  return false;
@@ -1038,36 +1119,93 @@ var CNSNetwork = class {
1038
1119
  }
1039
1120
  return true;
1040
1121
  }
1122
+ describeNeuron(neuron) {
1123
+ const axonKeys = Object.keys(neuron.axon);
1124
+ return `neuron #${neuron.index} (${axonKeys.length === 0 ? "no axon" : `axon: ${axonKeys.join(", ")}`})`;
1125
+ }
1126
+ describeCollateral(neuron, collateral) {
1127
+ const axonKey = Object.keys(neuron.axon).find((key) => neuron.axon[key] === collateral);
1128
+ return `"${axonKey}" #${collateral.index}`;
1129
+ }
1041
1130
  buildIndexes() {
1042
- this.subIndex.clear();
1131
+ var _a;
1132
+ this.allSubscribersByCollateralIndex = [];
1133
+ this.modalityCollateralIndices = [];
1134
+ this.subscriberTablesByModalityIndex = [];
1043
1135
  this.parentNeuronByCollateral.clear();
1044
- for (const neuron2 of this.neurons) {
1045
- for (const dendrite of neuron2.dendrites) {
1046
- const key = dendrite.collateral;
1047
- const arr = this.subIndex.get(key) ?? [];
1048
- arr.push({ neuron: neuron2, dendrite });
1049
- this.subIndex.set(
1050
- key,
1051
- arr
1052
- );
1136
+ const listedNeurons = /* @__PURE__ */ new Set();
1137
+ for (const neuron of this.neurons) {
1138
+ if (listedNeurons.has(neuron)) {
1139
+ throw new Error(`${this.describeNeuron(neuron)} is listed twice; every dendrite of it would run twice`);
1053
1140
  }
1054
- Object.values(neuron2.axon).forEach((collateral2) => {
1141
+ listedNeurons.add(neuron);
1142
+ for (const dendrite of neuron.dendrites) {
1143
+ const index = dendrite.collateral.index;
1144
+ const subscribers = (_a = this.allSubscribersByCollateralIndex)[index] ?? (_a[index] = []);
1145
+ subscribers.push({ neuron, dendrite });
1146
+ if (dendrite.modalitySelection !== void 0 && this.modalityCollateralIndices.indexOf(index) < 0) {
1147
+ this.modalityCollateralIndices.push(index);
1148
+ }
1149
+ }
1150
+ Object.values(neuron.axon).forEach((collateral) => {
1151
+ const owner = this.parentNeuronByCollateral.get(collateral);
1152
+ if (owner !== void 0 && owner !== neuron) {
1153
+ throw new Error(
1154
+ `collateral ${this.describeCollateral(neuron, collateral)} is in the axons of ${this.describeNeuron(owner)} and ${this.describeNeuron(neuron)}; a collateral is owned by one neuron`
1155
+ );
1156
+ }
1055
1157
  this.parentNeuronByCollateral.set(
1056
- collateral2,
1057
- neuron2
1158
+ collateral,
1159
+ neuron
1058
1160
  );
1059
1161
  });
1060
1162
  }
1163
+ this.subscribersByCollateralIndex = this.buildSubscriberTable(void 0, void 0);
1164
+ }
1165
+ getSubscriberTable(modality, afferentPath) {
1166
+ var _a, _b;
1167
+ if (modality === void 0) return this.subscribersByCollateralIndex;
1168
+ const subscriberTablesByAfferentPathIndex = (_a = this.subscriberTablesByModalityIndex)[_b = modality.index] ?? (_a[_b] = []);
1169
+ const afferentPathSlot = afferentPath === void 0 ? 0 : afferentPath.index + 1;
1170
+ return subscriberTablesByAfferentPathIndex[afferentPathSlot] ?? (subscriberTablesByAfferentPathIndex[afferentPathSlot] = this.buildSubscriberTable(modality, afferentPath));
1171
+ }
1172
+ buildSubscriberTable(modality, afferentPath) {
1173
+ const subscriberTable = this.allSubscribersByCollateralIndex.slice();
1174
+ const modalityCollateralIndices = this.modalityCollateralIndices;
1175
+ for (let i = 0; i < modalityCollateralIndices.length; i++) {
1176
+ const index = modalityCollateralIndices[i];
1177
+ const resolvedSubscribers = [];
1178
+ const subscribers = this.allSubscribersByCollateralIndex[index];
1179
+ for (let j = 0; j < subscribers.length; j++) {
1180
+ const subscriber = subscribers[j];
1181
+ const modalitySelection = subscriber.dendrite.modalitySelection;
1182
+ if (modalitySelection === void 0) {
1183
+ resolvedSubscribers.push(subscriber);
1184
+ continue;
1185
+ }
1186
+ const handler = cnsResolveModalityHandler(modalitySelection, modality, afferentPath);
1187
+ if (handler === void 0) continue;
1188
+ resolvedSubscribers.push({
1189
+ neuron: subscriber.neuron,
1190
+ dendrite: {
1191
+ collateral: subscriber.dendrite.collateral,
1192
+ response: cnsComposeModalityResponse(handler, modalitySelection.output)
1193
+ }
1194
+ });
1195
+ }
1196
+ subscriberTable[index] = resolvedSubscribers;
1197
+ }
1198
+ return subscriberTable;
1061
1199
  }
1062
- getParentNeuronByCollateral(collateral2) {
1063
- return this.parentNeuronByCollateral.get(collateral2);
1200
+ getParentNeuronByCollateral(collateral) {
1201
+ return this.parentNeuronByCollateral.get(collateral);
1064
1202
  }
1065
1203
  // Computed on demand (rarely used): one dendrite per collateral, last wins —
1066
1204
  // preserving the previous index semantics without building it every construct.
1067
1205
  getDendrites() {
1068
1206
  const byCollateral = /* @__PURE__ */ new Map();
1069
- for (const neuron2 of this.neurons) {
1070
- for (const dendrite of neuron2.dendrites) {
1207
+ for (const neuron of this.neurons) {
1208
+ for (const dendrite of neuron.dendrites) {
1071
1209
  byCollateral.set(
1072
1210
  dendrite.collateral,
1073
1211
  dendrite
@@ -1078,15 +1216,15 @@ var CNSNetwork = class {
1078
1216
  }
1079
1217
  getCollaterals() {
1080
1218
  const collaterals = /* @__PURE__ */ new Set();
1081
- for (const neuron2 of this.neurons) {
1082
- for (const collateral2 of Object.values(neuron2.axon)) {
1083
- collaterals.add(collateral2);
1219
+ for (const neuron of this.neurons) {
1220
+ for (const collateral of Object.values(neuron.axon)) {
1221
+ collaterals.add(collateral);
1084
1222
  }
1085
1223
  }
1086
1224
  return Array.from(collaterals);
1087
1225
  }
1088
- getSubscribers(collateral2) {
1089
- return this.subIndex.get(collateral2) ?? [];
1226
+ getSubscribers(collateral) {
1227
+ return this.subscribersByCollateralIndex[collateral.index] ?? NO_SUBSCRIBERS;
1090
1228
  }
1091
1229
  };
1092
1230
 
@@ -1108,36 +1246,47 @@ var CNS = class {
1108
1246
  * Global batch-boundary listeners applied to every stimulation.
1109
1247
  */
1110
1248
  this.globalDrainListeners = [];
1249
+ this.batchDepth = 0;
1250
+ this.batchedDrains = [];
1111
1251
  this.network = new CNSNetwork(this.neurons);
1112
1252
  this.global = global;
1113
1253
  }
1114
- addResponseListener(listener) {
1115
- this.globalResponseListeners.push(listener);
1254
+ /** Push a listener onto a list; return an idempotent unsubscribe. */
1255
+ addListener(list, listener) {
1256
+ list.push(listener);
1116
1257
  let active = true;
1117
1258
  return () => {
1118
1259
  if (!active) return;
1119
1260
  active = false;
1120
- const idx = this.globalResponseListeners.indexOf(listener);
1121
- if (idx >= 0) this.globalResponseListeners.splice(idx, 1);
1261
+ const idx = list.indexOf(listener);
1262
+ if (idx >= 0) list.splice(idx, 1);
1122
1263
  };
1123
1264
  }
1265
+ addResponseListener(listener) {
1266
+ return this.addListener(this.globalResponseListeners, listener);
1267
+ }
1124
1268
  /**
1125
1269
  * Register a listener called once at the end of every synchronous turn of
1126
- * every stimulation - see {@link TCNSStimulationDrain}. This is how an
1270
+ * every stimulation (inside `batch`, when the batch ends) - see {@link TCNSStimulationDrain}. This is how an
1127
1271
  * integration (a state manager, an in-memory database) installs a single
1128
1272
  * commit point for the whole organism instead of passing `onDrain` per call.
1129
1273
  *
1130
1274
  * Returns an unsubscribe function.
1131
1275
  */
1132
1276
  addDrainListener(listener) {
1133
- this.globalDrainListeners.push(listener);
1134
- let active = true;
1135
- return () => {
1136
- if (!active) return;
1137
- active = false;
1138
- const idx = this.globalDrainListeners.indexOf(listener);
1139
- if (idx >= 0) this.globalDrainListeners.splice(idx, 1);
1140
- };
1277
+ return this.addListener(this.globalDrainListeners, listener);
1278
+ }
1279
+ batch(run) {
1280
+ this.batchDepth++;
1281
+ try {
1282
+ return run();
1283
+ } finally {
1284
+ if (--this.batchDepth === 0) {
1285
+ const batchedDrains = this.batchedDrains;
1286
+ this.batchedDrains = [];
1287
+ for (const [announce, drain] of batchedDrains) announce(drain);
1288
+ }
1289
+ }
1141
1290
  }
1142
1291
  /**
1143
1292
  * Fan a drain out to the local and global listeners, or return undefined when
@@ -1148,7 +1297,7 @@ var CNS = class {
1148
1297
  */
1149
1298
  wrapOnDrain(local) {
1150
1299
  if (this.globalDrainListeners.length === 0 && !local) return void 0;
1151
- return (d) => {
1300
+ const announce = (d) => {
1152
1301
  if (local) {
1153
1302
  try {
1154
1303
  local(d);
@@ -1164,39 +1313,38 @@ var CNS = class {
1164
1313
  }
1165
1314
  }
1166
1315
  };
1316
+ return (d) => {
1317
+ if (this.batchDepth > 0) this.batchedDrains.push([announce, d]);
1318
+ else announce(d);
1319
+ };
1167
1320
  }
1168
1321
  wrapOnResponse(local) {
1169
1322
  if (this.globalResponseListeners.length === 0 && !local) {
1170
1323
  return void 0;
1171
1324
  }
1172
1325
  return (r) => {
1173
- let anyPromise = false;
1174
- const promises = [];
1326
+ let promises;
1175
1327
  if (local) {
1176
1328
  try {
1177
1329
  const res = local(r);
1178
1330
  if (res && typeof res.then === "function") {
1179
- anyPromise = true;
1180
- promises.push(res);
1331
+ (promises ?? (promises = [])).push(res);
1181
1332
  }
1182
1333
  } catch (error) {
1183
- anyPromise = true;
1184
- promises.push(Promise.reject(error));
1334
+ (promises ?? (promises = [])).push(Promise.reject(error));
1185
1335
  }
1186
1336
  }
1187
1337
  for (let i = 0; i < this.globalResponseListeners.length; i++) {
1188
1338
  try {
1189
1339
  const res = this.globalResponseListeners[i](r);
1190
1340
  if (res && typeof res.then === "function") {
1191
- anyPromise = true;
1192
- promises.push(res);
1341
+ (promises ?? (promises = [])).push(res);
1193
1342
  }
1194
1343
  } catch (error) {
1195
- anyPromise = true;
1196
- promises.push(Promise.reject(error));
1344
+ (promises ?? (promises = [])).push(Promise.reject(error));
1197
1345
  }
1198
1346
  }
1199
- if (anyPromise) {
1347
+ if (promises) {
1200
1348
  return Promise.allSettled(promises).then((results) => {
1201
1349
  const rejected = results.find(
1202
1350
  (r2) => r2.status === "rejected"
@@ -1206,15 +1354,18 @@ var CNS = class {
1206
1354
  }
1207
1355
  };
1208
1356
  }
1209
- stimulate(signalOrSignals, options) {
1210
- const wrapped = this.wrapOnResponse(options?.onResponse);
1211
- const stimulation = new CNSStimulation(
1357
+ /** Build a stimulation (wired listeners, unseeded). Shared by stimulate/activate. */
1358
+ createStimulation(options) {
1359
+ return new CNSStimulation(
1212
1360
  this,
1213
1361
  this.instanceNeuronQueue,
1214
1362
  options,
1215
- wrapped,
1363
+ this.wrapOnResponse(options?.onResponse),
1216
1364
  this.wrapOnDrain(options?.onDrain)
1217
1365
  );
1366
+ }
1367
+ stimulate(signalOrSignals, options) {
1368
+ const stimulation = this.createStimulation(options);
1218
1369
  stimulation.responseToSignal(signalOrSignals);
1219
1370
  return stimulation;
1220
1371
  }
@@ -1222,21 +1373,26 @@ var CNS = class {
1222
1373
  * Start a stimulation with activation tasks directly
1223
1374
  */
1224
1375
  activate(tasks, options) {
1225
- const wrapped = this.wrapOnResponse(options?.onResponse);
1226
- const stimulation = new CNSStimulation(
1227
- this,
1228
- this.instanceNeuronQueue,
1229
- options,
1230
- wrapped,
1231
- this.wrapOnDrain(options?.onDrain)
1232
- );
1376
+ const stimulation = this.createStimulation(options);
1233
1377
  stimulation.enqueueTasks(tasks);
1234
1378
  return stimulation;
1235
1379
  }
1236
1380
  };
1237
1381
 
1382
+ // src/createCNS.ts
1383
+ var createCNS = (neuronFactories, dependencies, options) => {
1384
+ if (new Set(neuronFactories).size !== neuronFactories.length) {
1385
+ throw new Error("createCNS: a neuron factory is listed twice; every dendrite of its neuron would run twice");
1386
+ }
1387
+ return new CNS(neuronFactories.map((createNeuron) => createNeuron(dependencies)), options);
1388
+ };
1389
+
1238
1390
  // src/CNSCollateral.ts
1391
+ var nextCollateralIndex = 0;
1239
1392
  var CNSCollateral = class {
1393
+ constructor() {
1394
+ this.index = nextCollateralIndex++;
1395
+ }
1240
1396
  createSignal(payload) {
1241
1397
  return { collateral: this, payload };
1242
1398
  }
@@ -1292,8 +1448,11 @@ var CNSStimulationGate = class {
1292
1448
  };
1293
1449
 
1294
1450
  // src/factory/index.ts
1295
- var collateral = () => new CNSCollateral();
1296
- var neuron = (axon) => {
1451
+ var nextNeuronIndex = 0;
1452
+ var nextAfferentPathIndex = 0;
1453
+ var nextModalityIndex = 0;
1454
+ var cnsCollateral = () => new CNSCollateral();
1455
+ var cnsNeuron = (axon) => {
1297
1456
  const dendrites = [];
1298
1457
  const api = {
1299
1458
  setConcurrency: (n) => {
@@ -1317,15 +1476,16 @@ var neuron = (axon) => {
1317
1476
  }
1318
1477
  return api;
1319
1478
  }),
1479
+ index: nextNeuronIndex++,
1320
1480
  axon,
1321
1481
  concurrency: void 0,
1322
1482
  maxDuration: void 0,
1323
1483
  dendrites,
1324
1484
  dendrite(s) {
1325
1485
  if ("collateral" in s && "response" in s && Array.isArray(s.collateral)) {
1326
- for (const collateral2 of s.collateral) {
1486
+ for (const collateral of s.collateral) {
1327
1487
  dendrites.push({
1328
- collateral: collateral2,
1488
+ collateral,
1329
1489
  response: s.response
1330
1490
  });
1331
1491
  }
@@ -1338,7 +1498,7 @@ var neuron = (axon) => {
1338
1498
  },
1339
1499
  modalityDendrite(s) {
1340
1500
  const {
1341
- collateral: collateral2,
1501
+ collateral,
1342
1502
  modality: singleModality,
1343
1503
  modalities: multipleModalities,
1344
1504
  afferentPaths: singleAfferentPaths,
@@ -1355,99 +1515,65 @@ var neuron = (axon) => {
1355
1515
  const response = (payload, axon2, ctx) => {
1356
1516
  const stimulation = ctx.stimulation;
1357
1517
  const stimOptions = stimulation?.options;
1358
- const runGlobalDefault = () => {
1359
- if (!defaultHandler) {
1360
- throw new Error(
1361
- `modalityDendrite: No handler found for modality and no default handler provided`
1362
- );
1363
- }
1364
- const res = defaultHandler(payload, axon2, ctx);
1365
- if (res && typeof res.then === "function") {
1366
- return res.then(
1367
- (result) => output(result, axon2, ctx)
1368
- );
1369
- }
1370
- return output(res, axon2, ctx);
1371
- };
1372
- if (!stimOptions || !stimOptions.modality) {
1373
- return runGlobalDefault();
1374
- }
1375
- const matchingConfig = modalityConfigs.find(
1376
- (config) => config.modality === stimOptions.modality
1518
+ const handler = cnsResolveModalityHandler(
1519
+ modalitySelection,
1520
+ stimOptions?.modality,
1521
+ stimOptions?.afferentPath
1377
1522
  );
1378
- if (!matchingConfig) {
1379
- return runGlobalDefault();
1380
- }
1381
- const stimAfferentPath = stimOptions.afferentPath;
1382
- const handler = stimAfferentPath && matchingConfig.afferentPaths ? matchingConfig.afferentPaths.get(stimAfferentPath) : void 0;
1383
- const effectiveHandler = handler ?? matchingConfig.default ?? defaultHandler;
1384
- if (!effectiveHandler) {
1523
+ if (handler === void 0) {
1385
1524
  throw new Error(
1386
1525
  `modalityDendrite: No handler found for afferent path in modality and no default handler provided`
1387
1526
  );
1388
1527
  }
1389
- const maybeResult = effectiveHandler(payload, axon2, ctx);
1390
- if (maybeResult && typeof maybeResult.then === "function") {
1391
- return maybeResult.then(
1392
- (result) => output(result, axon2, ctx)
1393
- );
1394
- }
1395
- try {
1396
- return output(maybeResult, axon2, ctx);
1397
- } catch (error) {
1398
- if (error instanceof Error && error.message.includes(
1399
- "Cannot read properties of undefined"
1400
- )) {
1401
- const axonKeys = axon2 && typeof axon2 === "object" ? Object.keys(axon2) : "not an object";
1402
- throw new Error(
1403
- `modalityDendrite: axon.output is undefined. Axon type: ${typeof axon2}, Axon keys: ${Array.isArray(axonKeys) ? axonKeys.join(", ") : axonKeys}. Make sure the neuron was created with the correct axon structure. Original error: ${error.message}`
1404
- );
1405
- }
1406
- throw error;
1407
- }
1528
+ return cnsComposeModalityResponse(handler, output)(payload, axon2, ctx);
1529
+ };
1530
+ const modalitySelection = {
1531
+ modalityConfigs,
1532
+ defaultHandler,
1533
+ output
1408
1534
  };
1409
1535
  dendrites.push({
1410
- collateral: collateral2,
1411
- response
1536
+ collateral,
1537
+ response,
1538
+ modalitySelection
1412
1539
  });
1413
1540
  return api;
1414
1541
  }
1415
1542
  };
1416
1543
  return api;
1417
1544
  };
1418
- var neuronFactory = () => {
1545
+ var cnsNeuronFactory = () => {
1419
1546
  const factory = {
1420
- withCtx: () => factory,
1421
1547
  withGlobal: () => factory,
1422
- neuron: (axon) => neuron(axon)
1548
+ neuron: (axon) => cnsNeuron(axon)
1423
1549
  };
1424
1550
  return factory;
1425
1551
  };
1426
- var withCtx = () => neuronFactory();
1427
- var withGlobal = () => neuronFactory();
1428
- var afferentPath = (parentAfferentPath) => {
1552
+ var withCNSGlobal = () => cnsNeuronFactory();
1553
+ var cnsAfferentPath = (parentAfferentPath) => {
1429
1554
  return {
1555
+ index: nextAfferentPathIndex++,
1430
1556
  ...parentAfferentPath !== void 0 && {
1431
1557
  parentAfferentPath
1432
1558
  }
1433
1559
  };
1434
1560
  };
1435
- var modality = (afferentPaths) => {
1561
+ var cnsModality = (afferentPaths) => {
1436
1562
  return {
1563
+ index: nextModalityIndex++,
1437
1564
  afferentPaths
1438
1565
  };
1439
1566
  };
1440
1567
 
1441
1568
  exports.CNS = CNS;
1442
1569
  exports.CNSCollateral = CNSCollateral;
1443
- exports.CNSStimulationContextStore = CNSStimulationContextStore;
1444
1570
  exports.CNSStimulationGate = CNSStimulationGate;
1445
- exports.afferentPath = afferentPath;
1446
- exports.collateral = collateral;
1447
- exports.modality = modality;
1448
- exports.neuron = neuron;
1449
- exports.neuronFactory = neuronFactory;
1450
- exports.withCtx = withCtx;
1451
- exports.withGlobal = withGlobal;
1571
+ exports.cnsAfferentPath = cnsAfferentPath;
1572
+ exports.cnsCollateral = cnsCollateral;
1573
+ exports.cnsModality = cnsModality;
1574
+ exports.cnsNeuron = cnsNeuron;
1575
+ exports.cnsNeuronFactory = cnsNeuronFactory;
1576
+ exports.createCNS = createCNS;
1577
+ exports.withCNSGlobal = withCNSGlobal;
1452
1578
  //# sourceMappingURL=index.cjs.map
1453
1579
  //# sourceMappingURL=index.cjs.map