@cnstra/core 2.3.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.cjs CHANGED
@@ -33,6 +33,12 @@ var CNSDendriteContext = class {
33
33
  this.abortSignal = abortSignal;
34
34
  this.cns = cns;
35
35
  }
36
+ // Prototype getter, not a constructor field: the per-activation allocation is
37
+ // unchanged (still 4 fields) and you only pay a single `cns.global` read on the
38
+ // dendrites that actually reach for it. Value getter, so `{ global }` destructuring is safe.
39
+ get global() {
40
+ return this.cns.global;
41
+ }
36
42
  get() {
37
43
  return this.stimulation.getContext().get(this.neuron);
38
44
  }
@@ -44,12 +50,14 @@ var CNSDendriteContext = class {
44
50
  }
45
51
  };
46
52
  var CNSStimulationResponseImpl = class {
47
- constructor(stimulation, store, inputSignal, outputSignal, queueLength, hops, error) {
53
+ constructor(stimulation, store, inputSignal, outputSignal, queueLength, pendingActivations, activeActivations, hops, error) {
48
54
  this.stimulation = stimulation;
49
55
  this.store = store;
50
56
  this.inputSignal = inputSignal;
51
57
  this.outputSignal = outputSignal;
52
58
  this.queueLength = queueLength;
59
+ this.pendingActivations = pendingActivations;
60
+ this.activeActivations = activeActivations;
53
61
  this.hops = hops;
54
62
  this.error = error;
55
63
  }
@@ -58,7 +66,7 @@ var CNSStimulationResponseImpl = class {
58
66
  }
59
67
  };
60
68
  var CNSStimulation = class {
61
- constructor(cns, instanceNeuronQueue, options, onResponse) {
69
+ constructor(cns, instanceNeuronQueue, options, onResponse, onDrain) {
62
70
  this.cns = cns;
63
71
  this.options = options;
64
72
  this.qHead = 0;
@@ -69,9 +77,20 @@ var CNSStimulation = class {
69
77
  // Re-entrancy guard for the pump loop.
70
78
  this.pumping = false;
71
79
  this.needsPump = false;
80
+ // Nesting depth of the current synchronous turn. Only the outermost close
81
+ // announces a drain - see beginTurn/endTurn.
82
+ this.turnDepth = 0;
72
83
  this.scheduledCount = 0;
84
+ // Activations belonging to signals of an in-progress array fan-out that have
85
+ // not been handed to processResponse yet, so the task objects do not exist.
86
+ // processResponse enqueues synchronously, so the first signal's whole subtree
87
+ // drains before the second signal is even looked at; without this counter
88
+ // that subtree's leaf sees an empty queue and reads as "all work done".
89
+ // Counted in activations, not signals, so every public counter shares a unit.
90
+ this.reservedActivations = 0;
73
91
  this.isCompleted = false;
74
92
  this.completionRejected = false;
93
+ this.onDrain = onDrain;
75
94
  this.ctxStore = options?.ctx;
76
95
  this.instanceNeuronQueue = instanceNeuronQueue;
77
96
  this.onResponse = onResponse;
@@ -168,18 +187,28 @@ var CNSStimulation = class {
168
187
  (cb) => {
169
188
  this.activeOperations--;
170
189
  this.activeTasks.delete(task);
171
- if (typeof cb === "function") cb();
172
- if (this.qSize > 0 && this.canStartOperation) {
173
- if (this.pumping) this.needsPump = true;
174
- else this.pump();
190
+ this.beginTurn();
191
+ try {
192
+ if (typeof cb === "function") cb();
193
+ if (this.qSize > 0 && this.canStartOperation) {
194
+ if (this.pumping) this.needsPump = true;
195
+ else this.pump();
196
+ }
197
+ } finally {
198
+ this.endTurn();
175
199
  }
176
200
  },
177
201
  (err) => {
178
202
  this.activeOperations--;
179
203
  this.activeTasks.delete(task);
180
- if (this.qSize > 0 && this.canStartOperation) {
181
- if (this.pumping) this.needsPump = true;
182
- else this.pump();
204
+ this.beginTurn();
205
+ try {
206
+ if (this.qSize > 0 && this.canStartOperation) {
207
+ if (this.pumping) this.needsPump = true;
208
+ else this.pump();
209
+ }
210
+ } finally {
211
+ this.endTurn();
183
212
  }
184
213
  throw err;
185
214
  }
@@ -195,10 +224,54 @@ var CNSStimulation = class {
195
224
  this.pump();
196
225
  }
197
226
  }
227
+ /**
228
+ * Open a synchronous turn. Turns nest: a drain is announced only when the
229
+ * outermost one closes, so an enqueue that happens while the pump is already
230
+ * running does not produce its own boundary.
231
+ */
232
+ beginTurn() {
233
+ this.turnDepth++;
234
+ }
235
+ /**
236
+ * Close a synchronous turn and, if it was the outermost and nothing more can
237
+ * run without yielding to the event loop, announce the drain.
238
+ *
239
+ * This is the only place the synchronous frontier is observable. A response
240
+ * cannot serve the purpose: responses are emitted when an activation
241
+ * *finishes*, but a turn just as often ends when one *starts* and returns a
242
+ * promise - at which point there is nothing left to emit a response from.
243
+ */
244
+ endTurn() {
245
+ if (--this.turnDepth > 0) return;
246
+ const onDrain = this.onDrain;
247
+ if (!onDrain) return;
248
+ if (this.pumping) return;
249
+ if (this.qSize > 0 && this.canStartOperation) return;
250
+ this.turnDepth++;
251
+ try {
252
+ onDrain({
253
+ stimulation: this,
254
+ queueLength: this.pendingActivations + this.activeOperations,
255
+ pendingActivations: this.pendingActivations,
256
+ activeActivations: this.activeOperations
257
+ });
258
+ } finally {
259
+ this.turnDepth--;
260
+ }
261
+ }
262
+ /**
263
+ * Activations that exist (or are reserved) but whose dendrite body has not
264
+ * been invoked yet: sitting in the ring buffer, created and held in
265
+ * pendingTasks, or reserved by an array fan-out mid-dispatch. Subscribers
266
+ * parked behind an async onResponse land here too - their body has not run.
267
+ */
268
+ get pendingActivations() {
269
+ return this.qSize + this.scheduledCount + this.reservedActivations;
270
+ }
198
271
  tryResolveCompleted() {
199
272
  if (this.isCompleted) return;
200
273
  const noActive = this.activeOperations === 0;
201
- const noPending = this.qSize + this.scheduledCount === 0;
274
+ const noPending = this.pendingActivations === 0;
202
275
  const aborted = !!this.options?.abortSignal?.aborted;
203
276
  if (noPending && noActive || aborted && noActive && !noPending) {
204
277
  if (aborted) {
@@ -270,10 +343,15 @@ var CNSStimulation = class {
270
343
  * Enqueue activation tasks directly into the stimulation queue
271
344
  */
272
345
  enqueueTasks(tasks) {
273
- for (const task of tasks) {
274
- this.qEnqueue(task);
346
+ this.beginTurn();
347
+ try {
348
+ for (const task of tasks) {
349
+ this.qEnqueue(task);
350
+ }
351
+ this.tryResolveCompleted();
352
+ } finally {
353
+ this.endTurn();
275
354
  }
276
- this.tryResolveCompleted();
277
355
  }
278
356
  get concurrencyEnabled() {
279
357
  return this.options?.concurrency !== void 0 && this.options?.concurrency > 0;
@@ -374,14 +452,19 @@ var CNSStimulation = class {
374
452
  }
375
453
  }
376
454
  if (!subscriber) {
455
+ const error = new Error(`Subscriber not found for activation task`);
377
456
  this.pushFailedTask({
378
457
  task: neuronActivationTask,
379
- error: new Error(
380
- `Subscriber not found for activation task`
381
- ),
458
+ error,
382
459
  aborted: false
383
460
  });
384
461
  return () => {
462
+ this.processResponseOrResponses(
463
+ void 0,
464
+ neuronActivationTask.input,
465
+ void 0,
466
+ error
467
+ );
385
468
  };
386
469
  }
387
470
  const neuron2 = subscriber.neuron;
@@ -428,13 +511,13 @@ var CNSStimulation = class {
428
511
  error: error instanceof Error ? error : new Error(String(error)),
429
512
  aborted: isAborted
430
513
  });
431
- this.processResponseOrResponses(
432
- neuron2,
433
- inputSignal,
434
- void 0,
435
- error
436
- );
437
514
  return () => {
515
+ this.processResponseOrResponses(
516
+ neuron2,
517
+ inputSignal,
518
+ void 0,
519
+ error
520
+ );
438
521
  };
439
522
  }
440
523
  const maxDuration = neuron2.maxDuration;
@@ -504,8 +587,26 @@ var CNSStimulation = class {
504
587
  this.processResponse(emitter, inputSignal, void 0, error);
505
588
  return;
506
589
  }
507
- for (const signal of outputSignalOrSignals) {
508
- this.processResponse(emitter, inputSignal, signal, error);
590
+ let perSignal;
591
+ if (!error) {
592
+ perSignal = new Array(outputSignalOrSignals.length);
593
+ let reserved = 0;
594
+ for (let i = 0; i < outputSignalOrSignals.length; i++) {
595
+ const c = outputSignalOrSignals[i]?.collateral;
596
+ const n = c ? this.cns.network.getSubscribers(c).length : 0;
597
+ perSignal[i] = n;
598
+ reserved += n;
599
+ }
600
+ this.reservedActivations += reserved;
601
+ }
602
+ for (let i = 0; i < outputSignalOrSignals.length; i++) {
603
+ if (perSignal) this.reservedActivations -= perSignal[i];
604
+ this.processResponse(
605
+ emitter,
606
+ inputSignal,
607
+ outputSignalOrSignals[i],
608
+ error
609
+ );
509
610
  }
510
611
  return;
511
612
  }
@@ -559,7 +660,9 @@ var CNSStimulation = class {
559
660
  this.ctxStore,
560
661
  inputSignal,
561
662
  outputSignal,
562
- this.qSize + this.scheduledCount,
663
+ this.pendingActivations + this.activeOperations,
664
+ this.pendingActivations,
665
+ this.activeOperations,
563
666
  this.options?.maxNeuronHops && emitter ? this.neuronVisitMap?.get(emitter) ?? 0 : void 0,
564
667
  error
565
668
  )
@@ -577,21 +680,35 @@ var CNSStimulation = class {
577
680
  if (maybePromise && typeof maybePromise.then === "function") {
578
681
  maybePromise.then(
579
682
  () => {
580
- for (let i = 0; i < subscriberActivationTasks.length; i++) {
581
- this.scheduledCount--;
582
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
583
- this.qEnqueue(subscriberActivationTasks[i]);
683
+ this.beginTurn();
684
+ try {
685
+ for (let i = 0; i < subscriberActivationTasks.length; i++) {
686
+ this.scheduledCount--;
687
+ this.pendingTasks?.delete(
688
+ subscriberActivationTasks[i]
689
+ );
690
+ this.qEnqueue(subscriberActivationTasks[i]);
691
+ }
692
+ this.tryResolveCompleted();
693
+ } finally {
694
+ this.endTurn();
584
695
  }
585
- this.tryResolveCompleted();
586
696
  },
587
697
  (error2) => {
588
698
  this.onResponseError = error2 instanceof Error ? error2 : new Error(String(error2));
589
- for (let i = 0; i < subscriberActivationTasks.length; i++) {
590
- this.scheduledCount--;
591
- this.pendingTasks?.delete(subscriberActivationTasks[i]);
592
- this.qEnqueue(subscriberActivationTasks[i]);
699
+ this.beginTurn();
700
+ try {
701
+ for (let i = 0; i < subscriberActivationTasks.length; i++) {
702
+ this.scheduledCount--;
703
+ this.pendingTasks?.delete(
704
+ subscriberActivationTasks[i]
705
+ );
706
+ this.qEnqueue(subscriberActivationTasks[i]);
707
+ }
708
+ this.tryResolveCompleted();
709
+ } finally {
710
+ this.endTurn();
593
711
  }
594
- this.tryResolveCompleted();
595
712
  }
596
713
  );
597
714
  return;
@@ -604,12 +721,21 @@ var CNSStimulation = class {
604
721
  this.tryResolveCompleted();
605
722
  }
606
723
  responseToSignal(signalOrSignals) {
607
- if (Array.isArray(signalOrSignals)) {
608
- this.processResponseOrResponses(void 0, void 0, signalOrSignals);
609
- return;
724
+ this.beginTurn();
725
+ try {
726
+ if (Array.isArray(signalOrSignals)) {
727
+ this.processResponseOrResponses(
728
+ void 0,
729
+ void 0,
730
+ signalOrSignals
731
+ );
732
+ return;
733
+ }
734
+ this.processResponse(void 0, void 0, signalOrSignals);
735
+ this.tryResolveCompleted();
736
+ } finally {
737
+ this.endTurn();
610
738
  }
611
- this.processResponse(void 0, void 0, signalOrSignals);
612
- this.tryResolveCompleted();
613
739
  }
614
740
  };
615
741
 
@@ -944,7 +1070,7 @@ var CNSNetwork = class {
944
1070
 
945
1071
  // src/CNS.ts
946
1072
  var CNS = class {
947
- constructor(neurons, options) {
1073
+ constructor(neurons, options, global) {
948
1074
  this.neurons = neurons;
949
1075
  this.options = options;
950
1076
  /**
@@ -956,7 +1082,12 @@ var CNS = class {
956
1082
  * Global response listeners applied to every stimulation.
957
1083
  */
958
1084
  this.globalResponseListeners = [];
1085
+ /**
1086
+ * Global batch-boundary listeners applied to every stimulation.
1087
+ */
1088
+ this.globalDrainListeners = [];
959
1089
  this.network = new CNSNetwork(this.neurons);
1090
+ this.global = global;
960
1091
  }
961
1092
  addResponseListener(listener) {
962
1093
  this.globalResponseListeners.push(listener);
@@ -968,6 +1099,50 @@ var CNS = class {
968
1099
  if (idx >= 0) this.globalResponseListeners.splice(idx, 1);
969
1100
  };
970
1101
  }
1102
+ /**
1103
+ * Register a listener called once at the end of every synchronous turn of
1104
+ * every stimulation - see {@link TCNSStimulationDrain}. This is how an
1105
+ * integration (a state manager, an in-memory database) installs a single
1106
+ * commit point for the whole organism instead of passing `onDrain` per call.
1107
+ *
1108
+ * Returns an unsubscribe function.
1109
+ */
1110
+ addDrainListener(listener) {
1111
+ this.globalDrainListeners.push(listener);
1112
+ let active = true;
1113
+ return () => {
1114
+ if (!active) return;
1115
+ active = false;
1116
+ const idx = this.globalDrainListeners.indexOf(listener);
1117
+ if (idx >= 0) this.globalDrainListeners.splice(idx, 1);
1118
+ };
1119
+ }
1120
+ /**
1121
+ * Fan a drain out to the local and global listeners, or return undefined when
1122
+ * there are none so the stimulation can skip the boundary check entirely.
1123
+ *
1124
+ * Listener errors are isolated: a drain is a notification, and one broken
1125
+ * flush must not stop the others or derail the stimulation that triggered it.
1126
+ */
1127
+ wrapOnDrain(local) {
1128
+ if (this.globalDrainListeners.length === 0 && !local) return void 0;
1129
+ return (d) => {
1130
+ if (local) {
1131
+ try {
1132
+ local(d);
1133
+ } catch (error) {
1134
+ console.error("[CNS] onDrain listener threw", error);
1135
+ }
1136
+ }
1137
+ for (let i = 0; i < this.globalDrainListeners.length; i++) {
1138
+ try {
1139
+ this.globalDrainListeners[i](d);
1140
+ } catch (error) {
1141
+ console.error("[CNS] drain listener threw", error);
1142
+ }
1143
+ }
1144
+ };
1145
+ }
971
1146
  wrapOnResponse(local) {
972
1147
  if (this.globalResponseListeners.length === 0 && !local) {
973
1148
  return void 0;
@@ -1015,7 +1190,8 @@ var CNS = class {
1015
1190
  this,
1016
1191
  this.instanceNeuronQueue,
1017
1192
  options,
1018
- wrapped
1193
+ wrapped,
1194
+ this.wrapOnDrain(options?.onDrain)
1019
1195
  );
1020
1196
  stimulation.responseToSignal(signalOrSignals);
1021
1197
  return stimulation;
@@ -1029,7 +1205,8 @@ var CNS = class {
1029
1205
  this,
1030
1206
  this.instanceNeuronQueue,
1031
1207
  options,
1032
- wrapped
1208
+ wrapped,
1209
+ this.wrapOnDrain(options?.onDrain)
1033
1210
  );
1034
1211
  stimulation.enqueueTasks(tasks);
1035
1212
  return stimulation;
@@ -1282,11 +1459,16 @@ var neuron = (axon) => {
1282
1459
  };
1283
1460
  return api;
1284
1461
  };
1285
- var withCtx = () => ({
1286
- neuron: (axon) => {
1287
- return neuron(axon);
1288
- }
1289
- });
1462
+ var neuronFactory = () => {
1463
+ const factory = {
1464
+ withCtx: () => factory,
1465
+ withGlobal: () => factory,
1466
+ neuron: (axon) => neuron(axon)
1467
+ };
1468
+ return factory;
1469
+ };
1470
+ var withCtx = () => neuronFactory();
1471
+ var withGlobal = () => neuronFactory();
1290
1472
  var afferentPath = (parentAfferentPath) => {
1291
1473
  return {
1292
1474
  ...parentAfferentPath !== void 0 && {
@@ -1315,6 +1497,14 @@ var createPersistRegistry = (namedNeurons) => {
1315
1497
  }
1316
1498
  return registry;
1317
1499
  };
1500
+ var createCNS = (namedNeurons, options) => {
1501
+ const neurons = Object.values(namedNeurons).map(
1502
+ (entry) => entry && typeof entry === "object" && "neuron" in entry ? entry.neuron : entry
1503
+ );
1504
+ const cns = new CNS(neurons, options, options?.global);
1505
+ const registry = createPersistRegistry(namedNeurons);
1506
+ return { cns, registry };
1507
+ };
1318
1508
 
1319
1509
  exports.CNS = CNS;
1320
1510
  exports.CNSCollateral = CNSCollateral;
@@ -1323,9 +1513,12 @@ exports.CNSStimulationContextStore = CNSStimulationContextStore;
1323
1513
  exports.CNSStimulationGate = CNSStimulationGate;
1324
1514
  exports.afferentPath = afferentPath;
1325
1515
  exports.collateral = collateral;
1516
+ exports.createCNS = createCNS;
1326
1517
  exports.createPersistRegistry = createPersistRegistry;
1327
1518
  exports.modality = modality;
1328
1519
  exports.neuron = neuron;
1520
+ exports.neuronFactory = neuronFactory;
1329
1521
  exports.withCtx = withCtx;
1522
+ exports.withGlobal = withGlobal;
1330
1523
  //# sourceMappingURL=index.cjs.map
1331
1524
  //# sourceMappingURL=index.cjs.map