@cnstra/core 2.2.1 → 2.4.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
@@ -25,6 +25,7 @@ var CNSStimulationContextStore = class {
25
25
  };
26
26
 
27
27
  // src/CNSStimulation.ts
28
+ var TASK_DENDRITE = /* @__PURE__ */ Symbol("cnstra.taskDendrite");
28
29
  var CNSDendriteContext = class {
29
30
  constructor(stimulation, neuron2, abortSignal, cns) {
30
31
  this.stimulation = stimulation;
@@ -32,6 +33,12 @@ var CNSDendriteContext = class {
32
33
  this.abortSignal = abortSignal;
33
34
  this.cns = cns;
34
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
+ }
35
42
  get() {
36
43
  return this.stimulation.getContext().get(this.neuron);
37
44
  }
@@ -75,7 +82,7 @@ var CNSStimulation = class {
75
82
  this.instanceNeuronQueue = instanceNeuronQueue;
76
83
  this.onResponse = onResponse;
77
84
  if (this.options?.maxNeuronHops) {
78
- this.nueronVisitMap = /* @__PURE__ */ new Map();
85
+ this.neuronVisitMap = /* @__PURE__ */ new Map();
79
86
  }
80
87
  if (this.autoCleanupContextsEnabled) {
81
88
  this.activeSccCounts = /* @__PURE__ */ new Map();
@@ -340,14 +347,14 @@ var CNSStimulation = class {
340
347
  }
341
348
  createSubscriberQueueItem(subscriber, inputSignal) {
342
349
  if (this.options?.maxNeuronHops) {
343
- if ((this.nueronVisitMap?.get(subscriber.neuron) ?? 0) >= this.options?.maxNeuronHops) {
350
+ if ((this.neuronVisitMap?.get(subscriber.neuron) ?? 0) >= this.options?.maxNeuronHops) {
344
351
  throw new Error(
345
352
  `Max neuron hops reached when trying to enqueue subscriber`
346
353
  );
347
354
  } else {
348
- this.nueronVisitMap?.set(
355
+ this.neuronVisitMap?.set(
349
356
  subscriber.neuron,
350
- (this.nueronVisitMap?.get(subscriber.neuron) ?? 0) + 1
357
+ (this.neuronVisitMap?.get(subscriber.neuron) ?? 0) + 1
351
358
  );
352
359
  }
353
360
  }
@@ -356,17 +363,20 @@ var CNSStimulation = class {
356
363
  dendriteCollateral: subscriber.dendrite.collateral,
357
364
  input: inputSignal
358
365
  };
366
+ neuronActivationTask[TASK_DENDRITE] = subscriber;
359
367
  return neuronActivationTask;
360
368
  }
361
369
  executeActivationTask(neuronActivationTask) {
362
- const subscribers = this.cns.network.getSubscribers(
363
- neuronActivationTask.dendriteCollateral
364
- );
365
- let subscriber;
366
- for (let i = 0; i < subscribers.length; i++) {
367
- if (subscribers[i].neuron === neuronActivationTask.neuron) {
368
- subscriber = subscribers[i];
369
- break;
370
+ let subscriber = neuronActivationTask[TASK_DENDRITE];
371
+ if (!subscriber) {
372
+ const subscribers = this.cns.network.getSubscribers(
373
+ neuronActivationTask.dendriteCollateral
374
+ );
375
+ for (let i = 0; i < subscribers.length; i++) {
376
+ if (subscribers[i].neuron === neuronActivationTask.neuron) {
377
+ subscriber = subscribers[i];
378
+ break;
379
+ }
370
380
  }
371
381
  }
372
382
  if (!subscriber) {
@@ -556,7 +566,7 @@ var CNSStimulation = class {
556
566
  inputSignal,
557
567
  outputSignal,
558
568
  this.qSize + this.scheduledCount,
559
- this.options?.maxNeuronHops && emitter ? this.nueronVisitMap?.get(emitter) ?? 0 : void 0,
569
+ this.options?.maxNeuronHops && emitter ? this.neuronVisitMap?.get(emitter) ?? 0 : void 0,
560
570
  error
561
571
  )
562
572
  );
@@ -671,8 +681,12 @@ var CNSNetwork = class {
671
681
  /**
672
682
  * Strongly Connected Components of the neuron graph.
673
683
  * Each component is a set of neuron objects that can reach each other.
684
+ *
685
+ * Computed lazily on first access: the SCC machinery is only needed for
686
+ * `autoCleanupContexts` (off by default), so most networks never pay for it.
674
687
  */
675
- this.stronglyConnectedComponents = [];
688
+ this._stronglyConnectedComponents = [];
689
+ this.sccBuilt = false;
676
690
  /**
677
691
  * Quick lookup: neuron -> SCC index for fast reachability checks.
678
692
  */
@@ -690,21 +704,18 @@ var CNSNetwork = class {
690
704
  * Built once at construction time.
691
705
  */
692
706
  this.subIndex = /* @__PURE__ */ new Map();
693
- this.dendriteIndex = /* @__PURE__ */ new Map();
694
- this.collateralIndex = /* @__PURE__ */ new Set();
695
707
  this.parentNeuronByCollateral = /* @__PURE__ */ new Map();
696
- this.validateUniqueIdentifiers();
697
708
  this.buildIndexes();
698
- this.buildSCC();
699
709
  }
700
- /**
701
- * Validate uniqueness constraints:
702
- * - (legacy) collateral instances used to be single-owner; in object-identity routing
703
- * a collateral can be emitted by multiple neurons (fan-in), so we no longer
704
- * enforce single ownership here.
705
- * Throws aggregated error if violations are found.
706
- */
707
- validateUniqueIdentifiers() {
710
+ get stronglyConnectedComponents() {
711
+ this.ensureSCC();
712
+ return this._stronglyConnectedComponents;
713
+ }
714
+ /** Build the SCC analysis the first time anything needs it. */
715
+ ensureSCC() {
716
+ if (this.sccBuilt) return;
717
+ this.sccBuilt = true;
718
+ this.buildSCC();
708
719
  }
709
720
  /**
710
721
  * Build the neuron graph where neurons are connected if one can stimulate the other.
@@ -779,7 +790,7 @@ var CNSNetwork = class {
779
790
  strongConnect(neuron2);
780
791
  }
781
792
  }
782
- this.stronglyConnectedComponents = components;
793
+ this._stronglyConnectedComponents = components;
783
794
  for (let i = 0; i < components.length; i++) {
784
795
  for (const neuronId of Array.from(components[i])) {
785
796
  this.neuronToSCC.set(neuronId, i);
@@ -793,11 +804,11 @@ var CNSNetwork = class {
793
804
  */
794
805
  buildSCCDAG(neuronGraph) {
795
806
  this.sccDag.clear();
796
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
807
+ for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
797
808
  this.sccDag.set(i, /* @__PURE__ */ new Set());
798
809
  }
799
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
800
- const scc = this.stronglyConnectedComponents[i];
810
+ for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
811
+ const scc = this._stronglyConnectedComponents[i];
801
812
  for (const neuron2 of Array.from(scc)) {
802
813
  const neighbors = neuronGraph.get(neuron2) || /* @__PURE__ */ new Set();
803
814
  for (const neighbor of Array.from(neighbors)) {
@@ -815,25 +826,27 @@ var CNSNetwork = class {
815
826
  */
816
827
  buildSCCAncestors() {
817
828
  this.sccAncestors.clear();
818
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
829
+ for (let i = 0; i < this._stronglyConnectedComponents.length; i++) {
819
830
  this.sccAncestors.set(i, /* @__PURE__ */ new Set());
820
831
  }
832
+ const n = this._stronglyConnectedComponents.length;
833
+ const forward = Array.from({ length: n }, () => []);
821
834
  const inDegree = /* @__PURE__ */ new Map();
822
835
  const queue = [];
823
- for (let i = 0; i < this.stronglyConnectedComponents.length; i++) {
824
- const incomingEdges = this.sccDag.get(i) || /* @__PURE__ */ new Set();
825
- inDegree.set(i, incomingEdges.size);
826
- if (incomingEdges.size === 0) {
827
- queue.push(i);
836
+ for (let target = 0; target < n; target++) {
837
+ const sources = this.sccDag.get(target) || /* @__PURE__ */ new Set();
838
+ inDegree.set(target, sources.size);
839
+ if (sources.size === 0) queue.push(target);
840
+ for (const source of Array.from(sources)) {
841
+ forward[source].push(target);
828
842
  }
829
843
  }
830
844
  while (queue.length > 0) {
831
845
  const current = queue.shift();
832
- const outgoingEdges = this.getOutgoingEdges(current);
833
- for (const neighbor of Array.from(outgoingEdges)) {
846
+ const currentAncestors = this.sccAncestors.get(current);
847
+ for (const neighbor of forward[current]) {
834
848
  const neighborAncestors = this.sccAncestors.get(neighbor);
835
849
  neighborAncestors.add(current);
836
- const currentAncestors = this.sccAncestors.get(current);
837
850
  for (const ancestor of Array.from(currentAncestors)) {
838
851
  neighborAncestors.add(ancestor);
839
852
  }
@@ -845,32 +858,22 @@ var CNSNetwork = class {
845
858
  }
846
859
  }
847
860
  }
848
- /**
849
- * Get outgoing edges for a given SCC index
850
- */
851
- getOutgoingEdges(sccIndex) {
852
- const outgoing = /* @__PURE__ */ new Set();
853
- for (const [targetScc, incomingEdges] of Array.from(this.sccDag)) {
854
- if (incomingEdges.has(sccIndex)) {
855
- outgoing.add(targetScc);
856
- }
857
- }
858
- return outgoing;
859
- }
860
861
  /**
861
862
  * Check if a neuron can be reached again (including self-calling).
862
863
  * Returns true if the neuron is in a strongly connected component with more than one member,
863
864
  * or if it's in a single-member SCC that can reach itself.
864
865
  */
865
866
  getSCCSetByNeuron(neuron2) {
867
+ this.ensureSCC();
866
868
  const sccIndex = this.neuronToSCC.get(neuron2);
867
869
  if (sccIndex === void 0) return;
868
- return this.stronglyConnectedComponents[sccIndex];
870
+ return this._stronglyConnectedComponents[sccIndex];
869
871
  }
870
872
  /**
871
873
  * Get the SCC index for a given neuron
872
874
  */
873
875
  getSccIndexByNeuron(neuron2) {
876
+ this.ensureSCC();
874
877
  return this.neuronToSCC.get(neuron2);
875
878
  }
876
879
  /**
@@ -878,6 +881,7 @@ var CNSNetwork = class {
878
881
  * This is the core logic for safe context cleanup.
879
882
  */
880
883
  canNeuronBeGuaranteedDone(neuron2, activeSccCounts) {
884
+ this.ensureSCC();
881
885
  const sccIndex = this.neuronToSCC.get(neuron2);
882
886
  if (sccIndex === void 0) return true;
883
887
  if (activeSccCounts.get(sccIndex) && activeSccCounts.get(sccIndex) > 0) {
@@ -895,8 +899,6 @@ var CNSNetwork = class {
895
899
  buildIndexes() {
896
900
  this.subIndex.clear();
897
901
  this.parentNeuronByCollateral.clear();
898
- this.dendriteIndex.clear();
899
- this.collateralIndex.clear();
900
902
  for (const neuron2 of this.neurons) {
901
903
  for (const dendrite of neuron2.dendrites) {
902
904
  const key = dendrite.collateral;
@@ -906,25 +908,40 @@ var CNSNetwork = class {
906
908
  key,
907
909
  arr
908
910
  );
909
- this.dendriteIndex.set(key, dendrite);
910
911
  }
911
912
  Object.values(neuron2.axon).forEach((collateral2) => {
912
913
  this.parentNeuronByCollateral.set(
913
914
  collateral2,
914
915
  neuron2
915
916
  );
916
- this.collateralIndex.add(collateral2);
917
917
  });
918
918
  }
919
919
  }
920
920
  getParentNeuronByCollateral(collateral2) {
921
921
  return this.parentNeuronByCollateral.get(collateral2);
922
922
  }
923
+ // Computed on demand (rarely used): one dendrite per collateral, last wins —
924
+ // preserving the previous index semantics without building it every construct.
923
925
  getDendrites() {
924
- return Array.from(this.dendriteIndex.values());
926
+ const byCollateral = /* @__PURE__ */ new Map();
927
+ for (const neuron2 of this.neurons) {
928
+ for (const dendrite of neuron2.dendrites) {
929
+ byCollateral.set(
930
+ dendrite.collateral,
931
+ dendrite
932
+ );
933
+ }
934
+ }
935
+ return Array.from(byCollateral.values());
925
936
  }
926
937
  getCollaterals() {
927
- return Array.from(this.collateralIndex.values());
938
+ const collaterals = /* @__PURE__ */ new Set();
939
+ for (const neuron2 of this.neurons) {
940
+ for (const collateral2 of Object.values(neuron2.axon)) {
941
+ collaterals.add(collateral2);
942
+ }
943
+ }
944
+ return Array.from(collaterals);
928
945
  }
929
946
  getSubscribers(collateral2) {
930
947
  return this.subIndex.get(collateral2) ?? [];
@@ -933,7 +950,7 @@ var CNSNetwork = class {
933
950
 
934
951
  // src/CNS.ts
935
952
  var CNS = class {
936
- constructor(neurons, options) {
953
+ constructor(neurons, options, global) {
937
954
  this.neurons = neurons;
938
955
  this.options = options;
939
956
  /**
@@ -946,6 +963,7 @@ var CNS = class {
946
963
  */
947
964
  this.globalResponseListeners = [];
948
965
  this.network = new CNSNetwork(this.neurons);
966
+ this.global = global;
949
967
  }
950
968
  addResponseListener(listener) {
951
969
  this.globalResponseListeners.push(listener);
@@ -1198,27 +1216,26 @@ var neuron = (axon) => {
1198
1216
  modality: singleModality,
1199
1217
  modalities: multipleModalities,
1200
1218
  afferentPaths: singleAfferentPaths,
1201
- default: singleDefaultHandler,
1202
- default: globalDefaultHandler,
1219
+ default: defaultHandler,
1203
1220
  output
1204
1221
  } = s;
1205
1222
  const modalityConfigs = multipleModalities ? multipleModalities : singleModality ? [
1206
1223
  {
1207
1224
  modality: singleModality,
1208
1225
  afferentPaths: singleAfferentPaths,
1209
- default: singleDefaultHandler
1226
+ default: defaultHandler
1210
1227
  }
1211
1228
  ] : [];
1212
1229
  const response = (payload, axon2, ctx) => {
1213
1230
  const stimulation = ctx.stimulation;
1214
1231
  const stimOptions = stimulation?.options;
1215
1232
  const runGlobalDefault = () => {
1216
- if (!globalDefaultHandler) {
1233
+ if (!defaultHandler) {
1217
1234
  throw new Error(
1218
1235
  `modalityDendrite: No handler found for modality and no default handler provided`
1219
1236
  );
1220
1237
  }
1221
- const res = globalDefaultHandler(payload, axon2, ctx);
1238
+ const res = defaultHandler(payload, axon2, ctx);
1222
1239
  if (res && typeof res.then === "function") {
1223
1240
  return res.then(
1224
1241
  (result) => output(result, axon2, ctx)
@@ -1237,7 +1254,7 @@ var neuron = (axon) => {
1237
1254
  }
1238
1255
  const stimAfferentPath = stimOptions.afferentPath;
1239
1256
  const handler = stimAfferentPath && matchingConfig.afferentPaths ? matchingConfig.afferentPaths.get(stimAfferentPath) : void 0;
1240
- const effectiveHandler = handler ?? matchingConfig.default ?? globalDefaultHandler;
1257
+ const effectiveHandler = handler ?? matchingConfig.default ?? defaultHandler;
1241
1258
  if (!effectiveHandler) {
1242
1259
  throw new Error(
1243
1260
  `modalityDendrite: No handler found for afferent path in modality and no default handler provided`
@@ -1272,11 +1289,16 @@ var neuron = (axon) => {
1272
1289
  };
1273
1290
  return api;
1274
1291
  };
1275
- var withCtx = () => ({
1276
- neuron: (axon) => {
1277
- return neuron(axon);
1278
- }
1279
- });
1292
+ var neuronFactory = () => {
1293
+ const factory = {
1294
+ withCtx: () => factory,
1295
+ withGlobal: () => factory,
1296
+ neuron: (axon) => neuron(axon)
1297
+ };
1298
+ return factory;
1299
+ };
1300
+ var withCtx = () => neuronFactory();
1301
+ var withGlobal = () => neuronFactory();
1280
1302
  var afferentPath = (parentAfferentPath) => {
1281
1303
  return {
1282
1304
  ...parentAfferentPath !== void 0 && {
@@ -1305,6 +1327,14 @@ var createPersistRegistry = (namedNeurons) => {
1305
1327
  }
1306
1328
  return registry;
1307
1329
  };
1330
+ var createCNS = (namedNeurons, options) => {
1331
+ const neurons = Object.values(namedNeurons).map(
1332
+ (entry) => entry && typeof entry === "object" && "neuron" in entry ? entry.neuron : entry
1333
+ );
1334
+ const cns = new CNS(neurons, options, options?.global);
1335
+ const registry = createPersistRegistry(namedNeurons);
1336
+ return { cns, registry };
1337
+ };
1308
1338
 
1309
1339
  exports.CNS = CNS;
1310
1340
  exports.CNSCollateral = CNSCollateral;
@@ -1313,9 +1343,12 @@ exports.CNSStimulationContextStore = CNSStimulationContextStore;
1313
1343
  exports.CNSStimulationGate = CNSStimulationGate;
1314
1344
  exports.afferentPath = afferentPath;
1315
1345
  exports.collateral = collateral;
1346
+ exports.createCNS = createCNS;
1316
1347
  exports.createPersistRegistry = createPersistRegistry;
1317
1348
  exports.modality = modality;
1318
1349
  exports.neuron = neuron;
1350
+ exports.neuronFactory = neuronFactory;
1319
1351
  exports.withCtx = withCtx;
1352
+ exports.withGlobal = withGlobal;
1320
1353
  //# sourceMappingURL=index.cjs.map
1321
1354
  //# sourceMappingURL=index.cjs.map