@cnstra/core 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export type { TCNSHandlerContext } from './types/TCNSHandlerContext';
11
11
  export type { TCNSSignal } from './types/TCNSSignal';
12
12
  export type { TCNSNeuronActivationTask } from './types/TCNSNeuronActivationTask';
13
13
  export type { TCNSStimulationOptions } from './types/TCNSStimulationOptions';
14
+ export type { TCNSStimulationResponse } from './types/TCNSStimulationResponse';
15
+ export type { TCNSStimulationDrain } from './types/TCNSStimulationDrain';
14
16
  export type { TCNSModality } from './types/TCNSModality';
15
17
  export type { TCNSAfferentPath } from './types/TCNSAfferentPath';
16
18
  export type { TCNSNeuronActivationTaskFailure } from './types/TCNSNeuronActivationTaskFailure';
@@ -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;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,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC/E,YAAY,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACzE,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"}
package/dist/index.js CHANGED
@@ -48,12 +48,14 @@ var CNSDendriteContext = class {
48
48
  }
49
49
  };
50
50
  var CNSStimulationResponseImpl = class {
51
- constructor(stimulation, store, inputSignal, outputSignal, queueLength, hops, error) {
51
+ constructor(stimulation, store, inputSignal, outputSignal, queueLength, pendingActivations, activeActivations, hops, error) {
52
52
  this.stimulation = stimulation;
53
53
  this.store = store;
54
54
  this.inputSignal = inputSignal;
55
55
  this.outputSignal = outputSignal;
56
56
  this.queueLength = queueLength;
57
+ this.pendingActivations = pendingActivations;
58
+ this.activeActivations = activeActivations;
57
59
  this.hops = hops;
58
60
  this.error = error;
59
61
  }
@@ -62,7 +64,7 @@ var CNSStimulationResponseImpl = class {
62
64
  }
63
65
  };
64
66
  var CNSStimulation = class {
65
- constructor(cns, instanceNeuronQueue, options, onResponse) {
67
+ constructor(cns, instanceNeuronQueue, options, onResponse, onDrain) {
66
68
  this.cns = cns;
67
69
  this.options = options;
68
70
  this.qHead = 0;
@@ -73,9 +75,20 @@ var CNSStimulation = class {
73
75
  // Re-entrancy guard for the pump loop.
74
76
  this.pumping = false;
75
77
  this.needsPump = false;
78
+ // Nesting depth of the current synchronous turn. Only the outermost close
79
+ // announces a drain - see beginTurn/endTurn.
80
+ this.turnDepth = 0;
76
81
  this.scheduledCount = 0;
82
+ // Activations belonging to signals of an in-progress array fan-out that have
83
+ // not been handed to processResponse yet, so the task objects do not exist.
84
+ // processResponse enqueues synchronously, so the first signal's whole subtree
85
+ // drains before the second signal is even looked at; without this counter
86
+ // that subtree's leaf sees an empty queue and reads as "all work done".
87
+ // Counted in activations, not signals, so every public counter shares a unit.
88
+ this.reservedActivations = 0;
77
89
  this.isCompleted = false;
78
90
  this.completionRejected = false;
91
+ this.onDrain = onDrain;
79
92
  this.ctxStore = options?.ctx;
80
93
  this.instanceNeuronQueue = instanceNeuronQueue;
81
94
  this.onResponse = onResponse;
@@ -172,18 +185,28 @@ var CNSStimulation = class {
172
185
  (cb) => {
173
186
  this.activeOperations--;
174
187
  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();
188
+ this.beginTurn();
189
+ try {
190
+ if (typeof cb === "function") cb();
191
+ if (this.qSize > 0 && this.canStartOperation) {
192
+ if (this.pumping) this.needsPump = true;
193
+ else this.pump();
194
+ }
195
+ } finally {
196
+ this.endTurn();
179
197
  }
180
198
  },
181
199
  (err) => {
182
200
  this.activeOperations--;
183
201
  this.activeTasks.delete(task);
184
- if (this.qSize > 0 && this.canStartOperation) {
185
- if (this.pumping) this.needsPump = true;
186
- else this.pump();
202
+ this.beginTurn();
203
+ try {
204
+ if (this.qSize > 0 && this.canStartOperation) {
205
+ if (this.pumping) this.needsPump = true;
206
+ else this.pump();
207
+ }
208
+ } finally {
209
+ this.endTurn();
187
210
  }
188
211
  throw err;
189
212
  }
@@ -199,10 +222,54 @@ var CNSStimulation = class {
199
222
  this.pump();
200
223
  }
201
224
  }
225
+ /**
226
+ * Open a synchronous turn. Turns nest: a drain is announced only when the
227
+ * outermost one closes, so an enqueue that happens while the pump is already
228
+ * running does not produce its own boundary.
229
+ */
230
+ beginTurn() {
231
+ this.turnDepth++;
232
+ }
233
+ /**
234
+ * Close a synchronous turn and, if it was the outermost and nothing more can
235
+ * run without yielding to the event loop, announce the drain.
236
+ *
237
+ * This is the only place the synchronous frontier is observable. A response
238
+ * cannot serve the purpose: responses are emitted when an activation
239
+ * *finishes*, but a turn just as often ends when one *starts* and returns a
240
+ * promise - at which point there is nothing left to emit a response from.
241
+ */
242
+ endTurn() {
243
+ if (--this.turnDepth > 0) return;
244
+ const onDrain = this.onDrain;
245
+ if (!onDrain) return;
246
+ if (this.pumping) return;
247
+ if (this.qSize > 0 && this.canStartOperation) return;
248
+ this.turnDepth++;
249
+ try {
250
+ onDrain({
251
+ stimulation: this,
252
+ queueLength: this.pendingActivations + this.activeOperations,
253
+ pendingActivations: this.pendingActivations,
254
+ activeActivations: this.activeOperations
255
+ });
256
+ } finally {
257
+ this.turnDepth--;
258
+ }
259
+ }
260
+ /**
261
+ * Activations that exist (or are reserved) but whose dendrite body has not
262
+ * been invoked yet: sitting in the ring buffer, created and held in
263
+ * pendingTasks, or reserved by an array fan-out mid-dispatch. Subscribers
264
+ * parked behind an async onResponse land here too - their body has not run.
265
+ */
266
+ get pendingActivations() {
267
+ return this.qSize + this.scheduledCount + this.reservedActivations;
268
+ }
202
269
  tryResolveCompleted() {
203
270
  if (this.isCompleted) return;
204
271
  const noActive = this.activeOperations === 0;
205
- const noPending = this.qSize + this.scheduledCount === 0;
272
+ const noPending = this.pendingActivations === 0;
206
273
  const aborted = !!this.options?.abortSignal?.aborted;
207
274
  if (noPending && noActive || aborted && noActive && !noPending) {
208
275
  if (aborted) {
@@ -274,10 +341,15 @@ var CNSStimulation = class {
274
341
  * Enqueue activation tasks directly into the stimulation queue
275
342
  */
276
343
  enqueueTasks(tasks) {
277
- for (const task of tasks) {
278
- this.qEnqueue(task);
344
+ this.beginTurn();
345
+ try {
346
+ for (const task of tasks) {
347
+ this.qEnqueue(task);
348
+ }
349
+ this.tryResolveCompleted();
350
+ } finally {
351
+ this.endTurn();
279
352
  }
280
- this.tryResolveCompleted();
281
353
  }
282
354
  get concurrencyEnabled() {
283
355
  return this.options?.concurrency !== void 0 && this.options?.concurrency > 0;
@@ -378,14 +450,19 @@ var CNSStimulation = class {
378
450
  }
379
451
  }
380
452
  if (!subscriber) {
453
+ const error = new Error(`Subscriber not found for activation task`);
381
454
  this.pushFailedTask({
382
455
  task: neuronActivationTask,
383
- error: new Error(
384
- `Subscriber not found for activation task`
385
- ),
456
+ error,
386
457
  aborted: false
387
458
  });
388
459
  return () => {
460
+ this.processResponseOrResponses(
461
+ void 0,
462
+ neuronActivationTask.input,
463
+ void 0,
464
+ error
465
+ );
389
466
  };
390
467
  }
391
468
  const neuron2 = subscriber.neuron;
@@ -432,13 +509,13 @@ var CNSStimulation = class {
432
509
  error: error instanceof Error ? error : new Error(String(error)),
433
510
  aborted: isAborted
434
511
  });
435
- this.processResponseOrResponses(
436
- neuron2,
437
- inputSignal,
438
- void 0,
439
- error
440
- );
441
512
  return () => {
513
+ this.processResponseOrResponses(
514
+ neuron2,
515
+ inputSignal,
516
+ void 0,
517
+ error
518
+ );
442
519
  };
443
520
  }
444
521
  const maxDuration = neuron2.maxDuration;
@@ -508,8 +585,26 @@ var CNSStimulation = class {
508
585
  this.processResponse(emitter, inputSignal, void 0, error);
509
586
  return;
510
587
  }
511
- for (const signal of outputSignalOrSignals) {
512
- this.processResponse(emitter, inputSignal, signal, error);
588
+ let perSignal;
589
+ if (!error) {
590
+ perSignal = new Array(outputSignalOrSignals.length);
591
+ let reserved = 0;
592
+ for (let i = 0; i < outputSignalOrSignals.length; i++) {
593
+ const c = outputSignalOrSignals[i]?.collateral;
594
+ const n = c ? this.cns.network.getSubscribers(c).length : 0;
595
+ perSignal[i] = n;
596
+ reserved += n;
597
+ }
598
+ this.reservedActivations += reserved;
599
+ }
600
+ for (let i = 0; i < outputSignalOrSignals.length; i++) {
601
+ if (perSignal) this.reservedActivations -= perSignal[i];
602
+ this.processResponse(
603
+ emitter,
604
+ inputSignal,
605
+ outputSignalOrSignals[i],
606
+ error
607
+ );
513
608
  }
514
609
  return;
515
610
  }
@@ -563,7 +658,9 @@ var CNSStimulation = class {
563
658
  this.ctxStore,
564
659
  inputSignal,
565
660
  outputSignal,
566
- this.qSize + this.scheduledCount,
661
+ this.pendingActivations + this.activeOperations,
662
+ this.pendingActivations,
663
+ this.activeOperations,
567
664
  this.options?.maxNeuronHops && emitter ? this.neuronVisitMap?.get(emitter) ?? 0 : void 0,
568
665
  error
569
666
  )
@@ -581,21 +678,35 @@ var CNSStimulation = class {
581
678
  if (maybePromise && typeof maybePromise.then === "function") {
582
679
  maybePromise.then(
583
680
  () => {
584
- for (let i = 0; i < subscriberActivationTasks.length; i++) {
585
- this.scheduledCount--;
586
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
587
- this.qEnqueue(subscriberActivationTasks[i]);
681
+ this.beginTurn();
682
+ try {
683
+ for (let i = 0; i < subscriberActivationTasks.length; i++) {
684
+ this.scheduledCount--;
685
+ this.pendingTasks?.delete(
686
+ subscriberActivationTasks[i]
687
+ );
688
+ this.qEnqueue(subscriberActivationTasks[i]);
689
+ }
690
+ this.tryResolveCompleted();
691
+ } finally {
692
+ this.endTurn();
588
693
  }
589
- this.tryResolveCompleted();
590
694
  },
591
695
  (error2) => {
592
696
  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]);
697
+ this.beginTurn();
698
+ try {
699
+ for (let i = 0; i < subscriberActivationTasks.length; i++) {
700
+ this.scheduledCount--;
701
+ this.pendingTasks?.delete(
702
+ subscriberActivationTasks[i]
703
+ );
704
+ this.qEnqueue(subscriberActivationTasks[i]);
705
+ }
706
+ this.tryResolveCompleted();
707
+ } finally {
708
+ this.endTurn();
597
709
  }
598
- this.tryResolveCompleted();
599
710
  }
600
711
  );
601
712
  return;
@@ -608,12 +719,21 @@ var CNSStimulation = class {
608
719
  this.tryResolveCompleted();
609
720
  }
610
721
  responseToSignal(signalOrSignals) {
611
- if (Array.isArray(signalOrSignals)) {
612
- this.processResponseOrResponses(void 0, void 0, signalOrSignals);
613
- return;
722
+ this.beginTurn();
723
+ try {
724
+ if (Array.isArray(signalOrSignals)) {
725
+ this.processResponseOrResponses(
726
+ void 0,
727
+ void 0,
728
+ signalOrSignals
729
+ );
730
+ return;
731
+ }
732
+ this.processResponse(void 0, void 0, signalOrSignals);
733
+ this.tryResolveCompleted();
734
+ } finally {
735
+ this.endTurn();
614
736
  }
615
- this.processResponse(void 0, void 0, signalOrSignals);
616
- this.tryResolveCompleted();
617
737
  }
618
738
  };
619
739
 
@@ -960,6 +1080,10 @@ var CNS = class {
960
1080
  * Global response listeners applied to every stimulation.
961
1081
  */
962
1082
  this.globalResponseListeners = [];
1083
+ /**
1084
+ * Global batch-boundary listeners applied to every stimulation.
1085
+ */
1086
+ this.globalDrainListeners = [];
963
1087
  this.network = new CNSNetwork(this.neurons);
964
1088
  this.global = global;
965
1089
  }
@@ -973,6 +1097,50 @@ var CNS = class {
973
1097
  if (idx >= 0) this.globalResponseListeners.splice(idx, 1);
974
1098
  };
975
1099
  }
1100
+ /**
1101
+ * Register a listener called once at the end of every synchronous turn of
1102
+ * every stimulation - see {@link TCNSStimulationDrain}. This is how an
1103
+ * integration (a state manager, an in-memory database) installs a single
1104
+ * commit point for the whole organism instead of passing `onDrain` per call.
1105
+ *
1106
+ * Returns an unsubscribe function.
1107
+ */
1108
+ addDrainListener(listener) {
1109
+ this.globalDrainListeners.push(listener);
1110
+ let active = true;
1111
+ return () => {
1112
+ if (!active) return;
1113
+ active = false;
1114
+ const idx = this.globalDrainListeners.indexOf(listener);
1115
+ if (idx >= 0) this.globalDrainListeners.splice(idx, 1);
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Fan a drain out to the local and global listeners, or return undefined when
1120
+ * there are none so the stimulation can skip the boundary check entirely.
1121
+ *
1122
+ * Listener errors are isolated: a drain is a notification, and one broken
1123
+ * flush must not stop the others or derail the stimulation that triggered it.
1124
+ */
1125
+ wrapOnDrain(local) {
1126
+ if (this.globalDrainListeners.length === 0 && !local) return void 0;
1127
+ return (d) => {
1128
+ if (local) {
1129
+ try {
1130
+ local(d);
1131
+ } catch (error) {
1132
+ console.error("[CNS] onDrain listener threw", error);
1133
+ }
1134
+ }
1135
+ for (let i = 0; i < this.globalDrainListeners.length; i++) {
1136
+ try {
1137
+ this.globalDrainListeners[i](d);
1138
+ } catch (error) {
1139
+ console.error("[CNS] drain listener threw", error);
1140
+ }
1141
+ }
1142
+ };
1143
+ }
976
1144
  wrapOnResponse(local) {
977
1145
  if (this.globalResponseListeners.length === 0 && !local) {
978
1146
  return void 0;
@@ -1020,7 +1188,8 @@ var CNS = class {
1020
1188
  this,
1021
1189
  this.instanceNeuronQueue,
1022
1190
  options,
1023
- wrapped
1191
+ wrapped,
1192
+ this.wrapOnDrain(options?.onDrain)
1024
1193
  );
1025
1194
  stimulation.responseToSignal(signalOrSignals);
1026
1195
  return stimulation;
@@ -1034,7 +1203,8 @@ var CNS = class {
1034
1203
  this,
1035
1204
  this.instanceNeuronQueue,
1036
1205
  options,
1037
- wrapped
1206
+ wrapped,
1207
+ this.wrapOnDrain(options?.onDrain)
1038
1208
  );
1039
1209
  stimulation.enqueueTasks(tasks);
1040
1210
  return stimulation;