rpc-bastion 0.3.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,324 @@
1
+ import { BreakerState, EndpointHealth, LoadBalancerStrategy, CircuitBreakerOptions, RpcSubscriptionsApiLike, Clock } from '@rpc-bastion/core';
2
+ import { ChaosPlan } from '@rpc-bastion/testkit';
3
+ import { SenderRpc, Commitment, ConfirmationOutcome } from '@rpc-bastion/sender';
4
+
5
+ /** The doctor only reads time; a `now()` source is all it needs. */
6
+ type DoctorClock = {
7
+ now(): number;
8
+ };
9
+ /** The minimal per-endpoint RPC surface `doctor` probes. */
10
+ interface DoctorRpc {
11
+ getSlot(config?: Record<string, unknown>): {
12
+ send(opts?: {
13
+ abortSignal?: AbortSignal;
14
+ }): Promise<bigint>;
15
+ };
16
+ getHealth(config?: Record<string, unknown>): {
17
+ send(opts?: {
18
+ abortSignal?: AbortSignal;
19
+ }): Promise<string>;
20
+ };
21
+ getVersion(config?: Record<string, unknown>): {
22
+ send(opts?: {
23
+ abortSignal?: AbortSignal;
24
+ }): Promise<{
25
+ 'solana-core'?: string;
26
+ 'feature-set'?: number;
27
+ }>;
28
+ };
29
+ }
30
+ /** Builds a {@link DoctorRpc} for an endpoint URL. */
31
+ type DoctorRpcFactory = (url: string) => DoctorRpc;
32
+ /** Per-endpoint diagnostic result. */
33
+ interface EndpointDiagnostic {
34
+ url: string;
35
+ /** `true` if the endpoint answered every probe. */
36
+ reachable: boolean;
37
+ /** Median latency over the probes (ms), or null if unreachable. */
38
+ latencyP50Ms: number | null;
39
+ /** `getHealth` result (`ok` / a behind message), or null. */
40
+ health: string | null;
41
+ /** Latest slot seen, or null. */
42
+ slot: bigint | null;
43
+ /** Slots behind the freshest endpoint in this run, or null. */
44
+ slotLag: number | null;
45
+ /** `solana-core` version, or null. */
46
+ version: string | null;
47
+ /** First error message, if any probe failed. */
48
+ error: string | null;
49
+ }
50
+ /** The full doctor report (the `--json` payload). */
51
+ interface DoctorReport {
52
+ endpoints: EndpointDiagnostic[];
53
+ /** Freshest slot across reachable endpoints, or null. */
54
+ clusterMaxSlot: bigint | null;
55
+ /** Count reachable / total. */
56
+ reachable: number;
57
+ total: number;
58
+ }
59
+ /** Options for {@link runDoctor}. */
60
+ interface DoctorOptions {
61
+ /** Latency probes per endpoint (default 5). */
62
+ probes?: number;
63
+ rpcFactory: DoctorRpcFactory;
64
+ clock?: DoctorClock;
65
+ signal?: AbortSignal;
66
+ }
67
+ /** Median of a numeric array (linear interpolation not needed for ms). */
68
+ declare function median(values: readonly number[]): number | null;
69
+ /**
70
+ * Turns a probe failure into a human-readable message.
71
+ *
72
+ * Works around an upstream Solana Kit defect: when an endpoint answers HTTP 200
73
+ * with a JSON-RPC *error* whose code Kit's message template can't render (e.g. a
74
+ * keyless gateway returning `-32000 "needs an API key"`), Kit's own formatter
75
+ * (`@solana/errors` → `getHumanReadableErrorMessage`, reached via
76
+ * `getSolanaErrorFromJsonRpcError` → `new SolanaError`) throws a bare
77
+ * `TypeError: Cannot read properties of undefined (reading 'length')` *instead of*
78
+ * the intended SolanaError. That TypeError carries no `context`/`code`, so the
79
+ * real RPC message is unrecoverable — but its stack unambiguously identifies the
80
+ * cause. Detect that signature and report something actionable rather than leaking
81
+ * Kit's internal failure.
82
+ */
83
+ declare function describeProbeError(e: unknown): string;
84
+ /**
85
+ * Probes a single endpoint: `probes` latency-timed `getSlot` calls (p50), one
86
+ * `getHealth`, and one `getVersion`. Never throws — failures are captured in the
87
+ * returned diagnostic. Pure given an injected `rpcFactory` + `clock`.
88
+ */
89
+ declare function probeEndpoint(url: string, options: {
90
+ probes: number;
91
+ rpc: DoctorRpc;
92
+ clock: DoctorClock;
93
+ signal?: AbortSignal;
94
+ }): Promise<EndpointDiagnostic>;
95
+ /**
96
+ * Runs `doctor` across all endpoints: probes each (in parallel), computes the
97
+ * cluster-max slot, and back-fills each endpoint's `slotLag`. Pure given the
98
+ * injected `rpcFactory`/`clock` — the rendering layer formats the result.
99
+ */
100
+ declare function runDoctor(urls: readonly string[], options: DoctorOptions): Promise<DoctorReport>;
101
+ /** Serializes a {@link DoctorReport} to stable JSON (bigint → string). */
102
+ declare function doctorReportToJson(report: DoctorReport): string;
103
+
104
+ /** The `bastion.config.json` shape. */
105
+ interface BastionConfig {
106
+ endpoints: string[];
107
+ jito?: {
108
+ region?: string;
109
+ baseUrl?: string;
110
+ };
111
+ }
112
+ /**
113
+ * Parses + validates a `bastion.config.json` payload. Pure (takes the already-read
114
+ * text) so it's testable without filesystem access. Throws a clear error on a
115
+ * malformed config.
116
+ */
117
+ declare function parseConfig(text: string): BastionConfig;
118
+ /**
119
+ * Resolves endpoints from either `--endpoints` (comma-separated) or a parsed
120
+ * config. `--endpoints` wins when both are present.
121
+ */
122
+ declare function resolveEndpoints(opts: {
123
+ endpoints?: string;
124
+ config?: BastionConfig;
125
+ }): string[];
126
+
127
+ /** A parsed command line: the command, positional args, and flags. */
128
+ interface ParsedArgs {
129
+ command: string | null;
130
+ positionals: string[];
131
+ /** `--flag value` and `--bool` (→ `true`). */
132
+ flags: Record<string, string | boolean>;
133
+ }
134
+ /**
135
+ * A tiny, dependency-free argv parser. `--key value` → `{ key: 'value' }`;
136
+ * `--key=value` → same; a bare `--flag` (followed by another flag or nothing) →
137
+ * `{ flag: true }`. The first non-flag token is the command; the rest are
138
+ * positionals. Pure and fully testable.
139
+ */
140
+ declare function parseArgs(argv: readonly string[]): ParsedArgs;
141
+ /** Reads a flag as a string, or returns `undefined`. */
142
+ declare function flagString(args: ParsedArgs, name: string): string | undefined;
143
+ /** Reads a flag as a boolean (presence or `true`). */
144
+ declare function flagBool(args: ParsedArgs, name: string): boolean;
145
+ /** Reads a flag as a positive integer, or `fallback`. */
146
+ declare function flagInt(args: ParsedArgs, name: string, fallback: number): number;
147
+
148
+ /** A formatted row for the monitor table (pure projection of EndpointHealth). */
149
+ interface MonitorRow {
150
+ url: string;
151
+ /** Status dot category. */
152
+ dot: 'green' | 'yellow' | 'red';
153
+ latencyMs: number;
154
+ errorRatePct: number;
155
+ slotLag: number | null;
156
+ breaker: BreakerState;
157
+ inFlight: number;
158
+ }
159
+ /** A summary across the whole pool. */
160
+ interface MonitorSummary {
161
+ rows: MonitorRow[];
162
+ healthy: number;
163
+ total: number;
164
+ /** Worst (highest) slot lag across endpoints, or null. */
165
+ maxSlotLag: number | null;
166
+ }
167
+ /** Categorizes an endpoint into a status dot from its health + breaker. */
168
+ declare function statusDot(health: EndpointHealth): MonitorRow['dot'];
169
+ /**
170
+ * Projects a pool health snapshot into renderable monitor rows + a summary.
171
+ * Pure — the rendering layer turns this into a table/sparkline. Reuses the same
172
+ * `EndpointHealth` the SDK pool produces (no parallel health model).
173
+ */
174
+ declare function summarizeSnapshot(snapshot: readonly EndpointHealth[]): MonitorSummary;
175
+ /**
176
+ * Appends a latency sample to a bounded ring buffer (for the monitor sparkline).
177
+ * Returns a NEW array (immutable), keeping at most `size` samples.
178
+ */
179
+ declare function pushSample(history: readonly number[], sample: number, size?: number): number[];
180
+
181
+ /** A named, scripted chaos scenario for `bastion simulate`. */
182
+ interface Scenario {
183
+ name: string;
184
+ description: string;
185
+ endpoints: string[];
186
+ /** Per-endpoint chaos plan, keyed by endpoint url. */
187
+ plans: Record<string, ChaosPlan>;
188
+ /** How many probe ticks to run (default 10). */
189
+ ticks: number;
190
+ /** Load-balancer strategy (default the pool's `health-weighted`). */
191
+ strategy?: LoadBalancerStrategy;
192
+ /**
193
+ * User-level calls issued per tick (default 1). A small burst lets a failing
194
+ * endpoint accumulate enough failures to trip its breaker — the drama beat.
195
+ */
196
+ callsPerTick?: number;
197
+ /** Per-scenario breaker tuning, so a scenario can show open → half-open → closed. */
198
+ breaker?: CircuitBreakerOptions;
199
+ }
200
+ /** A narrative event observed during one tick, projected from the bus. */
201
+ interface TickEvent {
202
+ /** Compact kind for the renderer to colour: which arc beat this is. */
203
+ kind: 'served' | 'error' | 'failover' | 'breaker-open' | 'breaker-half-open' | 'breaker-closed';
204
+ /** The endpoint this event is about (the `to` endpoint for a failover). */
205
+ endpoint: string;
206
+ /** A short human label, e.g. `rpc-a → rpc-b` or `OPEN`. */
207
+ detail: string;
208
+ }
209
+ /** A single tick of a simulation: the health snapshot after the tick's probe. */
210
+ interface SimulationTick {
211
+ tick: number;
212
+ snapshot: EndpointHealth[];
213
+ /**
214
+ * The endpoint that ultimately served this tick's user-level call, or null if
215
+ * the call failed on every survivor. Drives the per-endpoint traffic counters.
216
+ */
217
+ servedBy?: string | null;
218
+ /** `false` only when the user-level call threw after all failover/retry. */
219
+ userCallOk?: boolean;
220
+ /** Narrative events the bus emitted during this tick (failover, breaker flips). */
221
+ events?: TickEvent[];
222
+ }
223
+ /** The aggregate verdict of a simulation run — the "0 failed user calls" thesis. */
224
+ interface SimulationSummary {
225
+ /** Every tick is one user-level call. */
226
+ userCallsTotal: number;
227
+ /** Calls that threw after all failover + retry were exhausted. */
228
+ userCallsFailed: number;
229
+ /** Per-endpoint served count + error count over the run. */
230
+ perEndpoint: Array<{
231
+ url: string;
232
+ served: number;
233
+ errors: number;
234
+ }>;
235
+ /** Count of breaker open transitions observed. */
236
+ breakerOpens: number;
237
+ }
238
+ /** The built-in scenario catalogue. */
239
+ declare const SCENARIOS: Record<string, Scenario>;
240
+ /** Lists the available scenario names + descriptions. */
241
+ declare function listScenarios(): Array<{
242
+ name: string;
243
+ description: string;
244
+ }>;
245
+ /** Options for {@link runSimulation}. */
246
+ interface SimulationOptions {
247
+ /** Advances the simulated clock + mock chain by this many ms per tick (default 500). */
248
+ tickMs?: number;
249
+ seed?: number;
250
+ /** Called after each tick (for live rendering). */
251
+ onTick?: (tick: SimulationTick) => void;
252
+ /** Drives time + the mock chain forward between ticks (injected for tests). */
253
+ advance: (ms: number) => Promise<void> | void;
254
+ /** Reads "now" in ms (injected; defaults to a tick-counted clock). */
255
+ now: () => number;
256
+ }
257
+ /**
258
+ * Runs a named scenario against a resilient pool built over chaos transports
259
+ * (real SDK pool/health code), returning a snapshot per tick. The caller injects
260
+ * `advance`/`now` so tests drive it deterministically with fake timers + a
261
+ * ManualClock; the renderer just consumes the ticks.
262
+ */
263
+ declare function runSimulation(scenario: Scenario, options: SimulationOptions): Promise<SimulationTick[]>;
264
+ /**
265
+ * Projects a finished run into the headline verdict: how many user-level calls
266
+ * the SDK made, how many failed despite failover/retry (the thesis is **zero**),
267
+ * per-endpoint traffic, and how many times a breaker opened. Pure over the ticks.
268
+ */
269
+ declare function summarizeSimulation(ticks: readonly SimulationTick[]): SimulationSummary;
270
+
271
+ /** A point-in-time status line for the `watch-tx` UI. */
272
+ interface WatchTxUpdate {
273
+ at: number;
274
+ state: 'watching' | 'confirmed' | 'failed' | 'expired' | 'aborted';
275
+ detail: string;
276
+ }
277
+ /** Final result of watching a transaction. */
278
+ interface WatchTxResult {
279
+ signature: string;
280
+ outcome: ConfirmationOutcome['type'];
281
+ slot: bigint | null;
282
+ updates: WatchTxUpdate[];
283
+ }
284
+ /** Options for {@link watchTransaction}. */
285
+ interface WatchTxOptions {
286
+ rpc: SenderRpc;
287
+ rpcSubscriptions?: RpcSubscriptionsApiLike;
288
+ lastValidBlockHeight: bigint;
289
+ commitment?: Commitment;
290
+ clock?: Clock;
291
+ pollIntervalMs?: number;
292
+ blockHeightIntervalMs?: number;
293
+ signal?: AbortSignal;
294
+ /** Called on each status update (for live rendering). */
295
+ onUpdate?: (update: WatchTxUpdate) => void;
296
+ }
297
+ /**
298
+ * Watches a transaction signature to a terminal state, reusing the SDK's
299
+ * confirmation engine (WS + polling race). Returns the outcome plus a timeline of
300
+ * updates. Pure given injected `rpc`/`clock` — no rendering here.
301
+ */
302
+ declare function watchTransaction(signature: string, options: WatchTxOptions): Promise<WatchTxResult>;
303
+
304
+ /** The set of known commands. */
305
+ declare const COMMANDS: readonly ["doctor", "monitor", "watch-tx", "simulate", "help"];
306
+ type Command = (typeof COMMANDS)[number];
307
+ /**
308
+ * Resolves the command to run from parsed args. Pure + fully tested — this is the
309
+ * routing brain; the live `main()` (which does I/O) lives in `./ui/run`.
310
+ */
311
+ declare function route(args: ParsedArgs): Command;
312
+
313
+ /**
314
+ * rpc-bastion — the `bastion` diagnostics CLI: doctor, monitor, watch-tx, simulate.
315
+ *
316
+ * The data layer (endpoint probing, health aggregation, scenario simulation,
317
+ * config/arg parsing) is exported here for programmatic use and is fully
318
+ * unit-tested; the rendering + live-I/O layer lives under `src/ui/**` (excluded
319
+ * from coverage). The executable itself is `src/bin.ts` → `dist/bin.cjs`.
320
+ */
321
+ /** Package semver, surfaced for diagnostics and the CLI `doctor` command. */
322
+ declare const VERSION = "0.3.0";
323
+
324
+ export { type BastionConfig, COMMANDS, type Command, type DoctorClock, type DoctorOptions, type DoctorReport, type DoctorRpc, type DoctorRpcFactory, type EndpointDiagnostic, type MonitorRow, type MonitorSummary, type ParsedArgs, SCENARIOS, type Scenario, type SimulationOptions, type SimulationSummary, type SimulationTick, type TickEvent, VERSION, type WatchTxOptions, type WatchTxResult, type WatchTxUpdate, describeProbeError, doctorReportToJson, flagBool, flagInt, flagString, listScenarios, median, parseArgs, parseConfig, probeEndpoint, pushSample, resolveEndpoints, route, runDoctor, runSimulation, statusDot, summarizeSimulation, summarizeSnapshot, watchTransaction };
@@ -0,0 +1,324 @@
1
+ import { BreakerState, EndpointHealth, LoadBalancerStrategy, CircuitBreakerOptions, RpcSubscriptionsApiLike, Clock } from '@rpc-bastion/core';
2
+ import { ChaosPlan } from '@rpc-bastion/testkit';
3
+ import { SenderRpc, Commitment, ConfirmationOutcome } from '@rpc-bastion/sender';
4
+
5
+ /** The doctor only reads time; a `now()` source is all it needs. */
6
+ type DoctorClock = {
7
+ now(): number;
8
+ };
9
+ /** The minimal per-endpoint RPC surface `doctor` probes. */
10
+ interface DoctorRpc {
11
+ getSlot(config?: Record<string, unknown>): {
12
+ send(opts?: {
13
+ abortSignal?: AbortSignal;
14
+ }): Promise<bigint>;
15
+ };
16
+ getHealth(config?: Record<string, unknown>): {
17
+ send(opts?: {
18
+ abortSignal?: AbortSignal;
19
+ }): Promise<string>;
20
+ };
21
+ getVersion(config?: Record<string, unknown>): {
22
+ send(opts?: {
23
+ abortSignal?: AbortSignal;
24
+ }): Promise<{
25
+ 'solana-core'?: string;
26
+ 'feature-set'?: number;
27
+ }>;
28
+ };
29
+ }
30
+ /** Builds a {@link DoctorRpc} for an endpoint URL. */
31
+ type DoctorRpcFactory = (url: string) => DoctorRpc;
32
+ /** Per-endpoint diagnostic result. */
33
+ interface EndpointDiagnostic {
34
+ url: string;
35
+ /** `true` if the endpoint answered every probe. */
36
+ reachable: boolean;
37
+ /** Median latency over the probes (ms), or null if unreachable. */
38
+ latencyP50Ms: number | null;
39
+ /** `getHealth` result (`ok` / a behind message), or null. */
40
+ health: string | null;
41
+ /** Latest slot seen, or null. */
42
+ slot: bigint | null;
43
+ /** Slots behind the freshest endpoint in this run, or null. */
44
+ slotLag: number | null;
45
+ /** `solana-core` version, or null. */
46
+ version: string | null;
47
+ /** First error message, if any probe failed. */
48
+ error: string | null;
49
+ }
50
+ /** The full doctor report (the `--json` payload). */
51
+ interface DoctorReport {
52
+ endpoints: EndpointDiagnostic[];
53
+ /** Freshest slot across reachable endpoints, or null. */
54
+ clusterMaxSlot: bigint | null;
55
+ /** Count reachable / total. */
56
+ reachable: number;
57
+ total: number;
58
+ }
59
+ /** Options for {@link runDoctor}. */
60
+ interface DoctorOptions {
61
+ /** Latency probes per endpoint (default 5). */
62
+ probes?: number;
63
+ rpcFactory: DoctorRpcFactory;
64
+ clock?: DoctorClock;
65
+ signal?: AbortSignal;
66
+ }
67
+ /** Median of a numeric array (linear interpolation not needed for ms). */
68
+ declare function median(values: readonly number[]): number | null;
69
+ /**
70
+ * Turns a probe failure into a human-readable message.
71
+ *
72
+ * Works around an upstream Solana Kit defect: when an endpoint answers HTTP 200
73
+ * with a JSON-RPC *error* whose code Kit's message template can't render (e.g. a
74
+ * keyless gateway returning `-32000 "needs an API key"`), Kit's own formatter
75
+ * (`@solana/errors` → `getHumanReadableErrorMessage`, reached via
76
+ * `getSolanaErrorFromJsonRpcError` → `new SolanaError`) throws a bare
77
+ * `TypeError: Cannot read properties of undefined (reading 'length')` *instead of*
78
+ * the intended SolanaError. That TypeError carries no `context`/`code`, so the
79
+ * real RPC message is unrecoverable — but its stack unambiguously identifies the
80
+ * cause. Detect that signature and report something actionable rather than leaking
81
+ * Kit's internal failure.
82
+ */
83
+ declare function describeProbeError(e: unknown): string;
84
+ /**
85
+ * Probes a single endpoint: `probes` latency-timed `getSlot` calls (p50), one
86
+ * `getHealth`, and one `getVersion`. Never throws — failures are captured in the
87
+ * returned diagnostic. Pure given an injected `rpcFactory` + `clock`.
88
+ */
89
+ declare function probeEndpoint(url: string, options: {
90
+ probes: number;
91
+ rpc: DoctorRpc;
92
+ clock: DoctorClock;
93
+ signal?: AbortSignal;
94
+ }): Promise<EndpointDiagnostic>;
95
+ /**
96
+ * Runs `doctor` across all endpoints: probes each (in parallel), computes the
97
+ * cluster-max slot, and back-fills each endpoint's `slotLag`. Pure given the
98
+ * injected `rpcFactory`/`clock` — the rendering layer formats the result.
99
+ */
100
+ declare function runDoctor(urls: readonly string[], options: DoctorOptions): Promise<DoctorReport>;
101
+ /** Serializes a {@link DoctorReport} to stable JSON (bigint → string). */
102
+ declare function doctorReportToJson(report: DoctorReport): string;
103
+
104
+ /** The `bastion.config.json` shape. */
105
+ interface BastionConfig {
106
+ endpoints: string[];
107
+ jito?: {
108
+ region?: string;
109
+ baseUrl?: string;
110
+ };
111
+ }
112
+ /**
113
+ * Parses + validates a `bastion.config.json` payload. Pure (takes the already-read
114
+ * text) so it's testable without filesystem access. Throws a clear error on a
115
+ * malformed config.
116
+ */
117
+ declare function parseConfig(text: string): BastionConfig;
118
+ /**
119
+ * Resolves endpoints from either `--endpoints` (comma-separated) or a parsed
120
+ * config. `--endpoints` wins when both are present.
121
+ */
122
+ declare function resolveEndpoints(opts: {
123
+ endpoints?: string;
124
+ config?: BastionConfig;
125
+ }): string[];
126
+
127
+ /** A parsed command line: the command, positional args, and flags. */
128
+ interface ParsedArgs {
129
+ command: string | null;
130
+ positionals: string[];
131
+ /** `--flag value` and `--bool` (→ `true`). */
132
+ flags: Record<string, string | boolean>;
133
+ }
134
+ /**
135
+ * A tiny, dependency-free argv parser. `--key value` → `{ key: 'value' }`;
136
+ * `--key=value` → same; a bare `--flag` (followed by another flag or nothing) →
137
+ * `{ flag: true }`. The first non-flag token is the command; the rest are
138
+ * positionals. Pure and fully testable.
139
+ */
140
+ declare function parseArgs(argv: readonly string[]): ParsedArgs;
141
+ /** Reads a flag as a string, or returns `undefined`. */
142
+ declare function flagString(args: ParsedArgs, name: string): string | undefined;
143
+ /** Reads a flag as a boolean (presence or `true`). */
144
+ declare function flagBool(args: ParsedArgs, name: string): boolean;
145
+ /** Reads a flag as a positive integer, or `fallback`. */
146
+ declare function flagInt(args: ParsedArgs, name: string, fallback: number): number;
147
+
148
+ /** A formatted row for the monitor table (pure projection of EndpointHealth). */
149
+ interface MonitorRow {
150
+ url: string;
151
+ /** Status dot category. */
152
+ dot: 'green' | 'yellow' | 'red';
153
+ latencyMs: number;
154
+ errorRatePct: number;
155
+ slotLag: number | null;
156
+ breaker: BreakerState;
157
+ inFlight: number;
158
+ }
159
+ /** A summary across the whole pool. */
160
+ interface MonitorSummary {
161
+ rows: MonitorRow[];
162
+ healthy: number;
163
+ total: number;
164
+ /** Worst (highest) slot lag across endpoints, or null. */
165
+ maxSlotLag: number | null;
166
+ }
167
+ /** Categorizes an endpoint into a status dot from its health + breaker. */
168
+ declare function statusDot(health: EndpointHealth): MonitorRow['dot'];
169
+ /**
170
+ * Projects a pool health snapshot into renderable monitor rows + a summary.
171
+ * Pure — the rendering layer turns this into a table/sparkline. Reuses the same
172
+ * `EndpointHealth` the SDK pool produces (no parallel health model).
173
+ */
174
+ declare function summarizeSnapshot(snapshot: readonly EndpointHealth[]): MonitorSummary;
175
+ /**
176
+ * Appends a latency sample to a bounded ring buffer (for the monitor sparkline).
177
+ * Returns a NEW array (immutable), keeping at most `size` samples.
178
+ */
179
+ declare function pushSample(history: readonly number[], sample: number, size?: number): number[];
180
+
181
+ /** A named, scripted chaos scenario for `bastion simulate`. */
182
+ interface Scenario {
183
+ name: string;
184
+ description: string;
185
+ endpoints: string[];
186
+ /** Per-endpoint chaos plan, keyed by endpoint url. */
187
+ plans: Record<string, ChaosPlan>;
188
+ /** How many probe ticks to run (default 10). */
189
+ ticks: number;
190
+ /** Load-balancer strategy (default the pool's `health-weighted`). */
191
+ strategy?: LoadBalancerStrategy;
192
+ /**
193
+ * User-level calls issued per tick (default 1). A small burst lets a failing
194
+ * endpoint accumulate enough failures to trip its breaker — the drama beat.
195
+ */
196
+ callsPerTick?: number;
197
+ /** Per-scenario breaker tuning, so a scenario can show open → half-open → closed. */
198
+ breaker?: CircuitBreakerOptions;
199
+ }
200
+ /** A narrative event observed during one tick, projected from the bus. */
201
+ interface TickEvent {
202
+ /** Compact kind for the renderer to colour: which arc beat this is. */
203
+ kind: 'served' | 'error' | 'failover' | 'breaker-open' | 'breaker-half-open' | 'breaker-closed';
204
+ /** The endpoint this event is about (the `to` endpoint for a failover). */
205
+ endpoint: string;
206
+ /** A short human label, e.g. `rpc-a → rpc-b` or `OPEN`. */
207
+ detail: string;
208
+ }
209
+ /** A single tick of a simulation: the health snapshot after the tick's probe. */
210
+ interface SimulationTick {
211
+ tick: number;
212
+ snapshot: EndpointHealth[];
213
+ /**
214
+ * The endpoint that ultimately served this tick's user-level call, or null if
215
+ * the call failed on every survivor. Drives the per-endpoint traffic counters.
216
+ */
217
+ servedBy?: string | null;
218
+ /** `false` only when the user-level call threw after all failover/retry. */
219
+ userCallOk?: boolean;
220
+ /** Narrative events the bus emitted during this tick (failover, breaker flips). */
221
+ events?: TickEvent[];
222
+ }
223
+ /** The aggregate verdict of a simulation run — the "0 failed user calls" thesis. */
224
+ interface SimulationSummary {
225
+ /** Every tick is one user-level call. */
226
+ userCallsTotal: number;
227
+ /** Calls that threw after all failover + retry were exhausted. */
228
+ userCallsFailed: number;
229
+ /** Per-endpoint served count + error count over the run. */
230
+ perEndpoint: Array<{
231
+ url: string;
232
+ served: number;
233
+ errors: number;
234
+ }>;
235
+ /** Count of breaker open transitions observed. */
236
+ breakerOpens: number;
237
+ }
238
+ /** The built-in scenario catalogue. */
239
+ declare const SCENARIOS: Record<string, Scenario>;
240
+ /** Lists the available scenario names + descriptions. */
241
+ declare function listScenarios(): Array<{
242
+ name: string;
243
+ description: string;
244
+ }>;
245
+ /** Options for {@link runSimulation}. */
246
+ interface SimulationOptions {
247
+ /** Advances the simulated clock + mock chain by this many ms per tick (default 500). */
248
+ tickMs?: number;
249
+ seed?: number;
250
+ /** Called after each tick (for live rendering). */
251
+ onTick?: (tick: SimulationTick) => void;
252
+ /** Drives time + the mock chain forward between ticks (injected for tests). */
253
+ advance: (ms: number) => Promise<void> | void;
254
+ /** Reads "now" in ms (injected; defaults to a tick-counted clock). */
255
+ now: () => number;
256
+ }
257
+ /**
258
+ * Runs a named scenario against a resilient pool built over chaos transports
259
+ * (real SDK pool/health code), returning a snapshot per tick. The caller injects
260
+ * `advance`/`now` so tests drive it deterministically with fake timers + a
261
+ * ManualClock; the renderer just consumes the ticks.
262
+ */
263
+ declare function runSimulation(scenario: Scenario, options: SimulationOptions): Promise<SimulationTick[]>;
264
+ /**
265
+ * Projects a finished run into the headline verdict: how many user-level calls
266
+ * the SDK made, how many failed despite failover/retry (the thesis is **zero**),
267
+ * per-endpoint traffic, and how many times a breaker opened. Pure over the ticks.
268
+ */
269
+ declare function summarizeSimulation(ticks: readonly SimulationTick[]): SimulationSummary;
270
+
271
+ /** A point-in-time status line for the `watch-tx` UI. */
272
+ interface WatchTxUpdate {
273
+ at: number;
274
+ state: 'watching' | 'confirmed' | 'failed' | 'expired' | 'aborted';
275
+ detail: string;
276
+ }
277
+ /** Final result of watching a transaction. */
278
+ interface WatchTxResult {
279
+ signature: string;
280
+ outcome: ConfirmationOutcome['type'];
281
+ slot: bigint | null;
282
+ updates: WatchTxUpdate[];
283
+ }
284
+ /** Options for {@link watchTransaction}. */
285
+ interface WatchTxOptions {
286
+ rpc: SenderRpc;
287
+ rpcSubscriptions?: RpcSubscriptionsApiLike;
288
+ lastValidBlockHeight: bigint;
289
+ commitment?: Commitment;
290
+ clock?: Clock;
291
+ pollIntervalMs?: number;
292
+ blockHeightIntervalMs?: number;
293
+ signal?: AbortSignal;
294
+ /** Called on each status update (for live rendering). */
295
+ onUpdate?: (update: WatchTxUpdate) => void;
296
+ }
297
+ /**
298
+ * Watches a transaction signature to a terminal state, reusing the SDK's
299
+ * confirmation engine (WS + polling race). Returns the outcome plus a timeline of
300
+ * updates. Pure given injected `rpc`/`clock` — no rendering here.
301
+ */
302
+ declare function watchTransaction(signature: string, options: WatchTxOptions): Promise<WatchTxResult>;
303
+
304
+ /** The set of known commands. */
305
+ declare const COMMANDS: readonly ["doctor", "monitor", "watch-tx", "simulate", "help"];
306
+ type Command = (typeof COMMANDS)[number];
307
+ /**
308
+ * Resolves the command to run from parsed args. Pure + fully tested — this is the
309
+ * routing brain; the live `main()` (which does I/O) lives in `./ui/run`.
310
+ */
311
+ declare function route(args: ParsedArgs): Command;
312
+
313
+ /**
314
+ * rpc-bastion — the `bastion` diagnostics CLI: doctor, monitor, watch-tx, simulate.
315
+ *
316
+ * The data layer (endpoint probing, health aggregation, scenario simulation,
317
+ * config/arg parsing) is exported here for programmatic use and is fully
318
+ * unit-tested; the rendering + live-I/O layer lives under `src/ui/**` (excluded
319
+ * from coverage). The executable itself is `src/bin.ts` → `dist/bin.cjs`.
320
+ */
321
+ /** Package semver, surfaced for diagnostics and the CLI `doctor` command. */
322
+ declare const VERSION = "0.3.0";
323
+
324
+ export { type BastionConfig, COMMANDS, type Command, type DoctorClock, type DoctorOptions, type DoctorReport, type DoctorRpc, type DoctorRpcFactory, type EndpointDiagnostic, type MonitorRow, type MonitorSummary, type ParsedArgs, SCENARIOS, type Scenario, type SimulationOptions, type SimulationSummary, type SimulationTick, type TickEvent, VERSION, type WatchTxOptions, type WatchTxResult, type WatchTxUpdate, describeProbeError, doctorReportToJson, flagBool, flagInt, flagString, listScenarios, median, parseArgs, parseConfig, probeEndpoint, pushSample, resolveEndpoints, route, runDoctor, runSimulation, statusDot, summarizeSimulation, summarizeSnapshot, watchTransaction };
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { SCENARIOS, describeProbeError, doctorReportToJson, listScenarios, median, parseConfig, probeEndpoint, pushSample, resolveEndpoints, runDoctor, runSimulation, statusDot, summarizeSimulation, summarizeSnapshot, watchTransaction } from './chunk-F5AAJEDR.js';
2
+ export { flagBool, flagInt, flagString, parseArgs } from './chunk-Q2OA5HXD.js';
3
+ export { COMMANDS, route } from './chunk-WU3Q4ZC6.js';
4
+
5
+ // src/index.ts
6
+ var VERSION = "0.3.0";
7
+
8
+ export { VERSION };
9
+ //# sourceMappingURL=index.js.map
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAUO,IAAM,OAAA,GAAU","file":"index.js","sourcesContent":["/**\n * rpc-bastion — the `bastion` diagnostics CLI: doctor, monitor, watch-tx, simulate.\n *\n * The data layer (endpoint probing, health aggregation, scenario simulation,\n * config/arg parsing) is exported here for programmatic use and is fully\n * unit-tested; the rendering + live-I/O layer lives under `src/ui/**` (excluded\n * from coverage). The executable itself is `src/bin.ts` → `dist/bin.cjs`.\n */\n\n/** Package semver, surfaced for diagnostics and the CLI `doctor` command. */\nexport const VERSION = '0.3.0';\n\n// Public data-layer API.\nexport * from './data/doctor';\nexport * from './data/config';\nexport * from './data/args';\nexport * from './data/monitor';\nexport * from './data/simulate';\nexport * from './data/watch-tx';\nexport { COMMANDS, route, type Command } from './cli';\n"]}