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