@cnstra/core 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/CNS.d.ts +32 -10
  2. package/dist/CNS.d.ts.map +1 -1
  3. package/dist/CNSCollateral.d.ts +2 -2
  4. package/dist/CNSCollateral.d.ts.map +1 -1
  5. package/dist/CNSNetwork.d.ts +7 -7
  6. package/dist/CNSNetwork.d.ts.map +1 -1
  7. package/dist/CNSStimulation.d.ts +55 -15
  8. package/dist/CNSStimulation.d.ts.map +1 -1
  9. package/dist/CNSStimulationContextStore.d.ts +1 -1
  10. package/dist/CNSStimulationContextStore.d.ts.map +1 -1
  11. package/dist/factory/index.d.ts +26 -82
  12. package/dist/factory/index.d.ts.map +1 -1
  13. package/dist/index.cjs +235 -136
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.ts +1 -14
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +236 -134
  18. package/dist/index.js.map +1 -1
  19. package/dist/utils/CNSStimulationGate.d.ts +7 -7
  20. package/dist/utils/CNSStimulationGate.d.ts.map +1 -1
  21. package/package.json +4 -1
  22. package/dist/CNSPersistOptionsRegistry.d.ts +0 -28
  23. package/dist/CNSPersistOptionsRegistry.d.ts.map +0 -1
  24. package/dist/interfaces/ICNS.d.ts +0 -20
  25. package/dist/interfaces/ICNS.d.ts.map +0 -1
  26. package/dist/interfaces/ICNSCollateral.d.ts +0 -6
  27. package/dist/interfaces/ICNSCollateral.d.ts.map +0 -1
  28. package/dist/interfaces/ICNSStimulationContextStore.d.ts +0 -8
  29. package/dist/interfaces/ICNSStimulationContextStore.d.ts.map +0 -1
  30. package/dist/types/TCNSAfferentPath.d.ts +0 -4
  31. package/dist/types/TCNSAfferentPath.d.ts.map +0 -1
  32. package/dist/types/TCNSAxon.d.ts +0 -26
  33. package/dist/types/TCNSAxon.d.ts.map +0 -1
  34. package/dist/types/TCNSDendrite.d.ts +0 -9
  35. package/dist/types/TCNSDendrite.d.ts.map +0 -1
  36. package/dist/types/TCNSHandlerContext.d.ts +0 -24
  37. package/dist/types/TCNSHandlerContext.d.ts.map +0 -1
  38. package/dist/types/TCNSLocalContextValueStore.d.ts +0 -6
  39. package/dist/types/TCNSLocalContextValueStore.d.ts.map +0 -1
  40. package/dist/types/TCNSModality.d.ts +0 -5
  41. package/dist/types/TCNSModality.d.ts.map +0 -1
  42. package/dist/types/TCNSNeuron.d.ts +0 -12
  43. package/dist/types/TCNSNeuron.d.ts.map +0 -1
  44. package/dist/types/TCNSNeuronActivationTask.d.ts +0 -8
  45. package/dist/types/TCNSNeuronActivationTask.d.ts.map +0 -1
  46. package/dist/types/TCNSNeuronActivationTaskFailure.d.ts +0 -7
  47. package/dist/types/TCNSNeuronActivationTaskFailure.d.ts.map +0 -1
  48. package/dist/types/TCNSNeuronResponseReturn.d.ts +0 -9
  49. package/dist/types/TCNSNeuronResponseReturn.d.ts.map +0 -1
  50. package/dist/types/TCNSOptions.d.ts +0 -11
  51. package/dist/types/TCNSOptions.d.ts.map +0 -1
  52. package/dist/types/TCNSPersist.d.ts +0 -16
  53. package/dist/types/TCNSPersist.d.ts.map +0 -1
  54. package/dist/types/TCNSSignal.d.ts +0 -6
  55. package/dist/types/TCNSSignal.d.ts.map +0 -1
  56. package/dist/types/TCNSStimulationOptions.d.ts +0 -14
  57. package/dist/types/TCNSStimulationOptions.d.ts.map +0 -1
  58. package/dist/types/TCNSStimulationQueueItem.d.ts +0 -11
  59. package/dist/types/TCNSStimulationQueueItem.d.ts.map +0 -1
  60. package/dist/types/TCNSStimulationResponse.d.ts +0 -17
  61. package/dist/types/TCNSStimulationResponse.d.ts.map +0 -1
  62. package/dist/types/TCNSStimulationSerializedContextValue.d.ts +0 -4
  63. package/dist/types/TCNSStimulationSerializedContextValue.d.ts.map +0 -1
  64. package/dist/types/TCNSSubscriber.d.ts +0 -7
  65. package/dist/types/TCNSSubscriber.d.ts.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,21 +1,8 @@
1
1
  export { CNS } from './CNS';
2
- export { CNSPersistOptionsRegistry } from './CNSPersistOptionsRegistry';
3
2
  export { CNSCollateral } from './CNSCollateral';
4
3
  export { CNSStimulationContextStore } from './CNSStimulationContextStore';
5
4
  export { CNSStimulationGate } from './utils/CNSStimulationGate';
6
- export type { ICNS } from './interfaces/ICNS';
7
- export type { TCNSNeuron } from './types/TCNSNeuron';
8
- export type { TCNSAxon } from './types/TCNSAxon';
9
- export type { TCNSDendrite } from './types/TCNSDendrite';
10
- export type { TCNSHandlerContext } from './types/TCNSHandlerContext';
11
- export type { TCNSSignal } from './types/TCNSSignal';
12
- export type { TCNSNeuronActivationTask } from './types/TCNSNeuronActivationTask';
13
- export type { TCNSStimulationOptions } from './types/TCNSStimulationOptions';
14
- export type { TCNSModality } from './types/TCNSModality';
15
- export type { TCNSAfferentPath } from './types/TCNSAfferentPath';
16
- export type { TCNSNeuronActivationTaskFailure } from './types/TCNSNeuronActivationTaskFailure';
17
- export type { TCNSNeuronPersistOptions, TCNSCollateralPersistOptions, TCNSStimulationPersistOptions } from './types/TCNSPersist';
18
5
  export type { TCNSStimulationGateOptions, TCNSStimulationGateSignal } from './utils/CNSStimulationGate';
19
- export { collateral, neuron, neuronFactory, withCtx, withGlobal, afferentPath, modality, createPersistRegistry, createCNS, } from './factory/index';
6
+ export { collateral, neuron, neuronFactory, withCtx, withGlobal, afferentPath, modality, } from './factory/index';
20
7
  export type { TNeuronFactory } from './factory/index';
21
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAC5B,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAEhE,YAAY,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAE9C,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AACjF,YAAY,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,YAAY,EAAE,+BAA+B,EAAE,MAAM,yCAAyC,CAAC;AAC/F,YAAY,EAAE,wBAAwB,EAAE,4BAA4B,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAC;AACjI,YAAY,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAGxG,OAAO,EACH,UAAU,EACV,MAAM,EACN,aAAa,EACb,OAAO,EACP,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,qBAAqB,EACrB,SAAS,GACZ,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAG5B,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAKhE,YAAY,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAGxG,OAAO,EACH,UAAU,EACV,MAAM,EACN,aAAa,EACb,OAAO,EACP,UAAU,EACV,YAAY,EACZ,QAAQ,GACX,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -37,6 +37,13 @@ var CNSDendriteContext = class {
37
37
  get global() {
38
38
  return this.cns.global;
39
39
  }
40
+ // The caller-supplied per-stimulation context (`stimulate(sig, {stimulationContext})`),
41
+ // the same value on every hop — the environment-agnostic way to correlate logs to a
42
+ // run (pass `{ stimulationId, attemptNumber }`). Prototype getter like `global`:
43
+ // zero extra allocation, read only where a handler reaches for it.
44
+ get stimulationContext() {
45
+ return this.stimulation.options?.stimulationContext;
46
+ }
40
47
  get() {
41
48
  return this.stimulation.getContext().get(this.neuron);
42
49
  }
@@ -48,21 +55,24 @@ var CNSDendriteContext = class {
48
55
  }
49
56
  };
50
57
  var CNSStimulationResponseImpl = class {
51
- constructor(stimulation, store, inputSignal, outputSignal, queueLength, hops, error) {
58
+ constructor(stimulation, store, inputSignal, outputSignal, queueLength, pendingActivations, activeActivations, hops, error, neuron2) {
52
59
  this.stimulation = stimulation;
53
60
  this.store = store;
54
61
  this.inputSignal = inputSignal;
55
62
  this.outputSignal = outputSignal;
56
63
  this.queueLength = queueLength;
64
+ this.pendingActivations = pendingActivations;
65
+ this.activeActivations = activeActivations;
57
66
  this.hops = hops;
58
67
  this.error = error;
68
+ this.neuron = neuron2;
59
69
  }
60
70
  get contextValue() {
61
71
  return this.store ? this.store.getAll() : /* @__PURE__ */ new Map();
62
72
  }
63
73
  };
64
74
  var CNSStimulation = class {
65
- constructor(cns, instanceNeuronQueue, options, onResponse) {
75
+ constructor(cns, instanceNeuronQueue, options, onResponse, onDrain) {
66
76
  this.cns = cns;
67
77
  this.options = options;
68
78
  this.qHead = 0;
@@ -73,9 +83,20 @@ var CNSStimulation = class {
73
83
  // Re-entrancy guard for the pump loop.
74
84
  this.pumping = false;
75
85
  this.needsPump = false;
86
+ // Nesting depth of the current synchronous turn. Only the outermost close
87
+ // announces a drain - see beginTurn/endTurn.
88
+ this.turnDepth = 0;
76
89
  this.scheduledCount = 0;
90
+ // Activations belonging to signals of an in-progress array fan-out that have
91
+ // not been handed to processResponse yet, so the task objects do not exist.
92
+ // processResponse enqueues synchronously, so the first signal's whole subtree
93
+ // drains before the second signal is even looked at; without this counter
94
+ // that subtree's leaf sees an empty queue and reads as "all work done".
95
+ // Counted in activations, not signals, so every public counter shares a unit.
96
+ this.reservedActivations = 0;
77
97
  this.isCompleted = false;
78
98
  this.completionRejected = false;
99
+ this.onDrain = onDrain;
79
100
  this.ctxStore = options?.ctx;
80
101
  this.instanceNeuronQueue = instanceNeuronQueue;
81
102
  this.onResponse = onResponse;
@@ -172,18 +193,28 @@ var CNSStimulation = class {
172
193
  (cb) => {
173
194
  this.activeOperations--;
174
195
  this.activeTasks.delete(task);
175
- if (typeof cb === "function") cb();
176
- if (this.qSize > 0 && this.canStartOperation) {
177
- if (this.pumping) this.needsPump = true;
178
- else this.pump();
196
+ this.beginTurn();
197
+ try {
198
+ if (typeof cb === "function") cb();
199
+ if (this.qSize > 0 && this.canStartOperation) {
200
+ if (this.pumping) this.needsPump = true;
201
+ else this.pump();
202
+ }
203
+ } finally {
204
+ this.endTurn();
179
205
  }
180
206
  },
181
207
  (err) => {
182
208
  this.activeOperations--;
183
209
  this.activeTasks.delete(task);
184
- if (this.qSize > 0 && this.canStartOperation) {
185
- if (this.pumping) this.needsPump = true;
186
- else this.pump();
210
+ this.beginTurn();
211
+ try {
212
+ if (this.qSize > 0 && this.canStartOperation) {
213
+ if (this.pumping) this.needsPump = true;
214
+ else this.pump();
215
+ }
216
+ } finally {
217
+ this.endTurn();
187
218
  }
188
219
  throw err;
189
220
  }
@@ -199,10 +230,54 @@ var CNSStimulation = class {
199
230
  this.pump();
200
231
  }
201
232
  }
233
+ /**
234
+ * Open a synchronous turn. Turns nest: a drain is announced only when the
235
+ * outermost one closes, so an enqueue that happens while the pump is already
236
+ * running does not produce its own boundary.
237
+ */
238
+ beginTurn() {
239
+ this.turnDepth++;
240
+ }
241
+ /**
242
+ * Close a synchronous turn and, if it was the outermost and nothing more can
243
+ * run without yielding to the event loop, announce the drain.
244
+ *
245
+ * This is the only place the synchronous frontier is observable. A response
246
+ * cannot serve the purpose: responses are emitted when an activation
247
+ * *finishes*, but a turn just as often ends when one *starts* and returns a
248
+ * promise - at which point there is nothing left to emit a response from.
249
+ */
250
+ endTurn() {
251
+ if (--this.turnDepth > 0) return;
252
+ const onDrain = this.onDrain;
253
+ if (!onDrain) return;
254
+ if (this.pumping) return;
255
+ if (this.qSize > 0 && this.canStartOperation) return;
256
+ this.turnDepth++;
257
+ try {
258
+ onDrain({
259
+ stimulation: this,
260
+ queueLength: this.pendingActivations + this.activeOperations,
261
+ pendingActivations: this.pendingActivations,
262
+ activeActivations: this.activeOperations
263
+ });
264
+ } finally {
265
+ this.turnDepth--;
266
+ }
267
+ }
268
+ /**
269
+ * Activations that exist (or are reserved) but whose dendrite body has not
270
+ * been invoked yet: sitting in the ring buffer, created and held in
271
+ * pendingTasks, or reserved by an array fan-out mid-dispatch. Subscribers
272
+ * parked behind an async onResponse land here too - their body has not run.
273
+ */
274
+ get pendingActivations() {
275
+ return this.qSize + this.scheduledCount + this.reservedActivations;
276
+ }
202
277
  tryResolveCompleted() {
203
278
  if (this.isCompleted) return;
204
279
  const noActive = this.activeOperations === 0;
205
- const noPending = this.qSize + this.scheduledCount === 0;
280
+ const noPending = this.pendingActivations === 0;
206
281
  const aborted = !!this.options?.abortSignal?.aborted;
207
282
  if (noPending && noActive || aborted && noActive && !noPending) {
208
283
  if (aborted) {
@@ -264,6 +339,19 @@ var CNSStimulation = class {
264
339
  getFailedTasks() {
265
340
  return this.failedTasks ? [...this.failedTasks] : [];
266
341
  }
342
+ /**
343
+ * The current outstanding frontier — every task that has not completed:
344
+ * queued, active, and pending (awaiting an async onResponse), plus any
345
+ * failed/aborted tasks. This is the set to persist for resume: re-activating
346
+ * it re-runs exactly the not-yet-done work. Safe to call at any point during
347
+ * a run (for progress) or after it fails.
348
+ */
349
+ getOutstandingTasks() {
350
+ return [
351
+ ...this.getAllActivationTasks(),
352
+ ...this.getFailedTasks().map((f) => f.task)
353
+ ];
354
+ }
267
355
  /**
268
356
  * Get the context store for this stimulation
269
357
  */
@@ -274,10 +362,15 @@ var CNSStimulation = class {
274
362
  * Enqueue activation tasks directly into the stimulation queue
275
363
  */
276
364
  enqueueTasks(tasks) {
277
- for (const task of tasks) {
278
- this.qEnqueue(task);
365
+ this.beginTurn();
366
+ try {
367
+ for (const task of tasks) {
368
+ this.qEnqueue(task);
369
+ }
370
+ this.tryResolveCompleted();
371
+ } finally {
372
+ this.endTurn();
279
373
  }
280
- this.tryResolveCompleted();
281
374
  }
282
375
  get concurrencyEnabled() {
283
376
  return this.options?.concurrency !== void 0 && this.options?.concurrency > 0;
@@ -378,14 +471,19 @@ var CNSStimulation = class {
378
471
  }
379
472
  }
380
473
  if (!subscriber) {
474
+ const error = new Error(`Subscriber not found for activation task`);
381
475
  this.pushFailedTask({
382
476
  task: neuronActivationTask,
383
- error: new Error(
384
- `Subscriber not found for activation task`
385
- ),
477
+ error,
386
478
  aborted: false
387
479
  });
388
480
  return () => {
481
+ this.processResponseOrResponses(
482
+ void 0,
483
+ neuronActivationTask.input,
484
+ void 0,
485
+ error
486
+ );
389
487
  };
390
488
  }
391
489
  const neuron2 = subscriber.neuron;
@@ -432,13 +530,13 @@ var CNSStimulation = class {
432
530
  error: error instanceof Error ? error : new Error(String(error)),
433
531
  aborted: isAborted
434
532
  });
435
- this.processResponseOrResponses(
436
- neuron2,
437
- inputSignal,
438
- void 0,
439
- error
440
- );
441
533
  return () => {
534
+ this.processResponseOrResponses(
535
+ neuron2,
536
+ inputSignal,
537
+ void 0,
538
+ error
539
+ );
442
540
  };
443
541
  }
444
542
  const maxDuration = neuron2.maxDuration;
@@ -508,8 +606,26 @@ var CNSStimulation = class {
508
606
  this.processResponse(emitter, inputSignal, void 0, error);
509
607
  return;
510
608
  }
511
- for (const signal of outputSignalOrSignals) {
512
- this.processResponse(emitter, inputSignal, signal, error);
609
+ let perSignal;
610
+ if (!error) {
611
+ perSignal = new Array(outputSignalOrSignals.length);
612
+ let reserved = 0;
613
+ for (let i = 0; i < outputSignalOrSignals.length; i++) {
614
+ const c = outputSignalOrSignals[i]?.collateral;
615
+ const n = c ? this.cns.network.getSubscribers(c).length : 0;
616
+ perSignal[i] = n;
617
+ reserved += n;
618
+ }
619
+ this.reservedActivations += reserved;
620
+ }
621
+ for (let i = 0; i < outputSignalOrSignals.length; i++) {
622
+ if (perSignal) this.reservedActivations -= perSignal[i];
623
+ this.processResponse(
624
+ emitter,
625
+ inputSignal,
626
+ outputSignalOrSignals[i],
627
+ error
628
+ );
513
629
  }
514
630
  return;
515
631
  }
@@ -563,9 +679,12 @@ var CNSStimulation = class {
563
679
  this.ctxStore,
564
680
  inputSignal,
565
681
  outputSignal,
566
- this.qSize + this.scheduledCount,
682
+ this.pendingActivations + this.activeOperations,
683
+ this.pendingActivations,
684
+ this.activeOperations,
567
685
  this.options?.maxNeuronHops && emitter ? this.neuronVisitMap?.get(emitter) ?? 0 : void 0,
568
- error
686
+ error,
687
+ emitter
569
688
  )
570
689
  );
571
690
  } catch (e) {
@@ -581,21 +700,35 @@ var CNSStimulation = class {
581
700
  if (maybePromise && typeof maybePromise.then === "function") {
582
701
  maybePromise.then(
583
702
  () => {
584
- for (let i = 0; i < subscriberActivationTasks.length; i++) {
585
- this.scheduledCount--;
586
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
587
- this.qEnqueue(subscriberActivationTasks[i]);
703
+ this.beginTurn();
704
+ try {
705
+ for (let i = 0; i < subscriberActivationTasks.length; i++) {
706
+ this.scheduledCount--;
707
+ this.pendingTasks?.delete(
708
+ subscriberActivationTasks[i]
709
+ );
710
+ this.qEnqueue(subscriberActivationTasks[i]);
711
+ }
712
+ this.tryResolveCompleted();
713
+ } finally {
714
+ this.endTurn();
588
715
  }
589
- this.tryResolveCompleted();
590
716
  },
591
717
  (error2) => {
592
718
  this.onResponseError = error2 instanceof Error ? error2 : new Error(String(error2));
593
- for (let i = 0; i < subscriberActivationTasks.length; i++) {
594
- this.scheduledCount--;
595
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
596
- this.qEnqueue(subscriberActivationTasks[i]);
719
+ this.beginTurn();
720
+ try {
721
+ for (let i = 0; i < subscriberActivationTasks.length; i++) {
722
+ this.scheduledCount--;
723
+ this.pendingTasks?.delete(
724
+ subscriberActivationTasks[i]
725
+ );
726
+ this.qEnqueue(subscriberActivationTasks[i]);
727
+ }
728
+ this.tryResolveCompleted();
729
+ } finally {
730
+ this.endTurn();
597
731
  }
598
- this.tryResolveCompleted();
599
732
  }
600
733
  );
601
734
  return;
@@ -608,12 +741,21 @@ var CNSStimulation = class {
608
741
  this.tryResolveCompleted();
609
742
  }
610
743
  responseToSignal(signalOrSignals) {
611
- if (Array.isArray(signalOrSignals)) {
612
- this.processResponseOrResponses(void 0, void 0, signalOrSignals);
613
- return;
744
+ this.beginTurn();
745
+ try {
746
+ if (Array.isArray(signalOrSignals)) {
747
+ this.processResponseOrResponses(
748
+ void 0,
749
+ void 0,
750
+ signalOrSignals
751
+ );
752
+ return;
753
+ }
754
+ this.processResponse(void 0, void 0, signalOrSignals);
755
+ this.tryResolveCompleted();
756
+ } finally {
757
+ this.endTurn();
614
758
  }
615
- this.processResponse(void 0, void 0, signalOrSignals);
616
- this.tryResolveCompleted();
617
759
  }
618
760
  };
619
761
 
@@ -960,6 +1102,10 @@ var CNS = class {
960
1102
  * Global response listeners applied to every stimulation.
961
1103
  */
962
1104
  this.globalResponseListeners = [];
1105
+ /**
1106
+ * Global batch-boundary listeners applied to every stimulation.
1107
+ */
1108
+ this.globalDrainListeners = [];
963
1109
  this.network = new CNSNetwork(this.neurons);
964
1110
  this.global = global;
965
1111
  }
@@ -973,6 +1119,50 @@ var CNS = class {
973
1119
  if (idx >= 0) this.globalResponseListeners.splice(idx, 1);
974
1120
  };
975
1121
  }
1122
+ /**
1123
+ * Register a listener called once at the end of every synchronous turn of
1124
+ * every stimulation - see {@link TCNSStimulationDrain}. This is how an
1125
+ * integration (a state manager, an in-memory database) installs a single
1126
+ * commit point for the whole organism instead of passing `onDrain` per call.
1127
+ *
1128
+ * Returns an unsubscribe function.
1129
+ */
1130
+ addDrainListener(listener) {
1131
+ this.globalDrainListeners.push(listener);
1132
+ let active = true;
1133
+ return () => {
1134
+ if (!active) return;
1135
+ active = false;
1136
+ const idx = this.globalDrainListeners.indexOf(listener);
1137
+ if (idx >= 0) this.globalDrainListeners.splice(idx, 1);
1138
+ };
1139
+ }
1140
+ /**
1141
+ * Fan a drain out to the local and global listeners, or return undefined when
1142
+ * there are none so the stimulation can skip the boundary check entirely.
1143
+ *
1144
+ * Listener errors are isolated: a drain is a notification, and one broken
1145
+ * flush must not stop the others or derail the stimulation that triggered it.
1146
+ */
1147
+ wrapOnDrain(local) {
1148
+ if (this.globalDrainListeners.length === 0 && !local) return void 0;
1149
+ return (d) => {
1150
+ if (local) {
1151
+ try {
1152
+ local(d);
1153
+ } catch (error) {
1154
+ console.error("[CNS] onDrain listener threw", error);
1155
+ }
1156
+ }
1157
+ for (let i = 0; i < this.globalDrainListeners.length; i++) {
1158
+ try {
1159
+ this.globalDrainListeners[i](d);
1160
+ } catch (error) {
1161
+ console.error("[CNS] drain listener threw", error);
1162
+ }
1163
+ }
1164
+ };
1165
+ }
976
1166
  wrapOnResponse(local) {
977
1167
  if (this.globalResponseListeners.length === 0 && !local) {
978
1168
  return void 0;
@@ -1020,7 +1210,8 @@ var CNS = class {
1020
1210
  this,
1021
1211
  this.instanceNeuronQueue,
1022
1212
  options,
1023
- wrapped
1213
+ wrapped,
1214
+ this.wrapOnDrain(options?.onDrain)
1024
1215
  );
1025
1216
  stimulation.responseToSignal(signalOrSignals);
1026
1217
  return stimulation;
@@ -1034,79 +1225,14 @@ var CNS = class {
1034
1225
  this,
1035
1226
  this.instanceNeuronQueue,
1036
1227
  options,
1037
- wrapped
1228
+ wrapped,
1229
+ this.wrapOnDrain(options?.onDrain)
1038
1230
  );
1039
1231
  stimulation.enqueueTasks(tasks);
1040
1232
  return stimulation;
1041
1233
  }
1042
1234
  };
1043
1235
 
1044
- // src/CNSPersistOptionsRegistry.ts
1045
- var CNSPersistOptionsRegistry = class {
1046
- constructor() {
1047
- this.neurons = /* @__PURE__ */ new Map();
1048
- this.neuronNames = /* @__PURE__ */ new WeakMap();
1049
- this.collaterals = /* @__PURE__ */ new Map();
1050
- this.stimulations = /* @__PURE__ */ new Map();
1051
- }
1052
- addNeuron(neuron2, options) {
1053
- this.neurons.set(options.name, neuron2);
1054
- this.neuronNames.set(neuron2, options.name);
1055
- }
1056
- getNeuron(name) {
1057
- return this.neurons.get(name);
1058
- }
1059
- getNeuronName(neuron2) {
1060
- return this.neuronNames.get(neuron2);
1061
- }
1062
- getNamedNeurons() {
1063
- return this.neurons;
1064
- }
1065
- getCollateralName(collateral2) {
1066
- for (const [name, col] of this.collaterals) {
1067
- if (col === collateral2) return name;
1068
- }
1069
- return void 0;
1070
- }
1071
- removeNeuron(name) {
1072
- const neuron2 = this.neurons.get(name);
1073
- if (neuron2) this.neuronNames.delete(neuron2);
1074
- this.neurons.delete(name);
1075
- }
1076
- addCollateral(collateral2, options) {
1077
- this.collaterals.set(options.name, collateral2);
1078
- }
1079
- getCollateral(name) {
1080
- return this.collaterals.get(name);
1081
- }
1082
- removeCollateral(name) {
1083
- this.collaterals.delete(name);
1084
- }
1085
- addStimulation(stimulation, options) {
1086
- this.stimulations.set(options.stimulationId, stimulation);
1087
- }
1088
- getStimulation(stimulationId) {
1089
- return this.stimulations.get(stimulationId);
1090
- }
1091
- removeStimulation(stimulationId) {
1092
- this.stimulations.delete(stimulationId);
1093
- }
1094
- register(name, neuron2, collateralNames) {
1095
- this.addNeuron(neuron2, { name, neuron: neuron2 });
1096
- for (const [key, col] of Object.entries(neuron2.axon)) {
1097
- this.addCollateral(col, {
1098
- name: collateralNames?.[key] ?? key,
1099
- collateral: col
1100
- });
1101
- }
1102
- return this;
1103
- }
1104
- registerCollateral(name, collateral2) {
1105
- this.addCollateral(collateral2, { name, collateral: collateral2 });
1106
- return this;
1107
- }
1108
- };
1109
-
1110
1236
  // src/CNSCollateral.ts
1111
1237
  var CNSCollateral = class {
1112
1238
  createSignal(payload) {
@@ -1309,31 +1435,7 @@ var modality = (afferentPaths) => {
1309
1435
  afferentPaths
1310
1436
  };
1311
1437
  };
1312
- var createPersistRegistry = (namedNeurons) => {
1313
- const registry = new CNSPersistOptionsRegistry();
1314
- for (const [name, entry] of Object.entries(namedNeurons)) {
1315
- const isEntry = "neuron" in entry && !("axon" in entry);
1316
- const neuron2 = isEntry ? entry.neuron : entry;
1317
- const colNames = isEntry ? entry.collaterals ?? {} : {};
1318
- registry.addNeuron(neuron2, { name, neuron: neuron2 });
1319
- for (const [key, col] of Object.entries(neuron2.axon)) {
1320
- registry.addCollateral(col, {
1321
- name: colNames[key] ?? key,
1322
- collateral: col
1323
- });
1324
- }
1325
- }
1326
- return registry;
1327
- };
1328
- var createCNS = (namedNeurons, options) => {
1329
- const neurons = Object.values(namedNeurons).map(
1330
- (entry) => entry && typeof entry === "object" && "neuron" in entry ? entry.neuron : entry
1331
- );
1332
- const cns = new CNS(neurons, options, options?.global);
1333
- const registry = createPersistRegistry(namedNeurons);
1334
- return { cns, registry };
1335
- };
1336
1438
 
1337
- export { CNS, CNSCollateral, CNSPersistOptionsRegistry, CNSStimulationContextStore, CNSStimulationGate, afferentPath, collateral, createCNS, createPersistRegistry, modality, neuron, neuronFactory, withCtx, withGlobal };
1439
+ export { CNS, CNSCollateral, CNSStimulationContextStore, CNSStimulationGate, afferentPath, collateral, modality, neuron, neuronFactory, withCtx, withGlobal };
1338
1440
  //# sourceMappingURL=index.js.map
1339
1441
  //# sourceMappingURL=index.js.map