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.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/args-O3XNV7QR.js +3 -0
- package/dist/args-O3XNV7QR.js.map +1 -0
- package/dist/bin.cjs +760 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.js +323 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-F5AAJEDR.js +359 -0
- package/dist/chunk-F5AAJEDR.js.map +1 -0
- package/dist/chunk-Q2OA5HXD.js +46 -0
- package/dist/chunk-Q2OA5HXD.js.map +1 -0
- package/dist/chunk-WU3Q4ZC6.js +12 -0
- package/dist/chunk-WU3Q4ZC6.js.map +1 -0
- package/dist/cli-64RAFQGB.js +3 -0
- package/dist/cli-64RAFQGB.js.map +1 -0
- package/dist/index.cjs +437 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +324 -0
- package/dist/index.d.ts +324 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { systemClock, createEventBus, createResilientRpc } from '@rpc-bastion/core';
|
|
2
|
+
import { createMockRpcServer, createChaosTransport } from '@rpc-bastion/testkit';
|
|
3
|
+
import { createConfirmationEngine } from '@rpc-bastion/sender';
|
|
4
|
+
|
|
5
|
+
// src/data/doctor.ts
|
|
6
|
+
function median(values) {
|
|
7
|
+
if (values.length === 0) return null;
|
|
8
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
9
|
+
const mid = Math.floor(sorted.length / 2);
|
|
10
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
11
|
+
}
|
|
12
|
+
function describeProbeError(e) {
|
|
13
|
+
if (e instanceof TypeError) {
|
|
14
|
+
const stack = e.stack ?? "";
|
|
15
|
+
if (stack.includes("getHumanReadableErrorMessage") || stack.includes("getSolanaErrorFromJsonRpcError") || stack.includes("@solana/errors")) {
|
|
16
|
+
return "endpoint returned a JSON-RPC error Solana Kit could not format (often a keyless/unauthorized gateway requiring an API key)";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return e instanceof Error ? e.message : String(e);
|
|
20
|
+
}
|
|
21
|
+
async function probeEndpoint(url, options) {
|
|
22
|
+
const { probes, rpc, clock, signal } = options;
|
|
23
|
+
const latencies = [];
|
|
24
|
+
let slot = null;
|
|
25
|
+
let error = null;
|
|
26
|
+
for (let i = 0; i < probes; i++) {
|
|
27
|
+
const start = clock.now();
|
|
28
|
+
try {
|
|
29
|
+
const s = await rpc.getSlot().send(signal ? { abortSignal: signal } : void 0);
|
|
30
|
+
latencies.push(clock.now() - start);
|
|
31
|
+
slot = s;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
error ??= describeProbeError(e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let health = null;
|
|
37
|
+
let version = null;
|
|
38
|
+
if (latencies.length > 0) {
|
|
39
|
+
try {
|
|
40
|
+
health = await rpc.getHealth().send(signal ? { abortSignal: signal } : void 0);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
error ??= describeProbeError(e);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const v = await rpc.getVersion().send(signal ? { abortSignal: signal } : void 0);
|
|
46
|
+
version = v["solana-core"] ?? null;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
error ??= describeProbeError(e);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const reachable = latencies.length > 0;
|
|
52
|
+
return {
|
|
53
|
+
url,
|
|
54
|
+
reachable,
|
|
55
|
+
latencyP50Ms: median(latencies),
|
|
56
|
+
health,
|
|
57
|
+
slot,
|
|
58
|
+
slotLag: null,
|
|
59
|
+
// filled in once the cluster max is known
|
|
60
|
+
version,
|
|
61
|
+
error
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function runDoctor(urls, options) {
|
|
65
|
+
const probes = options.probes ?? 5;
|
|
66
|
+
const clock = options.clock ?? systemClock;
|
|
67
|
+
const diagnostics = await Promise.all(
|
|
68
|
+
urls.map(
|
|
69
|
+
(url) => probeEndpoint(url, {
|
|
70
|
+
probes,
|
|
71
|
+
rpc: options.rpcFactory(url),
|
|
72
|
+
clock,
|
|
73
|
+
...options.signal ? { signal: options.signal } : {}
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
let clusterMaxSlot = null;
|
|
78
|
+
for (const d of diagnostics) {
|
|
79
|
+
if (d.slot !== null && (clusterMaxSlot === null || d.slot > clusterMaxSlot)) clusterMaxSlot = d.slot;
|
|
80
|
+
}
|
|
81
|
+
for (const d of diagnostics) {
|
|
82
|
+
d.slotLag = d.slot !== null && clusterMaxSlot !== null ? Number(clusterMaxSlot - d.slot) : null;
|
|
83
|
+
}
|
|
84
|
+
const reachable = diagnostics.filter((d) => d.reachable).length;
|
|
85
|
+
return { endpoints: diagnostics, clusterMaxSlot, reachable, total: urls.length };
|
|
86
|
+
}
|
|
87
|
+
function doctorReportToJson(report) {
|
|
88
|
+
return JSON.stringify(
|
|
89
|
+
report,
|
|
90
|
+
(_key, value) => typeof value === "bigint" ? value.toString() : value,
|
|
91
|
+
2
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/data/config.ts
|
|
96
|
+
function parseConfig(text) {
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = JSON.parse(text);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new Error(`invalid bastion.config.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
102
|
+
}
|
|
103
|
+
if (typeof raw !== "object" || raw === null) throw new Error("bastion.config.json must be an object");
|
|
104
|
+
const endpoints = raw.endpoints;
|
|
105
|
+
if (!Array.isArray(endpoints) || endpoints.length === 0 || !endpoints.every((e) => typeof e === "string")) {
|
|
106
|
+
throw new Error('bastion.config.json must have a non-empty "endpoints" string array');
|
|
107
|
+
}
|
|
108
|
+
const jito = raw.jito;
|
|
109
|
+
const config = { endpoints };
|
|
110
|
+
if (jito && typeof jito === "object") {
|
|
111
|
+
const j = jito;
|
|
112
|
+
config.jito = {
|
|
113
|
+
...typeof j.region === "string" ? { region: j.region } : {},
|
|
114
|
+
...typeof j.baseUrl === "string" ? { baseUrl: j.baseUrl } : {}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return config;
|
|
118
|
+
}
|
|
119
|
+
function resolveEndpoints(opts) {
|
|
120
|
+
if (opts.endpoints) {
|
|
121
|
+
const list = opts.endpoints.split(",").map((s) => s.trim()).filter(Boolean);
|
|
122
|
+
if (list.length === 0) throw new Error("--endpoints provided but empty");
|
|
123
|
+
return list;
|
|
124
|
+
}
|
|
125
|
+
if (opts.config) return opts.config.endpoints;
|
|
126
|
+
throw new Error("no endpoints: pass --endpoints <urls> or --config <bastion.config.json>");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/data/monitor.ts
|
|
130
|
+
function statusDot(health) {
|
|
131
|
+
if (health.breaker === "open" || health.status === "broken") return "red";
|
|
132
|
+
if (health.breaker === "half-open" || health.status === "degraded" || health.errorRate > 0.1 || (health.slotLag ?? 0) > 50)
|
|
133
|
+
return "yellow";
|
|
134
|
+
return "green";
|
|
135
|
+
}
|
|
136
|
+
function summarizeSnapshot(snapshot) {
|
|
137
|
+
const rows = snapshot.map((h) => ({
|
|
138
|
+
url: h.url,
|
|
139
|
+
dot: statusDot(h),
|
|
140
|
+
latencyMs: Math.round(h.latencyEwmaMs),
|
|
141
|
+
errorRatePct: Math.round(h.errorRate * 100),
|
|
142
|
+
slotLag: h.slotLag,
|
|
143
|
+
breaker: h.breaker,
|
|
144
|
+
inFlight: h.inFlight
|
|
145
|
+
}));
|
|
146
|
+
const healthy = rows.filter((r) => r.dot === "green").length;
|
|
147
|
+
let maxSlotLag = null;
|
|
148
|
+
for (const r of rows) {
|
|
149
|
+
if (r.slotLag !== null && (maxSlotLag === null || r.slotLag > maxSlotLag)) maxSlotLag = r.slotLag;
|
|
150
|
+
}
|
|
151
|
+
return { rows, healthy, total: rows.length, maxSlotLag };
|
|
152
|
+
}
|
|
153
|
+
function pushSample(history, sample, size = 30) {
|
|
154
|
+
const next = [...history, sample];
|
|
155
|
+
return next.length > size ? next.slice(next.length - size) : next;
|
|
156
|
+
}
|
|
157
|
+
var SCENARIOS = {
|
|
158
|
+
"live-outage": {
|
|
159
|
+
name: "live-outage",
|
|
160
|
+
description: "The movie: 3 healthy endpoints \u2192 the primary blacks out (breaker opens, traffic reroutes) \u2192 it recovers (half-open trial \u2192 closed). Zero failed user calls.",
|
|
161
|
+
endpoints: ["rpc-a", "rpc-b", "rpc-c"],
|
|
162
|
+
plans: {
|
|
163
|
+
// Healthy (mild latency) → total blackout from 1.5s → recovery from 4s.
|
|
164
|
+
"rpc-a": [
|
|
165
|
+
{ atMs: 0, set: { latency: { minMs: 18, maxMs: 45 } } },
|
|
166
|
+
{ atMs: 1500, set: { dropRate: 1 } },
|
|
167
|
+
{ atMs: 4e3, set: { dropRate: 0, latency: { minMs: 22, maxMs: 50 } } }
|
|
168
|
+
],
|
|
169
|
+
"rpc-b": [{ atMs: 0, set: { latency: { minMs: 25, maxMs: 70 } } }],
|
|
170
|
+
"rpc-c": [{ atMs: 0, set: { latency: { minMs: 30, maxMs: 85 } } }]
|
|
171
|
+
},
|
|
172
|
+
ticks: 14,
|
|
173
|
+
// Round-robin keeps probing rpc-a during the outage so its failures pile up
|
|
174
|
+
// and the breaker actually opens; the burst gets there within one tick.
|
|
175
|
+
strategy: "round-robin",
|
|
176
|
+
callsPerTick: 3,
|
|
177
|
+
breaker: { failureThreshold: 3, windowMs: 5e3, cooldownMs: 1e3 }
|
|
178
|
+
},
|
|
179
|
+
"flaky-endpoint": {
|
|
180
|
+
name: "flaky-endpoint",
|
|
181
|
+
description: "One endpoint degrades (latency + 500s) mid-run; the pool routes away from it.",
|
|
182
|
+
endpoints: ["rpc-a", "rpc-b"],
|
|
183
|
+
plans: {
|
|
184
|
+
"rpc-a": [
|
|
185
|
+
{ atMs: 0, set: {} },
|
|
186
|
+
{ atMs: 2e3, set: { latency: { minMs: 400, maxMs: 800 }, errorRate: { http500: 0.5 } } }
|
|
187
|
+
],
|
|
188
|
+
"rpc-b": []
|
|
189
|
+
},
|
|
190
|
+
ticks: 10
|
|
191
|
+
},
|
|
192
|
+
"cascading-failure": {
|
|
193
|
+
name: "cascading-failure",
|
|
194
|
+
description: "Endpoints fail one after another; the breaker opens and probes recover them.",
|
|
195
|
+
endpoints: ["rpc-a", "rpc-b", "rpc-c"],
|
|
196
|
+
plans: {
|
|
197
|
+
"rpc-a": [{ atMs: 1e3, set: { dropRate: 1 } }],
|
|
198
|
+
"rpc-b": [{ atMs: 3e3, set: { dropRate: 1 } }],
|
|
199
|
+
"rpc-c": []
|
|
200
|
+
},
|
|
201
|
+
ticks: 12
|
|
202
|
+
},
|
|
203
|
+
"rate-limited": {
|
|
204
|
+
name: "rate-limited",
|
|
205
|
+
description: "All endpoints throw 429s intermittently; retries with backoff absorb them.",
|
|
206
|
+
endpoints: ["rpc-a", "rpc-b"],
|
|
207
|
+
plans: {
|
|
208
|
+
"rpc-a": [{ atMs: 0, set: { errorRate: { http429: 0.4 } } }],
|
|
209
|
+
"rpc-b": [{ atMs: 0, set: { errorRate: { http429: 0.4 } } }]
|
|
210
|
+
},
|
|
211
|
+
ticks: 10
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
function listScenarios() {
|
|
215
|
+
return Object.values(SCENARIOS).map((s) => ({ name: s.name, description: s.description }));
|
|
216
|
+
}
|
|
217
|
+
async function runSimulation(scenario, options) {
|
|
218
|
+
const tickMs = options.tickMs ?? 500;
|
|
219
|
+
const clock = { now: options.now, sleep: async () => void 0 };
|
|
220
|
+
const bus = createEventBus();
|
|
221
|
+
const server = createMockRpcServer({ slot: 1e3, blockHeight: 1e3 });
|
|
222
|
+
const { pool, rpc } = createResilientRpc(
|
|
223
|
+
scenario.endpoints.map((url) => ({ url })),
|
|
224
|
+
{
|
|
225
|
+
bus,
|
|
226
|
+
clock,
|
|
227
|
+
...scenario.strategy ? { strategy: scenario.strategy } : {},
|
|
228
|
+
...scenario.breaker ? { breaker: scenario.breaker } : {},
|
|
229
|
+
pool: {
|
|
230
|
+
probe: { enabled: false },
|
|
231
|
+
transportFactory: (config) => createChaosTransport(server.transport, {
|
|
232
|
+
plan: scenario.plans[config.url] ?? [],
|
|
233
|
+
endpointId: config.url,
|
|
234
|
+
clock,
|
|
235
|
+
seed: options.seed ?? 42
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
let pending = [];
|
|
241
|
+
let lastServed = null;
|
|
242
|
+
bus.onAny((event) => {
|
|
243
|
+
switch (event.type) {
|
|
244
|
+
case "rpc.response":
|
|
245
|
+
if (event.ok) {
|
|
246
|
+
lastServed = event.endpoint;
|
|
247
|
+
pending.push({ kind: "served", endpoint: event.endpoint, detail: `${Math.round(event.latencyMs)}ms` });
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
case "rpc.error":
|
|
251
|
+
pending.push({ kind: "error", endpoint: event.endpoint, detail: event.errorClass });
|
|
252
|
+
break;
|
|
253
|
+
case "rpc.failover":
|
|
254
|
+
if (event.to) pending.push({ kind: "failover", endpoint: event.to, detail: `${event.from} \u2192 ${event.to}` });
|
|
255
|
+
break;
|
|
256
|
+
case "breaker.state":
|
|
257
|
+
pending.push({
|
|
258
|
+
kind: event.state === "open" ? "breaker-open" : event.state === "half-open" ? "breaker-half-open" : "breaker-closed",
|
|
259
|
+
endpoint: event.endpoint,
|
|
260
|
+
detail: event.state.toUpperCase()
|
|
261
|
+
});
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
const callsPerTick = Math.max(1, scenario.callsPerTick ?? 1);
|
|
266
|
+
const ticks = [];
|
|
267
|
+
for (let i = 0; i < scenario.ticks; i++) {
|
|
268
|
+
pending = [];
|
|
269
|
+
lastServed = null;
|
|
270
|
+
let userCallOk = true;
|
|
271
|
+
for (let c = 0; c < callsPerTick; c++) {
|
|
272
|
+
try {
|
|
273
|
+
await rpc.getSlot().send();
|
|
274
|
+
} catch {
|
|
275
|
+
userCallOk = false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const snapshot = pool.getHealthSnapshot();
|
|
279
|
+
const tick = {
|
|
280
|
+
tick: i,
|
|
281
|
+
snapshot,
|
|
282
|
+
servedBy: userCallOk ? lastServed : null,
|
|
283
|
+
userCallOk,
|
|
284
|
+
events: pending
|
|
285
|
+
};
|
|
286
|
+
ticks.push(tick);
|
|
287
|
+
options.onTick?.(tick);
|
|
288
|
+
await options.advance(tickMs);
|
|
289
|
+
}
|
|
290
|
+
return ticks;
|
|
291
|
+
}
|
|
292
|
+
function summarizeSimulation(ticks) {
|
|
293
|
+
const perEndpoint = /* @__PURE__ */ new Map();
|
|
294
|
+
const ensure = (url) => {
|
|
295
|
+
let row = perEndpoint.get(url);
|
|
296
|
+
if (!row) {
|
|
297
|
+
row = { url, served: 0, errors: 0 };
|
|
298
|
+
perEndpoint.set(url, row);
|
|
299
|
+
}
|
|
300
|
+
return row;
|
|
301
|
+
};
|
|
302
|
+
let userCallsFailed = 0;
|
|
303
|
+
let breakerOpens = 0;
|
|
304
|
+
for (const tick of ticks) {
|
|
305
|
+
if (tick.userCallOk === false) userCallsFailed++;
|
|
306
|
+
if (tick.servedBy) ensure(tick.servedBy).served++;
|
|
307
|
+
for (const ev of tick.events ?? []) {
|
|
308
|
+
if (ev.kind === "error") ensure(ev.endpoint).errors++;
|
|
309
|
+
if (ev.kind === "breaker-open") breakerOpens++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
userCallsTotal: ticks.length,
|
|
314
|
+
userCallsFailed,
|
|
315
|
+
perEndpoint: [...perEndpoint.values()],
|
|
316
|
+
breakerOpens
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async function watchTransaction(signature, options) {
|
|
320
|
+
const clock = options.clock;
|
|
321
|
+
const engine = createConfirmationEngine({
|
|
322
|
+
rpc: options.rpc,
|
|
323
|
+
...options.rpcSubscriptions ? { rpcSubscriptions: options.rpcSubscriptions } : {},
|
|
324
|
+
...clock ? { clock } : {}
|
|
325
|
+
});
|
|
326
|
+
const updates = [];
|
|
327
|
+
const now = () => clock?.now() ?? 0;
|
|
328
|
+
const push = (state, detail) => {
|
|
329
|
+
const update = { at: now(), state, detail };
|
|
330
|
+
updates.push(update);
|
|
331
|
+
options.onUpdate?.(update);
|
|
332
|
+
};
|
|
333
|
+
push("watching", `commitment=${options.commitment ?? "confirmed"}`);
|
|
334
|
+
const outcome = await engine.confirm(signature, {
|
|
335
|
+
commitment: options.commitment ?? "confirmed",
|
|
336
|
+
lastValidBlockHeight: options.lastValidBlockHeight,
|
|
337
|
+
...options.pollIntervalMs !== void 0 ? { pollIntervalMs: options.pollIntervalMs } : {},
|
|
338
|
+
...options.blockHeightIntervalMs !== void 0 ? { blockHeightIntervalMs: options.blockHeightIntervalMs } : {},
|
|
339
|
+
...options.signal ? { signal: options.signal } : {}
|
|
340
|
+
});
|
|
341
|
+
switch (outcome.type) {
|
|
342
|
+
case "confirmed":
|
|
343
|
+
push("confirmed", `slot ${outcome.slot} (${outcome.confirmationStatus})`);
|
|
344
|
+
return { signature, outcome: "confirmed", slot: outcome.slot, updates };
|
|
345
|
+
case "failed":
|
|
346
|
+
push("failed", "on-chain execution error");
|
|
347
|
+
return { signature, outcome: "failed", slot: outcome.slot, updates };
|
|
348
|
+
case "expired":
|
|
349
|
+
push("expired", `block height ${outcome.blockHeight} > ${outcome.lastValidBlockHeight}`);
|
|
350
|
+
return { signature, outcome: "expired", slot: null, updates };
|
|
351
|
+
default:
|
|
352
|
+
push("aborted", "watch aborted");
|
|
353
|
+
return { signature, outcome: "aborted", slot: null, updates };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export { SCENARIOS, describeProbeError, doctorReportToJson, listScenarios, median, parseConfig, probeEndpoint, pushSample, resolveEndpoints, runDoctor, runSimulation, statusDot, summarizeSimulation, summarizeSnapshot, watchTransaction };
|
|
358
|
+
//# sourceMappingURL=chunk-F5AAJEDR.js.map
|
|
359
|
+
//# sourceMappingURL=chunk-F5AAJEDR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/doctor.ts","../src/data/config.ts","../src/data/monitor.ts","../src/data/simulate.ts","../src/data/watch-tx.ts"],"names":[],"mappings":";;;;;AAwDO,SAAS,OAAO,MAAA,EAA0C;AAC/D,EAAA,IAAI,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAChC,EAAA,MAAM,MAAA,GAAS,CAAC,GAAG,MAAM,CAAA,CAAE,KAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,GAAI,CAAC,CAAA;AAC/C,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,SAAS,CAAC,CAAA;AACxC,EAAA,OAAO,MAAA,CAAO,MAAA,GAAS,CAAA,KAAM,CAAA,GAAA,CAAK,MAAA,CAAO,GAAA,GAAM,CAAC,CAAA,GAAK,MAAA,CAAO,GAAG,CAAA,IAAM,CAAA,GAAI,OAAO,GAAG,CAAA;AACrF;AAgBO,SAAS,mBAAmB,CAAA,EAAoB;AACrD,EAAA,IAAI,aAAa,SAAA,EAAW;AAC1B,IAAA,MAAM,KAAA,GAAQ,EAAE,KAAA,IAAS,EAAA;AACzB,IAAA,IACE,KAAA,CAAM,QAAA,CAAS,8BAA8B,CAAA,IAC7C,KAAA,CAAM,QAAA,CAAS,gCAAgC,CAAA,IAC/C,KAAA,CAAM,QAAA,CAAS,gBAAgB,CAAA,EAC/B;AACA,MAAA,OAAO,4HAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,OAAO,CAAA,YAAa,KAAA,GAAQ,CAAA,CAAE,OAAA,GAAU,OAAO,CAAC,CAAA;AAClD;AAOA,eAAsB,aAAA,CACpB,KACA,OAAA,EAC6B;AAC7B,EAAA,MAAM,EAAE,MAAA,EAAQ,GAAA,EAAK,KAAA,EAAO,QAAO,GAAI,OAAA;AACvC,EAAA,MAAM,YAAsB,EAAC;AAC7B,EAAA,IAAI,IAAA,GAAsB,IAAA;AAC1B,EAAA,IAAI,KAAA,GAAuB,IAAA;AAE3B,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,EAAQ,CAAA,EAAA,EAAK;AAC/B,IAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,EAAI;AACxB,IAAA,IAAI;AACF,MAAA,MAAM,CAAA,GAAI,MAAM,GAAA,CAAI,OAAA,EAAQ,CAAE,IAAA,CAAK,MAAA,GAAS,EAAE,WAAA,EAAa,MAAA,EAAO,GAAI,KAAA,CAAS,CAAA;AAC/E,MAAA,SAAA,CAAU,IAAA,CAAK,KAAA,CAAM,GAAA,EAAI,GAAI,KAAK,CAAA;AAClC,MAAA,IAAA,GAAO,CAAA;AAAA,IACT,SAAS,CAAA,EAAG;AACV,MAAA,KAAA,KAAU,mBAAmB,CAAC,CAAA;AAAA,IAChC;AAAA,EACF;AAEA,EAAA,IAAI,MAAA,GAAwB,IAAA;AAC5B,EAAA,IAAI,OAAA,GAAyB,IAAA;AAC7B,EAAA,IAAI,SAAA,CAAU,SAAS,CAAA,EAAG;AACxB,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,MAAM,GAAA,CAAI,SAAA,EAAU,CAAE,IAAA,CAAK,SAAS,EAAE,WAAA,EAAa,MAAA,EAAO,GAAI,KAAA,CAAS,CAAA;AAAA,IAClF,SAAS,CAAA,EAAG;AACV,MAAA,KAAA,KAAU,mBAAmB,CAAC,CAAA;AAAA,IAChC;AACA,IAAA,IAAI;AACF,MAAA,MAAM,CAAA,GAAI,MAAM,GAAA,CAAI,UAAA,EAAW,CAAE,IAAA,CAAK,MAAA,GAAS,EAAE,WAAA,EAAa,MAAA,EAAO,GAAI,KAAA,CAAS,CAAA;AAClF,MAAA,OAAA,GAAU,CAAA,CAAE,aAAa,CAAA,IAAK,IAAA;AAAA,IAChC,SAAS,CAAA,EAAG;AACV,MAAA,KAAA,KAAU,mBAAmB,CAAC,CAAA;AAAA,IAChC;AAAA,EACF;AAEA,EAAA,MAAM,SAAA,GAAY,UAAU,MAAA,GAAS,CAAA;AACrC,EAAA,OAAO;AAAA,IACL,GAAA;AAAA,IACA,SAAA;AAAA,IACA,YAAA,EAAc,OAAO,SAAS,CAAA;AAAA,IAC9B,MAAA;AAAA,IACA,IAAA;AAAA,IACA,OAAA,EAAS,IAAA;AAAA;AAAA,IACT,OAAA;AAAA,IACA;AAAA,GACF;AACF;AAOA,eAAsB,SAAA,CAAU,MAAyB,OAAA,EAA+C;AACtG,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,CAAA;AACjC,EAAA,MAAM,KAAA,GAAQ,QAAQ,KAAA,IAAS,WAAA;AAE/B,EAAA,MAAM,WAAA,GAAc,MAAM,OAAA,CAAQ,GAAA;AAAA,IAChC,IAAA,CAAK,GAAA;AAAA,MAAI,CAAC,GAAA,KACR,aAAA,CAAc,GAAA,EAAK;AAAA,QACjB,MAAA;AAAA,QACA,GAAA,EAAK,OAAA,CAAQ,UAAA,CAAW,GAAG,CAAA;AAAA,QAC3B,KAAA;AAAA,QACA,GAAI,QAAQ,MAAA,GAAS,EAAE,QAAQ,OAAA,CAAQ,MAAA,KAAW;AAAC,OACpD;AAAA;AACH,GACF;AAEA,EAAA,IAAI,cAAA,GAAgC,IAAA;AACpC,EAAA,KAAA,MAAW,KAAK,WAAA,EAAa;AAC3B,IAAA,IAAI,CAAA,CAAE,SAAS,IAAA,KAAS,cAAA,KAAmB,QAAQ,CAAA,CAAE,IAAA,GAAO,cAAA,CAAA,EAAiB,cAAA,GAAiB,CAAA,CAAE,IAAA;AAAA,EAClG;AACA,EAAA,KAAA,MAAW,KAAK,WAAA,EAAa;AAC3B,IAAA,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,IAAA,KAAS,IAAA,IAAQ,cAAA,KAAmB,OAAO,MAAA,CAAO,cAAA,GAAiB,CAAA,CAAE,IAAI,CAAA,GAAI,IAAA;AAAA,EAC7F;AAEA,EAAA,MAAM,YAAY,WAAA,CAAY,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,SAAS,CAAA,CAAE,MAAA;AACzD,EAAA,OAAO,EAAE,SAAA,EAAW,WAAA,EAAa,gBAAgB,SAAA,EAAW,KAAA,EAAO,KAAK,MAAA,EAAO;AACjF;AAGO,SAAS,mBAAmB,MAAA,EAA8B;AAC/D,EAAA,OAAO,IAAA,CAAK,SAAA;AAAA,IACV,MAAA;AAAA,IACA,CAAC,MAAM,KAAA,KAAW,OAAO,UAAU,QAAA,GAAW,KAAA,CAAM,UAAS,GAAI,KAAA;AAAA,IACjE;AAAA,GACF;AACF;;;AC7KO,SAAS,YAAY,IAAA,EAA6B;AACvD,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACvB,SAAS,CAAA,EAAG;AACV,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6BAAA,EAAgC,CAAA,YAAa,KAAA,GAAQ,EAAE,OAAA,GAAU,MAAA,CAAO,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,EAC9F;AACA,EAAA,IAAI,OAAO,QAAQ,QAAA,IAAY,GAAA,KAAQ,MAAM,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACpG,EAAA,MAAM,YAAa,GAAA,CAAgC,SAAA;AACnD,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA,IAAK,UAAU,MAAA,KAAW,CAAA,IAAK,CAAC,SAAA,CAAU,MAAM,CAAC,CAAA,KAAM,OAAO,CAAA,KAAM,QAAQ,CAAA,EAAG;AACzG,IAAA,MAAM,IAAI,MAAM,oEAAoE,CAAA;AAAA,EACtF;AACA,EAAA,MAAM,OAAQ,GAAA,CAA2B,IAAA;AACzC,EAAA,MAAM,MAAA,GAAwB,EAAE,SAAA,EAAiC;AACjE,EAAA,IAAI,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,EAAU;AACpC,IAAA,MAAM,CAAA,GAAI,IAAA;AACV,IAAA,MAAA,CAAO,IAAA,GAAO;AAAA,MACZ,GAAI,OAAO,CAAA,CAAE,MAAA,KAAW,QAAA,GAAW,EAAE,MAAA,EAAQ,CAAA,CAAE,MAAA,EAAO,GAAI,EAAC;AAAA,MAC3D,GAAI,OAAO,CAAA,CAAE,OAAA,KAAY,QAAA,GAAW,EAAE,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ,GAAI;AAAC,KAChE;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAMO,SAAS,iBAAiB,IAAA,EAAgE;AAC/F,EAAA,IAAI,KAAK,SAAA,EAAW;AAClB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CACf,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAA,EAAM,CAAA,CACnB,OAAO,OAAO,CAAA;AACjB,IAAA,IAAI,KAAK,MAAA,KAAW,CAAA,EAAG,MAAM,IAAI,MAAM,gCAAgC,CAAA;AACvE,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,IAAA,CAAK,MAAA,EAAQ,OAAO,IAAA,CAAK,MAAA,CAAO,SAAA;AACpC,EAAA,MAAM,IAAI,MAAM,yEAAyE,CAAA;AAC3F;;;AC1BO,SAAS,UAAU,MAAA,EAA2C;AACnE,EAAA,IAAI,OAAO,OAAA,KAAY,MAAA,IAAU,MAAA,CAAO,MAAA,KAAW,UAAU,OAAO,KAAA;AACpE,EAAA,IAAI,MAAA,CAAO,OAAA,KAAY,WAAA,IAAe,MAAA,CAAO,MAAA,KAAW,UAAA,IAAc,MAAA,CAAO,SAAA,GAAY,GAAA,IAAA,CAAQ,MAAA,CAAO,OAAA,IAAW,CAAA,IAAK,EAAA;AACtH,IAAA,OAAO,QAAA;AACT,EAAA,OAAO,OAAA;AACT;AAOO,SAAS,kBAAkB,QAAA,EAAqD;AACrF,EAAA,MAAM,IAAA,GAAqB,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IAC9C,KAAK,CAAA,CAAE,GAAA;AAAA,IACP,GAAA,EAAK,UAAU,CAAC,CAAA;AAAA,IAChB,SAAA,EAAW,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,aAAa,CAAA;AAAA,IACrC,YAAA,EAAc,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,YAAY,GAAG,CAAA;AAAA,IAC1C,SAAS,CAAA,CAAE,OAAA;AAAA,IACX,SAAS,CAAA,CAAE,OAAA;AAAA,IACX,UAAU,CAAA,CAAE;AAAA,GACd,CAAE,CAAA;AAEF,EAAA,MAAM,OAAA,GAAU,KAAK,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,GAAA,KAAQ,OAAO,CAAA,CAAE,MAAA;AACtD,EAAA,IAAI,UAAA,GAA4B,IAAA;AAChC,EAAA,KAAA,MAAW,KAAK,IAAA,EAAM;AACpB,IAAA,IAAI,CAAA,CAAE,YAAY,IAAA,KAAS,UAAA,KAAe,QAAQ,CAAA,CAAE,OAAA,GAAU,UAAA,CAAA,EAAa,UAAA,GAAa,CAAA,CAAE,OAAA;AAAA,EAC5F;AACA,EAAA,OAAO,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,IAAA,CAAK,QAAQ,UAAA,EAAW;AACzD;AAMO,SAAS,UAAA,CAAW,OAAA,EAA4B,MAAA,EAAgB,IAAA,GAAO,EAAA,EAAc;AAC1F,EAAA,MAAM,IAAA,GAAO,CAAC,GAAG,OAAA,EAAS,MAAM,CAAA;AAChC,EAAA,OAAO,IAAA,CAAK,SAAS,IAAA,GAAO,IAAA,CAAK,MAAM,IAAA,CAAK,MAAA,GAAS,IAAI,CAAA,GAAI,IAAA;AAC/D;ACMO,IAAM,SAAA,GAAsC;AAAA,EACjD,aAAA,EAAe;AAAA,IACb,IAAA,EAAM,aAAA;AAAA,IACN,WAAA,EACE,4KAAA;AAAA,IACF,SAAA,EAAW,CAAC,OAAA,EAAS,OAAA,EAAS,OAAO,CAAA;AAAA,IACrC,KAAA,EAAO;AAAA;AAAA,MAEL,OAAA,EAAS;AAAA,QACP,EAAE,IAAA,EAAM,CAAA,EAAG,GAAA,EAAK,EAAE,OAAA,EAAS,EAAE,KAAA,EAAO,EAAA,EAAI,KAAA,EAAO,EAAA,EAAG,EAAE,EAAE;AAAA,QACtD,EAAE,IAAA,EAAM,IAAA,EAAO,KAAK,EAAE,QAAA,EAAU,GAAE,EAAE;AAAA,QACpC,EAAE,IAAA,EAAM,GAAA,EAAO,GAAA,EAAK,EAAE,QAAA,EAAU,CAAA,EAAG,OAAA,EAAS,EAAE,KAAA,EAAO,EAAA,EAAI,KAAA,EAAO,EAAA,IAAK;AAAE,OACzE;AAAA,MACA,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,GAAG,GAAA,EAAK,EAAE,OAAA,EAAS,EAAE,OAAO,EAAA,EAAI,KAAA,EAAO,EAAA,EAAG,IAAK,CAAA;AAAA,MACjE,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,GAAG,GAAA,EAAK,EAAE,OAAA,EAAS,EAAE,OAAO,EAAA,EAAI,KAAA,EAAO,EAAA,EAAG,IAAK;AAAA,KACnE;AAAA,IACA,KAAA,EAAO,EAAA;AAAA;AAAA;AAAA,IAGP,QAAA,EAAU,aAAA;AAAA,IACV,YAAA,EAAc,CAAA;AAAA,IACd,SAAS,EAAE,gBAAA,EAAkB,GAAG,QAAA,EAAU,GAAA,EAAO,YAAY,GAAA;AAAM,GACrE;AAAA,EACA,gBAAA,EAAkB;AAAA,IAChB,IAAA,EAAM,gBAAA;AAAA,IACN,WAAA,EAAa,+EAAA;AAAA,IACb,SAAA,EAAW,CAAC,OAAA,EAAS,OAAO,CAAA;AAAA,IAC5B,KAAA,EAAO;AAAA,MACL,OAAA,EAAS;AAAA,QACP,EAAE,IAAA,EAAM,CAAA,EAAG,GAAA,EAAK,EAAC,EAAE;AAAA,QACnB,EAAE,IAAA,EAAM,GAAA,EAAO,GAAA,EAAK,EAAE,SAAS,EAAE,KAAA,EAAO,GAAA,EAAK,KAAA,EAAO,KAAI,EAAG,SAAA,EAAW,EAAE,OAAA,EAAS,GAAA,IAAM;AAAE,OAC3F;AAAA,MACA,SAAS;AAAC,KACZ;AAAA,IACA,KAAA,EAAO;AAAA,GACT;AAAA,EACA,mBAAA,EAAqB;AAAA,IACnB,IAAA,EAAM,mBAAA;AAAA,IACN,WAAA,EAAa,8EAAA;AAAA,IACb,SAAA,EAAW,CAAC,OAAA,EAAS,OAAA,EAAS,OAAO,CAAA;AAAA,IACrC,KAAA,EAAO;AAAA,MACL,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,GAAA,EAAO,KAAK,EAAE,QAAA,EAAU,CAAA,EAAE,EAAG,CAAA;AAAA,MAC/C,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,GAAA,EAAO,KAAK,EAAE,QAAA,EAAU,CAAA,EAAE,EAAG,CAAA;AAAA,MAC/C,SAAS;AAAC,KACZ;AAAA,IACA,KAAA,EAAO;AAAA,GACT;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,IAAA,EAAM,cAAA;AAAA,IACN,WAAA,EAAa,4EAAA;AAAA,IACb,SAAA,EAAW,CAAC,OAAA,EAAS,OAAO,CAAA;AAAA,IAC5B,KAAA,EAAO;AAAA,MACL,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,CAAA,EAAG,GAAA,EAAK,EAAE,SAAA,EAAW,EAAE,OAAA,EAAS,GAAA,EAAI,IAAK,CAAA;AAAA,MAC3D,OAAA,EAAS,CAAC,EAAE,IAAA,EAAM,CAAA,EAAG,GAAA,EAAK,EAAE,SAAA,EAAW,EAAE,OAAA,EAAS,GAAA,EAAI,IAAK;AAAA,KAC7D;AAAA,IACA,KAAA,EAAO;AAAA;AAEX;AAGO,SAAS,aAAA,GAA8D;AAC5E,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA,CAAE,IAAI,CAAC,CAAA,MAAO,EAAE,IAAA,EAAM,CAAA,CAAE,IAAA,EAAM,WAAA,EAAa,CAAA,CAAE,aAAY,CAAE,CAAA;AAC3F;AAqBA,eAAsB,aAAA,CAAc,UAAoB,OAAA,EAAuD;AAC7G,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,IAAU,GAAA;AACjC,EAAA,MAAM,QAAQ,EAAE,GAAA,EAAK,QAAQ,GAAA,EAAK,KAAA,EAAO,YAAY,MAAA,EAAU;AAG/D,EAAA,MAAM,MAAM,cAAA,EAAe;AAG3B,EAAA,MAAM,SAAS,mBAAA,CAAoB,EAAE,MAAM,GAAA,EAAM,WAAA,EAAa,KAAM,CAAA;AACpE,EAAA,MAAM,EAAE,IAAA,EAAM,GAAA,EAAI,GAAI,kBAAA;AAAA,IACpB,SAAS,SAAA,CAAU,GAAA,CAAI,CAAC,GAAA,MAAS,EAAE,KAAI,CAAE,CAAA;AAAA,IACzC;AAAA,MACE,GAAA;AAAA,MACA,KAAA;AAAA,MACA,GAAI,SAAS,QAAA,GAAW,EAAE,UAAU,QAAA,CAAS,QAAA,KAAa,EAAC;AAAA,MAC3D,GAAI,SAAS,OAAA,GAAU,EAAE,SAAS,QAAA,CAAS,OAAA,KAAY,EAAC;AAAA,MACxD,IAAA,EAAM;AAAA,QACJ,KAAA,EAAO,EAAE,OAAA,EAAS,KAAA,EAAM;AAAA,QACxB,gBAAA,EAAkB,CAAC,MAAA,KACjB,oBAAA,CAAqB,OAAO,SAAA,EAAW;AAAA,UACrC,MAAM,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,GAAG,KAAK,EAAC;AAAA,UACrC,YAAY,MAAA,CAAO,GAAA;AAAA,UACnB,KAAA;AAAA,UACA,IAAA,EAAM,QAAQ,IAAA,IAAQ;AAAA,SACvB;AAAA;AACL;AACF,GACF;AAGA,EAAA,IAAI,UAAuB,EAAC;AAC5B,EAAA,IAAI,UAAA,GAA4B,IAAA;AAChC,EAAA,GAAA,CAAI,KAAA,CAAM,CAAC,KAAA,KAAwB;AACjC,IAAA,QAAQ,MAAM,IAAA;AAAM,MAClB,KAAK,cAAA;AACH,QAAA,IAAI,MAAM,EAAA,EAAI;AACZ,UAAA,UAAA,GAAa,KAAA,CAAM,QAAA;AACnB,UAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,KAAA,CAAM,QAAA,EAAU,MAAA,EAAQ,CAAA,EAAG,KAAK,KAAA,CAAM,KAAA,CAAM,SAAS,CAAC,MAAM,CAAA;AAAA,QACvG;AACA,QAAA;AAAA,MACF,KAAK,WAAA;AACH,QAAA,OAAA,CAAQ,IAAA,CAAK,EAAE,IAAA,EAAM,OAAA,EAAS,QAAA,EAAU,MAAM,QAAA,EAAU,MAAA,EAAQ,KAAA,CAAM,UAAA,EAAY,CAAA;AAClF,QAAA;AAAA,MACF,KAAK,cAAA;AACH,QAAA,IAAI,MAAM,EAAA,EAAI,OAAA,CAAQ,KAAK,EAAE,IAAA,EAAM,YAAY,QAAA,EAAU,KAAA,CAAM,EAAA,EAAI,MAAA,EAAQ,GAAG,KAAA,CAAM,IAAI,WAAM,KAAA,CAAM,EAAE,IAAI,CAAA;AAC1G,QAAA;AAAA,MACF,KAAK,eAAA;AACH,QAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,UACX,IAAA,EACE,MAAM,KAAA,KAAU,MAAA,GACZ,iBACA,KAAA,CAAM,KAAA,KAAU,cACd,mBAAA,GACA,gBAAA;AAAA,UACR,UAAU,KAAA,CAAM,QAAA;AAAA,UAChB,MAAA,EAAQ,KAAA,CAAM,KAAA,CAAM,WAAA;AAAY,SACjC,CAAA;AACD,QAAA;AAEA;AACJ,EACF,CAAC,CAAA;AAED,EAAA,MAAM,eAAe,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAA,CAAS,gBAAgB,CAAC,CAAA;AAC3D,EAAA,MAAM,QAA0B,EAAC;AACjC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,QAAA,CAAS,OAAO,CAAA,EAAA,EAAK;AACvC,IAAA,OAAA,GAAU,EAAC;AACX,IAAA,UAAA,GAAa,IAAA;AACb,IAAA,IAAI,UAAA,GAAa,IAAA;AAIjB,IAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,YAAA,EAAc,CAAA,EAAA,EAAK;AACrC,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,CAAI,OAAA,EAAQ,CAAE,IAAA,EAAK;AAAA,MAC3B,CAAA,CAAA,MAAQ;AACN,QAAA,UAAA,GAAa,KAAA;AAAA,MACf;AAAA,IACF;AACA,IAAA,MAAM,QAAA,GAAW,KAAK,iBAAA,EAAkB;AACxC,IAAA,MAAM,IAAA,GAAuB;AAAA,MAC3B,IAAA,EAAM,CAAA;AAAA,MACN,QAAA;AAAA,MACA,QAAA,EAAU,aAAa,UAAA,GAAa,IAAA;AAAA,MACpC,UAAA;AAAA,MACA,MAAA,EAAQ;AAAA,KACV;AACA,IAAA,KAAA,CAAM,KAAK,IAAI,CAAA;AACf,IAAA,OAAA,CAAQ,SAAS,IAAI,CAAA;AACrB,IAAA,MAAM,OAAA,CAAQ,QAAQ,MAAM,CAAA;AAAA,EAC9B;AACA,EAAA,OAAO,KAAA;AACT;AAOO,SAAS,oBAAoB,KAAA,EAAqD;AACvF,EAAA,MAAM,WAAA,uBAAkB,GAAA,EAA6D;AACrF,EAAA,MAAM,MAAA,GAAS,CAAC,GAAA,KAAgB;AAC9B,IAAA,IAAI,GAAA,GAAM,WAAA,CAAY,GAAA,CAAI,GAAG,CAAA;AAC7B,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,GAAA,GAAM,EAAE,GAAA,EAAK,MAAA,EAAQ,CAAA,EAAG,QAAQ,CAAA,EAAE;AAClC,MAAA,WAAA,CAAY,GAAA,CAAI,KAAK,GAAG,CAAA;AAAA,IAC1B;AACA,IAAA,OAAO,GAAA;AAAA,EACT,CAAA;AAEA,EAAA,IAAI,eAAA,GAAkB,CAAA;AACtB,EAAA,IAAI,YAAA,GAAe,CAAA;AACnB,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,IAAI,IAAA,CAAK,eAAe,KAAA,EAAO,eAAA,EAAA;AAC/B,IAAA,IAAI,IAAA,CAAK,QAAA,EAAU,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA,CAAE,MAAA,EAAA;AACzC,IAAA,KAAA,MAAW,EAAA,IAAM,IAAA,CAAK,MAAA,IAAU,EAAC,EAAG;AAClC,MAAA,IAAI,GAAG,IAAA,KAAS,OAAA,EAAS,MAAA,CAAO,EAAA,CAAG,QAAQ,CAAA,CAAE,MAAA,EAAA;AAC7C,MAAA,IAAI,EAAA,CAAG,SAAS,cAAA,EAAgB,YAAA,EAAA;AAAA,IAClC;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,gBAAgB,KAAA,CAAM,MAAA;AAAA,IACtB,eAAA;AAAA,IACA,WAAA,EAAa,CAAC,GAAG,WAAA,CAAY,QAAQ,CAAA;AAAA,IACrC;AAAA,GACF;AACF;AC5OA,eAAsB,gBAAA,CAAiB,WAAmB,OAAA,EAAiD;AACzG,EAAA,MAAM,QAAQ,OAAA,CAAQ,KAAA;AACtB,EAAA,MAAM,SAAS,wBAAA,CAAyB;AAAA,IACtC,KAAK,OAAA,CAAQ,GAAA;AAAA,IACb,GAAI,QAAQ,gBAAA,GAAmB,EAAE,kBAAkB,OAAA,CAAQ,gBAAA,KAAqB,EAAC;AAAA,IACjF,GAAI,KAAA,GAAQ,EAAE,KAAA,KAAU;AAAC,GAC1B,CAAA;AAED,EAAA,MAAM,UAA2B,EAAC;AAClC,EAAA,MAAM,GAAA,GAAM,MAAc,KAAA,EAAO,GAAA,EAAI,IAAK,CAAA;AAC1C,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,EAA+B,MAAA,KAAyB;AACpE,IAAA,MAAM,SAAwB,EAAE,EAAA,EAAI,GAAA,EAAI,EAAG,OAAO,MAAA,EAAO;AACzD,IAAA,OAAA,CAAQ,KAAK,MAAM,CAAA;AACnB,IAAA,OAAA,CAAQ,WAAW,MAAM,CAAA;AAAA,EAC3B,CAAA;AAEA,EAAA,IAAA,CAAK,UAAA,EAAY,CAAA,WAAA,EAAc,OAAA,CAAQ,UAAA,IAAc,WAAW,CAAA,CAAE,CAAA;AAElE,EAAA,MAAM,OAAA,GAAU,MAAM,MAAA,CAAO,OAAA,CAAQ,SAAA,EAAW;AAAA,IAC9C,UAAA,EAAY,QAAQ,UAAA,IAAc,WAAA;AAAA,IAClC,sBAAsB,OAAA,CAAQ,oBAAA;AAAA,IAC9B,GAAI,QAAQ,cAAA,KAAmB,MAAA,GAAY,EAAE,cAAA,EAAgB,OAAA,CAAQ,cAAA,EAAe,GAAI,EAAC;AAAA,IACzF,GAAI,QAAQ,qBAAA,KAA0B,MAAA,GAAY,EAAE,qBAAA,EAAuB,OAAA,CAAQ,qBAAA,EAAsB,GAAI,EAAC;AAAA,IAC9G,GAAI,QAAQ,MAAA,GAAS,EAAE,QAAQ,OAAA,CAAQ,MAAA,KAAW;AAAC,GACpD,CAAA;AAED,EAAA,QAAQ,QAAQ,IAAA;AAAM,IACpB,KAAK,WAAA;AACH,MAAA,IAAA,CAAK,aAAa,CAAA,KAAA,EAAQ,OAAA,CAAQ,IAAI,CAAA,EAAA,EAAK,OAAA,CAAQ,kBAAkB,CAAA,CAAA,CAAG,CAAA;AACxE,MAAA,OAAO,EAAE,SAAA,EAAW,OAAA,EAAS,aAAa,IAAA,EAAM,OAAA,CAAQ,MAAM,OAAA,EAAQ;AAAA,IACxE,KAAK,QAAA;AACH,MAAA,IAAA,CAAK,UAAU,0BAA0B,CAAA;AACzC,MAAA,OAAO,EAAE,SAAA,EAAW,OAAA,EAAS,UAAU,IAAA,EAAM,OAAA,CAAQ,MAAM,OAAA,EAAQ;AAAA,IACrE,KAAK,SAAA;AACH,MAAA,IAAA,CAAK,WAAW,CAAA,aAAA,EAAgB,OAAA,CAAQ,WAAW,CAAA,GAAA,EAAM,OAAA,CAAQ,oBAAoB,CAAA,CAAE,CAAA;AACvF,MAAA,OAAO,EAAE,SAAA,EAAW,OAAA,EAAS,SAAA,EAAW,IAAA,EAAM,MAAM,OAAA,EAAQ;AAAA,IAC9D;AACE,MAAA,IAAA,CAAK,WAAW,eAAe,CAAA;AAC/B,MAAA,OAAO,EAAE,SAAA,EAAW,OAAA,EAAS,SAAA,EAAW,IAAA,EAAM,MAAM,OAAA,EAAQ;AAAA;AAElE","file":"chunk-F5AAJEDR.js","sourcesContent":["import { systemClock } from '@rpc-bastion/core';\n\n/** The doctor only reads time; a `now()` source is all it needs. */\nexport type DoctorClock = { now(): number };\n\n/** The minimal per-endpoint RPC surface `doctor` probes. */\nexport interface DoctorRpc {\n getSlot(config?: Record<string, unknown>): { send(opts?: { abortSignal?: AbortSignal }): Promise<bigint> };\n getHealth(config?: Record<string, unknown>): { send(opts?: { abortSignal?: AbortSignal }): Promise<string> };\n getVersion(config?: Record<string, unknown>): {\n send(opts?: { abortSignal?: AbortSignal }): Promise<{ 'solana-core'?: string; 'feature-set'?: number }>;\n };\n}\n\n/** Builds a {@link DoctorRpc} for an endpoint URL. */\nexport type DoctorRpcFactory = (url: string) => DoctorRpc;\n\n/** Per-endpoint diagnostic result. */\nexport interface EndpointDiagnostic {\n url: string;\n /** `true` if the endpoint answered every probe. */\n reachable: boolean;\n /** Median latency over the probes (ms), or null if unreachable. */\n latencyP50Ms: number | null;\n /** `getHealth` result (`ok` / a behind message), or null. */\n health: string | null;\n /** Latest slot seen, or null. */\n slot: bigint | null;\n /** Slots behind the freshest endpoint in this run, or null. */\n slotLag: number | null;\n /** `solana-core` version, or null. */\n version: string | null;\n /** First error message, if any probe failed. */\n error: string | null;\n}\n\n/** The full doctor report (the `--json` payload). */\nexport interface DoctorReport {\n endpoints: EndpointDiagnostic[];\n /** Freshest slot across reachable endpoints, or null. */\n clusterMaxSlot: bigint | null;\n /** Count reachable / total. */\n reachable: number;\n total: number;\n}\n\n/** Options for {@link runDoctor}. */\nexport interface DoctorOptions {\n /** Latency probes per endpoint (default 5). */\n probes?: number;\n rpcFactory: DoctorRpcFactory;\n clock?: DoctorClock;\n signal?: AbortSignal;\n}\n\n/** Median of a numeric array (linear interpolation not needed for ms). */\nexport function median(values: readonly number[]): number | null {\n if (values.length === 0) return null;\n const sorted = [...values].sort((a, b) => a - b);\n const mid = Math.floor(sorted.length / 2);\n return sorted.length % 2 === 0 ? (sorted[mid - 1]! + sorted[mid]!) / 2 : sorted[mid]!;\n}\n\n/**\n * Turns a probe failure into a human-readable message.\n *\n * Works around an upstream Solana Kit defect: when an endpoint answers HTTP 200\n * with a JSON-RPC *error* whose code Kit's message template can't render (e.g. a\n * keyless gateway returning `-32000 \"needs an API key\"`), Kit's own formatter\n * (`@solana/errors` → `getHumanReadableErrorMessage`, reached via\n * `getSolanaErrorFromJsonRpcError` → `new SolanaError`) throws a bare\n * `TypeError: Cannot read properties of undefined (reading 'length')` *instead of*\n * the intended SolanaError. That TypeError carries no `context`/`code`, so the\n * real RPC message is unrecoverable — but its stack unambiguously identifies the\n * cause. Detect that signature and report something actionable rather than leaking\n * Kit's internal failure.\n */\nexport function describeProbeError(e: unknown): string {\n if (e instanceof TypeError) {\n const stack = e.stack ?? '';\n if (\n stack.includes('getHumanReadableErrorMessage') ||\n stack.includes('getSolanaErrorFromJsonRpcError') ||\n stack.includes('@solana/errors')\n ) {\n return 'endpoint returned a JSON-RPC error Solana Kit could not format (often a keyless/unauthorized gateway requiring an API key)';\n }\n }\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Probes a single endpoint: `probes` latency-timed `getSlot` calls (p50), one\n * `getHealth`, and one `getVersion`. Never throws — failures are captured in the\n * returned diagnostic. Pure given an injected `rpcFactory` + `clock`.\n */\nexport async function probeEndpoint(\n url: string,\n options: { probes: number; rpc: DoctorRpc; clock: DoctorClock; signal?: AbortSignal },\n): Promise<EndpointDiagnostic> {\n const { probes, rpc, clock, signal } = options;\n const latencies: number[] = [];\n let slot: bigint | null = null;\n let error: string | null = null;\n\n for (let i = 0; i < probes; i++) {\n const start = clock.now();\n try {\n const s = await rpc.getSlot().send(signal ? { abortSignal: signal } : undefined);\n latencies.push(clock.now() - start);\n slot = s;\n } catch (e) {\n error ??= describeProbeError(e);\n }\n }\n\n let health: string | null = null;\n let version: string | null = null;\n if (latencies.length > 0) {\n try {\n health = await rpc.getHealth().send(signal ? { abortSignal: signal } : undefined);\n } catch (e) {\n error ??= describeProbeError(e);\n }\n try {\n const v = await rpc.getVersion().send(signal ? { abortSignal: signal } : undefined);\n version = v['solana-core'] ?? null;\n } catch (e) {\n error ??= describeProbeError(e);\n }\n }\n\n const reachable = latencies.length > 0;\n return {\n url,\n reachable,\n latencyP50Ms: median(latencies),\n health,\n slot,\n slotLag: null, // filled in once the cluster max is known\n version,\n error,\n };\n}\n\n/**\n * Runs `doctor` across all endpoints: probes each (in parallel), computes the\n * cluster-max slot, and back-fills each endpoint's `slotLag`. Pure given the\n * injected `rpcFactory`/`clock` — the rendering layer formats the result.\n */\nexport async function runDoctor(urls: readonly string[], options: DoctorOptions): Promise<DoctorReport> {\n const probes = options.probes ?? 5;\n const clock = options.clock ?? systemClock;\n\n const diagnostics = await Promise.all(\n urls.map((url) =>\n probeEndpoint(url, {\n probes,\n rpc: options.rpcFactory(url),\n clock,\n ...(options.signal ? { signal: options.signal } : {}),\n }),\n ),\n );\n\n let clusterMaxSlot: bigint | null = null;\n for (const d of diagnostics) {\n if (d.slot !== null && (clusterMaxSlot === null || d.slot > clusterMaxSlot)) clusterMaxSlot = d.slot;\n }\n for (const d of diagnostics) {\n d.slotLag = d.slot !== null && clusterMaxSlot !== null ? Number(clusterMaxSlot - d.slot) : null;\n }\n\n const reachable = diagnostics.filter((d) => d.reachable).length;\n return { endpoints: diagnostics, clusterMaxSlot, reachable, total: urls.length };\n}\n\n/** Serializes a {@link DoctorReport} to stable JSON (bigint → string). */\nexport function doctorReportToJson(report: DoctorReport): string {\n return JSON.stringify(\n report,\n (_key, value) => (typeof value === 'bigint' ? value.toString() : value),\n 2,\n );\n}\n","/** The `bastion.config.json` shape. */\nexport interface BastionConfig {\n endpoints: string[];\n jito?: { region?: string; baseUrl?: string };\n}\n\n/**\n * Parses + validates a `bastion.config.json` payload. Pure (takes the already-read\n * text) so it's testable without filesystem access. Throws a clear error on a\n * malformed config.\n */\nexport function parseConfig(text: string): BastionConfig {\n let raw: unknown;\n try {\n raw = JSON.parse(text);\n } catch (e) {\n throw new Error(`invalid bastion.config.json: ${e instanceof Error ? e.message : String(e)}`);\n }\n if (typeof raw !== 'object' || raw === null) throw new Error('bastion.config.json must be an object');\n const endpoints = (raw as { endpoints?: unknown }).endpoints;\n if (!Array.isArray(endpoints) || endpoints.length === 0 || !endpoints.every((e) => typeof e === 'string')) {\n throw new Error('bastion.config.json must have a non-empty \"endpoints\" string array');\n }\n const jito = (raw as { jito?: unknown }).jito;\n const config: BastionConfig = { endpoints: endpoints as string[] };\n if (jito && typeof jito === 'object') {\n const j = jito as { region?: unknown; baseUrl?: unknown };\n config.jito = {\n ...(typeof j.region === 'string' ? { region: j.region } : {}),\n ...(typeof j.baseUrl === 'string' ? { baseUrl: j.baseUrl } : {}),\n };\n }\n return config;\n}\n\n/**\n * Resolves endpoints from either `--endpoints` (comma-separated) or a parsed\n * config. `--endpoints` wins when both are present.\n */\nexport function resolveEndpoints(opts: { endpoints?: string; config?: BastionConfig }): string[] {\n if (opts.endpoints) {\n const list = opts.endpoints\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (list.length === 0) throw new Error('--endpoints provided but empty');\n return list;\n }\n if (opts.config) return opts.config.endpoints;\n throw new Error('no endpoints: pass --endpoints <urls> or --config <bastion.config.json>');\n}\n","import type { BreakerState, EndpointHealth } from '@rpc-bastion/core';\n\n/** A formatted row for the monitor table (pure projection of EndpointHealth). */\nexport interface MonitorRow {\n url: string;\n /** Status dot category. */\n dot: 'green' | 'yellow' | 'red';\n latencyMs: number;\n errorRatePct: number;\n slotLag: number | null;\n breaker: BreakerState;\n inFlight: number;\n}\n\n/** A summary across the whole pool. */\nexport interface MonitorSummary {\n rows: MonitorRow[];\n healthy: number;\n total: number;\n /** Worst (highest) slot lag across endpoints, or null. */\n maxSlotLag: number | null;\n}\n\n/** Categorizes an endpoint into a status dot from its health + breaker. */\nexport function statusDot(health: EndpointHealth): MonitorRow['dot'] {\n if (health.breaker === 'open' || health.status === 'broken') return 'red';\n if (health.breaker === 'half-open' || health.status === 'degraded' || health.errorRate > 0.1 || (health.slotLag ?? 0) > 50)\n return 'yellow';\n return 'green';\n}\n\n/**\n * Projects a pool health snapshot into renderable monitor rows + a summary.\n * Pure — the rendering layer turns this into a table/sparkline. Reuses the same\n * `EndpointHealth` the SDK pool produces (no parallel health model).\n */\nexport function summarizeSnapshot(snapshot: readonly EndpointHealth[]): MonitorSummary {\n const rows: MonitorRow[] = snapshot.map((h) => ({\n url: h.url,\n dot: statusDot(h),\n latencyMs: Math.round(h.latencyEwmaMs),\n errorRatePct: Math.round(h.errorRate * 100),\n slotLag: h.slotLag,\n breaker: h.breaker,\n inFlight: h.inFlight,\n }));\n\n const healthy = rows.filter((r) => r.dot === 'green').length;\n let maxSlotLag: number | null = null;\n for (const r of rows) {\n if (r.slotLag !== null && (maxSlotLag === null || r.slotLag > maxSlotLag)) maxSlotLag = r.slotLag;\n }\n return { rows, healthy, total: rows.length, maxSlotLag };\n}\n\n/**\n * Appends a latency sample to a bounded ring buffer (for the monitor sparkline).\n * Returns a NEW array (immutable), keeping at most `size` samples.\n */\nexport function pushSample(history: readonly number[], sample: number, size = 30): number[] {\n const next = [...history, sample];\n return next.length > size ? next.slice(next.length - size) : next;\n}\n","import {\n createEventBus,\n createResilientRpc,\n type BastionEvent,\n type CircuitBreakerOptions,\n type EndpointHealth,\n type LoadBalancerStrategy,\n} from '@rpc-bastion/core';\nimport { createChaosTransport, createMockRpcServer, type ChaosPlan } from '@rpc-bastion/testkit';\n\n/** A named, scripted chaos scenario for `bastion simulate`. */\nexport interface Scenario {\n name: string;\n description: string;\n endpoints: string[];\n /** Per-endpoint chaos plan, keyed by endpoint url. */\n plans: Record<string, ChaosPlan>;\n /** How many probe ticks to run (default 10). */\n ticks: number;\n /** Load-balancer strategy (default the pool's `health-weighted`). */\n strategy?: LoadBalancerStrategy;\n /**\n * User-level calls issued per tick (default 1). A small burst lets a failing\n * endpoint accumulate enough failures to trip its breaker — the drama beat.\n */\n callsPerTick?: number;\n /** Per-scenario breaker tuning, so a scenario can show open → half-open → closed. */\n breaker?: CircuitBreakerOptions;\n}\n\n/** A narrative event observed during one tick, projected from the bus. */\nexport interface TickEvent {\n /** Compact kind for the renderer to colour: which arc beat this is. */\n kind: 'served' | 'error' | 'failover' | 'breaker-open' | 'breaker-half-open' | 'breaker-closed';\n /** The endpoint this event is about (the `to` endpoint for a failover). */\n endpoint: string;\n /** A short human label, e.g. `rpc-a → rpc-b` or `OPEN`. */\n detail: string;\n}\n\n/** A single tick of a simulation: the health snapshot after the tick's probe. */\nexport interface SimulationTick {\n tick: number;\n snapshot: EndpointHealth[];\n /**\n * The endpoint that ultimately served this tick's user-level call, or null if\n * the call failed on every survivor. Drives the per-endpoint traffic counters.\n */\n servedBy?: string | null;\n /** `false` only when the user-level call threw after all failover/retry. */\n userCallOk?: boolean;\n /** Narrative events the bus emitted during this tick (failover, breaker flips). */\n events?: TickEvent[];\n}\n\n/** The aggregate verdict of a simulation run — the \"0 failed user calls\" thesis. */\nexport interface SimulationSummary {\n /** Every tick is one user-level call. */\n userCallsTotal: number;\n /** Calls that threw after all failover + retry were exhausted. */\n userCallsFailed: number;\n /** Per-endpoint served count + error count over the run. */\n perEndpoint: Array<{ url: string; served: number; errors: number }>;\n /** Count of breaker open transitions observed. */\n breakerOpens: number;\n}\n\n/** The built-in scenario catalogue. */\nexport const SCENARIOS: Record<string, Scenario> = {\n 'live-outage': {\n name: 'live-outage',\n description:\n 'The movie: 3 healthy endpoints → the primary blacks out (breaker opens, traffic reroutes) → it recovers (half-open trial → closed). Zero failed user calls.',\n endpoints: ['rpc-a', 'rpc-b', 'rpc-c'],\n plans: {\n // Healthy (mild latency) → total blackout from 1.5s → recovery from 4s.\n 'rpc-a': [\n { atMs: 0, set: { latency: { minMs: 18, maxMs: 45 } } },\n { atMs: 1_500, set: { dropRate: 1 } },\n { atMs: 4_000, set: { dropRate: 0, latency: { minMs: 22, maxMs: 50 } } },\n ],\n 'rpc-b': [{ atMs: 0, set: { latency: { minMs: 25, maxMs: 70 } } }],\n 'rpc-c': [{ atMs: 0, set: { latency: { minMs: 30, maxMs: 85 } } }],\n },\n ticks: 14,\n // Round-robin keeps probing rpc-a during the outage so its failures pile up\n // and the breaker actually opens; the burst gets there within one tick.\n strategy: 'round-robin',\n callsPerTick: 3,\n breaker: { failureThreshold: 3, windowMs: 5_000, cooldownMs: 1_000 },\n },\n 'flaky-endpoint': {\n name: 'flaky-endpoint',\n description: 'One endpoint degrades (latency + 500s) mid-run; the pool routes away from it.',\n endpoints: ['rpc-a', 'rpc-b'],\n plans: {\n 'rpc-a': [\n { atMs: 0, set: {} },\n { atMs: 2_000, set: { latency: { minMs: 400, maxMs: 800 }, errorRate: { http500: 0.5 } } },\n ],\n 'rpc-b': [],\n },\n ticks: 10,\n },\n 'cascading-failure': {\n name: 'cascading-failure',\n description: 'Endpoints fail one after another; the breaker opens and probes recover them.',\n endpoints: ['rpc-a', 'rpc-b', 'rpc-c'],\n plans: {\n 'rpc-a': [{ atMs: 1_000, set: { dropRate: 1 } }],\n 'rpc-b': [{ atMs: 3_000, set: { dropRate: 1 } }],\n 'rpc-c': [],\n },\n ticks: 12,\n },\n 'rate-limited': {\n name: 'rate-limited',\n description: 'All endpoints throw 429s intermittently; retries with backoff absorb them.',\n endpoints: ['rpc-a', 'rpc-b'],\n plans: {\n 'rpc-a': [{ atMs: 0, set: { errorRate: { http429: 0.4 } } }],\n 'rpc-b': [{ atMs: 0, set: { errorRate: { http429: 0.4 } } }],\n },\n ticks: 10,\n },\n};\n\n/** Lists the available scenario names + descriptions. */\nexport function listScenarios(): Array<{ name: string; description: string }> {\n return Object.values(SCENARIOS).map((s) => ({ name: s.name, description: s.description }));\n}\n\n/** Options for {@link runSimulation}. */\nexport interface SimulationOptions {\n /** Advances the simulated clock + mock chain by this many ms per tick (default 500). */\n tickMs?: number;\n seed?: number;\n /** Called after each tick (for live rendering). */\n onTick?: (tick: SimulationTick) => void;\n /** Drives time + the mock chain forward between ticks (injected for tests). */\n advance: (ms: number) => Promise<void> | void;\n /** Reads \"now\" in ms (injected; defaults to a tick-counted clock). */\n now: () => number;\n}\n\n/**\n * Runs a named scenario against a resilient pool built over chaos transports\n * (real SDK pool/health code), returning a snapshot per tick. The caller injects\n * `advance`/`now` so tests drive it deterministically with fake timers + a\n * ManualClock; the renderer just consumes the ticks.\n */\nexport async function runSimulation(scenario: Scenario, options: SimulationOptions): Promise<SimulationTick[]> {\n const tickMs = options.tickMs ?? 500;\n const clock = { now: options.now, sleep: async () => undefined };\n\n // A bus so each tick can show the real failover/breaker events the stack emits.\n const bus = createEventBus();\n\n // A chaos transport per endpoint, fed by a shared mock chain.\n const server = createMockRpcServer({ slot: 1000, blockHeight: 1000 });\n const { pool, rpc } = createResilientRpc(\n scenario.endpoints.map((url) => ({ url })),\n {\n bus,\n clock,\n ...(scenario.strategy ? { strategy: scenario.strategy } : {}),\n ...(scenario.breaker ? { breaker: scenario.breaker } : {}),\n pool: {\n probe: { enabled: false },\n transportFactory: (config: { url: string }) =>\n createChaosTransport(server.transport, {\n plan: scenario.plans[config.url] ?? [],\n endpointId: config.url,\n clock,\n seed: options.seed ?? 42,\n }),\n },\n },\n );\n\n // Collect this tick's narrative events; the loop drains + clears it each tick.\n let pending: TickEvent[] = [];\n let lastServed: string | null = null;\n bus.onAny((event: BastionEvent) => {\n switch (event.type) {\n case 'rpc.response':\n if (event.ok) {\n lastServed = event.endpoint;\n pending.push({ kind: 'served', endpoint: event.endpoint, detail: `${Math.round(event.latencyMs)}ms` });\n }\n break;\n case 'rpc.error':\n pending.push({ kind: 'error', endpoint: event.endpoint, detail: event.errorClass });\n break;\n case 'rpc.failover':\n if (event.to) pending.push({ kind: 'failover', endpoint: event.to, detail: `${event.from} → ${event.to}` });\n break;\n case 'breaker.state':\n pending.push({\n kind:\n event.state === 'open'\n ? 'breaker-open'\n : event.state === 'half-open'\n ? 'breaker-half-open'\n : 'breaker-closed',\n endpoint: event.endpoint,\n detail: event.state.toUpperCase(),\n });\n break;\n default:\n break;\n }\n });\n\n const callsPerTick = Math.max(1, scenario.callsPerTick ?? 1);\n const ticks: SimulationTick[] = [];\n for (let i = 0; i < scenario.ticks; i++) {\n pending = [];\n lastServed = null;\n let userCallOk = true;\n // A small burst of user-level calls per tick through the resilient pool\n // (exercises balancer/failover/breaker/retry). A user call only fails if\n // every survivor is down for that call — the thesis is that never happens.\n for (let c = 0; c < callsPerTick; c++) {\n try {\n await rpc.getSlot().send();\n } catch {\n userCallOk = false;\n }\n }\n const snapshot = pool.getHealthSnapshot();\n const tick: SimulationTick = {\n tick: i,\n snapshot,\n servedBy: userCallOk ? lastServed : null,\n userCallOk,\n events: pending,\n };\n ticks.push(tick);\n options.onTick?.(tick);\n await options.advance(tickMs);\n }\n return ticks;\n}\n\n/**\n * Projects a finished run into the headline verdict: how many user-level calls\n * the SDK made, how many failed despite failover/retry (the thesis is **zero**),\n * per-endpoint traffic, and how many times a breaker opened. Pure over the ticks.\n */\nexport function summarizeSimulation(ticks: readonly SimulationTick[]): SimulationSummary {\n const perEndpoint = new Map<string, { url: string; served: number; errors: number }>();\n const ensure = (url: string) => {\n let row = perEndpoint.get(url);\n if (!row) {\n row = { url, served: 0, errors: 0 };\n perEndpoint.set(url, row);\n }\n return row;\n };\n\n let userCallsFailed = 0;\n let breakerOpens = 0;\n for (const tick of ticks) {\n if (tick.userCallOk === false) userCallsFailed++;\n if (tick.servedBy) ensure(tick.servedBy).served++;\n for (const ev of tick.events ?? []) {\n if (ev.kind === 'error') ensure(ev.endpoint).errors++;\n if (ev.kind === 'breaker-open') breakerOpens++;\n }\n }\n\n return {\n userCallsTotal: ticks.length,\n userCallsFailed,\n perEndpoint: [...perEndpoint.values()],\n breakerOpens,\n };\n}\n","import {\n createConfirmationEngine,\n type ConfirmationOutcome,\n type Commitment,\n type SenderRpc,\n} from '@rpc-bastion/sender';\nimport type { Clock, RpcSubscriptionsApiLike } from '@rpc-bastion/core';\n\n/** A point-in-time status line for the `watch-tx` UI. */\nexport interface WatchTxUpdate {\n at: number;\n state: 'watching' | 'confirmed' | 'failed' | 'expired' | 'aborted';\n detail: string;\n}\n\n/** Final result of watching a transaction. */\nexport interface WatchTxResult {\n signature: string;\n outcome: ConfirmationOutcome['type'];\n slot: bigint | null;\n updates: WatchTxUpdate[];\n}\n\n/** Options for {@link watchTransaction}. */\nexport interface WatchTxOptions {\n rpc: SenderRpc;\n rpcSubscriptions?: RpcSubscriptionsApiLike;\n lastValidBlockHeight: bigint;\n commitment?: Commitment;\n clock?: Clock;\n pollIntervalMs?: number;\n blockHeightIntervalMs?: number;\n signal?: AbortSignal;\n /** Called on each status update (for live rendering). */\n onUpdate?: (update: WatchTxUpdate) => void;\n}\n\n/**\n * Watches a transaction signature to a terminal state, reusing the SDK's\n * confirmation engine (WS + polling race). Returns the outcome plus a timeline of\n * updates. Pure given injected `rpc`/`clock` — no rendering here.\n */\nexport async function watchTransaction(signature: string, options: WatchTxOptions): Promise<WatchTxResult> {\n const clock = options.clock;\n const engine = createConfirmationEngine({\n rpc: options.rpc,\n ...(options.rpcSubscriptions ? { rpcSubscriptions: options.rpcSubscriptions } : {}),\n ...(clock ? { clock } : {}),\n });\n\n const updates: WatchTxUpdate[] = [];\n const now = (): number => clock?.now() ?? 0;\n const push = (state: WatchTxUpdate['state'], detail: string): void => {\n const update: WatchTxUpdate = { at: now(), state, detail };\n updates.push(update);\n options.onUpdate?.(update);\n };\n\n push('watching', `commitment=${options.commitment ?? 'confirmed'}`);\n\n const outcome = await engine.confirm(signature, {\n commitment: options.commitment ?? 'confirmed',\n lastValidBlockHeight: options.lastValidBlockHeight,\n ...(options.pollIntervalMs !== undefined ? { pollIntervalMs: options.pollIntervalMs } : {}),\n ...(options.blockHeightIntervalMs !== undefined ? { blockHeightIntervalMs: options.blockHeightIntervalMs } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n });\n\n switch (outcome.type) {\n case 'confirmed':\n push('confirmed', `slot ${outcome.slot} (${outcome.confirmationStatus})`);\n return { signature, outcome: 'confirmed', slot: outcome.slot, updates };\n case 'failed':\n push('failed', 'on-chain execution error');\n return { signature, outcome: 'failed', slot: outcome.slot, updates };\n case 'expired':\n push('expired', `block height ${outcome.blockHeight} > ${outcome.lastValidBlockHeight}`);\n return { signature, outcome: 'expired', slot: null, updates };\n default:\n push('aborted', 'watch aborted');\n return { signature, outcome: 'aborted', slot: null, updates };\n }\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/data/args.ts
|
|
2
|
+
function parseArgs(argv) {
|
|
3
|
+
const flags = {};
|
|
4
|
+
const positionals = [];
|
|
5
|
+
let command = null;
|
|
6
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7
|
+
const tok = argv[i];
|
|
8
|
+
if (tok.startsWith("--")) {
|
|
9
|
+
const body = tok.slice(2);
|
|
10
|
+
const eq = body.indexOf("=");
|
|
11
|
+
if (eq >= 0) {
|
|
12
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
13
|
+
} else {
|
|
14
|
+
const next = argv[i + 1];
|
|
15
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
16
|
+
flags[body] = next;
|
|
17
|
+
i++;
|
|
18
|
+
} else {
|
|
19
|
+
flags[body] = true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} else if (command === null) {
|
|
23
|
+
command = tok;
|
|
24
|
+
} else {
|
|
25
|
+
positionals.push(tok);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { command, positionals, flags };
|
|
29
|
+
}
|
|
30
|
+
function flagString(args, name) {
|
|
31
|
+
const v = args.flags[name];
|
|
32
|
+
return typeof v === "string" ? v : void 0;
|
|
33
|
+
}
|
|
34
|
+
function flagBool(args, name) {
|
|
35
|
+
return args.flags[name] === true || args.flags[name] === "true";
|
|
36
|
+
}
|
|
37
|
+
function flagInt(args, name, fallback) {
|
|
38
|
+
const v = args.flags[name];
|
|
39
|
+
if (typeof v !== "string") return fallback;
|
|
40
|
+
const n = Number.parseInt(v, 10);
|
|
41
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { flagBool, flagInt, flagString, parseArgs };
|
|
45
|
+
//# sourceMappingURL=chunk-Q2OA5HXD.js.map
|
|
46
|
+
//# sourceMappingURL=chunk-Q2OA5HXD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/args.ts"],"names":[],"mappings":";AAcO,SAAS,UAAU,IAAA,EAAqC;AAC7D,EAAA,MAAM,QAA0C,EAAC;AACjD,EAAA,MAAM,cAAwB,EAAC;AAC/B,EAAA,IAAI,OAAA,GAAyB,IAAA;AAE7B,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AACpC,IAAA,MAAM,GAAA,GAAM,KAAK,CAAC,CAAA;AAClB,IAAA,IAAI,GAAA,CAAI,UAAA,CAAW,IAAI,CAAA,EAAG;AACxB,MAAA,MAAM,IAAA,GAAO,GAAA,CAAI,KAAA,CAAM,CAAC,CAAA;AACxB,MAAA,MAAM,EAAA,GAAK,IAAA,CAAK,OAAA,CAAQ,GAAG,CAAA;AAC3B,MAAA,IAAI,MAAM,CAAA,EAAG;AACX,QAAA,KAAA,CAAM,IAAA,CAAK,MAAM,CAAA,EAAG,EAAE,CAAC,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,CAAC,CAAA;AAAA,MAC9C,CAAA,MAAO;AACL,QAAA,MAAM,IAAA,GAAO,IAAA,CAAK,CAAA,GAAI,CAAC,CAAA;AACvB,QAAA,IAAI,SAAS,MAAA,IAAa,CAAC,IAAA,CAAK,UAAA,CAAW,IAAI,CAAA,EAAG;AAChD,UAAA,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AACd,UAAA,CAAA,EAAA;AAAA,QACF,CAAA,MAAO;AACL,UAAA,KAAA,CAAM,IAAI,CAAA,GAAI,IAAA;AAAA,QAChB;AAAA,MACF;AAAA,IACF,CAAA,MAAA,IAAW,YAAY,IAAA,EAAM;AAC3B,MAAA,OAAA,GAAU,GAAA;AAAA,IACZ,CAAA,MAAO;AACL,MAAA,WAAA,CAAY,KAAK,GAAG,CAAA;AAAA,IACtB;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,OAAA,EAAS,WAAA,EAAa,KAAA,EAAM;AACvC;AAGO,SAAS,UAAA,CAAW,MAAkB,IAAA,EAAkC;AAC7E,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACzB,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,GAAW,CAAA,GAAI,MAAA;AACrC;AAGO,SAAS,QAAA,CAAS,MAAkB,IAAA,EAAuB;AAChE,EAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA,KAAM,QAAQ,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA,KAAM,MAAA;AAC3D;AAGO,SAAS,OAAA,CAAQ,IAAA,EAAkB,IAAA,EAAc,QAAA,EAA0B;AAChF,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACzB,EAAA,IAAI,OAAO,CAAA,KAAM,QAAA,EAAU,OAAO,QAAA;AAClC,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,CAAA,EAAG,EAAE,CAAA;AAC/B,EAAA,OAAO,OAAO,QAAA,CAAS,CAAC,CAAA,IAAK,CAAA,GAAI,IAAI,CAAA,GAAI,QAAA;AAC3C","file":"chunk-Q2OA5HXD.js","sourcesContent":["/** A parsed command line: the command, positional args, and flags. */\nexport interface ParsedArgs {\n command: string | null;\n positionals: string[];\n /** `--flag value` and `--bool` (→ `true`). */\n flags: Record<string, string | boolean>;\n}\n\n/**\n * A tiny, dependency-free argv parser. `--key value` → `{ key: 'value' }`;\n * `--key=value` → same; a bare `--flag` (followed by another flag or nothing) →\n * `{ flag: true }`. The first non-flag token is the command; the rest are\n * positionals. Pure and fully testable.\n */\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const flags: Record<string, string | boolean> = {};\n const positionals: string[] = [];\n let command: string | null = null;\n\n for (let i = 0; i < argv.length; i++) {\n const tok = argv[i]!;\n if (tok.startsWith('--')) {\n const body = tok.slice(2);\n const eq = body.indexOf('=');\n if (eq >= 0) {\n flags[body.slice(0, eq)] = body.slice(eq + 1);\n } else {\n const next = argv[i + 1];\n if (next !== undefined && !next.startsWith('--')) {\n flags[body] = next;\n i++;\n } else {\n flags[body] = true;\n }\n }\n } else if (command === null) {\n command = tok;\n } else {\n positionals.push(tok);\n }\n }\n\n return { command, positionals, flags };\n}\n\n/** Reads a flag as a string, or returns `undefined`. */\nexport function flagString(args: ParsedArgs, name: string): string | undefined {\n const v = args.flags[name];\n return typeof v === 'string' ? v : undefined;\n}\n\n/** Reads a flag as a boolean (presence or `true`). */\nexport function flagBool(args: ParsedArgs, name: string): boolean {\n return args.flags[name] === true || args.flags[name] === 'true';\n}\n\n/** Reads a flag as a positive integer, or `fallback`. */\nexport function flagInt(args: ParsedArgs, name: string, fallback: number): number {\n const v = args.flags[name];\n if (typeof v !== 'string') return fallback;\n const n = Number.parseInt(v, 10);\n return Number.isFinite(n) && n > 0 ? n : fallback;\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
var COMMANDS = ["doctor", "monitor", "watch-tx", "simulate", "help"];
|
|
3
|
+
function route(args) {
|
|
4
|
+
if (args.flags["help"] === true || args.flags["version"] === true) return "help";
|
|
5
|
+
const c = args.command;
|
|
6
|
+
if (c && COMMANDS.includes(c)) return c;
|
|
7
|
+
return "help";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { COMMANDS, route };
|
|
11
|
+
//# sourceMappingURL=chunk-WU3Q4ZC6.js.map
|
|
12
|
+
//# sourceMappingURL=chunk-WU3Q4ZC6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"names":[],"mappings":";AAGO,IAAM,WAAW,CAAC,QAAA,EAAU,SAAA,EAAW,UAAA,EAAY,YAAY,MAAM;AAOrE,SAAS,MAAM,IAAA,EAA2B;AAC/C,EAAA,IAAI,IAAA,CAAK,KAAA,CAAM,MAAM,CAAA,KAAM,IAAA,IAAQ,KAAK,KAAA,CAAM,SAAS,CAAA,KAAM,IAAA,EAAM,OAAO,MAAA;AAC1E,EAAA,MAAM,IAAI,IAAA,CAAK,OAAA;AACf,EAAA,IAAI,CAAA,IAAM,QAAA,CAA+B,QAAA,CAAS,CAAC,GAAG,OAAO,CAAA;AAC7D,EAAA,OAAO,MAAA;AACT","file":"chunk-WU3Q4ZC6.js","sourcesContent":["import type { ParsedArgs } from './data/args';\n\n/** The set of known commands. */\nexport const COMMANDS = ['doctor', 'monitor', 'watch-tx', 'simulate', 'help'] as const;\nexport type Command = (typeof COMMANDS)[number];\n\n/**\n * Resolves the command to run from parsed args. Pure + fully tested — this is the\n * routing brain; the live `main()` (which does I/O) lives in `./ui/run`.\n */\nexport function route(args: ParsedArgs): Command {\n if (args.flags['help'] === true || args.flags['version'] === true) return 'help';\n const c = args.command;\n if (c && (COMMANDS as readonly string[]).includes(c)) return c as Command;\n return 'help';\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"cli-64RAFQGB.js"}
|