lovewebconnection 0.0.0-stage → 0.2.1
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/.env.example +38 -0
- package/LICENSE +21 -0
- package/README.md +1937 -2
- package/dist/alert-queue.d.ts +72 -0
- package/dist/alert-queue.js +155 -0
- package/dist/alert-store.d.ts +29 -0
- package/dist/alert-store.js +75 -0
- package/dist/balances.d.ts +221 -0
- package/dist/balances.js +1232 -0
- package/dist/cache.d.ts +37 -0
- package/dist/cache.js +128 -0
- package/dist/call.d.ts +46 -0
- package/dist/call.js +377 -0
- package/dist/chain.d.ts +97 -0
- package/dist/chain.js +419 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1011 -0
- package/dist/compare-history.d.ts +82 -0
- package/dist/compare-history.js +340 -0
- package/dist/compare.d.ts +98 -0
- package/dist/compare.js +492 -0
- package/dist/contract.d.ts +107 -0
- package/dist/contract.js +162 -0
- package/dist/default-abi.d.ts +53 -0
- package/dist/default-abi.js +85 -0
- package/dist/history.d.ts +83 -0
- package/dist/history.js +422 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +27 -0
- package/dist/market-page.d.ts +8 -0
- package/dist/market-page.js +554 -0
- package/dist/market-server.d.ts +46 -0
- package/dist/market-server.js +473 -0
- package/dist/market.d.ts +323 -0
- package/dist/market.js +835 -0
- package/dist/metadata-cache.d.ts +72 -0
- package/dist/metadata-cache.js +197 -0
- package/dist/metrics.d.ts +64 -0
- package/dist/metrics.js +138 -0
- package/dist/plot.d.ts +60 -0
- package/dist/plot.js +317 -0
- package/dist/presets-history.d.ts +372 -0
- package/dist/presets-history.js +1243 -0
- package/dist/presets-summary.d.ts +17 -0
- package/dist/presets-summary.js +55 -0
- package/dist/presets.d.ts +59 -0
- package/dist/presets.js +181 -0
- package/dist/prices.d.ts +63 -0
- package/dist/prices.js +154 -0
- package/dist/rotate.d.ts +19 -0
- package/dist/rotate.js +34 -0
- package/dist/scan.d.ts +81 -0
- package/dist/scan.js +501 -0
- package/dist/sessions.d.ts +104 -0
- package/dist/sessions.js +515 -0
- package/dist/signatures.d.ts +13 -0
- package/dist/signatures.js +43 -0
- package/dist/tokenfile.d.ts +50 -0
- package/dist/tokenfile.js +218 -0
- package/dist/views.d.ts +63 -0
- package/dist/views.js +415 -0
- package/dist/watch.d.ts +306 -0
- package/dist/watch.js +1121 -0
- package/dist/webhook.d.ts +17 -0
- package/dist/webhook.js +35 -0
- package/examples/erc20.abi.json +57 -0
- package/examples/xlayer.tokens.json +4 -0
- package/package.json +77 -4
package/dist/plot.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
export class PlotArgError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "PlotArgError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export const DEFAULT_PLOT_WIDTH = 48;
|
|
9
|
+
const MIN_PLOT_WIDTH = 10;
|
|
10
|
+
const MAX_PLOT_WIDTH = 200;
|
|
11
|
+
function parseWidth(raw) {
|
|
12
|
+
if (!/^\d+$/.test(raw)) {
|
|
13
|
+
throw new PlotArgError(`--width must be an integer between ${MIN_PLOT_WIDTH} and ${MAX_PLOT_WIDTH}.`);
|
|
14
|
+
}
|
|
15
|
+
const width = Number(raw);
|
|
16
|
+
if (width < MIN_PLOT_WIDTH || width > MAX_PLOT_WIDTH) {
|
|
17
|
+
throw new PlotArgError(`--width must be between ${MIN_PLOT_WIDTH} and ${MAX_PLOT_WIDTH}, got ${width}.`);
|
|
18
|
+
}
|
|
19
|
+
return width;
|
|
20
|
+
}
|
|
21
|
+
export function parsePlotArgs(argv) {
|
|
22
|
+
const positional = [];
|
|
23
|
+
let width = DEFAULT_PLOT_WIDTH;
|
|
24
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
25
|
+
const arg = argv[index] ?? "";
|
|
26
|
+
if (arg === "--width") {
|
|
27
|
+
const value = argv[index + 1];
|
|
28
|
+
if (!value || value.startsWith("-")) {
|
|
29
|
+
throw new PlotArgError("--width requires a number.");
|
|
30
|
+
}
|
|
31
|
+
width = parseWidth(value);
|
|
32
|
+
index += 1;
|
|
33
|
+
}
|
|
34
|
+
else if (arg.startsWith("--width=")) {
|
|
35
|
+
width = parseWidth(arg.slice("--width=".length));
|
|
36
|
+
}
|
|
37
|
+
else if (arg.startsWith("-")) {
|
|
38
|
+
continue; // --json and other global flags are handled by the CLI.
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
positional.push(arg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const [path] = positional;
|
|
45
|
+
if (!path) {
|
|
46
|
+
throw new PlotArgError("A history CSV path is required: lwc plot ./balances.csv [--width 60]");
|
|
47
|
+
}
|
|
48
|
+
if (positional.length > 1) {
|
|
49
|
+
throw new PlotArgError(`Unexpected extra argument: ${positional[1]}.`);
|
|
50
|
+
}
|
|
51
|
+
return { path, width };
|
|
52
|
+
}
|
|
53
|
+
export function readHistoryCsv(path) {
|
|
54
|
+
if (!existsSync(path)) {
|
|
55
|
+
throw new PlotArgError(`history CSV not found: ${path}`);
|
|
56
|
+
}
|
|
57
|
+
return readFileSync(path, "utf8");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Minimal RFC 4180 reader: quoted cells may hold commas, newlines, and doubled
|
|
61
|
+
* quotes, which is exactly what the history CSV writes for labels and flags.
|
|
62
|
+
*/
|
|
63
|
+
export function parseCsv(text) {
|
|
64
|
+
const rows = [];
|
|
65
|
+
let row = [];
|
|
66
|
+
let cell = "";
|
|
67
|
+
let quoted = false;
|
|
68
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
69
|
+
const ch = text[index] ?? "";
|
|
70
|
+
if (quoted) {
|
|
71
|
+
if (ch === '"') {
|
|
72
|
+
if (text[index + 1] === '"') {
|
|
73
|
+
cell += '"';
|
|
74
|
+
index += 1;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
quoted = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
cell += ch;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === '"') {
|
|
86
|
+
quoted = true;
|
|
87
|
+
}
|
|
88
|
+
else if (ch === ",") {
|
|
89
|
+
row.push(cell);
|
|
90
|
+
cell = "";
|
|
91
|
+
}
|
|
92
|
+
else if (ch === "\n") {
|
|
93
|
+
row.push(cell);
|
|
94
|
+
rows.push(row);
|
|
95
|
+
row = [];
|
|
96
|
+
cell = "";
|
|
97
|
+
}
|
|
98
|
+
else if (ch !== "\r") {
|
|
99
|
+
cell += ch;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (cell !== "" || row.length > 0) {
|
|
103
|
+
row.push(cell);
|
|
104
|
+
rows.push(row);
|
|
105
|
+
}
|
|
106
|
+
return rows.filter((entry) => entry.length > 1 || (entry[0] ?? "") !== "");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Turn a history CSV into one point per portfolio sample. The long format
|
|
110
|
+
* repeats `usd_total` on every asset row of a sample, so rows are grouped by
|
|
111
|
+
* session and timestamp; when `usd_total` is empty, that sample's row-level
|
|
112
|
+
* `usd` values are summed instead so a partly priced portfolio still charts.
|
|
113
|
+
* The `--wide` pivot is read the same way, except its `usd_total` column is
|
|
114
|
+
* already one value per sample: `--wide`, `--wide --usd`, and `--wide
|
|
115
|
+
* --usd-only` all chart from it without summing asset columns.
|
|
116
|
+
*/
|
|
117
|
+
export function buildPlotReport(text, options) {
|
|
118
|
+
const width = options.width ?? DEFAULT_PLOT_WIDTH;
|
|
119
|
+
const rows = parseCsv(text);
|
|
120
|
+
const header = (rows[0] ?? []).map((cell) => cell.trim().toLowerCase());
|
|
121
|
+
const atIndex = header.indexOf("at");
|
|
122
|
+
const sessionIndex = header.indexOf("session");
|
|
123
|
+
const totalIndex = header.indexOf("usd_total");
|
|
124
|
+
const usdIndex = header.indexOf("usd");
|
|
125
|
+
if (atIndex === -1) {
|
|
126
|
+
throw new PlotArgError(`not a history CSV: ${options.path} has no at column (write it with history --csv).`);
|
|
127
|
+
}
|
|
128
|
+
if (totalIndex === -1 && usdIndex === -1) {
|
|
129
|
+
throw new PlotArgError(`no usd_total column in ${options.path}; write it with history --csv, long or --wide, from a watch --usd run.`);
|
|
130
|
+
}
|
|
131
|
+
const cell = (row, index) => index === -1 ? "" : (row[index] ?? "").trim();
|
|
132
|
+
const partials = new Map();
|
|
133
|
+
for (const row of rows.slice(1)) {
|
|
134
|
+
const atText = cell(row, atIndex);
|
|
135
|
+
if (atText === "")
|
|
136
|
+
continue;
|
|
137
|
+
const at = Date.parse(atText);
|
|
138
|
+
if (!Number.isFinite(at))
|
|
139
|
+
continue;
|
|
140
|
+
const session = cell(row, sessionIndex);
|
|
141
|
+
const key = `${session}\u0000${at}`;
|
|
142
|
+
let partial = partials.get(key);
|
|
143
|
+
if (partial === undefined) {
|
|
144
|
+
partial = { session, at, total: null, usdSum: 0, hasUsd: false };
|
|
145
|
+
partials.set(key, partial);
|
|
146
|
+
}
|
|
147
|
+
const totalText = cell(row, totalIndex);
|
|
148
|
+
if (totalText !== "" && Number.isFinite(Number(totalText))) {
|
|
149
|
+
partial.total = Number(totalText);
|
|
150
|
+
}
|
|
151
|
+
const usdText = cell(row, usdIndex);
|
|
152
|
+
if (usdText !== "" && Number.isFinite(Number(usdText))) {
|
|
153
|
+
partial.usdSum += Number(usdText);
|
|
154
|
+
partial.hasUsd = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const samples = [];
|
|
158
|
+
let skippedSamples = 0;
|
|
159
|
+
for (const partial of partials.values()) {
|
|
160
|
+
if (partial.total !== null) {
|
|
161
|
+
samples.push({ session: partial.session, at: partial.at, total: partial.total });
|
|
162
|
+
}
|
|
163
|
+
else if (partial.hasUsd) {
|
|
164
|
+
samples.push({
|
|
165
|
+
session: partial.session,
|
|
166
|
+
at: partial.at,
|
|
167
|
+
total: Math.round(partial.usdSum * 100) / 100,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
skippedSamples += 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
samples.sort((a, b) => a.at - b.at || a.session.localeCompare(b.session));
|
|
175
|
+
const sessions = [];
|
|
176
|
+
for (const sample of samples) {
|
|
177
|
+
if (!sessions.includes(sample.session))
|
|
178
|
+
sessions.push(sample.session);
|
|
179
|
+
}
|
|
180
|
+
const totals = samples.map((sample) => sample.total);
|
|
181
|
+
const bounds = totals.length > 0 ? extent(totals) : null;
|
|
182
|
+
const first = samples[0]?.total ?? null;
|
|
183
|
+
const last = samples[samples.length - 1]?.total ?? null;
|
|
184
|
+
const change = first !== null && last !== null ? last - first : null;
|
|
185
|
+
const changePercent = change !== null && first !== null && first !== 0
|
|
186
|
+
? (change / Math.abs(first)) * 100
|
|
187
|
+
: null;
|
|
188
|
+
return {
|
|
189
|
+
path: options.path,
|
|
190
|
+
width,
|
|
191
|
+
samples,
|
|
192
|
+
sessions,
|
|
193
|
+
skippedSamples,
|
|
194
|
+
firstAt: samples[0]?.at ?? null,
|
|
195
|
+
lastAt: samples[samples.length - 1]?.at ?? null,
|
|
196
|
+
first,
|
|
197
|
+
last,
|
|
198
|
+
min: bounds?.min ?? null,
|
|
199
|
+
max: bounds?.max ?? null,
|
|
200
|
+
change,
|
|
201
|
+
changePercent,
|
|
202
|
+
chart: sparkline(totals, width),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function extent(values) {
|
|
206
|
+
let min = values[0] ?? 0;
|
|
207
|
+
let max = min;
|
|
208
|
+
for (const value of values) {
|
|
209
|
+
if (value < min)
|
|
210
|
+
min = value;
|
|
211
|
+
if (value > max)
|
|
212
|
+
max = value;
|
|
213
|
+
}
|
|
214
|
+
return { min, max };
|
|
215
|
+
}
|
|
216
|
+
const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
217
|
+
const FLAT_CHAR = SPARK_CHARS[3] ?? "▄";
|
|
218
|
+
/** Average a longer series into `width` buckets so the chart fits one line. */
|
|
219
|
+
function resample(values, width) {
|
|
220
|
+
if (values.length <= width)
|
|
221
|
+
return values;
|
|
222
|
+
const buckets = [];
|
|
223
|
+
for (let index = 0; index < width; index += 1) {
|
|
224
|
+
const start = Math.floor((index * values.length) / width);
|
|
225
|
+
const end = Math.max(start + 1, Math.floor(((index + 1) * values.length) / width));
|
|
226
|
+
let sum = 0;
|
|
227
|
+
for (let cursor = start; cursor < end; cursor += 1) {
|
|
228
|
+
sum += values[cursor] ?? 0;
|
|
229
|
+
}
|
|
230
|
+
buckets.push(sum / (end - start));
|
|
231
|
+
}
|
|
232
|
+
return buckets;
|
|
233
|
+
}
|
|
234
|
+
/** One line of block characters: the smallest value is lowest, the largest is full. */
|
|
235
|
+
export function sparkline(values, width = DEFAULT_PLOT_WIDTH) {
|
|
236
|
+
if (values.length === 0)
|
|
237
|
+
return "";
|
|
238
|
+
const points = resample(values, Math.max(1, width));
|
|
239
|
+
const { min, max } = extent(values);
|
|
240
|
+
if (max === min)
|
|
241
|
+
return FLAT_CHAR.repeat(points.length);
|
|
242
|
+
return points
|
|
243
|
+
.map((value) => {
|
|
244
|
+
const position = Math.round(((value - min) / (max - min)) * (SPARK_CHARS.length - 1));
|
|
245
|
+
const index = Math.max(0, Math.min(SPARK_CHARS.length - 1, position));
|
|
246
|
+
return SPARK_CHARS[index] ?? FLAT_CHAR;
|
|
247
|
+
})
|
|
248
|
+
.join("");
|
|
249
|
+
}
|
|
250
|
+
function formatUsd(value) {
|
|
251
|
+
return `$${value.toLocaleString("en-US", {
|
|
252
|
+
minimumFractionDigits: 2,
|
|
253
|
+
maximumFractionDigits: 2,
|
|
254
|
+
})}`;
|
|
255
|
+
}
|
|
256
|
+
function signedUsd(value) {
|
|
257
|
+
return `${value >= 0 ? "+" : "-"}${formatUsd(Math.abs(value))}`;
|
|
258
|
+
}
|
|
259
|
+
function clockTime(at) {
|
|
260
|
+
return new Date(at).toISOString().slice(11, 19);
|
|
261
|
+
}
|
|
262
|
+
const LABEL_WIDTH = 12;
|
|
263
|
+
function line(label, value) {
|
|
264
|
+
return ` ${label.padEnd(LABEL_WIDTH)}${value}`;
|
|
265
|
+
}
|
|
266
|
+
export function formatPlot(report) {
|
|
267
|
+
const lines = ["lwc — portfolio USD total", "", line("csv", report.path)];
|
|
268
|
+
if (report.samples.length === 0 ||
|
|
269
|
+
report.first === null ||
|
|
270
|
+
report.last === null ||
|
|
271
|
+
report.min === null ||
|
|
272
|
+
report.max === null ||
|
|
273
|
+
report.firstAt === null ||
|
|
274
|
+
report.lastAt === null) {
|
|
275
|
+
lines.push(line("samples", "0"));
|
|
276
|
+
if (report.skippedSamples > 0) {
|
|
277
|
+
lines.push(line("unpriced", `${report.skippedSamples} sample(s) skipped`));
|
|
278
|
+
}
|
|
279
|
+
lines.push("", " no priced portfolio samples here (run watch with --usd)");
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
282
|
+
const percent = report.changePercent === null
|
|
283
|
+
? "n/a"
|
|
284
|
+
: `${report.changePercent >= 0 ? "+" : ""}${report.changePercent.toFixed(2)}%`;
|
|
285
|
+
const change = report.change === null ? "n/a" : signedUsd(report.change);
|
|
286
|
+
lines.push(line("samples", `${report.samples.length}${report.sessions.length > 1 ? ` (${report.sessions.length} sessions)` : ""}`), line("window", `${new Date(report.firstAt).toISOString()} → ${new Date(report.lastAt).toISOString()}`), line("total", `${formatUsd(report.first)} → ${formatUsd(report.last)} (${change}, ${percent})`), line("range", `${formatUsd(report.min)} – ${formatUsd(report.max)}`));
|
|
287
|
+
if (report.skippedSamples > 0) {
|
|
288
|
+
lines.push(line("unpriced", `${report.skippedSamples} sample(s) skipped`));
|
|
289
|
+
}
|
|
290
|
+
const maxLabel = formatUsd(report.max);
|
|
291
|
+
const minLabel = formatUsd(report.min);
|
|
292
|
+
const labelWidth = Math.max(maxLabel.length, minLabel.length);
|
|
293
|
+
lines.push("", ` ${maxLabel.padStart(labelWidth)} ┤${report.chart}`, ` ${minLabel.padStart(labelWidth)} ┤`, ` ${" ".repeat(labelWidth)} └─ ${clockTime(report.firstAt)} → ${clockTime(report.lastAt)} UTC`);
|
|
294
|
+
return lines.join("\n");
|
|
295
|
+
}
|
|
296
|
+
export function toPlotJson(report) {
|
|
297
|
+
return {
|
|
298
|
+
ok: true,
|
|
299
|
+
csv: report.path,
|
|
300
|
+
width: report.width,
|
|
301
|
+
sessions: report.sessions,
|
|
302
|
+
samples: report.samples.length,
|
|
303
|
+
skippedSamples: report.skippedSamples,
|
|
304
|
+
first: report.first,
|
|
305
|
+
last: report.last,
|
|
306
|
+
change: report.change,
|
|
307
|
+
changePercent: report.changePercent,
|
|
308
|
+
min: report.min,
|
|
309
|
+
max: report.max,
|
|
310
|
+
chart: report.chart,
|
|
311
|
+
series: report.samples.map((sample) => ({
|
|
312
|
+
session: sample.session,
|
|
313
|
+
at: new Date(sample.at).toISOString(),
|
|
314
|
+
usdTotal: sample.total,
|
|
315
|
+
})),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { toPresetsJson, type PresetsReport } from "./presets.ts";
|
|
2
|
+
type PresetsJson = ReturnType<typeof toPresetsJson>;
|
|
3
|
+
type PresetEndpoint = PresetsJson["endpoints"][number];
|
|
4
|
+
/** One probe run, appended to a JSONL file. */
|
|
5
|
+
export interface PresetHistoryRecord extends PresetsJson {
|
|
6
|
+
/** When the probe ran, ISO 8601. */
|
|
7
|
+
at: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class PresetsHistoryArgError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/** The window `--since`, `--until` and `--limit` narrow a history file to. */
|
|
13
|
+
export interface PresetsHistorySelection {
|
|
14
|
+
/** Epoch-ms cutoff: keep runs recorded at or after this time. */
|
|
15
|
+
since?: number | null;
|
|
16
|
+
/** Epoch-ms bound: keep runs recorded at or before this time. */
|
|
17
|
+
until?: number | null;
|
|
18
|
+
/** Keep only the newest N runs left inside the window. */
|
|
19
|
+
limit?: number | null;
|
|
20
|
+
}
|
|
21
|
+
/** What the file the runs came from says about itself. */
|
|
22
|
+
export interface PresetsHistoryContext {
|
|
23
|
+
/** Retention declared by the file's header, or null when it declares none. */
|
|
24
|
+
retainMs?: number | null;
|
|
25
|
+
}
|
|
26
|
+
export interface PresetHistoryRow {
|
|
27
|
+
preset: string;
|
|
28
|
+
chainId: number;
|
|
29
|
+
nativeSymbol: string;
|
|
30
|
+
runs: number;
|
|
31
|
+
healthy: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
/** Runs where the backup answered for this chain. */
|
|
34
|
+
failovers: number;
|
|
35
|
+
uptimePct: number;
|
|
36
|
+
p50LatencyMs: number | null;
|
|
37
|
+
maxLatencyMs: number | null;
|
|
38
|
+
lastAt: string | null;
|
|
39
|
+
lastFailureAt: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface PresetsHistorySummary {
|
|
42
|
+
/** Runs inside the window. */
|
|
43
|
+
runs: number;
|
|
44
|
+
/** Runs in the file before the window was applied. */
|
|
45
|
+
totalRuns: number;
|
|
46
|
+
from: string | null;
|
|
47
|
+
to: string | null;
|
|
48
|
+
/** The applied --since cutoff in epoch ms, or null. */
|
|
49
|
+
since: number | null;
|
|
50
|
+
/** The applied --until bound in epoch ms, or null. */
|
|
51
|
+
until: number | null;
|
|
52
|
+
/** The applied --limit, or null. */
|
|
53
|
+
limit: number | null;
|
|
54
|
+
healthyRuns: number;
|
|
55
|
+
failedRuns: number;
|
|
56
|
+
failoverRuns: number;
|
|
57
|
+
/** Oldest run in the file, before the window was applied. */
|
|
58
|
+
availableFrom: string | null;
|
|
59
|
+
/** Newest run in the file, before the window was applied. */
|
|
60
|
+
availableTo: string | null;
|
|
61
|
+
/** Retention the source file declares, or null when it declares none. */
|
|
62
|
+
retainMs: number | null;
|
|
63
|
+
presets: PresetHistoryRow[];
|
|
64
|
+
}
|
|
65
|
+
/** The earlier window `--compare <from>..<to>` reads as the "before" side. */
|
|
66
|
+
export interface PresetsHistoryWindow {
|
|
67
|
+
/** Epoch-ms start, inclusive, reusing the --since syntax. */
|
|
68
|
+
since: number;
|
|
69
|
+
/** Epoch-ms end, inclusive, reusing the --until syntax. */
|
|
70
|
+
until: number;
|
|
71
|
+
}
|
|
72
|
+
/** One preset's uptime on each side of a comparison. */
|
|
73
|
+
export interface PresetUptimeChange {
|
|
74
|
+
preset: string;
|
|
75
|
+
chainId: number;
|
|
76
|
+
nativeSymbol: string;
|
|
77
|
+
/** This preset in the earlier window, or null when it was not probed then. */
|
|
78
|
+
before: PresetHistoryRow | null;
|
|
79
|
+
/** This preset in the current window, or null when it was not probed now. */
|
|
80
|
+
after: PresetHistoryRow | null;
|
|
81
|
+
/** Percentage-point change (after − before), or null when a side is missing. */
|
|
82
|
+
deltaPct: number | null;
|
|
83
|
+
}
|
|
84
|
+
export interface PresetsHistoryComparison {
|
|
85
|
+
/** The earlier window, with the same shape as any other summary. */
|
|
86
|
+
before: PresetsHistorySummary;
|
|
87
|
+
/** The current window (`--since`/`--until`, no `--limit`). */
|
|
88
|
+
after: PresetsHistorySummary;
|
|
89
|
+
/** One row per preset seen on either side, worst regression first. */
|
|
90
|
+
presets: PresetUptimeChange[];
|
|
91
|
+
/** The same comparison pooled per provider host, worst regression first. */
|
|
92
|
+
providers: ProviderUptimeChange[];
|
|
93
|
+
}
|
|
94
|
+
/** One RPC provider across every preset that probes it in a window. */
|
|
95
|
+
export interface ProviderHistoryRow {
|
|
96
|
+
/** Host of the primary endpoint: the provider being judged. */
|
|
97
|
+
host: string;
|
|
98
|
+
/** Preset probes attributed to this provider. */
|
|
99
|
+
probes: number;
|
|
100
|
+
/** Probes this provider's own endpoint answered. */
|
|
101
|
+
healthy: number;
|
|
102
|
+
/** Probes where it failed and a backup answered instead. */
|
|
103
|
+
failovers: number;
|
|
104
|
+
/** Probes where neither endpoint answered. */
|
|
105
|
+
failed: number;
|
|
106
|
+
/** Presets this provider serves in the window, sorted. */
|
|
107
|
+
presets: string[];
|
|
108
|
+
/** Share of probes the provider answered itself; a failover does not count. */
|
|
109
|
+
uptimePct: number;
|
|
110
|
+
}
|
|
111
|
+
/** One provider's uptime on each side of a comparison. */
|
|
112
|
+
export interface ProviderUptimeChange {
|
|
113
|
+
/** Host of the primary endpoint, which identifies the provider. */
|
|
114
|
+
host: string;
|
|
115
|
+
/** This provider in the earlier window, or null when it was not probed then. */
|
|
116
|
+
before: ProviderHistoryRow | null;
|
|
117
|
+
/** This provider in the current window, or null when it is gone now. */
|
|
118
|
+
after: ProviderHistoryRow | null;
|
|
119
|
+
/** Percentage-point change (after − before), or null when a side is missing. */
|
|
120
|
+
deltaPct: number | null;
|
|
121
|
+
}
|
|
122
|
+
/** Append this run's report, creating the file and its directory if needed. */
|
|
123
|
+
export declare function appendPresetHistory(path: string, report: PresetsReport, now?: Date): PresetHistoryRecord;
|
|
124
|
+
/** A history file's runs plus the retention it was written with. */
|
|
125
|
+
export interface PresetHistoryFile {
|
|
126
|
+
records: PresetHistoryRecord[];
|
|
127
|
+
/** Retention span declared in the file's header, or null when it has none. */
|
|
128
|
+
retainMs: number | null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read a history file, oldest first. A truncated or hand-edited line is skipped
|
|
132
|
+
* rather than losing every run that came before it. The first line may be a
|
|
133
|
+
* `lwc_history` header recording the retention the file was trimmed to, so a
|
|
134
|
+
* read that happens later can still explain which runs are simply gone.
|
|
135
|
+
*/
|
|
136
|
+
export declare function readPresetHistoryFile(path: string): PresetHistoryFile;
|
|
137
|
+
/** The runs in a history file, oldest first; its retention header is ignored. */
|
|
138
|
+
export declare function readPresetHistory(path: string): PresetHistoryRecord[];
|
|
139
|
+
/** The union of several history files, with what the merge had to drop. */
|
|
140
|
+
export interface PresetHistoryMerge {
|
|
141
|
+
/** Every run from every input, oldest first, duplicates removed. */
|
|
142
|
+
records: PresetHistoryRecord[];
|
|
143
|
+
/** Runs that appeared in more than one input. */
|
|
144
|
+
duplicates: number;
|
|
145
|
+
/** Runs each input contributed, in the order they were passed. */
|
|
146
|
+
counts: number[];
|
|
147
|
+
/** Runs dropped by the retention window. */
|
|
148
|
+
pruned: number;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Combine history files into one. The cache and the release asset each keep
|
|
152
|
+
* runs the other may have lost, so restore their union instead of whichever
|
|
153
|
+
* file happens to be longer. Identical runs are kept once, oldest first.
|
|
154
|
+
*/
|
|
155
|
+
/**
|
|
156
|
+
* Warns when a window being read starts before the runs retention kept.
|
|
157
|
+
*
|
|
158
|
+
* A trimmed history looks exactly like a provider that went quiet: the earlier
|
|
159
|
+
* side of a comparison has no runs for it, so its uptime is missing and no
|
|
160
|
+
* regression is reported. Naming the cutoff keeps that silence from being read
|
|
161
|
+
* as an outage.
|
|
162
|
+
*/
|
|
163
|
+
export declare function retentionWindowWarnings(newestAt: string | null, retainMs: number | null, windows: Array<{
|
|
164
|
+
label: string;
|
|
165
|
+
since: number | null;
|
|
166
|
+
}>): string[];
|
|
167
|
+
export declare function mergePresetHistory(inputs: PresetHistoryRecord[][], options?: {
|
|
168
|
+
retainMs?: number | null;
|
|
169
|
+
}): PresetHistoryMerge;
|
|
170
|
+
/**
|
|
171
|
+
* The tightest retention declared by the files being combined. A merge cannot
|
|
172
|
+
* hold runs that one input already pruned, so the union is bounded by the
|
|
173
|
+
* narrowest window rather than the widest.
|
|
174
|
+
*/
|
|
175
|
+
export declare function narrowestRetentionMs(values: Array<number | null>): number | null;
|
|
176
|
+
/**
|
|
177
|
+
* Write a whole history file, replacing it, and create its directory. A
|
|
178
|
+
* retention window is recorded as the header readPresetHistoryFile returns, so
|
|
179
|
+
* the file itself says what it does not keep.
|
|
180
|
+
*/
|
|
181
|
+
export declare function savePresetHistory(path: string, records: PresetHistoryRecord[], options?: {
|
|
182
|
+
retainMs?: number | null;
|
|
183
|
+
}): void;
|
|
184
|
+
/**
|
|
185
|
+
* Apply --since/--until/--limit to a history, oldest first. A limit is only
|
|
186
|
+
* ever used to look at recent runs, so it keeps the tail.
|
|
187
|
+
*/
|
|
188
|
+
export declare function selectPresetHistory(records: PresetHistoryRecord[], selection?: PresetsHistorySelection): PresetHistoryRecord[];
|
|
189
|
+
/**
|
|
190
|
+
* Attribute each probe to the host of the endpoint the preset is configured
|
|
191
|
+
* with, and pool those by provider: one host that serves several chains is
|
|
192
|
+
* judged once. A failover counts against the provider, because the backup
|
|
193
|
+
* answering is the same fact as the provider under test not answering.
|
|
194
|
+
*/
|
|
195
|
+
export declare function summarizeProviderHistory(records: PresetHistoryRecord[], selection?: PresetsHistorySelection): ProviderHistoryRow[];
|
|
196
|
+
export declare function summarizePresetHistory(records: PresetHistoryRecord[], selection?: PresetsHistorySelection, context?: PresetsHistoryContext): PresetsHistorySummary;
|
|
197
|
+
/** A preset whose uptime in the window sits below the --fail-under threshold. */
|
|
198
|
+
export interface UptimeShortfall {
|
|
199
|
+
preset: string;
|
|
200
|
+
chainId: number;
|
|
201
|
+
uptimePct: number;
|
|
202
|
+
runs: number;
|
|
203
|
+
failed: number;
|
|
204
|
+
}
|
|
205
|
+
/** Presets below the threshold, worst first; empty when every preset clears it. */
|
|
206
|
+
export declare function presetsBelowUptime(summary: PresetsHistorySummary, threshold: number): UptimeShortfall[];
|
|
207
|
+
export interface PresetsGateReport {
|
|
208
|
+
/** False when any preset is below the threshold or the window is empty. */
|
|
209
|
+
passed: boolean;
|
|
210
|
+
threshold: number;
|
|
211
|
+
shortfalls: UptimeShortfall[];
|
|
212
|
+
/** The outcome as markdown for a CI run summary. */
|
|
213
|
+
markdown: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Judge a window against a minimum uptime for CI: markdown for the run summary
|
|
217
|
+
* plus the verdict the caller exits on. An empty window fails, because there is
|
|
218
|
+
* no run left that could clear the threshold.
|
|
219
|
+
*/
|
|
220
|
+
export declare function summarizePresetGate(summary: PresetsHistorySummary, threshold: number): PresetsGateReport;
|
|
221
|
+
export declare function formatPresetHistory(summary: PresetsHistorySummary): string;
|
|
222
|
+
/** One local pass over a history file: the gate plus what the file can cover. */
|
|
223
|
+
export interface PresetsHistoryCheck {
|
|
224
|
+
/** The history file the report was made from. */
|
|
225
|
+
path: string;
|
|
226
|
+
/** Runs the gate judged. */
|
|
227
|
+
runs: number;
|
|
228
|
+
/** Presets judged in the window. */
|
|
229
|
+
presets: number;
|
|
230
|
+
/** The whole file's span and recorded count, before the window was applied. */
|
|
231
|
+
coverage: {
|
|
232
|
+
from: string | null;
|
|
233
|
+
to: string | null;
|
|
234
|
+
recorded: number;
|
|
235
|
+
};
|
|
236
|
+
/** The retention the file declares, or null when it declares none. */
|
|
237
|
+
retention: {
|
|
238
|
+
ms: number;
|
|
239
|
+
label: string;
|
|
240
|
+
cutoff: string;
|
|
241
|
+
} | null;
|
|
242
|
+
/** The freshness bound the gate applied, or null when it does not check age. */
|
|
243
|
+
maxAge: {
|
|
244
|
+
ms: number;
|
|
245
|
+
label: string;
|
|
246
|
+
} | null;
|
|
247
|
+
/** The newest run's age when it exceeds maxAge, or null when the file is fresh. */
|
|
248
|
+
stale: {
|
|
249
|
+
newest: string;
|
|
250
|
+
ageMs: number;
|
|
251
|
+
age: string;
|
|
252
|
+
} | null;
|
|
253
|
+
/** The filters the gate window applied. */
|
|
254
|
+
window: {
|
|
255
|
+
since: number | null;
|
|
256
|
+
until: number | null;
|
|
257
|
+
limit: number | null;
|
|
258
|
+
};
|
|
259
|
+
gate: {
|
|
260
|
+
threshold: number;
|
|
261
|
+
passed: boolean;
|
|
262
|
+
shortfalls: UptimeShortfall[];
|
|
263
|
+
};
|
|
264
|
+
/** Why the window may be missing runs retention pruned, or null. */
|
|
265
|
+
note: string | null;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Build the local check: the gate over the selected window plus the file's
|
|
269
|
+
* coverage and declared retention, so one command answers "did it pass, and
|
|
270
|
+
* what was it able to see?" without a CI run to assemble the pieces.
|
|
271
|
+
*/
|
|
272
|
+
export declare function presetHistoryCheck(path: string, summary: PresetsHistorySummary, threshold: number, options?: {
|
|
273
|
+
maxAgeMs?: number | null;
|
|
274
|
+
now?: number;
|
|
275
|
+
}): PresetsHistoryCheck;
|
|
276
|
+
/**
|
|
277
|
+
* One line per reason the check failed, in the order the report reads: a stale
|
|
278
|
+
* file first, then an empty window, then each preset below the threshold. The
|
|
279
|
+
* pre-commit hook prints these and CI annotates them, so both say the same
|
|
280
|
+
* thing without copying the wording.
|
|
281
|
+
*/
|
|
282
|
+
export declare function presetHistoryFailureReasons(check: PresetsHistoryCheck): string[];
|
|
283
|
+
/**
|
|
284
|
+
* The newest runs with their age, as a markdown table for the CI summary. A
|
|
285
|
+
* verdict line cannot show that every row is old; a table can. The gate uses it
|
|
286
|
+
* only for a stale file, but the table reports either verdict so the wording
|
|
287
|
+
* lives in one tested place.
|
|
288
|
+
*/
|
|
289
|
+
export declare function presetFreshnessMarkdown(check: PresetsHistoryCheck, records: readonly PresetHistoryRecord[], options?: {
|
|
290
|
+
now?: number;
|
|
291
|
+
limit?: number;
|
|
292
|
+
}): string | null;
|
|
293
|
+
/**
|
|
294
|
+
* Render the local check as one aligned pass, ending in the verdict. Passing the
|
|
295
|
+
* records replaces the single stale line with the newest runs and their age, so
|
|
296
|
+
* a stopped schedule is visible row by row (five by default, or `rows`); without
|
|
297
|
+
* them the summary line stays.
|
|
298
|
+
*/
|
|
299
|
+
export declare function formatPresetHistoryCheck(check: PresetsHistoryCheck, records?: readonly PresetHistoryRecord[], options?: {
|
|
300
|
+
rows?: number;
|
|
301
|
+
}): string;
|
|
302
|
+
/**
|
|
303
|
+
* Judge a provider change by reading the same presets in two windows: the
|
|
304
|
+
* current one from the usual filters, and an explicit earlier one. Both sides
|
|
305
|
+
* are ordinary summaries, so a delta cannot disagree with either table.
|
|
306
|
+
*/
|
|
307
|
+
export declare function comparePresetHistory(records: PresetHistoryRecord[], selection: PresetsHistorySelection, compare: PresetsHistoryWindow, context?: PresetsHistoryContext): PresetsHistoryComparison;
|
|
308
|
+
export declare function formatPresetHistoryComparison(comparison: PresetsHistoryComparison): string;
|
|
309
|
+
/**
|
|
310
|
+
* Markdown for a CI run summary: the current window beside an earlier one, so
|
|
311
|
+
* a scheduled run reports the week-over-week delta without anyone asking.
|
|
312
|
+
*/
|
|
313
|
+
export declare function presetComparisonMarkdown(comparison: PresetsHistoryComparison, heading?: string): string;
|
|
314
|
+
/**
|
|
315
|
+
* One line per preset whose uptime fell, worst first, for a CI annotation. A
|
|
316
|
+
* provider change that looks fine in this week's numbers still shows up here if
|
|
317
|
+
* it made any preset worse than the week it replaced.
|
|
318
|
+
*/
|
|
319
|
+
export declare function presetRegressionWarnings(comparison: PresetsHistoryComparison): string[];
|
|
320
|
+
/**
|
|
321
|
+
* One line per provider host whose uptime fell, worst first. The same week can
|
|
322
|
+
* regress several presets for one reason; this names the upstream once, so the
|
|
323
|
+
* run log says which provider got worse rather than only which chains did.
|
|
324
|
+
*/
|
|
325
|
+
export declare function providerRegressionWarnings(comparison: PresetsHistoryComparison): string[];
|
|
326
|
+
export declare function toPresetHistoryJson(summary: PresetsHistorySummary): PresetsHistorySummary;
|
|
327
|
+
/** One CSV row per preset in the window, for charting uptime and latency. */
|
|
328
|
+
export declare function presetHistoryCsv(summary: PresetsHistorySummary): string;
|
|
329
|
+
/** Write the CSV to a file, creating its directory. */
|
|
330
|
+
export declare function savePresetHistoryCsv(path: string, csv: string): void;
|
|
331
|
+
/**
|
|
332
|
+
* One CSV row per preset per run, oldest first, for charting health over time
|
|
333
|
+
* rather than summarizing it: a failure keeps its latency and via cells blank.
|
|
334
|
+
*/
|
|
335
|
+
export declare function presetHistoryTimelineCsv(records: PresetHistoryRecord[], selection?: PresetsHistorySelection): string;
|
|
336
|
+
export interface PresetsHistoryArgs {
|
|
337
|
+
path: string;
|
|
338
|
+
/** Epoch-ms cutoff from --since, or null for the whole file. */
|
|
339
|
+
since: number | null;
|
|
340
|
+
/** Epoch-ms bound from --until, or null to keep everything after --since. */
|
|
341
|
+
until: number | null;
|
|
342
|
+
/** Newest N runs to keep, or null for all of them. */
|
|
343
|
+
limit: number | null;
|
|
344
|
+
/** Exit non-zero below this uptime percentage, or null to only report. */
|
|
345
|
+
failUnder: number | null;
|
|
346
|
+
/** Where to write the per-preset CSV, "-" for stdout, or null for the table. */
|
|
347
|
+
csvPath: string | null;
|
|
348
|
+
/** Where to write the per-run timeline CSV, "-" for stdout, or null. */
|
|
349
|
+
timelinePath: string | null;
|
|
350
|
+
/** The earlier window from --compare, or null to report one window. */
|
|
351
|
+
compare: PresetsHistoryWindow | null;
|
|
352
|
+
/** Extra history files from --merge, folded into `path` before anything reads it. */
|
|
353
|
+
merge: string[];
|
|
354
|
+
/** Where --out writes the merged history, or null to only read it. */
|
|
355
|
+
out: string | null;
|
|
356
|
+
/** Epoch-ms retention window for the merge from --retain, or null to keep everything. */
|
|
357
|
+
retain: number | null;
|
|
358
|
+
/** Epoch-ms freshness bound from --max-age, or null to judge the gate on uptime alone. */
|
|
359
|
+
maxAge: number | null;
|
|
360
|
+
/** Rows the stale report prints from --freshness-rows, or null for the default. */
|
|
361
|
+
freshnessRows: number | null;
|
|
362
|
+
/** Report the file's coverage and retention beside the gate in one local pass. */
|
|
363
|
+
check: boolean;
|
|
364
|
+
}
|
|
365
|
+
export declare function parsePresetHistoryArgs(argv: string[], now?: number): PresetsHistoryArgs;
|
|
366
|
+
/** A retention window as a positive span (`180d`), for --retain and the CI merge. */
|
|
367
|
+
export declare function parseRetentionMs(raw: string): number;
|
|
368
|
+
/** A freshness row count as a positive whole number (`5`), for --freshness-rows. */
|
|
369
|
+
export declare function parseFreshnessRows(raw: string): number;
|
|
370
|
+
/** A freshness bound as a positive span (`7d`), for --max-age. */
|
|
371
|
+
export declare function parseMaxAgeMs(raw: string): number;
|
|
372
|
+
export type { PresetsReport, PresetEndpoint };
|