@cnstra/core 3.0.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
@@ -2,11 +2,14 @@
2
2
 
3
3
  // src/CNSStimulation.ts
4
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");
5
7
  var CNSDendriteContext = class {
6
- constructor(stimulation, abortSignal, cns) {
8
+ constructor(stimulation, abortSignal, cns, task) {
7
9
  this.stimulation = stimulation;
8
10
  this.abortSignal = abortSignal;
9
11
  this.cns = cns;
12
+ this.task = task;
10
13
  }
11
14
  // Prototype getter, not a constructor field: you only pay a single `cns.global`
12
15
  // read on the dendrites that actually reach for it. Value getter, so
@@ -21,9 +24,15 @@ var CNSDendriteContext = class {
21
24
  get stimulationContext() {
22
25
  return this.stimulation.options?.stimulationContext;
23
26
  }
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;
32
+ }
24
33
  };
25
34
  var CNSStimulationResponseImpl = class {
26
- constructor(stimulation, 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) {
27
36
  this.stimulation = stimulation;
28
37
  this.inputSignal = inputSignal;
29
38
  this.outputSignal = outputSignal;
@@ -32,9 +41,15 @@ var CNSStimulationResponseImpl = class {
32
41
  this.activeActivations = activeActivations;
33
42
  this.hops = hops;
34
43
  this.error = error;
35
- this.neuron = neuron2;
44
+ this.neuron = neuron;
45
+ this.startedAt = startedAt;
46
+ this.duration = duration;
47
+ this.scheduledActivationTasks = scheduledActivationTasks;
48
+ this.activationContext = activationContext;
36
49
  }
37
50
  };
51
+ var EMPTY_TASKS = [];
52
+ var EMPTY_SUBSCRIBERS = [];
38
53
  var CNSStimulation = class {
39
54
  constructor(cns, instanceNeuronQueue, options, onResponse, onDrain) {
40
55
  this.cns = cns;
@@ -63,8 +78,14 @@ var CNSStimulation = class {
63
78
  this.onDrain = onDrain;
64
79
  this.instanceNeuronQueue = instanceNeuronQueue;
65
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
+ );
66
87
  if (this.options?.maxNeuronHops) {
67
- this.neuronVisitMap = /* @__PURE__ */ new Map();
88
+ this.neuronVisitCounts = [];
68
89
  }
69
90
  }
70
91
  pushFailedTask(failure) {
@@ -81,8 +102,7 @@ var CNSStimulation = class {
81
102
  }
82
103
  // ----- Inlined activation queue / pump -----
83
104
  get canStartOperation() {
84
- const limit = this.options?.concurrency ?? Infinity;
85
- return this.activeOperations < limit && !this.options?.abortSignal?.aborted;
105
+ return this.activeOperations < this.concurrencyLimit && !this.abortSignal?.aborted;
86
106
  }
87
107
  qResize() {
88
108
  const oldCapacity = this.qCapacity;
@@ -139,48 +159,56 @@ var CNSStimulation = class {
139
159
  return;
140
160
  }
141
161
  this.pumping = true;
142
- while (this.canStartOperation && this.qSize > 0) {
143
- const task = this.qDequeue();
144
- this.activeOperations++;
145
- const ret = this.executeActivationTask(task);
146
- if (ret && typeof ret.then === "function") {
147
- (this.activeTasks ?? (this.activeTasks = /* @__PURE__ */ new Set())).add(task);
148
- ret.then(
149
- (cb) => {
150
- this.activeOperations--;
151
- this.activeTasks.delete(task);
152
- this.beginTurn();
153
- try {
154
- if (typeof cb === "function") cb();
155
- if (this.qSize > 0 && this.canStartOperation) {
156
- if (this.pumping) this.needsPump = true;
157
- 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();
158
184
  }
159
- } finally {
160
- this.endTurn();
161
- }
162
- },
163
- (err) => {
164
- this.activeOperations--;
165
- this.activeTasks.delete(task);
166
- this.beginTurn();
167
- try {
168
- if (this.qSize > 0 && this.canStartOperation) {
169
- if (this.pumping) this.needsPump = true;
170
- 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();
171
197
  }
172
- } finally {
173
- this.endTurn();
198
+ throw err;
174
199
  }
175
- throw err;
176
- }
177
- );
178
- continue;
200
+ );
201
+ continue;
202
+ }
203
+ this.activeOperations--;
204
+ if (typeof ret === "function") ret();
179
205
  }
180
- this.activeOperations--;
181
- if (typeof ret === "function") ret();
206
+ } catch (error) {
207
+ this.endRunWithError(error);
208
+ throw error;
209
+ } finally {
210
+ this.pumping = false;
182
211
  }
183
- this.pumping = false;
184
212
  if (this.needsPump) {
185
213
  this.needsPump = false;
186
214
  this.pump();
@@ -222,14 +250,30 @@ var CNSStimulation = class {
222
250
  }
223
251
  }
224
252
  /**
225
- * Activations that exist (or are reserved) but whose dendrite body has not
226
- * 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
227
255
  * pendingTasks, or reserved by an array fan-out mid-dispatch. Subscribers
228
256
  * parked behind an async onResponse land here too - their body has not run.
229
257
  */
230
258
  get pendingActivations() {
231
259
  return this.qSize + this.scheduledCount + this.reservedActivations;
232
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
+ }
233
277
  tryResolveCompleted() {
234
278
  if (this.isCompleted) return;
235
279
  const noActive = this.activeOperations === 0;
@@ -253,6 +297,7 @@ var CNSStimulation = class {
253
297
  }
254
298
  }
255
299
  this.isCompleted = true;
300
+ if (!noPending) this.emitAbortedResponse();
256
301
  this.finalizeReject(new Error("Stimulation aborted"));
257
302
  return;
258
303
  }
@@ -285,10 +330,34 @@ var CNSStimulation = class {
285
330
  */
286
331
  getAllActivationTasks() {
287
332
  const queuedTasks = this.qGetQueuedTasks();
288
- const activeTasks = this.activeTasks ? Array.from(this.activeTasks) : [];
289
- const pendingTasks = this.pendingTasks ? Array.from(this.pendingTasks) : [];
333
+ const activeTasks = this.activeTasks ?? EMPTY_TASKS;
334
+ const pendingTasks = this.pendingTasks ?? EMPTY_TASKS;
290
335
  return [...queuedTasks, ...activeTasks, ...pendingTasks];
291
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
+ }
292
361
  /**
293
362
  * Returns all tasks that failed or were aborted
294
363
  */
@@ -301,12 +370,25 @@ var CNSStimulation = class {
301
370
  * failed/aborted tasks. This is the set to persist for resume: re-activating
302
371
  * it re-runs exactly the not-yet-done work. Safe to call at any point during
303
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.
304
380
  */
305
381
  getOutstandingTasks() {
306
- return [
307
- ...this.getAllActivationTasks(),
308
- ...this.getFailedTasks().map((f) => f.task)
309
- ];
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;
310
392
  }
311
393
  /**
312
394
  * Enqueue activation tasks directly into the stimulation queue
@@ -326,17 +408,16 @@ var CNSStimulation = class {
326
408
  return this.options?.concurrency !== void 0 && this.options?.concurrency > 0;
327
409
  }
328
410
  createSubscriberQueueItem(subscriber, inputSignal) {
329
- if (this.options?.maxNeuronHops) {
330
- 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) {
331
416
  throw new Error(
332
417
  `Max neuron hops reached when trying to enqueue subscriber`
333
418
  );
334
- } else {
335
- this.neuronVisitMap?.set(
336
- subscriber.neuron,
337
- (this.neuronVisitMap?.get(subscriber.neuron) ?? 0) + 1
338
- );
339
419
  }
420
+ neuronVisitCounts[neuronIndex] = neuronVisitCount + 1;
340
421
  }
341
422
  const neuronActivationTask = {
342
423
  neuron: subscriber.neuron,
@@ -344,12 +425,14 @@ var CNSStimulation = class {
344
425
  input: inputSignal
345
426
  };
346
427
  neuronActivationTask[TASK_DENDRITE] = subscriber;
428
+ neuronActivationTask[TASK_ACTIVE_POSITION] = void 0;
429
+ neuronActivationTask[TASK_PENDING_POSITION] = void 0;
347
430
  return neuronActivationTask;
348
431
  }
349
432
  executeActivationTask(neuronActivationTask) {
350
433
  let subscriber = neuronActivationTask[TASK_DENDRITE];
351
434
  if (!subscriber) {
352
- const subscribers = this.cns.network.getSubscribers(
435
+ const subscribers = this.getSubscribers(
353
436
  neuronActivationTask.dendriteCollateral
354
437
  );
355
438
  for (let i = 0; i < subscribers.length; i++) {
@@ -371,42 +454,46 @@ var CNSStimulation = class {
371
454
  void 0,
372
455
  neuronActivationTask.input,
373
456
  void 0,
374
- error
457
+ error,
458
+ void 0,
459
+ neuronActivationTask.activationContext
375
460
  );
376
461
  };
377
462
  }
378
- const neuron2 = subscriber.neuron;
463
+ const neuron = subscriber.neuron;
379
464
  const dendrite = subscriber.dendrite;
380
465
  const inputSignal = neuronActivationTask.input;
381
- const concurrency = neuron2.concurrency;
466
+ const concurrency = neuron.concurrency;
382
467
  if (concurrency === void 0 || concurrency <= 0) {
383
468
  return this.runStarter(
384
- neuron2,
469
+ neuron,
385
470
  dendrite,
386
471
  inputSignal,
387
472
  neuronActivationTask
388
473
  );
389
474
  }
390
475
  return this.instanceNeuronQueue.run(
391
- neuron2,
476
+ neuron,
392
477
  () => this.runStarter(
393
- neuron2,
478
+ neuron,
394
479
  dendrite,
395
480
  inputSignal,
396
481
  neuronActivationTask
397
482
  )
398
483
  );
399
484
  }
400
- runStarter(neuron2, dendrite, inputSignal, neuronActivationTask) {
485
+ runStarter(neuron, dendrite, inputSignal, neuronActivationTask) {
401
486
  let response;
487
+ const startedAt = this.onResponse === void 0 ? void 0 : Date.now();
402
488
  try {
403
489
  response = dendrite.response(
404
490
  inputSignal?.payload,
405
- neuron2.axon,
491
+ neuron.axon,
406
492
  new CNSDendriteContext(
407
493
  this,
408
494
  this.options?.abortSignal,
409
- this.cns
495
+ this.cns,
496
+ neuronActivationTask
410
497
  )
411
498
  );
412
499
  } catch (error) {
@@ -418,14 +505,16 @@ var CNSStimulation = class {
418
505
  });
419
506
  return () => {
420
507
  this.processResponseOrResponses(
421
- neuron2,
508
+ neuron,
422
509
  inputSignal,
423
510
  void 0,
424
- error
511
+ error,
512
+ startedAt,
513
+ neuronActivationTask.activationContext
425
514
  );
426
515
  };
427
516
  }
428
- const maxDuration = neuron2.maxDuration;
517
+ const maxDuration = neuron.maxDuration;
429
518
  if (response instanceof Promise || maxDuration) {
430
519
  const asPromise = response instanceof Promise ? response : Promise.resolve(response);
431
520
  const timedPromise = maxDuration && maxDuration > 0 ? new Promise((resolve, reject) => {
@@ -450,9 +539,12 @@ var CNSStimulation = class {
450
539
  (signal) => {
451
540
  return () => {
452
541
  return this.processResponseOrResponses(
453
- neuron2,
542
+ neuron,
454
543
  inputSignal,
455
- signal
544
+ signal,
545
+ void 0,
546
+ startedAt,
547
+ neuronActivationTask.activationContext
456
548
  );
457
549
  };
458
550
  },
@@ -465,10 +557,12 @@ var CNSStimulation = class {
465
557
  aborted: isAborted
466
558
  });
467
559
  return this.processResponseOrResponses(
468
- neuron2,
560
+ neuron,
469
561
  inputSignal,
470
562
  void 0,
471
- error
563
+ error,
564
+ startedAt,
565
+ neuronActivationTask.activationContext
472
566
  );
473
567
  };
474
568
  }
@@ -476,17 +570,29 @@ var CNSStimulation = class {
476
570
  } else {
477
571
  return () => {
478
572
  return this.processResponseOrResponses(
479
- neuron2,
573
+ neuron,
480
574
  inputSignal,
481
- response
575
+ response,
576
+ void 0,
577
+ startedAt,
578
+ neuronActivationTask.activationContext
482
579
  );
483
580
  };
484
581
  }
485
582
  }
486
- processResponseOrResponses(emitter, inputSignal, outputSignalOrSignals, error) {
583
+ processResponseOrResponses(emitter, inputSignal, outputSignalOrSignals, error, startedAt, activationContext) {
584
+ const duration = startedAt === void 0 ? void 0 : Date.now() - startedAt;
487
585
  if (Array.isArray(outputSignalOrSignals)) {
488
586
  if (outputSignalOrSignals.length === 0) {
489
- 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
+ );
490
596
  return;
491
597
  }
492
598
  let perSignal;
@@ -495,31 +601,82 @@ var CNSStimulation = class {
495
601
  let reserved = 0;
496
602
  for (let i = 0; i < outputSignalOrSignals.length; i++) {
497
603
  const c = outputSignalOrSignals[i]?.collateral;
498
- const n = c ? this.cns.network.getSubscribers(c).length : 0;
604
+ const n = c ? this.getSubscribers(c).length : 0;
499
605
  perSignal[i] = n;
500
606
  reserved += n;
501
607
  }
502
608
  this.reservedActivations += reserved;
503
609
  }
504
- for (let i = 0; i < outputSignalOrSignals.length; i++) {
505
- if (perSignal) this.reservedActivations -= perSignal[i];
506
- this.processResponse(
507
- emitter,
508
- inputSignal,
509
- outputSignalOrSignals[i],
510
- error
511
- );
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
+ }
512
631
  }
513
632
  return;
514
633
  }
515
- this.processResponse(emitter, inputSignal, outputSignalOrSignals, error);
634
+ this.processResponse(
635
+ emitter,
636
+ inputSignal,
637
+ outputSignalOrSignals,
638
+ error,
639
+ startedAt,
640
+ duration,
641
+ activationContext
642
+ );
643
+ }
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
+ }
516
673
  }
517
- processResponse(emitter, inputSignal, outputSignal, error) {
518
- const collateral2 = outputSignal?.collateral;
519
- const subscribers = collateral2 ? this.cns.network.getSubscribers(collateral2) : [];
674
+ processResponse(emitter, inputSignal, outputSignal, error, startedAt, duration, activationContext) {
675
+ const collateral = outputSignal?.collateral;
676
+ const subscribers = collateral ? this.getSubscribers(collateral) : EMPTY_SUBSCRIBERS;
520
677
  const onResponse = this.onResponse;
521
678
  if (!onResponse) {
522
- if (collateral2 && !error) {
679
+ if (collateral && !error) {
523
680
  const n = subscribers.length;
524
681
  if (n > 0) {
525
682
  const tasks = new Array(n);
@@ -539,16 +696,17 @@ var CNSStimulation = class {
539
696
  this.tryResolveCompleted();
540
697
  return;
541
698
  }
542
- const subscriberActivationTasks = [];
543
- if (collateral2 && !error) {
544
- 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);
545
703
  for (let i = 0; i < subscribers.length; i++) {
546
704
  const task = this.createSubscriberQueueItem(
547
705
  subscribers[i],
548
706
  outputSignal
549
707
  );
550
- subscriberActivationTasks.push(task);
551
- pending.add(task);
708
+ subscriberActivationTasks[i] = task;
709
+ this.addPositionedTask(pending, task, TASK_PENDING_POSITION);
552
710
  }
553
711
  }
554
712
  this.scheduledCount += subscriberActivationTasks.length;
@@ -562,16 +720,20 @@ var CNSStimulation = class {
562
720
  this.pendingActivations + this.activeOperations,
563
721
  this.pendingActivations,
564
722
  this.activeOperations,
565
- this.options?.maxNeuronHops && emitter ? this.neuronVisitMap?.get(emitter) ?? 0 : void 0,
723
+ this.options?.maxNeuronHops && emitter ? this.neuronVisitCounts?.[emitter.index] ?? 0 : void 0,
566
724
  error,
567
- emitter
725
+ emitter,
726
+ startedAt,
727
+ duration,
728
+ subscriberActivationTasks,
729
+ activationContext
568
730
  )
569
731
  );
570
732
  } catch (e) {
571
733
  this.onResponseError = e instanceof Error ? e : new Error(String(e));
572
734
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
573
735
  this.scheduledCount--;
574
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
736
+ this.removePendingTask(subscriberActivationTasks[i]);
575
737
  this.qEnqueue(subscriberActivationTasks[i]);
576
738
  }
577
739
  this.tryResolveCompleted();
@@ -584,7 +746,7 @@ var CNSStimulation = class {
584
746
  try {
585
747
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
586
748
  this.scheduledCount--;
587
- this.pendingTasks?.delete(
749
+ this.removePendingTask(
588
750
  subscriberActivationTasks[i]
589
751
  );
590
752
  this.qEnqueue(subscriberActivationTasks[i]);
@@ -600,7 +762,7 @@ var CNSStimulation = class {
600
762
  try {
601
763
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
602
764
  this.scheduledCount--;
603
- this.pendingTasks?.delete(
765
+ this.removePendingTask(
604
766
  subscriberActivationTasks[i]
605
767
  );
606
768
  this.qEnqueue(subscriberActivationTasks[i]);
@@ -615,7 +777,7 @@ var CNSStimulation = class {
615
777
  }
616
778
  for (let i = 0; i < subscriberActivationTasks.length; i++) {
617
779
  this.scheduledCount--;
618
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
780
+ this.removePendingTask(subscriberActivationTasks[i]);
619
781
  this.qEnqueue(subscriberActivationTasks[i]);
620
782
  }
621
783
  this.tryResolveCompleted();
@@ -642,36 +804,36 @@ var CNSStimulation = class {
642
804
  // src/CNSInstanceNeuronQueue.ts
643
805
  var CNSInstanceNeuronQueue = class {
644
806
  constructor() {
645
- this.gates = /* @__PURE__ */ new Map();
807
+ this.gatesByNeuronIndex = [];
646
808
  }
647
- run(neuron2, fn) {
648
- const limit = neuron2?.concurrency;
809
+ run(neuron, fn) {
810
+ const limit = neuron?.concurrency;
649
811
  if (limit === void 0 || limit <= 0) {
650
812
  return fn();
651
813
  }
652
- let gate = this.gates.get(neuron2);
814
+ let gate = this.gatesByNeuronIndex[neuron.index];
653
815
  if (!gate) {
654
816
  gate = { limit, active: 0, waiters: [] };
655
- this.gates.set(neuron2, gate);
817
+ this.gatesByNeuronIndex[neuron.index] = gate;
656
818
  } else {
657
819
  gate.limit = limit;
658
820
  }
821
+ const ownGate = gate;
659
822
  const wrap = (cb) => {
660
823
  return () => {
661
824
  try {
662
825
  cb();
663
826
  } finally {
664
- const g = this.gates.get(neuron2);
665
- g.active = Math.max(0, g.active - 1);
666
- if (g.waiters.length > 0) {
667
- 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();
668
830
  next();
669
831
  }
670
832
  }
671
833
  };
672
834
  };
673
- if (gate.active < gate.limit) {
674
- gate.active++;
835
+ if (ownGate.active < ownGate.limit) {
836
+ ownGate.active++;
675
837
  const ret = fn();
676
838
  if (ret && typeof ret.then === "function") {
677
839
  return ret.then((cb) => wrap(cb));
@@ -680,8 +842,7 @@ var CNSInstanceNeuronQueue = class {
680
842
  }
681
843
  return new Promise((resolve) => {
682
844
  const starter = () => {
683
- const g = this.gates.get(neuron2);
684
- g.active++;
845
+ ownGate.active++;
685
846
  const ret = fn();
686
847
  if (ret && typeof ret.then === "function") {
687
848
  ret.then((cb) => resolve(wrap(cb)));
@@ -689,12 +850,52 @@ var CNSInstanceNeuronQueue = class {
689
850
  resolve(wrap(ret));
690
851
  }
691
852
  };
692
- gate.waiters.push(starter);
853
+ ownGate.waiters.push(starter);
693
854
  });
694
855
  }
695
856
  };
696
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
+
697
897
  // src/CNSNetwork.ts
898
+ var NO_SUBSCRIBERS = [];
698
899
  var CNSNetwork = class {
699
900
  constructor(neurons) {
700
901
  this.neurons = neurons;
@@ -724,16 +925,19 @@ var CNSNetwork = class {
724
925
  * Fast lookup: collateral -> list of (neuron, dendrite) subscribers.
725
926
  * Built once at construction time.
726
927
  */
727
- this.subIndex = /* @__PURE__ */ new Map();
928
+ this.subscribersByCollateralIndex = [];
929
+ this.allSubscribersByCollateralIndex = [];
930
+ this.modalityCollateralIndices = [];
931
+ this.subscriberTablesByModalityIndex = [];
728
932
  this.parentNeuronByCollateral = /* @__PURE__ */ new Map();
729
933
  this.buildIndexes();
730
934
  }
731
935
  get stronglyConnectedComponents() {
732
- this.ensureSCC();
936
+ this.buildSccIndex();
733
937
  return this._stronglyConnectedComponents;
734
938
  }
735
939
  /** Build the SCC analysis the first time anything needs it. */
736
- ensureSCC() {
940
+ buildSccIndex() {
737
941
  if (this.sccBuilt) return;
738
942
  this.sccBuilt = true;
739
943
  this.buildSCC();
@@ -743,23 +947,21 @@ var CNSNetwork = class {
743
947
  */
744
948
  buildNeuronGraph() {
745
949
  const graph = /* @__PURE__ */ new Map();
746
- for (const neuron2 of this.neurons) {
747
- graph.set(neuron2, /* @__PURE__ */ new Set());
950
+ for (const neuron of this.neurons) {
951
+ graph.set(neuron, /* @__PURE__ */ new Set());
748
952
  }
749
- for (const neuron2 of this.neurons) {
953
+ for (const neuron of this.neurons) {
750
954
  const reachableNeurons = /* @__PURE__ */ new Set();
751
- for (const axonKey in neuron2.axon) {
752
- const collateral2 = neuron2.axon[axonKey];
753
- const subscribers = this.subIndex.get(
754
- collateral2
755
- );
955
+ for (const axonKey in neuron.axon) {
956
+ const collateral = neuron.axon[axonKey];
957
+ const subscribers = this.allSubscribersByCollateralIndex[collateral.index];
756
958
  if (subscribers) {
757
959
  for (const { neuron: targetNeuron } of subscribers) {
758
960
  reachableNeurons.add(targetNeuron);
759
961
  }
760
962
  }
761
963
  }
762
- graph.set(neuron2, reachableNeurons);
964
+ graph.set(neuron, reachableNeurons);
763
965
  }
764
966
  return graph;
765
967
  }
@@ -774,41 +976,41 @@ var CNSNetwork = class {
774
976
  const stack = [];
775
977
  const components = [];
776
978
  let currentIndex = 0;
777
- const strongConnect = (neuron2) => {
778
- index.set(neuron2, currentIndex);
779
- lowlink.set(neuron2, currentIndex);
979
+ const strongConnect = (neuron) => {
980
+ index.set(neuron, currentIndex);
981
+ lowlink.set(neuron, currentIndex);
780
982
  currentIndex++;
781
- stack.push(neuron2);
782
- onStack.add(neuron2);
783
- 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();
784
986
  for (const neighbor of Array.from(neighbors)) {
785
987
  if (!index.has(neighbor)) {
786
988
  strongConnect(neighbor);
787
989
  lowlink.set(
788
- neuron2,
789
- Math.min(lowlink.get(neuron2), lowlink.get(neighbor))
990
+ neuron,
991
+ Math.min(lowlink.get(neuron), lowlink.get(neighbor))
790
992
  );
791
993
  } else if (onStack.has(neighbor)) {
792
994
  lowlink.set(
793
- neuron2,
794
- Math.min(lowlink.get(neuron2), index.get(neighbor))
995
+ neuron,
996
+ Math.min(lowlink.get(neuron), index.get(neighbor))
795
997
  );
796
998
  }
797
999
  }
798
- if (lowlink.get(neuron2) === index.get(neuron2)) {
1000
+ if (lowlink.get(neuron) === index.get(neuron)) {
799
1001
  const component = /* @__PURE__ */ new Set();
800
1002
  let w;
801
1003
  do {
802
1004
  w = stack.pop();
803
1005
  onStack.delete(w);
804
1006
  component.add(w);
805
- } while (w !== neuron2);
1007
+ } while (w !== neuron);
806
1008
  components.push(component);
807
1009
  }
808
1010
  };
809
- for (const neuron2 of this.neurons) {
810
- if (!index.has(neuron2)) {
811
- strongConnect(neuron2);
1011
+ for (const neuron of this.neurons) {
1012
+ if (!index.has(neuron)) {
1013
+ strongConnect(neuron);
812
1014
  }
813
1015
  }
814
1016
  this._stronglyConnectedComponents = components;
@@ -830,8 +1032,8 @@ var CNSNetwork = class {
830
1032
  }
831
1033
  for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
832
1034
  const scc = this._stronglyConnectedComponents[i];
833
- for (const neuron2 of Array.from(scc)) {
834
- 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();
835
1037
  for (const neighbor of Array.from(neighbors)) {
836
1038
  const neighborSccIndex = this.neuronToSCC.get(neighbor);
837
1039
  if (neighborSccIndex !== void 0 && neighborSccIndex !== i) {
@@ -884,26 +1086,26 @@ var CNSNetwork = class {
884
1086
  * Returns true if the neuron is in a strongly connected component with more than one member,
885
1087
  * or if it's in a single-member SCC that can reach itself.
886
1088
  */
887
- getSCCSetByNeuron(neuron2) {
888
- this.ensureSCC();
889
- const sccIndex = this.neuronToSCC.get(neuron2);
1089
+ getSCCSetByNeuron(neuron) {
1090
+ this.buildSccIndex();
1091
+ const sccIndex = this.neuronToSCC.get(neuron);
890
1092
  if (sccIndex === void 0) return;
891
1093
  return this._stronglyConnectedComponents[sccIndex];
892
1094
  }
893
1095
  /**
894
1096
  * Get the SCC index for a given neuron
895
1097
  */
896
- getSccIndexByNeuron(neuron2) {
897
- this.ensureSCC();
898
- return this.neuronToSCC.get(neuron2);
1098
+ getSccIndexByNeuron(neuron) {
1099
+ this.buildSccIndex();
1100
+ return this.neuronToSCC.get(neuron);
899
1101
  }
900
1102
  /**
901
1103
  * Check if a neuron can be guaranteed not to be visited again during the current propagation.
902
1104
  * This is the core logic for safe context cleanup.
903
1105
  */
904
- canNeuronBeGuaranteedDone(neuron2, activeSccCounts) {
905
- this.ensureSCC();
906
- const sccIndex = this.neuronToSCC.get(neuron2);
1106
+ canNeuronBeGuaranteedDone(neuron, activeSccCounts) {
1107
+ this.buildSccIndex();
1108
+ const sccIndex = this.neuronToSCC.get(neuron);
907
1109
  if (sccIndex === void 0) return true;
908
1110
  if (activeSccCounts.get(sccIndex) && activeSccCounts.get(sccIndex) > 0) {
909
1111
  return false;
@@ -917,36 +1119,93 @@ var CNSNetwork = class {
917
1119
  }
918
1120
  return true;
919
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
+ }
920
1130
  buildIndexes() {
921
- this.subIndex.clear();
1131
+ var _a;
1132
+ this.allSubscribersByCollateralIndex = [];
1133
+ this.modalityCollateralIndices = [];
1134
+ this.subscriberTablesByModalityIndex = [];
922
1135
  this.parentNeuronByCollateral.clear();
923
- for (const neuron2 of this.neurons) {
924
- for (const dendrite of neuron2.dendrites) {
925
- const key = dendrite.collateral;
926
- const arr = this.subIndex.get(key) ?? [];
927
- arr.push({ neuron: neuron2, dendrite });
928
- this.subIndex.set(
929
- key,
930
- arr
931
- );
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`);
1140
+ }
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
+ }
932
1149
  }
933
- Object.values(neuron2.axon).forEach((collateral2) => {
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
+ }
934
1157
  this.parentNeuronByCollateral.set(
935
- collateral2,
936
- neuron2
1158
+ collateral,
1159
+ neuron
937
1160
  );
938
1161
  });
939
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;
940
1199
  }
941
- getParentNeuronByCollateral(collateral2) {
942
- return this.parentNeuronByCollateral.get(collateral2);
1200
+ getParentNeuronByCollateral(collateral) {
1201
+ return this.parentNeuronByCollateral.get(collateral);
943
1202
  }
944
1203
  // Computed on demand (rarely used): one dendrite per collateral, last wins —
945
1204
  // preserving the previous index semantics without building it every construct.
946
1205
  getDendrites() {
947
1206
  const byCollateral = /* @__PURE__ */ new Map();
948
- for (const neuron2 of this.neurons) {
949
- for (const dendrite of neuron2.dendrites) {
1207
+ for (const neuron of this.neurons) {
1208
+ for (const dendrite of neuron.dendrites) {
950
1209
  byCollateral.set(
951
1210
  dendrite.collateral,
952
1211
  dendrite
@@ -957,15 +1216,15 @@ var CNSNetwork = class {
957
1216
  }
958
1217
  getCollaterals() {
959
1218
  const collaterals = /* @__PURE__ */ new Set();
960
- for (const neuron2 of this.neurons) {
961
- for (const collateral2 of Object.values(neuron2.axon)) {
962
- collaterals.add(collateral2);
1219
+ for (const neuron of this.neurons) {
1220
+ for (const collateral of Object.values(neuron.axon)) {
1221
+ collaterals.add(collateral);
963
1222
  }
964
1223
  }
965
1224
  return Array.from(collaterals);
966
1225
  }
967
- getSubscribers(collateral2) {
968
- return this.subIndex.get(collateral2) ?? [];
1226
+ getSubscribers(collateral) {
1227
+ return this.subscribersByCollateralIndex[collateral.index] ?? NO_SUBSCRIBERS;
969
1228
  }
970
1229
  };
971
1230
 
@@ -987,6 +1246,8 @@ var CNS = class {
987
1246
  * Global batch-boundary listeners applied to every stimulation.
988
1247
  */
989
1248
  this.globalDrainListeners = [];
1249
+ this.batchDepth = 0;
1250
+ this.batchedDrains = [];
990
1251
  this.network = new CNSNetwork(this.neurons);
991
1252
  this.global = global;
992
1253
  }
@@ -1006,7 +1267,7 @@ var CNS = class {
1006
1267
  }
1007
1268
  /**
1008
1269
  * Register a listener called once at the end of every synchronous turn of
1009
- * 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
1010
1271
  * integration (a state manager, an in-memory database) installs a single
1011
1272
  * commit point for the whole organism instead of passing `onDrain` per call.
1012
1273
  *
@@ -1015,6 +1276,18 @@ var CNS = class {
1015
1276
  addDrainListener(listener) {
1016
1277
  return this.addListener(this.globalDrainListeners, listener);
1017
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
+ }
1290
+ }
1018
1291
  /**
1019
1292
  * Fan a drain out to the local and global listeners, or return undefined when
1020
1293
  * there are none so the stimulation can skip the boundary check entirely.
@@ -1024,7 +1297,7 @@ var CNS = class {
1024
1297
  */
1025
1298
  wrapOnDrain(local) {
1026
1299
  if (this.globalDrainListeners.length === 0 && !local) return void 0;
1027
- return (d) => {
1300
+ const announce = (d) => {
1028
1301
  if (local) {
1029
1302
  try {
1030
1303
  local(d);
@@ -1040,39 +1313,38 @@ var CNS = class {
1040
1313
  }
1041
1314
  }
1042
1315
  };
1316
+ return (d) => {
1317
+ if (this.batchDepth > 0) this.batchedDrains.push([announce, d]);
1318
+ else announce(d);
1319
+ };
1043
1320
  }
1044
1321
  wrapOnResponse(local) {
1045
1322
  if (this.globalResponseListeners.length === 0 && !local) {
1046
1323
  return void 0;
1047
1324
  }
1048
1325
  return (r) => {
1049
- let anyPromise = false;
1050
- const promises = [];
1326
+ let promises;
1051
1327
  if (local) {
1052
1328
  try {
1053
1329
  const res = local(r);
1054
1330
  if (res && typeof res.then === "function") {
1055
- anyPromise = true;
1056
- promises.push(res);
1331
+ (promises ?? (promises = [])).push(res);
1057
1332
  }
1058
1333
  } catch (error) {
1059
- anyPromise = true;
1060
- promises.push(Promise.reject(error));
1334
+ (promises ?? (promises = [])).push(Promise.reject(error));
1061
1335
  }
1062
1336
  }
1063
1337
  for (let i = 0; i < this.globalResponseListeners.length; i++) {
1064
1338
  try {
1065
1339
  const res = this.globalResponseListeners[i](r);
1066
1340
  if (res && typeof res.then === "function") {
1067
- anyPromise = true;
1068
- promises.push(res);
1341
+ (promises ?? (promises = [])).push(res);
1069
1342
  }
1070
1343
  } catch (error) {
1071
- anyPromise = true;
1072
- promises.push(Promise.reject(error));
1344
+ (promises ?? (promises = [])).push(Promise.reject(error));
1073
1345
  }
1074
1346
  }
1075
- if (anyPromise) {
1347
+ if (promises) {
1076
1348
  return Promise.allSettled(promises).then((results) => {
1077
1349
  const rejected = results.find(
1078
1350
  (r2) => r2.status === "rejected"
@@ -1107,8 +1379,20 @@ var CNS = class {
1107
1379
  }
1108
1380
  };
1109
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
+
1110
1390
  // src/CNSCollateral.ts
1391
+ var nextCollateralIndex = 0;
1111
1392
  var CNSCollateral = class {
1393
+ constructor() {
1394
+ this.index = nextCollateralIndex++;
1395
+ }
1112
1396
  createSignal(payload) {
1113
1397
  return { collateral: this, payload };
1114
1398
  }
@@ -1164,8 +1448,11 @@ var CNSStimulationGate = class {
1164
1448
  };
1165
1449
 
1166
1450
  // src/factory/index.ts
1167
- var collateral = () => new CNSCollateral();
1168
- var neuron = (axon) => {
1451
+ var nextNeuronIndex = 0;
1452
+ var nextAfferentPathIndex = 0;
1453
+ var nextModalityIndex = 0;
1454
+ var cnsCollateral = () => new CNSCollateral();
1455
+ var cnsNeuron = (axon) => {
1169
1456
  const dendrites = [];
1170
1457
  const api = {
1171
1458
  setConcurrency: (n) => {
@@ -1189,15 +1476,16 @@ var neuron = (axon) => {
1189
1476
  }
1190
1477
  return api;
1191
1478
  }),
1479
+ index: nextNeuronIndex++,
1192
1480
  axon,
1193
1481
  concurrency: void 0,
1194
1482
  maxDuration: void 0,
1195
1483
  dendrites,
1196
1484
  dendrite(s) {
1197
1485
  if ("collateral" in s && "response" in s && Array.isArray(s.collateral)) {
1198
- for (const collateral2 of s.collateral) {
1486
+ for (const collateral of s.collateral) {
1199
1487
  dendrites.push({
1200
- collateral: collateral2,
1488
+ collateral,
1201
1489
  response: s.response
1202
1490
  });
1203
1491
  }
@@ -1210,7 +1498,7 @@ var neuron = (axon) => {
1210
1498
  },
1211
1499
  modalityDendrite(s) {
1212
1500
  const {
1213
- collateral: collateral2,
1501
+ collateral,
1214
1502
  modality: singleModality,
1215
1503
  modalities: multipleModalities,
1216
1504
  afferentPaths: singleAfferentPaths,
@@ -1227,83 +1515,52 @@ var neuron = (axon) => {
1227
1515
  const response = (payload, axon2, ctx) => {
1228
1516
  const stimulation = ctx.stimulation;
1229
1517
  const stimOptions = stimulation?.options;
1230
- const runGlobalDefault = () => {
1231
- if (!defaultHandler) {
1232
- throw new Error(
1233
- `modalityDendrite: No handler found for modality and no default handler provided`
1234
- );
1235
- }
1236
- const res = defaultHandler(payload, axon2, ctx);
1237
- if (res && typeof res.then === "function") {
1238
- return res.then(
1239
- (result) => output(result, axon2, ctx)
1240
- );
1241
- }
1242
- return output(res, axon2, ctx);
1243
- };
1244
- if (!stimOptions || !stimOptions.modality) {
1245
- return runGlobalDefault();
1246
- }
1247
- const matchingConfig = modalityConfigs.find(
1248
- (config) => config.modality === stimOptions.modality
1518
+ const handler = cnsResolveModalityHandler(
1519
+ modalitySelection,
1520
+ stimOptions?.modality,
1521
+ stimOptions?.afferentPath
1249
1522
  );
1250
- if (!matchingConfig) {
1251
- return runGlobalDefault();
1252
- }
1253
- const stimAfferentPath = stimOptions.afferentPath;
1254
- const handler = stimAfferentPath && matchingConfig.afferentPaths ? matchingConfig.afferentPaths.get(stimAfferentPath) : void 0;
1255
- const effectiveHandler = handler ?? matchingConfig.default ?? defaultHandler;
1256
- if (!effectiveHandler) {
1523
+ if (handler === void 0) {
1257
1524
  throw new Error(
1258
1525
  `modalityDendrite: No handler found for afferent path in modality and no default handler provided`
1259
1526
  );
1260
1527
  }
1261
- const maybeResult = effectiveHandler(payload, axon2, ctx);
1262
- if (maybeResult && typeof maybeResult.then === "function") {
1263
- return maybeResult.then(
1264
- (result) => output(result, axon2, ctx)
1265
- );
1266
- }
1267
- try {
1268
- return output(maybeResult, axon2, ctx);
1269
- } catch (error) {
1270
- if (error instanceof Error && error.message.includes(
1271
- "Cannot read properties of undefined"
1272
- )) {
1273
- const axonKeys = axon2 && typeof axon2 === "object" ? Object.keys(axon2) : "not an object";
1274
- throw new Error(
1275
- `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}`
1276
- );
1277
- }
1278
- throw error;
1279
- }
1528
+ return cnsComposeModalityResponse(handler, output)(payload, axon2, ctx);
1529
+ };
1530
+ const modalitySelection = {
1531
+ modalityConfigs,
1532
+ defaultHandler,
1533
+ output
1280
1534
  };
1281
1535
  dendrites.push({
1282
- collateral: collateral2,
1283
- response
1536
+ collateral,
1537
+ response,
1538
+ modalitySelection
1284
1539
  });
1285
1540
  return api;
1286
1541
  }
1287
1542
  };
1288
1543
  return api;
1289
1544
  };
1290
- var neuronFactory = () => {
1545
+ var cnsNeuronFactory = () => {
1291
1546
  const factory = {
1292
1547
  withGlobal: () => factory,
1293
- neuron: (axon) => neuron(axon)
1548
+ neuron: (axon) => cnsNeuron(axon)
1294
1549
  };
1295
1550
  return factory;
1296
1551
  };
1297
- var withGlobal = () => neuronFactory();
1298
- var afferentPath = (parentAfferentPath) => {
1552
+ var withCNSGlobal = () => cnsNeuronFactory();
1553
+ var cnsAfferentPath = (parentAfferentPath) => {
1299
1554
  return {
1555
+ index: nextAfferentPathIndex++,
1300
1556
  ...parentAfferentPath !== void 0 && {
1301
1557
  parentAfferentPath
1302
1558
  }
1303
1559
  };
1304
1560
  };
1305
- var modality = (afferentPaths) => {
1561
+ var cnsModality = (afferentPaths) => {
1306
1562
  return {
1563
+ index: nextModalityIndex++,
1307
1564
  afferentPaths
1308
1565
  };
1309
1566
  };
@@ -1311,11 +1568,12 @@ var modality = (afferentPaths) => {
1311
1568
  exports.CNS = CNS;
1312
1569
  exports.CNSCollateral = CNSCollateral;
1313
1570
  exports.CNSStimulationGate = CNSStimulationGate;
1314
- exports.afferentPath = afferentPath;
1315
- exports.collateral = collateral;
1316
- exports.modality = modality;
1317
- exports.neuron = neuron;
1318
- exports.neuronFactory = neuronFactory;
1319
- 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;
1320
1578
  //# sourceMappingURL=index.cjs.map
1321
1579
  //# sourceMappingURL=index.cjs.map