@cnstra/types 0.1.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.
@@ -0,0 +1,345 @@
1
+ type TCNSAfferentPath = {
2
+ parentAfferentPath?: TCNSAfferentPath;
3
+ };
4
+
5
+ type TCNSSignal<TCollateral extends ICNSCollateral<unknown>> = {
6
+ collateral: TCollateral;
7
+ payload?: TCollateral extends ICNSCollateral<infer P> ? P : never;
8
+ };
9
+
10
+ interface ICNSCollateral<TPayload> {
11
+ createSignal(): TCNSSignal<ICNSCollateral<TPayload>>;
12
+ createSignal(payload: TPayload): TCNSSignal<ICNSCollateral<TPayload>>;
13
+ }
14
+
15
+ /**
16
+ * Axon type used by neurons.
17
+ *
18
+ * Historically this type only took a collateral name union and a single payload
19
+ * type, which meant every collateral on the axon was typed with the same
20
+ * payload (typically a union of all possible payloads).
21
+ *
22
+ * That broke property-level typing on `axon.*.createSignal(...)` – the payload
23
+ * parameter became a big union instead of the specific payload for that
24
+ * collateral.
25
+ *
26
+ * To fix this while keeping existing generic call sites mostly intact, we add
27
+ * a third type parameter that carries the original heterogeneous axon object.
28
+ * When that third parameter is provided, we preserve the precise payload type
29
+ * for each collateral.
30
+ *
31
+ * - `TCollateralName` / `TInputPayload` are still useful for higher-level
32
+ * generic constraints (unions of names / payloads).
33
+ * - `TCollaterals` is the concrete axon map used to recover the per-key
34
+ * payload types.
35
+ */
36
+ type TCNSAxon<TCollaterals extends Record<string, ICNSCollateral<unknown>> = Record<string, ICNSCollateral<unknown>>> = {
37
+ [K in keyof TCollaterals]: TCollaterals[K];
38
+ };
39
+
40
+ /**
41
+ * Allowed return types from a neuron dendrite `response` function.
42
+ *
43
+ * Signals route by collateral object identity (not by name).
44
+ */
45
+ type TNCNeuronResponseReturn<TAxon extends TCNSAxon> = Promise<TCNSSignal<TAxon[keyof TAxon]>> | TCNSSignal<TAxon[keyof TAxon]> | Promise<TCNSSignal<TAxon[keyof TAxon]>[]> | TCNSSignal<TAxon[keyof TAxon]>[] | Promise<void> | void;
46
+
47
+ type TCNSLocalContextValueStore<TContextValue> = {
48
+ get: () => TContextValue | undefined;
49
+ set: (value: TContextValue) => void;
50
+ delete: () => void;
51
+ };
52
+
53
+ type TCNSNeuron<TContextValue, TAxonType extends TCNSAxon = TCNSAxon> = {
54
+ axon: TAxonType;
55
+ /** Optional per-neuron concurrency limit. If undefined or <= 0, no limit is applied. */
56
+ concurrency?: number;
57
+ /** Optional per-neuron max processing duration in milliseconds. */
58
+ maxDuration?: number;
59
+ dendrites: TCNSDendrite<TContextValue, ICNSCollateral<unknown>, TAxonType, any>[];
60
+ };
61
+
62
+ type TCNSOptions = {
63
+ /**
64
+ * Automatically cleanup context stores when the stimulation is finished.
65
+ * Default is false.
66
+ *
67
+ * CAN AFFECT PERFORMANCE!
68
+ * Better to use only to solve problems with memory quickly.
69
+ */
70
+ autoCleanupContexts?: boolean;
71
+ };
72
+
73
+ interface ICNSStimulationContextStore {
74
+ get: (key: object) => unknown;
75
+ set: (key: object, value: unknown) => void;
76
+ getAll: () => Map<object, unknown>;
77
+ setAll: (values: Map<object, unknown>) => void;
78
+ delete: (key: object) => void;
79
+ }
80
+
81
+ type TCNSModality = {
82
+ afferentPaths: Record<string | number, TCNSAfferentPath>;
83
+ };
84
+
85
+ type TCNSNeuronActivationTask<TNeuron extends object = object, TCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>> = {
86
+ neuron: TNeuron;
87
+ dendriteCollateral: TCollateral;
88
+ input?: TCNSSignal<TCollateral>;
89
+ };
90
+
91
+ type TCNSNeuronActivationTaskFailure<TNeuron extends object = object> = {
92
+ task: TCNSNeuronActivationTask<TNeuron>;
93
+ error: Error;
94
+ aborted: boolean;
95
+ };
96
+
97
+ /**
98
+ * The public contract of a running stimulation, as seen by consumers (via the
99
+ * return of `stimulate`/`activate` and `response.stimulation`). The concrete
100
+ * `CNSStimulation` class in `@cnstra/core` implements it; internal enqueue/dispatch
101
+ * methods are intentionally not part of the public interface.
102
+ */
103
+ interface ICNSStimulation<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any> = TCNSDendrite<any, any, any>> {
104
+ /** Resolves when the run completes; rejects on failed tasks / abort. */
105
+ waitUntilComplete(): Promise<void>;
106
+ /** All activation tasks seen this run. */
107
+ getAllActivationTasks(): TCNSNeuronActivationTask<TNeuron>[];
108
+ /** Tasks that failed or were aborted. */
109
+ getFailedTasks(): Array<TCNSNeuronActivationTaskFailure<TNeuron>>;
110
+ /**
111
+ * The current outstanding frontier — not-yet-completed tasks (queued, active,
112
+ * pending) plus failed/aborted ones. The set to persist for resume.
113
+ */
114
+ getOutstandingTasks(): TCNSNeuronActivationTask<TNeuron>[];
115
+ /** The per-neuron context store for this run. */
116
+ getContext(): ICNSStimulationContextStore;
117
+ }
118
+
119
+ /**
120
+ * Announced when a stimulation's synchronous turn ends: everything reachable
121
+ * without yielding to the event loop has run, and what remains (if anything) is
122
+ * waiting on a promise or blocked by a concurrency limit.
123
+ *
124
+ * This is the batch boundary. It is a property of the scheduler loop exhausting,
125
+ * not of any individual activation starting or finishing - which is why it cannot
126
+ * be derived from `onResponse`. A turn ends in six ways, and a response is only
127
+ * emitted in the first:
128
+ *
129
+ * 1. an activation finished and left the queue empty
130
+ * 2. the last activation *started* and returned a promise
131
+ * 3. the stimulation-level `concurrency` limit was reached
132
+ * 4. the stimulation was aborted
133
+ * 5. subscribers are parked behind an async `onResponse` listener
134
+ * 6. a per-neuron `setConcurrency` gate forced an activation onto a promise
135
+ *
136
+ * Fires exactly once per turn, including the final one - so a listener that
137
+ * flushes on every drain needs no separate completion handling.
138
+ *
139
+ * The counters carry the same meaning as on `TCNSStimulationResponse`, but they
140
+ * are measured *after* the loop exhausted rather than before the work runs. On a
141
+ * response `pendingActivations` is a forecast; here it is a fact, and a non-zero
142
+ * value can only mean blocked - by a concurrency limit or by an abort - never
143
+ * "about to run".
144
+ */
145
+ type TCNSStimulationDrain = {
146
+ stimulation: ICNSStimulation<any, any>;
147
+ /** `pendingActivations + activeActivations`. Zero means the stimulation is finished. */
148
+ queueLength: number;
149
+ /** Activations whose dendrite body has not been invoked. Non-zero here means blocked. */
150
+ pendingActivations: number;
151
+ /** Activations awaiting an unsettled promise. Non-zero means another drain is coming. */
152
+ activeActivations: number;
153
+ };
154
+
155
+ type TCNSStimulationOptions<TResponse, TStimulationContext extends Object = {}> = {
156
+ maxNeuronHops?: number;
157
+ onResponse?: (response: TResponse) => void | Promise<void>;
158
+ /**
159
+ * Called once at the end of every synchronous turn - see
160
+ * {@link TCNSStimulationDrain}. This is the batch boundary for integrations
161
+ * that commit accumulated writes (state managers, in-memory databases).
162
+ *
163
+ * Returns `void` by design: a returned promise is not awaited. There is
164
+ * nothing to gate on - the next turn is started by someone else's promise
165
+ * settling, so awaiting here would only delay the callback. For asynchronous
166
+ * work at a boundary use `onResponse`, which does support promises.
167
+ *
168
+ * Re-entrant calls are safe. A nested `stimulate()` is a separate stimulation
169
+ * and announces its own boundary inline - that is the supported way to react
170
+ * to a drain. Feeding work back into the *same* stimulation via
171
+ * `enqueueTasks()` runs it, but folds it into the turn that is already
172
+ * closing, so it produces no additional boundary of its own.
173
+ */
174
+ onDrain?: (drain: TCNSStimulationDrain) => void;
175
+ abortSignal?: AbortSignal;
176
+ ctx?: ICNSStimulationContextStore;
177
+ concurrency?: number;
178
+ modality?: TCNSModality;
179
+ afferentPath?: TCNSAfferentPath;
180
+ stimulationContext?: TStimulationContext;
181
+ };
182
+
183
+ type TCNSStimulationResponse<TInputCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>, TOutputCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>> = {
184
+ inputSignal?: TCNSSignal<TInputCollateral>;
185
+ outputSignal?: TCNSSignal<TOutputCollateral>;
186
+ modality?: TCNSModality;
187
+ afferentPath?: TCNSAfferentPath;
188
+ contextValue: Map<object, unknown>;
189
+ /**
190
+ * Every activation this stimulation still owns, in any state:
191
+ * `pendingActivations + activeActivations`.
192
+ *
193
+ * The unit is an activation - one `TCNSNeuronActivationTask`, i.e. one
194
+ * (neuron, dendrite, signal) triple. A single signal produces one activation
195
+ * per subscriber of its collateral.
196
+ *
197
+ * Active activations are counted deliberately: the queue owns work that has
198
+ * been handed to a neuron but not finished, the same way an AMQP queue counts
199
+ * unacknowledged messages in its total. `getAllActivationTasks()` follows the
200
+ * same model.
201
+ *
202
+ * `queueLength === 0` means the stimulation is finished - it is the exact
203
+ * condition used internally to settle `waitUntilComplete()`, so it holds on
204
+ * the terminal response and nowhere else.
205
+ *
206
+ * NOT a synchronous-batch boundary. A response is emitted when an activation
207
+ * finishes, but a batch can also end when one *starts* and goes async, and no
208
+ * response is emitted at that moment. Use `onDrain` for batch boundaries.
209
+ */
210
+ queueLength: number;
211
+ /**
212
+ * Activations that exist (or are reserved) but whose dendrite body has not
213
+ * been invoked yet: waiting in the ring buffer, created and held before being
214
+ * enqueued, or reserved by an array fan-out that is still mid-dispatch.
215
+ *
216
+ * Subscribers parked behind an async `onResponse` listener are counted here,
217
+ * not in {@link activeActivations}: the split is by lifecycle stage - whether
218
+ * the body has started - not by whether something is being awaited.
219
+ */
220
+ pendingActivations: number;
221
+ /**
222
+ * Activations whose dendrite body has been invoked and returned a promise
223
+ * that has not settled.
224
+ *
225
+ * A synchronous body passes through this state within a single pump
226
+ * iteration and is decremented before its response is emitted, so only
227
+ * genuinely asynchronous activations are ever observed here.
228
+ */
229
+ activeActivations: number;
230
+ stimulation: ICNSStimulation<any, any>;
231
+ /**
232
+ * The neuron that produced this response (present on both the success and the
233
+ * throw path). Enables name-based consumers (persist, devtools) to resolve the
234
+ * task's neuron reliably — including for sink/failed tasks that have no output
235
+ * collateral to trace an owner from.
236
+ */
237
+ neuron?: TCNSNeuron<any, any>;
238
+ error?: Error;
239
+ hops?: number;
240
+ };
241
+
242
+ type TCNSSubscriber<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any>> = {
243
+ neuron: TNeuron;
244
+ dendrite: TDendrite;
245
+ };
246
+
247
+ /**
248
+ * The public contract of the compiled neuron network, exposed on `ICNS.network`.
249
+ * The concrete `CNSNetwork` class in `@cnstra/core` implements it.
250
+ */
251
+ interface ICNSNetwork<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any> = TCNSDendrite<any, any, any>> {
252
+ readonly stronglyConnectedComponents: Set<TNeuron>[];
253
+ getSCCSetByNeuron(neuron: TNeuron): Set<TNeuron> | undefined;
254
+ getSccIndexByNeuron(neuron: TNeuron): number | undefined;
255
+ canNeuronBeGuaranteedDone(neuron: TNeuron, activeSccCounts: Map<number, number>): boolean;
256
+ getParentNeuronByCollateral(collateral: ICNSCollateral<unknown>): TNeuron | undefined;
257
+ getDendrites(): TDendrite[];
258
+ getCollaterals(): ICNSCollateral<unknown>[];
259
+ getSubscribers(collateral: ICNSCollateral<unknown>): TCNSSubscriber<TNeuron, TDendrite>[];
260
+ }
261
+
262
+ interface ICNS<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any> = TCNSDendrite<any, any, any>> {
263
+ options?: TCNSOptions;
264
+ network: ICNSNetwork<TNeuron, TDendrite>;
265
+ /**
266
+ * Add a global response listener applied to all stimulations.
267
+ * Returns an unsubscribe function.
268
+ */
269
+ addResponseListener(listener: (response: TCNSStimulationResponse) => void): () => void;
270
+ /**
271
+ * Add a global batch-boundary listener applied to all stimulations, called
272
+ * once at the end of every synchronous turn.
273
+ * Returns an unsubscribe function.
274
+ */
275
+ addDrainListener(listener: (drain: TCNSStimulationDrain) => void): () => void;
276
+ stimulate(signalOrSignals: TCNSSignal<ICNSCollateral<unknown>> | TCNSSignal<ICNSCollateral<unknown>>[], options?: TCNSStimulationOptions<TCNSStimulationResponse>): ICNSStimulation<TNeuron, TDendrite>;
277
+ /**
278
+ * Resume a run from a set of activation tasks (e.g. an outstanding frontier
279
+ * hydrated from persisted progress, or `getFailedTasks()` for a partial
280
+ * retry). Unlike {@link stimulate}, no entry signal is derived — the given
281
+ * tasks are enqueued directly. Pass `options.ctx` to restore neuron context.
282
+ */
283
+ activate(tasks: TCNSNeuronActivationTask<TNeuron>[], options?: TCNSStimulationOptions<TCNSStimulationResponse>): ICNSStimulation<TNeuron, TDendrite>;
284
+ }
285
+
286
+ /**
287
+ * The base `ctx` bag handed to every dendrite handler — nothing app-specific
288
+ * lives here. Factory layers add their own fields via the `TExt` extension
289
+ * (see {@link TCNSHandlerContext}), so the base stays clean for neurons that
290
+ * don't opt into anything.
291
+ */
292
+ type TCNSHandlerContextBase<TContextValue> = TCNSLocalContextValueStore<TContextValue> & {
293
+ abortSignal?: AbortSignal;
294
+ cns?: ICNS<any, any>;
295
+ stimulation?: any;
296
+ /**
297
+ * The caller-supplied value passed as `stimulate(sig, { stimulationContext })`
298
+ * (or `activate(...)`) — the same object on every hop of this stimulation, in
299
+ * any environment (sync or async, node or browser). Use it to correlate logs
300
+ * to a run: pass `{ stimulationId, attemptNumber }` and read it here. Typed
301
+ * `unknown` because the neuron doesn't own the shape — the caller does; cast at
302
+ * the read site.
303
+ */
304
+ stimulationContext?: unknown;
305
+ };
306
+ /**
307
+ * The `ctx` a dendrite handler receives. `TExt` is whatever the factory layers
308
+ * poured in (e.g. `withGlobal<T>()` adds `{ global: T }`) — it's a plain
309
+ * intersection, so layers add exactly what they want and nothing else, and a
310
+ * base neuron (no layers) gets `TExt = unknown`, i.e. just the base bag.
311
+ *
312
+ * The handler signature stays `(payload, axon, ctx)` no matter how many layers
313
+ * you stack; each layer widens the type of `ctx`, never the argument list.
314
+ */
315
+ type TCNSHandlerContext<TContextValue, TExt = unknown> = TCNSHandlerContextBase<TContextValue> & TExt;
316
+
317
+ type TCNSDendrite<TContextValue, TSenderCollateral extends ICNSCollateral<unknown>, TAxonType extends TCNSAxon, TExt = unknown> = {
318
+ collateral: TSenderCollateral;
319
+ response: (payload: TSenderCollateral extends ICNSCollateral<infer P> ? P : never, axon: TAxonType, ctx: TCNSHandlerContext<TContextValue, TExt>) => TNCNeuronResponseReturn<TAxonType>;
320
+ };
321
+
322
+ type CNSCollateralLike = ICNSCollateral<unknown>;
323
+ type TCNSNeuronPersistOptions<TNeuron extends TCNSNeuron<unknown, any> = TCNSNeuron<unknown, any>> = {
324
+ name: string;
325
+ neuron: TNeuron;
326
+ };
327
+ type TCNSCollateralPersistOptions<TCollateral extends CNSCollateralLike = CNSCollateralLike> = {
328
+ name: string;
329
+ collateral: TCollateral;
330
+ };
331
+ type TCNSStimulationPersistOptions = {
332
+ stimulationId: string;
333
+ };
334
+
335
+ type TCNSStimulationQueueItem<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any>, TInputCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>> = {
336
+ neuron: TNeuron;
337
+ subscriber: TCNSSubscriber<TNeuron, TDendrite>;
338
+ inputSignal?: TCNSSignal<TInputCollateral>;
339
+ };
340
+
341
+ type TCNSStimulationSerializedContextValue = {
342
+ [key: string]: unknown;
343
+ };
344
+
345
+ export type { ICNS, ICNSCollateral, ICNSNetwork, ICNSStimulation, ICNSStimulationContextStore, TCNSAfferentPath, TCNSAxon, TCNSCollateralPersistOptions, TCNSDendrite, TCNSHandlerContext, TCNSHandlerContextBase, TCNSLocalContextValueStore, TCNSModality, TCNSNeuron, TCNSNeuronActivationTask, TCNSNeuronActivationTaskFailure, TCNSNeuronPersistOptions, TCNSOptions, TCNSSignal, TCNSStimulationDrain, TCNSStimulationOptions, TCNSStimulationPersistOptions, TCNSStimulationQueueItem, TCNSStimulationResponse, TCNSStimulationSerializedContextValue, TCNSSubscriber, TNCNeuronResponseReturn };
@@ -0,0 +1,345 @@
1
+ type TCNSAfferentPath = {
2
+ parentAfferentPath?: TCNSAfferentPath;
3
+ };
4
+
5
+ type TCNSSignal<TCollateral extends ICNSCollateral<unknown>> = {
6
+ collateral: TCollateral;
7
+ payload?: TCollateral extends ICNSCollateral<infer P> ? P : never;
8
+ };
9
+
10
+ interface ICNSCollateral<TPayload> {
11
+ createSignal(): TCNSSignal<ICNSCollateral<TPayload>>;
12
+ createSignal(payload: TPayload): TCNSSignal<ICNSCollateral<TPayload>>;
13
+ }
14
+
15
+ /**
16
+ * Axon type used by neurons.
17
+ *
18
+ * Historically this type only took a collateral name union and a single payload
19
+ * type, which meant every collateral on the axon was typed with the same
20
+ * payload (typically a union of all possible payloads).
21
+ *
22
+ * That broke property-level typing on `axon.*.createSignal(...)` – the payload
23
+ * parameter became a big union instead of the specific payload for that
24
+ * collateral.
25
+ *
26
+ * To fix this while keeping existing generic call sites mostly intact, we add
27
+ * a third type parameter that carries the original heterogeneous axon object.
28
+ * When that third parameter is provided, we preserve the precise payload type
29
+ * for each collateral.
30
+ *
31
+ * - `TCollateralName` / `TInputPayload` are still useful for higher-level
32
+ * generic constraints (unions of names / payloads).
33
+ * - `TCollaterals` is the concrete axon map used to recover the per-key
34
+ * payload types.
35
+ */
36
+ type TCNSAxon<TCollaterals extends Record<string, ICNSCollateral<unknown>> = Record<string, ICNSCollateral<unknown>>> = {
37
+ [K in keyof TCollaterals]: TCollaterals[K];
38
+ };
39
+
40
+ /**
41
+ * Allowed return types from a neuron dendrite `response` function.
42
+ *
43
+ * Signals route by collateral object identity (not by name).
44
+ */
45
+ type TNCNeuronResponseReturn<TAxon extends TCNSAxon> = Promise<TCNSSignal<TAxon[keyof TAxon]>> | TCNSSignal<TAxon[keyof TAxon]> | Promise<TCNSSignal<TAxon[keyof TAxon]>[]> | TCNSSignal<TAxon[keyof TAxon]>[] | Promise<void> | void;
46
+
47
+ type TCNSLocalContextValueStore<TContextValue> = {
48
+ get: () => TContextValue | undefined;
49
+ set: (value: TContextValue) => void;
50
+ delete: () => void;
51
+ };
52
+
53
+ type TCNSNeuron<TContextValue, TAxonType extends TCNSAxon = TCNSAxon> = {
54
+ axon: TAxonType;
55
+ /** Optional per-neuron concurrency limit. If undefined or <= 0, no limit is applied. */
56
+ concurrency?: number;
57
+ /** Optional per-neuron max processing duration in milliseconds. */
58
+ maxDuration?: number;
59
+ dendrites: TCNSDendrite<TContextValue, ICNSCollateral<unknown>, TAxonType, any>[];
60
+ };
61
+
62
+ type TCNSOptions = {
63
+ /**
64
+ * Automatically cleanup context stores when the stimulation is finished.
65
+ * Default is false.
66
+ *
67
+ * CAN AFFECT PERFORMANCE!
68
+ * Better to use only to solve problems with memory quickly.
69
+ */
70
+ autoCleanupContexts?: boolean;
71
+ };
72
+
73
+ interface ICNSStimulationContextStore {
74
+ get: (key: object) => unknown;
75
+ set: (key: object, value: unknown) => void;
76
+ getAll: () => Map<object, unknown>;
77
+ setAll: (values: Map<object, unknown>) => void;
78
+ delete: (key: object) => void;
79
+ }
80
+
81
+ type TCNSModality = {
82
+ afferentPaths: Record<string | number, TCNSAfferentPath>;
83
+ };
84
+
85
+ type TCNSNeuronActivationTask<TNeuron extends object = object, TCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>> = {
86
+ neuron: TNeuron;
87
+ dendriteCollateral: TCollateral;
88
+ input?: TCNSSignal<TCollateral>;
89
+ };
90
+
91
+ type TCNSNeuronActivationTaskFailure<TNeuron extends object = object> = {
92
+ task: TCNSNeuronActivationTask<TNeuron>;
93
+ error: Error;
94
+ aborted: boolean;
95
+ };
96
+
97
+ /**
98
+ * The public contract of a running stimulation, as seen by consumers (via the
99
+ * return of `stimulate`/`activate` and `response.stimulation`). The concrete
100
+ * `CNSStimulation` class in `@cnstra/core` implements it; internal enqueue/dispatch
101
+ * methods are intentionally not part of the public interface.
102
+ */
103
+ interface ICNSStimulation<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any> = TCNSDendrite<any, any, any>> {
104
+ /** Resolves when the run completes; rejects on failed tasks / abort. */
105
+ waitUntilComplete(): Promise<void>;
106
+ /** All activation tasks seen this run. */
107
+ getAllActivationTasks(): TCNSNeuronActivationTask<TNeuron>[];
108
+ /** Tasks that failed or were aborted. */
109
+ getFailedTasks(): Array<TCNSNeuronActivationTaskFailure<TNeuron>>;
110
+ /**
111
+ * The current outstanding frontier — not-yet-completed tasks (queued, active,
112
+ * pending) plus failed/aborted ones. The set to persist for resume.
113
+ */
114
+ getOutstandingTasks(): TCNSNeuronActivationTask<TNeuron>[];
115
+ /** The per-neuron context store for this run. */
116
+ getContext(): ICNSStimulationContextStore;
117
+ }
118
+
119
+ /**
120
+ * Announced when a stimulation's synchronous turn ends: everything reachable
121
+ * without yielding to the event loop has run, and what remains (if anything) is
122
+ * waiting on a promise or blocked by a concurrency limit.
123
+ *
124
+ * This is the batch boundary. It is a property of the scheduler loop exhausting,
125
+ * not of any individual activation starting or finishing - which is why it cannot
126
+ * be derived from `onResponse`. A turn ends in six ways, and a response is only
127
+ * emitted in the first:
128
+ *
129
+ * 1. an activation finished and left the queue empty
130
+ * 2. the last activation *started* and returned a promise
131
+ * 3. the stimulation-level `concurrency` limit was reached
132
+ * 4. the stimulation was aborted
133
+ * 5. subscribers are parked behind an async `onResponse` listener
134
+ * 6. a per-neuron `setConcurrency` gate forced an activation onto a promise
135
+ *
136
+ * Fires exactly once per turn, including the final one - so a listener that
137
+ * flushes on every drain needs no separate completion handling.
138
+ *
139
+ * The counters carry the same meaning as on `TCNSStimulationResponse`, but they
140
+ * are measured *after* the loop exhausted rather than before the work runs. On a
141
+ * response `pendingActivations` is a forecast; here it is a fact, and a non-zero
142
+ * value can only mean blocked - by a concurrency limit or by an abort - never
143
+ * "about to run".
144
+ */
145
+ type TCNSStimulationDrain = {
146
+ stimulation: ICNSStimulation<any, any>;
147
+ /** `pendingActivations + activeActivations`. Zero means the stimulation is finished. */
148
+ queueLength: number;
149
+ /** Activations whose dendrite body has not been invoked. Non-zero here means blocked. */
150
+ pendingActivations: number;
151
+ /** Activations awaiting an unsettled promise. Non-zero means another drain is coming. */
152
+ activeActivations: number;
153
+ };
154
+
155
+ type TCNSStimulationOptions<TResponse, TStimulationContext extends Object = {}> = {
156
+ maxNeuronHops?: number;
157
+ onResponse?: (response: TResponse) => void | Promise<void>;
158
+ /**
159
+ * Called once at the end of every synchronous turn - see
160
+ * {@link TCNSStimulationDrain}. This is the batch boundary for integrations
161
+ * that commit accumulated writes (state managers, in-memory databases).
162
+ *
163
+ * Returns `void` by design: a returned promise is not awaited. There is
164
+ * nothing to gate on - the next turn is started by someone else's promise
165
+ * settling, so awaiting here would only delay the callback. For asynchronous
166
+ * work at a boundary use `onResponse`, which does support promises.
167
+ *
168
+ * Re-entrant calls are safe. A nested `stimulate()` is a separate stimulation
169
+ * and announces its own boundary inline - that is the supported way to react
170
+ * to a drain. Feeding work back into the *same* stimulation via
171
+ * `enqueueTasks()` runs it, but folds it into the turn that is already
172
+ * closing, so it produces no additional boundary of its own.
173
+ */
174
+ onDrain?: (drain: TCNSStimulationDrain) => void;
175
+ abortSignal?: AbortSignal;
176
+ ctx?: ICNSStimulationContextStore;
177
+ concurrency?: number;
178
+ modality?: TCNSModality;
179
+ afferentPath?: TCNSAfferentPath;
180
+ stimulationContext?: TStimulationContext;
181
+ };
182
+
183
+ type TCNSStimulationResponse<TInputCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>, TOutputCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>> = {
184
+ inputSignal?: TCNSSignal<TInputCollateral>;
185
+ outputSignal?: TCNSSignal<TOutputCollateral>;
186
+ modality?: TCNSModality;
187
+ afferentPath?: TCNSAfferentPath;
188
+ contextValue: Map<object, unknown>;
189
+ /**
190
+ * Every activation this stimulation still owns, in any state:
191
+ * `pendingActivations + activeActivations`.
192
+ *
193
+ * The unit is an activation - one `TCNSNeuronActivationTask`, i.e. one
194
+ * (neuron, dendrite, signal) triple. A single signal produces one activation
195
+ * per subscriber of its collateral.
196
+ *
197
+ * Active activations are counted deliberately: the queue owns work that has
198
+ * been handed to a neuron but not finished, the same way an AMQP queue counts
199
+ * unacknowledged messages in its total. `getAllActivationTasks()` follows the
200
+ * same model.
201
+ *
202
+ * `queueLength === 0` means the stimulation is finished - it is the exact
203
+ * condition used internally to settle `waitUntilComplete()`, so it holds on
204
+ * the terminal response and nowhere else.
205
+ *
206
+ * NOT a synchronous-batch boundary. A response is emitted when an activation
207
+ * finishes, but a batch can also end when one *starts* and goes async, and no
208
+ * response is emitted at that moment. Use `onDrain` for batch boundaries.
209
+ */
210
+ queueLength: number;
211
+ /**
212
+ * Activations that exist (or are reserved) but whose dendrite body has not
213
+ * been invoked yet: waiting in the ring buffer, created and held before being
214
+ * enqueued, or reserved by an array fan-out that is still mid-dispatch.
215
+ *
216
+ * Subscribers parked behind an async `onResponse` listener are counted here,
217
+ * not in {@link activeActivations}: the split is by lifecycle stage - whether
218
+ * the body has started - not by whether something is being awaited.
219
+ */
220
+ pendingActivations: number;
221
+ /**
222
+ * Activations whose dendrite body has been invoked and returned a promise
223
+ * that has not settled.
224
+ *
225
+ * A synchronous body passes through this state within a single pump
226
+ * iteration and is decremented before its response is emitted, so only
227
+ * genuinely asynchronous activations are ever observed here.
228
+ */
229
+ activeActivations: number;
230
+ stimulation: ICNSStimulation<any, any>;
231
+ /**
232
+ * The neuron that produced this response (present on both the success and the
233
+ * throw path). Enables name-based consumers (persist, devtools) to resolve the
234
+ * task's neuron reliably — including for sink/failed tasks that have no output
235
+ * collateral to trace an owner from.
236
+ */
237
+ neuron?: TCNSNeuron<any, any>;
238
+ error?: Error;
239
+ hops?: number;
240
+ };
241
+
242
+ type TCNSSubscriber<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any>> = {
243
+ neuron: TNeuron;
244
+ dendrite: TDendrite;
245
+ };
246
+
247
+ /**
248
+ * The public contract of the compiled neuron network, exposed on `ICNS.network`.
249
+ * The concrete `CNSNetwork` class in `@cnstra/core` implements it.
250
+ */
251
+ interface ICNSNetwork<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any> = TCNSDendrite<any, any, any>> {
252
+ readonly stronglyConnectedComponents: Set<TNeuron>[];
253
+ getSCCSetByNeuron(neuron: TNeuron): Set<TNeuron> | undefined;
254
+ getSccIndexByNeuron(neuron: TNeuron): number | undefined;
255
+ canNeuronBeGuaranteedDone(neuron: TNeuron, activeSccCounts: Map<number, number>): boolean;
256
+ getParentNeuronByCollateral(collateral: ICNSCollateral<unknown>): TNeuron | undefined;
257
+ getDendrites(): TDendrite[];
258
+ getCollaterals(): ICNSCollateral<unknown>[];
259
+ getSubscribers(collateral: ICNSCollateral<unknown>): TCNSSubscriber<TNeuron, TDendrite>[];
260
+ }
261
+
262
+ interface ICNS<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any> = TCNSDendrite<any, any, any>> {
263
+ options?: TCNSOptions;
264
+ network: ICNSNetwork<TNeuron, TDendrite>;
265
+ /**
266
+ * Add a global response listener applied to all stimulations.
267
+ * Returns an unsubscribe function.
268
+ */
269
+ addResponseListener(listener: (response: TCNSStimulationResponse) => void): () => void;
270
+ /**
271
+ * Add a global batch-boundary listener applied to all stimulations, called
272
+ * once at the end of every synchronous turn.
273
+ * Returns an unsubscribe function.
274
+ */
275
+ addDrainListener(listener: (drain: TCNSStimulationDrain) => void): () => void;
276
+ stimulate(signalOrSignals: TCNSSignal<ICNSCollateral<unknown>> | TCNSSignal<ICNSCollateral<unknown>>[], options?: TCNSStimulationOptions<TCNSStimulationResponse>): ICNSStimulation<TNeuron, TDendrite>;
277
+ /**
278
+ * Resume a run from a set of activation tasks (e.g. an outstanding frontier
279
+ * hydrated from persisted progress, or `getFailedTasks()` for a partial
280
+ * retry). Unlike {@link stimulate}, no entry signal is derived — the given
281
+ * tasks are enqueued directly. Pass `options.ctx` to restore neuron context.
282
+ */
283
+ activate(tasks: TCNSNeuronActivationTask<TNeuron>[], options?: TCNSStimulationOptions<TCNSStimulationResponse>): ICNSStimulation<TNeuron, TDendrite>;
284
+ }
285
+
286
+ /**
287
+ * The base `ctx` bag handed to every dendrite handler — nothing app-specific
288
+ * lives here. Factory layers add their own fields via the `TExt` extension
289
+ * (see {@link TCNSHandlerContext}), so the base stays clean for neurons that
290
+ * don't opt into anything.
291
+ */
292
+ type TCNSHandlerContextBase<TContextValue> = TCNSLocalContextValueStore<TContextValue> & {
293
+ abortSignal?: AbortSignal;
294
+ cns?: ICNS<any, any>;
295
+ stimulation?: any;
296
+ /**
297
+ * The caller-supplied value passed as `stimulate(sig, { stimulationContext })`
298
+ * (or `activate(...)`) — the same object on every hop of this stimulation, in
299
+ * any environment (sync or async, node or browser). Use it to correlate logs
300
+ * to a run: pass `{ stimulationId, attemptNumber }` and read it here. Typed
301
+ * `unknown` because the neuron doesn't own the shape — the caller does; cast at
302
+ * the read site.
303
+ */
304
+ stimulationContext?: unknown;
305
+ };
306
+ /**
307
+ * The `ctx` a dendrite handler receives. `TExt` is whatever the factory layers
308
+ * poured in (e.g. `withGlobal<T>()` adds `{ global: T }`) — it's a plain
309
+ * intersection, so layers add exactly what they want and nothing else, and a
310
+ * base neuron (no layers) gets `TExt = unknown`, i.e. just the base bag.
311
+ *
312
+ * The handler signature stays `(payload, axon, ctx)` no matter how many layers
313
+ * you stack; each layer widens the type of `ctx`, never the argument list.
314
+ */
315
+ type TCNSHandlerContext<TContextValue, TExt = unknown> = TCNSHandlerContextBase<TContextValue> & TExt;
316
+
317
+ type TCNSDendrite<TContextValue, TSenderCollateral extends ICNSCollateral<unknown>, TAxonType extends TCNSAxon, TExt = unknown> = {
318
+ collateral: TSenderCollateral;
319
+ response: (payload: TSenderCollateral extends ICNSCollateral<infer P> ? P : never, axon: TAxonType, ctx: TCNSHandlerContext<TContextValue, TExt>) => TNCNeuronResponseReturn<TAxonType>;
320
+ };
321
+
322
+ type CNSCollateralLike = ICNSCollateral<unknown>;
323
+ type TCNSNeuronPersistOptions<TNeuron extends TCNSNeuron<unknown, any> = TCNSNeuron<unknown, any>> = {
324
+ name: string;
325
+ neuron: TNeuron;
326
+ };
327
+ type TCNSCollateralPersistOptions<TCollateral extends CNSCollateralLike = CNSCollateralLike> = {
328
+ name: string;
329
+ collateral: TCollateral;
330
+ };
331
+ type TCNSStimulationPersistOptions = {
332
+ stimulationId: string;
333
+ };
334
+
335
+ type TCNSStimulationQueueItem<TNeuron extends TCNSNeuron<any, any>, TDendrite extends TCNSDendrite<any, any, any>, TInputCollateral extends ICNSCollateral<unknown> = ICNSCollateral<unknown>> = {
336
+ neuron: TNeuron;
337
+ subscriber: TCNSSubscriber<TNeuron, TDendrite>;
338
+ inputSignal?: TCNSSignal<TInputCollateral>;
339
+ };
340
+
341
+ type TCNSStimulationSerializedContextValue = {
342
+ [key: string]: unknown;
343
+ };
344
+
345
+ export type { ICNS, ICNSCollateral, ICNSNetwork, ICNSStimulation, ICNSStimulationContextStore, TCNSAfferentPath, TCNSAxon, TCNSCollateralPersistOptions, TCNSDendrite, TCNSHandlerContext, TCNSHandlerContextBase, TCNSLocalContextValueStore, TCNSModality, TCNSNeuron, TCNSNeuronActivationTask, TCNSNeuronActivationTaskFailure, TCNSNeuronPersistOptions, TCNSOptions, TCNSSignal, TCNSStimulationDrain, TCNSStimulationOptions, TCNSStimulationPersistOptions, TCNSStimulationQueueItem, TCNSStimulationResponse, TCNSStimulationSerializedContextValue, TCNSSubscriber, TNCNeuronResponseReturn };
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __copyProps = (to, from, except, desc) => {
6
+ if (from && typeof from === "object" || typeof from === "function") {
7
+ for (let key of __getOwnPropNames(from))
8
+ if (!__hasOwnProp.call(to, key) && key !== except)
9
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
10
+ }
11
+ return to;
12
+ };
13
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
14
+
15
+ // src/index.ts
16
+ var index_exports = {};
17
+ module.exports = __toCommonJS(index_exports);
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./types/TCNSAfferentPath\";\nexport * from \"./types/TCNSAxon\";\nexport * from \"./types/TCNSDendrite\";\nexport * from \"./types/TCNSHandlerContext\";\nexport * from \"./types/TCNSLocalContextValueStore\";\nexport * from \"./types/TCNSModality\";\nexport * from \"./types/TCNSNeuron\";\nexport * from \"./types/TCNSNeuronActivationTask\";\nexport * from \"./types/TCNSNeuronActivationTaskFailure\";\nexport * from \"./types/TCNSNeuronResponseReturn\";\nexport * from \"./types/TCNSOptions\";\nexport * from \"./types/TCNSPersist\";\nexport * from \"./types/TCNSSignal\";\nexport * from \"./types/TCNSStimulationDrain\";\nexport * from \"./types/TCNSStimulationOptions\";\nexport * from \"./types/TCNSStimulationQueueItem\";\nexport * from \"./types/TCNSStimulationResponse\";\nexport * from \"./types/TCNSStimulationSerializedContextValue\";\nexport * from \"./types/TCNSSubscriber\";\nexport * from \"./interfaces/ICNS\";\nexport * from \"./interfaces/ICNSCollateral\";\nexport * from \"./interfaces/ICNSNetwork\";\nexport * from \"./interfaces/ICNSStimulation\";\nexport * from \"./interfaces/ICNSStimulationContextStore\";\n"],"mappings":";;;;;;;;;;;;;;;AAAA;;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@cnstra/types",
3
+ "version": "0.1.0",
4
+ "description": "Shared TypeScript contract for CNStra: signals, tasks, axons, neuron/collateral/stimulation/network interfaces. Zero runtime — the base package core and integrations build on.",
5
+ "keywords": ["cnstra", "types", "contract", "interfaces"],
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "files": ["dist"],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "test:types": "tsc --noEmit",
21
+ "clean": "rm -rf dist"
22
+ },
23
+ "devDependencies": {
24
+ "tsup": "8.5.1",
25
+ "typescript": "5.7.2"
26
+ },
27
+ "author": "Andrei Baikov <abaikov23@gmail.com>",
28
+ "license": "MIT",
29
+ "publishConfig": { "access": "public" }
30
+ }