playlist-data-engine 1.5.8 → 1.6.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/README.md +11 -6
- package/dist/__vite-browser-external-BcPniuRQ.cjs +1 -0
- package/dist/analysis.d.ts +27 -0
- package/dist/analysis.d.ts.map +1 -0
- package/dist/analysis.js +2 -0
- package/dist/analysis.mjs +7518 -0
- package/dist/arweaveGatewayManager-BKUZ1Klm.js +1415 -0
- package/dist/arweaveGatewayManager-CeW9SP2P.cjs +1 -0
- package/dist/core/analysis/MusicClassifier.d.ts +14 -1
- package/dist/core/analysis/MusicClassifier.d.ts.map +1 -1
- package/dist/essentia-wasm.es-7mXF1zkQ.cjs +38 -0
- package/dist/essentia.js-model.es-h47asA0k.cjs +1 -0
- package/dist/gateway.d.ts +17 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +1 -0
- package/dist/gateway.mjs +317 -0
- package/dist/index.browser-g8_n-BSM.cjs +13 -0
- package/dist/index.d.ts +0 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/playlist-data-engine.js +65 -118
- package/dist/playlist-data-engine.mjs +10558 -29859
- package/dist/unifyBeatMap-BA5EG991.js +10234 -0
- package/dist/unifyBeatMap-sIbIw5OX.cjs +4 -0
- package/package.json +12 -1
|
@@ -0,0 +1,1415 @@
|
|
|
1
|
+
var N = /* @__PURE__ */ ((f) => (f[f.DEBUG = 0] = "DEBUG", f[f.INFO = 1] = "INFO", f[f.WARN = 2] = "WARN", f[f.ERROR = 3] = "ERROR", f[f.NONE = 4] = "NONE", f))(N || {});
|
|
2
|
+
const I = {
|
|
3
|
+
0: "DEBUG",
|
|
4
|
+
1: "INFO",
|
|
5
|
+
2: "WARN",
|
|
6
|
+
3: "ERROR",
|
|
7
|
+
4: "NONE"
|
|
8
|
+
};
|
|
9
|
+
let G = 1, U = !0, _ = !0, C = null, E = !1, S = !1;
|
|
10
|
+
class D {
|
|
11
|
+
/**
|
|
12
|
+
* Create a logger instance with a specific context (usually the class/module name)
|
|
13
|
+
* @param context - The context name to prefix log messages (e.g., 'WeatherAPIClient')
|
|
14
|
+
*/
|
|
15
|
+
constructor(e) {
|
|
16
|
+
this.context = e;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a named logger for a specific class or module
|
|
20
|
+
* @param context - The context name (e.g., 'WeatherAPIClient', 'EnvironmentalSensors')
|
|
21
|
+
* @returns A Logger instance configured with the given context
|
|
22
|
+
*/
|
|
23
|
+
static for(e) {
|
|
24
|
+
return new D(e);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Enable diagnostic mode
|
|
28
|
+
* Sets log level to DEBUG for maximum verbosity
|
|
29
|
+
*/
|
|
30
|
+
static enableDiagnosticMode() {
|
|
31
|
+
E = !0, G = 0;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Disable diagnostic mode
|
|
35
|
+
* Resets log level to INFO
|
|
36
|
+
*/
|
|
37
|
+
static disableDiagnosticMode() {
|
|
38
|
+
E = !1, G = 1;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if diagnostic mode is enabled
|
|
42
|
+
* @returns True if diagnostic mode is active
|
|
43
|
+
*/
|
|
44
|
+
static isDiagnosticMode() {
|
|
45
|
+
return E;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Enable verbose logging mode
|
|
49
|
+
* Sets log level to DEBUG for increased output verbosity
|
|
50
|
+
* This is a convenience method equivalent to Logger.setLevel(LogLevel.DEBUG)
|
|
51
|
+
* @example
|
|
52
|
+
* Logger.enableVerbose();
|
|
53
|
+
* // All debug messages will now be logged
|
|
54
|
+
*/
|
|
55
|
+
static enableVerbose() {
|
|
56
|
+
S = !0, G = 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Disable verbose logging mode
|
|
60
|
+
* Resets log level to INFO (default)
|
|
61
|
+
* @example
|
|
62
|
+
* Logger.disableVerbose();
|
|
63
|
+
* // Only INFO and above will be logged
|
|
64
|
+
*/
|
|
65
|
+
static disableVerbose() {
|
|
66
|
+
S = !1, G = 1;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if verbose mode is enabled
|
|
70
|
+
* @returns True if verbose mode is active
|
|
71
|
+
*/
|
|
72
|
+
static isVerbose() {
|
|
73
|
+
return S;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Set verbose mode on or off
|
|
77
|
+
* @param enabled - Whether to enable verbose logging
|
|
78
|
+
* @example
|
|
79
|
+
* Logger.setVerbose(true); // Enable verbose logging
|
|
80
|
+
* Logger.setVerbose(false); // Disable verbose logging
|
|
81
|
+
*/
|
|
82
|
+
static setVerbose(e) {
|
|
83
|
+
S = e, G = e ? 0 : 1;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Set the global log level
|
|
87
|
+
* @param level - The minimum log level to display
|
|
88
|
+
*/
|
|
89
|
+
static setLevel(e) {
|
|
90
|
+
G = e;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the current global log level
|
|
94
|
+
* @returns The current log level
|
|
95
|
+
*/
|
|
96
|
+
static getLevel() {
|
|
97
|
+
return G;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Configure the logger globally
|
|
101
|
+
* @param config - Logger configuration options
|
|
102
|
+
*/
|
|
103
|
+
static configure(e) {
|
|
104
|
+
e.level !== void 0 && (G = e.level), e.includeTimestamp !== void 0 && (U = e.includeTimestamp), e.includeContext !== void 0 && (_ = e.includeContext), e.customHandler !== void 0 && (C = e.customHandler);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reset logger to default configuration
|
|
108
|
+
*/
|
|
109
|
+
static reset() {
|
|
110
|
+
G = 1, U = !0, _ = !0, C = null, E = !1, S = !1;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Log a debug message (most verbose)
|
|
114
|
+
* @param message - The log message
|
|
115
|
+
* @param data - Optional data to include
|
|
116
|
+
*/
|
|
117
|
+
debug(e, t) {
|
|
118
|
+
this.log(0, e, t);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Log an info message (general operational info)
|
|
122
|
+
* @param message - The log message
|
|
123
|
+
* @param data - Optional data to include
|
|
124
|
+
*/
|
|
125
|
+
info(e, t) {
|
|
126
|
+
this.log(1, e, t);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Log a warning message (potential issues)
|
|
130
|
+
* @param message - The log message
|
|
131
|
+
* @param data - Optional data to include
|
|
132
|
+
*/
|
|
133
|
+
warn(e, t) {
|
|
134
|
+
this.log(2, e, t);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Log an error message (errors that need attention)
|
|
138
|
+
* @param message - The log message
|
|
139
|
+
* @param data - Optional error or data to include
|
|
140
|
+
*/
|
|
141
|
+
error(e, t) {
|
|
142
|
+
this.log(3, e, t);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Internal logging method
|
|
146
|
+
*/
|
|
147
|
+
log(e, t, s) {
|
|
148
|
+
if (e < G)
|
|
149
|
+
return;
|
|
150
|
+
const a = {
|
|
151
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
152
|
+
level: e,
|
|
153
|
+
context: this.context,
|
|
154
|
+
message: t,
|
|
155
|
+
data: s
|
|
156
|
+
};
|
|
157
|
+
if (C) {
|
|
158
|
+
C(a);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const r = this.formatMessage(a);
|
|
162
|
+
switch (e) {
|
|
163
|
+
case 0:
|
|
164
|
+
s !== void 0 ? console.debug(r, s) : console.debug(r);
|
|
165
|
+
break;
|
|
166
|
+
case 1:
|
|
167
|
+
s !== void 0 ? console.info(r, s) : console.info(r);
|
|
168
|
+
break;
|
|
169
|
+
case 2:
|
|
170
|
+
s !== void 0 ? console.warn(r, s) : console.warn(r);
|
|
171
|
+
break;
|
|
172
|
+
case 3:
|
|
173
|
+
s !== void 0 ? console.error(r, s) : console.error(r);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Format a log entry into a string
|
|
179
|
+
*/
|
|
180
|
+
formatMessage(e) {
|
|
181
|
+
const t = [];
|
|
182
|
+
return U && t.push(this.formatTimestamp(e.timestamp)), t.push(`[${I[e.level]}]`), _ && e.context && t.push(`[${e.context}]`), t.push(e.message), t.join(" ");
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Format a timestamp for log output
|
|
186
|
+
*/
|
|
187
|
+
formatTimestamp(e) {
|
|
188
|
+
const t = e.getHours().toString().padStart(2, "0"), s = e.getMinutes().toString().padStart(2, "0"), a = e.getSeconds().toString().padStart(2, "0"), r = e.getMilliseconds().toString().padStart(3, "0");
|
|
189
|
+
return `${t}:${s}:${a}.${r}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function V(f) {
|
|
193
|
+
return D.for(f);
|
|
194
|
+
}
|
|
195
|
+
const O = [
|
|
196
|
+
{ host: "arweave.net", protocol: "https", priority: 3 },
|
|
197
|
+
{ host: "ardrive.net", protocol: "https", priority: 2 },
|
|
198
|
+
{ host: "turbo-gateway.com", protocol: "https", priority: 1 }
|
|
199
|
+
], M = [
|
|
200
|
+
"arweave.net",
|
|
201
|
+
"arweave.dev",
|
|
202
|
+
"www.arweave.net",
|
|
203
|
+
"ardrive.net",
|
|
204
|
+
"turbo-gateway.com",
|
|
205
|
+
"irys.xyz",
|
|
206
|
+
"ar-io.dev",
|
|
207
|
+
"ar-io.net",
|
|
208
|
+
"arweave.rocks",
|
|
209
|
+
"g8way.io"
|
|
210
|
+
];
|
|
211
|
+
function A(f) {
|
|
212
|
+
if (!f || typeof f != "string")
|
|
213
|
+
return !1;
|
|
214
|
+
if (f.startsWith("ar://"))
|
|
215
|
+
return !0;
|
|
216
|
+
const e = f.toLowerCase();
|
|
217
|
+
try {
|
|
218
|
+
const t = new URL(f).hostname;
|
|
219
|
+
return M.some(
|
|
220
|
+
(s) => t === s || t.endsWith("." + s)
|
|
221
|
+
);
|
|
222
|
+
} catch {
|
|
223
|
+
return M.some((t) => e.includes(t));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function T(f) {
|
|
227
|
+
if (!f || typeof f != "string" || !A(f))
|
|
228
|
+
return null;
|
|
229
|
+
let e = null, t = "";
|
|
230
|
+
if (f.startsWith("ar://")) {
|
|
231
|
+
const l = f.slice(5), i = l.indexOf("/");
|
|
232
|
+
return i !== -1 ? (e = l.slice(0, i), t = l.slice(i)) : e = l, e.length === 43 && /^[A-Za-z0-9_-]+$/.test(e) ? { txId: e, originalUrl: f, pathSuffix: t } : null;
|
|
233
|
+
}
|
|
234
|
+
let s = f;
|
|
235
|
+
try {
|
|
236
|
+
const l = new URL(f);
|
|
237
|
+
s = l.pathname + l.search;
|
|
238
|
+
} catch {
|
|
239
|
+
}
|
|
240
|
+
const a = /[A-Za-z0-9_-]{43}/g, r = s.match(a);
|
|
241
|
+
if (r && r.length > 0) {
|
|
242
|
+
e = r[0];
|
|
243
|
+
const l = f.indexOf(e);
|
|
244
|
+
if (l !== -1) {
|
|
245
|
+
const i = f.slice(l + 43), c = Math.min(
|
|
246
|
+
i.indexOf("?") === -1 ? 1 / 0 : i.indexOf("?"),
|
|
247
|
+
i.indexOf("#") === -1 ? 1 / 0 : i.indexOf("#")
|
|
248
|
+
);
|
|
249
|
+
c === 1 / 0 ? t = i : t = i.slice(0, c);
|
|
250
|
+
}
|
|
251
|
+
return { txId: e, originalUrl: f, pathSuffix: t };
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
function k(f, e, t = "") {
|
|
256
|
+
return `${e.protocol || "https"}://${e.host}/${f}${t}`;
|
|
257
|
+
}
|
|
258
|
+
function j(f, e = O, t = "") {
|
|
259
|
+
return e.sort((s, a) => s.priority - a.priority).map((s) => k(f, s, t));
|
|
260
|
+
}
|
|
261
|
+
async function $() {
|
|
262
|
+
try {
|
|
263
|
+
return await import("@ar.io/wayfinder-core");
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function x() {
|
|
269
|
+
try {
|
|
270
|
+
return await import("@ar.io/sdk");
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const z = "https://solana-rpc.publicnode.com", B = 5e3, Y = 864e5, H = "arweave_active_gateway", L = "arweave_ranked_gateways";
|
|
276
|
+
class K {
|
|
277
|
+
constructor(e) {
|
|
278
|
+
this.cache = /* @__PURE__ */ new Map(), this.logger = D.for("ArweaveGateway"), this.hitCount = 0, this.missCount = 0, this.healthData = /* @__PURE__ */ new Map(), this.originalPriorities = /* @__PURE__ */ new Map(), this.MAX_HEALTH_RECORDS = 50, this.inflightResolves = /* @__PURE__ */ new Map(), this.inflightControllers = /* @__PURE__ */ new Map(), this.wayfinder = null, this.wayfinderGatewaysProvider = null, this.rankedGatewaysCache = null, this.rankedGatewaysCacheTimestamp = 0, this.RANKED_GATEWAYS_TTL_MS = 1440 * 60 * 1e3, this.activeGateway = null, this.lastFetchTiming = 0, this.consecutiveSlowResponses = 0, this.gateways = (e?.gateways ?? O).map((s) => ({ ...s })), this.timeout = e?.timeout ?? B, this.cacheTTL = e?.cacheTTL ?? Y, this.slowResponseThreshold = e?.slowResponseThreshold ?? 8e3, this.maxSlowResponses = e?.maxSlowResponses ?? 3, this.solanaRpcUrl = e?.solanaRpcUrl ?? z, this.hasCustomSolanaRpc = typeof e?.solanaRpcUrl == "string" && e.solanaRpcUrl.length > 0, this.wayfinderSortBy = e?.wayfinderSortBy ?? "weights.compositeWeight", this.wayfinderPrimaryLimit = e?.wayfinderPrimaryLimit ?? 50, this.wayfinderFallbackLimit = e?.wayfinderFallbackLimit ?? 100, this.wayfinderStrategy = e?.wayfinderStrategy ?? "composite-ping-random", this.gateways.sort((s, a) => s.priority - a.priority), this.gateways.forEach((s) => {
|
|
279
|
+
this.originalPriorities.set(s.host, s.priority);
|
|
280
|
+
}), this.activeGateway = this.loadPersistedGateway();
|
|
281
|
+
const t = this.loadPersistedRankedGateways();
|
|
282
|
+
t && (this.rankedGatewaysCache = t), Promise.all([$(), x(), import("./index.browser-CVv08iVS.js")]).then(([s, a, r]) => {
|
|
283
|
+
if (s && a)
|
|
284
|
+
try {
|
|
285
|
+
const {
|
|
286
|
+
FastestPingRoutingStrategy: l,
|
|
287
|
+
RandomRoutingStrategy: i,
|
|
288
|
+
CompositeRoutingStrategy: c,
|
|
289
|
+
RoundRobinRoutingStrategy: n,
|
|
290
|
+
NetworkGatewaysProvider: y
|
|
291
|
+
} = s, w = a.ARIO, o = r.createSolanaRpc(this.solanaRpcUrl), g = w.init({ rpc: o }), h = this.wrapProviderWithCache(
|
|
292
|
+
new y({
|
|
293
|
+
ario: g,
|
|
294
|
+
sortBy: this.wayfinderSortBy,
|
|
295
|
+
limit: this.wayfinderPrimaryLimit
|
|
296
|
+
})
|
|
297
|
+
), u = this.wrapProviderWithCache(
|
|
298
|
+
new y({
|
|
299
|
+
ario: g,
|
|
300
|
+
sortBy: this.wayfinderSortBy,
|
|
301
|
+
limit: this.wayfinderFallbackLimit
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
this.wayfinderGatewaysProvider = h;
|
|
305
|
+
const d = (() => {
|
|
306
|
+
switch (this.wayfinderStrategy) {
|
|
307
|
+
case "random-only":
|
|
308
|
+
return new i({ gatewaysProvider: u });
|
|
309
|
+
case "ping-only":
|
|
310
|
+
return new l({ timeoutMs: 3e3, gatewaysProvider: h });
|
|
311
|
+
case "round-robin":
|
|
312
|
+
return new n({ gateways: [], gatewaysProvider: h });
|
|
313
|
+
default:
|
|
314
|
+
return new c({
|
|
315
|
+
strategies: [
|
|
316
|
+
new l({ timeoutMs: 3e3, gatewaysProvider: h }),
|
|
317
|
+
new i({ gatewaysProvider: u })
|
|
318
|
+
]
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
})();
|
|
322
|
+
this.wayfinder = s.createWayfinderClient({ routingStrategy: d }), this.logger.info("Wayfinder client initialized", {
|
|
323
|
+
strategy: this.wayfinderStrategy,
|
|
324
|
+
sortBy: this.wayfinderSortBy,
|
|
325
|
+
primaryLimit: this.wayfinderPrimaryLimit,
|
|
326
|
+
fallbackLimit: this.wayfinderFallbackLimit
|
|
327
|
+
});
|
|
328
|
+
} catch (l) {
|
|
329
|
+
this.logger.warn("Failed to configure Wayfinder routing strategy, using defaults", { error: l }), this.wayfinder = s.createWayfinderClient(), this.logger.info("Wayfinder client initialized with default routing");
|
|
330
|
+
}
|
|
331
|
+
else s && (this.wayfinder = s.createWayfinderClient(), this.logger.info("Wayfinder client initialized with default routing (SDK unavailable)"));
|
|
332
|
+
}).catch(() => {
|
|
333
|
+
this.logger.debug("Wayfinder client initialization failed, using static gateways");
|
|
334
|
+
}), this.logger.info("Gateway manager initialized", {
|
|
335
|
+
gateways: this.gateways.map((s) => s.host),
|
|
336
|
+
timeout: this.timeout,
|
|
337
|
+
cacheTTL: this.cacheTTL,
|
|
338
|
+
activeGateway: this.activeGateway?.host ?? null
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Wrap a promise with a timeout to prevent hanging indefinitely.
|
|
343
|
+
* Useful for third-party calls (e.g., Wayfinder) that have no built-in timeout.
|
|
344
|
+
*/
|
|
345
|
+
withTimeout(e, t, s) {
|
|
346
|
+
return new Promise((a, r) => {
|
|
347
|
+
const l = setTimeout(() => r(new Error(s)), t);
|
|
348
|
+
e.then(
|
|
349
|
+
(i) => {
|
|
350
|
+
clearTimeout(l), a(i);
|
|
351
|
+
},
|
|
352
|
+
(i) => {
|
|
353
|
+
clearTimeout(l), r(i);
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Wrap a raw upstream gateways provider (NetworkGatewaysProvider) in a
|
|
360
|
+
* long-TTL cache. This is the single lever that keeps us off the Solana
|
|
361
|
+
* RPC (Helius): every Wayfinder routing strategy and the ranked walk in
|
|
362
|
+
* tryWayfinder() read through this wrapper, so the upstream is queried at
|
|
363
|
+
* most once per RANKED_GATEWAYS_TTL_MS regardless of how many resolves fail.
|
|
364
|
+
*
|
|
365
|
+
* Behaviour:
|
|
366
|
+
* - Fresh cache → return immediately, no network.
|
|
367
|
+
* - Cache miss → one upstream fetch; concurrent callers share the in-flight
|
|
368
|
+
* promise (dedupe) so we never fire two fetches for the same refresh.
|
|
369
|
+
* - Fetch fails → serve the stale in-memory list if present (better than
|
|
370
|
+
* empty); otherwise let the upstream's [] fall through. This keeps a
|
|
371
|
+
* Helius rate-limit blip from restarting the hammering.
|
|
372
|
+
* - On success → write to both in-memory cache and localStorage so the
|
|
373
|
+
* list survives reloads.
|
|
374
|
+
*/
|
|
375
|
+
wrapProviderWithCache(e) {
|
|
376
|
+
let t = null, s = 0, a = null;
|
|
377
|
+
const r = () => t !== null && t.length > 0 && Date.now() < s, i = this.loadPersistedRankedGateways();
|
|
378
|
+
return i && i.length > 0 && (t = i, s = (this.rankedGatewaysCacheTimestamp || Date.now()) + this.RANKED_GATEWAYS_TTL_MS), {
|
|
379
|
+
getGateways: async () => r() ? t : a || (a = (async () => {
|
|
380
|
+
try {
|
|
381
|
+
const c = await e.getGateways();
|
|
382
|
+
return c && c.length > 0 && (t = c, s = Date.now() + this.RANKED_GATEWAYS_TTL_MS, this.rankedGatewaysCacheTimestamp = Date.now(), this.rankedGatewaysCache = c, this.persistRankedGateways(c, this.rankedGatewaysCacheTimestamp)), c;
|
|
383
|
+
} catch (c) {
|
|
384
|
+
if (this.logger.warn("Ranked gateway fetch failed, serving stale list", { error: c }), t && t.length > 0) return t;
|
|
385
|
+
throw c;
|
|
386
|
+
} finally {
|
|
387
|
+
a = null;
|
|
388
|
+
}
|
|
389
|
+
})(), a)
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Resolve an Arweave URL to a gateway URL using the active gateway, with no network checks.
|
|
394
|
+
*
|
|
395
|
+
* This is the fast path — it returns instantly by reusing the current active gateway
|
|
396
|
+
* (or arweave.net if none is set). Use this for the hot path where latency matters.
|
|
397
|
+
* When real fetches fail, call {@link reportGatewayFailure} to trigger gateway rotation.
|
|
398
|
+
*
|
|
399
|
+
* @param url - The URL to resolve
|
|
400
|
+
* @returns A gateway URL constructed from the active gateway, or the original URL if not Arweave
|
|
401
|
+
*/
|
|
402
|
+
resolveUrlSimple(e) {
|
|
403
|
+
if (!A(e)) return e;
|
|
404
|
+
const t = T(e);
|
|
405
|
+
if (!t) return e;
|
|
406
|
+
const s = this.activeGateway ?? this.gateways[0];
|
|
407
|
+
return this.logger.debug("[gateway] resolveUrlSimple", {
|
|
408
|
+
txId: t.txId,
|
|
409
|
+
gateway: s.host,
|
|
410
|
+
pathSuffix: t.pathSuffix || void 0
|
|
411
|
+
}), k(t.txId, s, t.pathSuffix);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Set a preferred gateway by host name.
|
|
415
|
+
*
|
|
416
|
+
* If the host is in the known gateways list, it becomes the active gateway
|
|
417
|
+
* and is persisted to localStorage. Returns true on success.
|
|
418
|
+
*
|
|
419
|
+
* @param host - Gateway hostname (e.g. 'ardrive.net')
|
|
420
|
+
* @returns True if the gateway was found and set, false otherwise
|
|
421
|
+
*/
|
|
422
|
+
setPreferredGateway(e) {
|
|
423
|
+
const t = this.gateways.find((s) => s.host === e);
|
|
424
|
+
return t ? (this.activeGateway = t, this.lastFetchTiming = 0, this.consecutiveSlowResponses = 0, this.persistGateway(t), this.logger.info("Preferred gateway set", { host: e }), !0) : (this.logger.warn("Cannot set preferred gateway — not in gateway list", {
|
|
425
|
+
host: e,
|
|
426
|
+
available: this.gateways.map((s) => s.host)
|
|
427
|
+
}), !1);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Clear the preferred/active gateway.
|
|
431
|
+
*
|
|
432
|
+
* Removes the active gateway, clears the persisted gateway from localStorage,
|
|
433
|
+
* and clears the cache. The next resolveUrl() call will re-discover the best gateway.
|
|
434
|
+
*/
|
|
435
|
+
clearPreferredGateway() {
|
|
436
|
+
this.activeGateway = null, this.lastFetchTiming = 0, this.consecutiveSlowResponses = 0, this.clearPersistedGateway(), this.cache.clear(), this.logger.info("Preferred gateway cleared, will re-discover on next resolve");
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Get a diagnostic snapshot of the gateway manager's current state.
|
|
440
|
+
*
|
|
441
|
+
* Useful for debugging and console inspection.
|
|
442
|
+
*
|
|
443
|
+
* @returns Current gateway manager diagnostics
|
|
444
|
+
*/
|
|
445
|
+
getDiagnostics() {
|
|
446
|
+
const e = this.gateways.map((t) => {
|
|
447
|
+
const s = this.healthData.get(t.host) ?? [], a = s.length, r = a > 0 ? Math.round(s.reduce((i, c) => i + c.responseTime, 0) / a) : 0, l = a > 0 ? s.filter((i) => i.success).length / a : 1;
|
|
448
|
+
return { host: t.host, avgMs: r, successRate: Math.round(l * 100) / 100, checks: a };
|
|
449
|
+
});
|
|
450
|
+
return {
|
|
451
|
+
activeGateway: this.activeGateway?.host ?? null,
|
|
452
|
+
activeGatewayAge: null,
|
|
453
|
+
// not tracked currently — could add timestamp later
|
|
454
|
+
availableGateways: this.gateways.map((t) => t.host),
|
|
455
|
+
lastFetchTiming: this.lastFetchTiming,
|
|
456
|
+
consecutiveSlowResponses: this.consecutiveSlowResponses,
|
|
457
|
+
healthSummary: e
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Resolve an Arweave URL to a working gateway URL.
|
|
462
|
+
*
|
|
463
|
+
* Strategy: try static gateways first with real fetch checks (not HEAD),
|
|
464
|
+
* fall back to Wayfinder only if all static gateways fail.
|
|
465
|
+
*
|
|
466
|
+
* @param url - The URL to resolve
|
|
467
|
+
* @param options - Optional resolve options (signal, bypassArweaveNet, bypassWayfinder)
|
|
468
|
+
* @returns A working URL (or original URL if all gateways fail or signal is aborted)
|
|
469
|
+
*/
|
|
470
|
+
async resolveUrl(e, t) {
|
|
471
|
+
const s = t?.signal;
|
|
472
|
+
if (s?.aborted)
|
|
473
|
+
return this.logger.debug("resolveUrl called with already-aborted signal, returning original URL"), e;
|
|
474
|
+
if (!A(e))
|
|
475
|
+
return e;
|
|
476
|
+
const a = T(e);
|
|
477
|
+
if (!a)
|
|
478
|
+
return this.logger.warn("Failed to parse Arweave URL, returning original", { url: e }), e;
|
|
479
|
+
const { txId: r, pathSuffix: l } = a, i = this.getCachedGateway(r);
|
|
480
|
+
if (i) {
|
|
481
|
+
this.hitCount++;
|
|
482
|
+
const o = k(r, i, l);
|
|
483
|
+
return this.logger.debug("Cache hit for txId", { txId: r, gateway: i.host, pathSuffix: l }), o;
|
|
484
|
+
}
|
|
485
|
+
const c = this.activeGateway?.host ?? "none";
|
|
486
|
+
this.consecutiveSlowResponses >= this.maxSlowResponses && (this.logger.warn("Proactive gateway rotation triggered", {
|
|
487
|
+
slowCount: this.consecutiveSlowResponses,
|
|
488
|
+
threshold: this.maxSlowResponses,
|
|
489
|
+
previousGateway: c
|
|
490
|
+
}), this.activeGateway = null, this.consecutiveSlowResponses = 0, this.clearPersistedGateway(), this.cache.clear());
|
|
491
|
+
const n = this.inflightResolves.get(r);
|
|
492
|
+
if (n)
|
|
493
|
+
return this.logger.debug("Reusing in-flight resolve for txId", { txId: r }), n;
|
|
494
|
+
const y = new AbortController();
|
|
495
|
+
s && s.addEventListener("abort", () => y.abort(), { once: !0 });
|
|
496
|
+
const w = this.resolveGatewayChain(e, r, l, y.signal, t).finally(() => {
|
|
497
|
+
this.inflightResolves.delete(r), this.inflightControllers.delete(r);
|
|
498
|
+
});
|
|
499
|
+
return this.inflightResolves.set(r, w), this.inflightControllers.set(r, y), w;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* The actual gateway fallback chain. Extracted from resolveUrl() for
|
|
503
|
+
* request deduplication — concurrent callers for the same txId share
|
|
504
|
+
* one instance of this chain.
|
|
505
|
+
*/
|
|
506
|
+
async resolveGatewayChain(e, t, s, a, r) {
|
|
507
|
+
const l = Date.now(), i = r?.bypassArweaveNet ?? !1;
|
|
508
|
+
let c = null, n = null;
|
|
509
|
+
if (e.startsWith("https://") || e.startsWith("http://"))
|
|
510
|
+
try {
|
|
511
|
+
const u = new URL(e), d = {
|
|
512
|
+
host: u.host,
|
|
513
|
+
// includes port if present
|
|
514
|
+
protocol: u.protocol.replace(":", ""),
|
|
515
|
+
priority: 0
|
|
516
|
+
}, m = Date.now(), p = await this.checkAndSetGateway(e, t, s, d, a), R = Date.now() - m;
|
|
517
|
+
if (p) {
|
|
518
|
+
if (this.logger.info(`[gateway] Step 0 (original): ${p.kind}`, { host: d.host, ms: R, totalMs: Date.now() - l }), p.kind === "verified") return p.url;
|
|
519
|
+
n ??= p.url;
|
|
520
|
+
} else
|
|
521
|
+
this.logger.info("[gateway] Step 0 (original): failed", { host: d.host, ms: R });
|
|
522
|
+
c = d.host;
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
if (this.activeGateway && this.activeGateway.host !== c) {
|
|
526
|
+
const u = Date.now(), d = await this.checkAndSetGateway(e, t, s, this.activeGateway, a), m = Date.now() - u;
|
|
527
|
+
if (d) {
|
|
528
|
+
if (this.logger.info(`[gateway] Step 1 (persisted): ${d.kind}`, { host: this.activeGateway.host, ms: m, totalMs: Date.now() - l }), d.kind === "verified") return d.url;
|
|
529
|
+
n ??= d.url;
|
|
530
|
+
} else
|
|
531
|
+
this.logger.info("[gateway] Step 1 (persisted): failed, clearing", { ms: m }), this.activeGateway = null, this.clearPersistedGateway();
|
|
532
|
+
}
|
|
533
|
+
if (!i && c !== "arweave.net") {
|
|
534
|
+
const u = this.gateways.find((d) => d.host === "arweave.net");
|
|
535
|
+
if (u) {
|
|
536
|
+
const d = Date.now(), m = await this.checkAndSetGateway(e, t, s, u, a), p = Date.now() - d;
|
|
537
|
+
if (m) {
|
|
538
|
+
if (this.logger.info(`[gateway] Step 2 (arweave.net): ${m.kind}`, { ms: p, totalMs: Date.now() - l }), m.kind === "verified") return m.url;
|
|
539
|
+
n ??= m.url;
|
|
540
|
+
} else
|
|
541
|
+
this.logger.info("[gateway] Step 2 (arweave.net): failed", { ms: p });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const y = Date.now(), w = await this.tryFallbackGateways(e, t, s, a, c), o = Date.now() - y;
|
|
545
|
+
if (w) {
|
|
546
|
+
if (this.logger.info(`[gateway] Step 3 (fallbacks): ${w.kind}`, { ms: o, totalMs: Date.now() - l }), w.kind === "verified") return w.url;
|
|
547
|
+
n ??= w.url;
|
|
548
|
+
} else
|
|
549
|
+
this.logger.info("[gateway] Step 3 (fallbacks): failed", { ms: o });
|
|
550
|
+
const g = !this.hasCustomSolanaRpc, h = r?.bypassWayfinder ?? g;
|
|
551
|
+
if (this.wayfinder && !h) {
|
|
552
|
+
const u = Date.now();
|
|
553
|
+
this.logger.info("[gateway] Step 4 (wayfinder): trying");
|
|
554
|
+
const d = await this.tryWayfinder(t, s, a, c), m = Date.now() - u;
|
|
555
|
+
if (d) {
|
|
556
|
+
if (this.logger.info(`[gateway] Step 4 (wayfinder): ${d.kind}`, { ms: m, totalMs: Date.now() - l }), d.kind === "verified") return d.url;
|
|
557
|
+
n ??= d.url;
|
|
558
|
+
} else
|
|
559
|
+
this.logger.info("[gateway] Step 4 (wayfinder): failed", { ms: m });
|
|
560
|
+
}
|
|
561
|
+
return n ? (this.logger.warn("No gateway verified, returning best maybe-result", { txId: t, pathSuffix: s, maybeUrl: n }), n) : (this.logger.warn("All gateways (arweave.net + Wayfinder + fallbacks) failed, returning original URL", { txId: t, pathSuffix: s, originalUrl: e }), e);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Report that the active gateway failed a real fetch.
|
|
565
|
+
*
|
|
566
|
+
* Call this when the URL returned by resolveUrl() actually fails to load
|
|
567
|
+
* (e.g. fetch() returns a 4xx/5xx or throws). This clears the active gateway
|
|
568
|
+
* and finds a new one, persisting the result to localStorage.
|
|
569
|
+
*
|
|
570
|
+
* When reason is 'user-cancel-fast', the active gateway is preserved since
|
|
571
|
+
* the user intentionally cancelled and the gateway is likely fine.
|
|
572
|
+
*
|
|
573
|
+
* @param url - The URL that failed
|
|
574
|
+
* @param options - Optional signal for cancellation and reason to distinguish failure types
|
|
575
|
+
* @returns A new working URL (or original if all gateways fail or user cancelled quickly)
|
|
576
|
+
*/
|
|
577
|
+
async reportGatewayFailure(e, t) {
|
|
578
|
+
const { signal: s, reason: a, excludeHost: r } = t ?? {};
|
|
579
|
+
if (s?.aborted)
|
|
580
|
+
return this.logger.debug("reportGatewayFailure called with already-aborted signal, returning original URL"), e;
|
|
581
|
+
if (a === "user-cancel-fast")
|
|
582
|
+
return this.logger.debug("User cancelled quickly, keeping current gateway"), e;
|
|
583
|
+
const l = T(e);
|
|
584
|
+
if (!l) return e;
|
|
585
|
+
const { txId: i, pathSuffix: c } = l;
|
|
586
|
+
this.logger.warn("Gateway failure reported, finding new gateway", {
|
|
587
|
+
failedHost: this.activeGateway?.host,
|
|
588
|
+
txId: i,
|
|
589
|
+
reason: a ?? "load-error"
|
|
590
|
+
});
|
|
591
|
+
const n = this.activeGateway?.host ?? r;
|
|
592
|
+
this.activeGateway = null, this.cache.delete(i), this.clearPersistedGateway();
|
|
593
|
+
const y = this.gateways.find((g) => g.host === "arweave.net" && g.host !== n);
|
|
594
|
+
if (y) {
|
|
595
|
+
const g = await this.checkAndSetGateway(e, i, c, y, s);
|
|
596
|
+
if (g) return g.url;
|
|
597
|
+
}
|
|
598
|
+
if (this.wayfinder) {
|
|
599
|
+
const g = await this.tryWayfinder(i, c, s);
|
|
600
|
+
if (g) return g.url;
|
|
601
|
+
}
|
|
602
|
+
const w = this.gateways;
|
|
603
|
+
n && (this.gateways = this.gateways.filter((g) => g.host !== n));
|
|
604
|
+
const o = await this.tryFallbackGateways(e, i, c, s);
|
|
605
|
+
return this.gateways = w, o ? o.url : e;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Report that a fetch succeeded with timing data.
|
|
609
|
+
*
|
|
610
|
+
* Callers invoke this after a successful fetch to feed timing data back to the manager.
|
|
611
|
+
* If the fetch was fast, the consecutive slow counter resets. If slow, it increments
|
|
612
|
+
* and may trigger proactive gateway rotation on the next resolveUrl() call.
|
|
613
|
+
*
|
|
614
|
+
* @param timingMs - The time the fetch took in milliseconds
|
|
615
|
+
*/
|
|
616
|
+
reportFetchSuccess(e) {
|
|
617
|
+
this.lastFetchTiming = e, e >= this.slowResponseThreshold ? (this.consecutiveSlowResponses++, this.logger.warn("Slow fetch response recorded", {
|
|
618
|
+
timingMs: e,
|
|
619
|
+
threshold: this.slowResponseThreshold,
|
|
620
|
+
consecutiveSlow: this.consecutiveSlowResponses,
|
|
621
|
+
maxAllowed: this.maxSlowResponses,
|
|
622
|
+
gateway: this.activeGateway?.host ?? "none"
|
|
623
|
+
})) : (this.consecutiveSlowResponses > 0 && this.logger.info("Fetch timing recovered, resetting slow counter", {
|
|
624
|
+
timingMs: e,
|
|
625
|
+
previousSlowCount: this.consecutiveSlowResponses
|
|
626
|
+
}), this.consecutiveSlowResponses = 0);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Check a single gateway and set it as active if it works.
|
|
630
|
+
*
|
|
631
|
+
* Returns one of:
|
|
632
|
+
* - `{ kind: 'verified', url }` — HEAD returned 2xx/3xx, gateway is confirmed working
|
|
633
|
+
* - `{ kind: 'maybe', url }` — HEAD failed (likely CORS), no-cors GET didn't throw.
|
|
634
|
+
* Server responded but we can't verify status. Caller
|
|
635
|
+
* should keep looking for a 'verified' result and use
|
|
636
|
+
* 'maybe' only as a last resort.
|
|
637
|
+
* - `null` — gateway definitively failed
|
|
638
|
+
*
|
|
639
|
+
* Previously this returned `string | null` for both 'verified' and 'maybe', which forced
|
|
640
|
+
* `resolveGatewayChain` to bail on the first 'maybe' — even when other gateways might
|
|
641
|
+
* have a verified 2xx response. For manifest-path Arweave URLs (where the txid root
|
|
642
|
+
* serves a manifest and individual paths may 404), this meant the chain stopped at the
|
|
643
|
+
* first gateway that responded at all (even with 404s hidden behind no-cors opacity)
|
|
644
|
+
* and never tried alternates. Surfacing the distinction lets the chain prefer verified
|
|
645
|
+
* results and use 'maybe' only as a fallback.
|
|
646
|
+
*/
|
|
647
|
+
async checkAndSetGateway(e, t, s, a, r) {
|
|
648
|
+
if (r?.aborted) return null;
|
|
649
|
+
const l = await this.checkGateway(t, a, s, r);
|
|
650
|
+
if (l === !0) {
|
|
651
|
+
this.setActiveGateway(a, t);
|
|
652
|
+
const i = k(t, a, s);
|
|
653
|
+
return this.logger.info("Gateway resolved", {
|
|
654
|
+
txId: t,
|
|
655
|
+
gateway: a.host,
|
|
656
|
+
pathSuffix: s,
|
|
657
|
+
workingUrl: i
|
|
658
|
+
}), { kind: "verified", url: i };
|
|
659
|
+
}
|
|
660
|
+
if (l === "maybe") {
|
|
661
|
+
const i = k(t, a, s);
|
|
662
|
+
return this.logger.info("Gateway maybe resolved (no-cors)", {
|
|
663
|
+
txId: t,
|
|
664
|
+
gateway: a.host,
|
|
665
|
+
pathSuffix: s,
|
|
666
|
+
workingUrl: i
|
|
667
|
+
}), { kind: "maybe", url: i };
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Try remaining fallback gateways (everything except arweave.net) in parallel.
|
|
673
|
+
* Returns the URL of the first gateway that responds, or null if all fail.
|
|
674
|
+
*/
|
|
675
|
+
async tryFallbackGateways(e, t, s, a, r) {
|
|
676
|
+
this.missCount++;
|
|
677
|
+
const l = 0.7, i = 3, c = this.gateways.filter((h) => h.host !== "arweave.net" && h.host !== r), n = c.filter((h) => {
|
|
678
|
+
const { rate: u, totalChecks: d } = this.getFailureRate(h.host);
|
|
679
|
+
return !(d >= i && u > l);
|
|
680
|
+
}), y = n.length > 0 ? n : c;
|
|
681
|
+
if (a?.aborted || y.length === 0) return null;
|
|
682
|
+
const w = async (h) => {
|
|
683
|
+
if (a?.aborted || h.length === 0) return null;
|
|
684
|
+
const u = new AbortController(), d = () => u.abort();
|
|
685
|
+
a?.addEventListener("abort", d, { once: !0 });
|
|
686
|
+
let m = null, p = null, R = !1;
|
|
687
|
+
try {
|
|
688
|
+
const P = await Promise.allSettled(
|
|
689
|
+
h.map(async (v) => {
|
|
690
|
+
const b = await this.checkGateway(t, v, s, u.signal);
|
|
691
|
+
if (!b) throw new Error(`Gateway ${v.host} failed check`);
|
|
692
|
+
const W = k(t, v, s), F = b === !0 ? "verified" : "maybe";
|
|
693
|
+
return R || (R = !0, F === "verified" ? (this.setActiveGateway(v, t), this.logger.info("Gateway resolved via fallback", {
|
|
694
|
+
txId: t,
|
|
695
|
+
gateway: v.host,
|
|
696
|
+
pathSuffix: s,
|
|
697
|
+
workingUrl: W
|
|
698
|
+
})) : this.logger.info("Gateway maybe resolved via fallback (no-cors)", {
|
|
699
|
+
txId: t,
|
|
700
|
+
gateway: v.host,
|
|
701
|
+
pathSuffix: s,
|
|
702
|
+
workingUrl: W
|
|
703
|
+
})), { kind: F, url: W };
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
for (const v of P) {
|
|
707
|
+
if (v.status !== "fulfilled") continue;
|
|
708
|
+
const b = v.value;
|
|
709
|
+
if (b.kind === "verified" && !m ? m = { kind: "verified", url: b.url } : b.kind === "maybe" && !p && (p = { kind: "maybe", url: b.url }), m) break;
|
|
710
|
+
}
|
|
711
|
+
return m ?? p;
|
|
712
|
+
} finally {
|
|
713
|
+
a?.removeEventListener("abort", d);
|
|
714
|
+
}
|
|
715
|
+
}, o = await w(y);
|
|
716
|
+
if (o && o.kind === "verified") return o;
|
|
717
|
+
let g = o;
|
|
718
|
+
if (n.length < c.length && !a?.aborted) {
|
|
719
|
+
const h = c.filter((d) => !n.some((m) => m.host === d.host)).map((d) => d.host);
|
|
720
|
+
this.logger.debug("Healthy gateways exhausted, retrying with previously unhealthy", { excludedHosts: h });
|
|
721
|
+
const u = await w(c);
|
|
722
|
+
if (u && (u.kind === "verified" || !g))
|
|
723
|
+
return u;
|
|
724
|
+
}
|
|
725
|
+
return g;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Return the ranked gateway list. The provider itself is already wrapped in
|
|
729
|
+
* the 24h cache (see wrapProviderWithCache), so this is now a thin reader —
|
|
730
|
+
* no separate fetch, TTL, or stale logic here. Kept as an indirection point
|
|
731
|
+
* so tryWayfinder's Step B reads through a named method. Returns null if the
|
|
732
|
+
* provider is unavailable or both fetch and stale fallback fail.
|
|
733
|
+
*/
|
|
734
|
+
async getRankedGatewaysCached(e) {
|
|
735
|
+
if (!this.wayfinderGatewaysProvider) return null;
|
|
736
|
+
try {
|
|
737
|
+
const t = await this.withTimeout(
|
|
738
|
+
this.wayfinderGatewaysProvider.getGateways(),
|
|
739
|
+
this.timeout + 2e3,
|
|
740
|
+
"Wayfinder provider getGateways timed out"
|
|
741
|
+
);
|
|
742
|
+
return e?.aborted ? null : t && t.length > 0 ? t : null;
|
|
743
|
+
} catch (t) {
|
|
744
|
+
return this.logger.debug("Ranked gateway list unavailable", { error: t }), null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async tryWayfinder(e, t, s, a) {
|
|
748
|
+
if (!this.wayfinder) return null;
|
|
749
|
+
let r = null;
|
|
750
|
+
const l = 10, i = /* @__PURE__ */ new Set();
|
|
751
|
+
a && i.add(a);
|
|
752
|
+
const c = `https://arweave.net/${e}${t}`;
|
|
753
|
+
try {
|
|
754
|
+
this.logger.debug("Resolving via Wayfinder (first pick)", { txId: e, normalizedUrl: c });
|
|
755
|
+
const w = await this.withTimeout(
|
|
756
|
+
this.wayfinder.resolveUrl({ originalUrl: c }),
|
|
757
|
+
this.timeout + 2e3,
|
|
758
|
+
"Wayfinder resolution timed out"
|
|
759
|
+
);
|
|
760
|
+
if (s?.aborted) return null;
|
|
761
|
+
const o = w.hostname;
|
|
762
|
+
if (i.has(o))
|
|
763
|
+
this.logger.debug("Wayfinder first pick was already excluded, skipping to walk", { host: o });
|
|
764
|
+
else {
|
|
765
|
+
const g = w.protocol.replace(":", ""), h = {
|
|
766
|
+
host: o,
|
|
767
|
+
protocol: g,
|
|
768
|
+
priority: 0
|
|
769
|
+
}, u = await this.checkGateway(e, h, t, s);
|
|
770
|
+
if (u) {
|
|
771
|
+
const d = k(e, h, t), m = u === !0 ? "verified" : "maybe";
|
|
772
|
+
if (m === "verified" ? (this.setActiveGateway(h, e), this.logger.info("Gateway resolved via Wayfinder (first pick)", {
|
|
773
|
+
txId: e,
|
|
774
|
+
gateway: o,
|
|
775
|
+
pathSuffix: t,
|
|
776
|
+
workingUrl: d
|
|
777
|
+
})) : this.logger.info("Gateway maybe resolved via Wayfinder (first pick, no-cors)", {
|
|
778
|
+
txId: e,
|
|
779
|
+
gateway: o,
|
|
780
|
+
pathSuffix: t,
|
|
781
|
+
workingUrl: d
|
|
782
|
+
}), m === "verified") return { kind: m, url: d };
|
|
783
|
+
r ??= { kind: m, url: d };
|
|
784
|
+
} else
|
|
785
|
+
this.logger.debug("Wayfinder first pick failed verify, will walk ranked list", { host: o });
|
|
786
|
+
i.add(o);
|
|
787
|
+
}
|
|
788
|
+
} catch (w) {
|
|
789
|
+
this.logger.debug("Wayfinder first pick threw, will walk ranked list", { error: w });
|
|
790
|
+
}
|
|
791
|
+
if (s?.aborted) return r;
|
|
792
|
+
const n = await this.getRankedGatewaysCached(s);
|
|
793
|
+
if (!n || n.length === 0 || s?.aborted) return r;
|
|
794
|
+
const y = n.filter((w) => !i.has(w.hostname)).slice(0, l);
|
|
795
|
+
this.logger.debug("Walking Wayfinder ranked list", {
|
|
796
|
+
txId: e,
|
|
797
|
+
poolSize: n.length,
|
|
798
|
+
walkSize: y.length
|
|
799
|
+
});
|
|
800
|
+
for (const w of y) {
|
|
801
|
+
if (s?.aborted) return r;
|
|
802
|
+
const o = {
|
|
803
|
+
host: w.hostname,
|
|
804
|
+
protocol: w.protocol.replace(":", ""),
|
|
805
|
+
priority: 0
|
|
806
|
+
}, g = await this.checkGateway(e, o, t, s);
|
|
807
|
+
if (g) {
|
|
808
|
+
const h = k(e, o, t), u = g === !0 ? "verified" : "maybe";
|
|
809
|
+
if (u === "verified")
|
|
810
|
+
return this.setActiveGateway(o, e), this.logger.info("Gateway resolved via Wayfinder ranked walk", {
|
|
811
|
+
txId: e,
|
|
812
|
+
gateway: o.host,
|
|
813
|
+
pathSuffix: t,
|
|
814
|
+
workingUrl: h
|
|
815
|
+
}), { kind: u, url: h };
|
|
816
|
+
this.logger.info("Gateway maybe resolved via Wayfinder ranked walk (no-cors)", {
|
|
817
|
+
txId: e,
|
|
818
|
+
gateway: o.host,
|
|
819
|
+
pathSuffix: t,
|
|
820
|
+
workingUrl: h
|
|
821
|
+
}), r ??= { kind: u, url: h };
|
|
822
|
+
}
|
|
823
|
+
i.add(o.host);
|
|
824
|
+
}
|
|
825
|
+
return r ? this.logger.debug("Wayfinder returning best maybe-result (no verified match)", {
|
|
826
|
+
txId: e,
|
|
827
|
+
checkedHosts: Array.from(i),
|
|
828
|
+
maybeUrl: r.url
|
|
829
|
+
}) : this.logger.debug("Wayfinder ranked walk exhausted with no match", {
|
|
830
|
+
txId: e,
|
|
831
|
+
checkedHosts: Array.from(i)
|
|
832
|
+
}), r;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Set the active gateway, cache it for the txId, and persist to localStorage.
|
|
836
|
+
*/
|
|
837
|
+
setActiveGateway(e, t) {
|
|
838
|
+
this.activeGateway = e, this.lastFetchTiming = 0, this.consecutiveSlowResponses = 0, this.setCache(t, e), this.persistGateway(e);
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Load the persisted active gateway from localStorage.
|
|
842
|
+
*/
|
|
843
|
+
loadPersistedGateway() {
|
|
844
|
+
try {
|
|
845
|
+
if (typeof localStorage > "u") return null;
|
|
846
|
+
const e = localStorage.getItem(H);
|
|
847
|
+
if (!e) return null;
|
|
848
|
+
const t = JSON.parse(e);
|
|
849
|
+
if (t && typeof t.host == "string" && typeof t.protocol == "string") {
|
|
850
|
+
if (typeof t.timestamp == "number" && Date.now() - t.timestamp > 72e5)
|
|
851
|
+
return this.logger.info("Ignoring persisted gateway — expired (older than 2 hours)", { host: t.host }), this.clearPersistedGateway(), null;
|
|
852
|
+
this.logger.info("Restored active gateway from localStorage", { host: t.host });
|
|
853
|
+
const { timestamp: a, ...r } = t;
|
|
854
|
+
return r;
|
|
855
|
+
}
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Load the persisted ranked gateway list from localStorage. Returns null if
|
|
862
|
+
* unavailable, malformed, or expired.
|
|
863
|
+
*/
|
|
864
|
+
loadPersistedRankedGateways() {
|
|
865
|
+
try {
|
|
866
|
+
if (typeof localStorage > "u") return null;
|
|
867
|
+
const e = localStorage.getItem(L);
|
|
868
|
+
if (!e) return null;
|
|
869
|
+
const t = JSON.parse(e);
|
|
870
|
+
if (!t || !Array.isArray(t.gateways) || typeof t.timestamp != "number") return null;
|
|
871
|
+
const s = Date.now() - t.timestamp;
|
|
872
|
+
if (s > this.RANKED_GATEWAYS_TTL_MS)
|
|
873
|
+
return this.logger.info("Ignoring persisted ranked gateway list — expired", { ageMs: s }), this.clearPersistedRankedGateways(), null;
|
|
874
|
+
const a = [];
|
|
875
|
+
for (const r of t.gateways)
|
|
876
|
+
if (typeof r == "string")
|
|
877
|
+
try {
|
|
878
|
+
a.push(new URL(r));
|
|
879
|
+
} catch {
|
|
880
|
+
}
|
|
881
|
+
return a.length === 0 ? null : (this.rankedGatewaysCacheTimestamp = t.timestamp, this.logger.info("Restored ranked gateway list from localStorage", { size: a.length, ageMs: s }), a);
|
|
882
|
+
} catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Persist the ranked gateway list to localStorage so it survives reloads.
|
|
888
|
+
*/
|
|
889
|
+
persistRankedGateways(e, t) {
|
|
890
|
+
try {
|
|
891
|
+
if (typeof localStorage > "u") return;
|
|
892
|
+
const s = {
|
|
893
|
+
gateways: e.map((a) => a.toString()),
|
|
894
|
+
timestamp: t
|
|
895
|
+
};
|
|
896
|
+
localStorage.setItem(L, JSON.stringify(s));
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Clear the persisted ranked gateway list from localStorage.
|
|
902
|
+
*/
|
|
903
|
+
clearPersistedRankedGateways() {
|
|
904
|
+
try {
|
|
905
|
+
if (typeof localStorage > "u") return;
|
|
906
|
+
localStorage.removeItem(L);
|
|
907
|
+
} catch {
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Persist the active gateway to localStorage.
|
|
912
|
+
*/
|
|
913
|
+
persistGateway(e) {
|
|
914
|
+
try {
|
|
915
|
+
if (typeof localStorage > "u") return;
|
|
916
|
+
const t = { ...e, timestamp: Date.now() };
|
|
917
|
+
localStorage.setItem(H, JSON.stringify(t));
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Clear the persisted gateway from localStorage.
|
|
923
|
+
*/
|
|
924
|
+
clearPersistedGateway() {
|
|
925
|
+
try {
|
|
926
|
+
if (typeof localStorage > "u") return;
|
|
927
|
+
localStorage.removeItem(H);
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Check if a gateway can serve a transaction
|
|
933
|
+
*
|
|
934
|
+
* Uses a HEAD request with timeout to check if the gateway responds.
|
|
935
|
+
* Handles CORS errors gracefully (treats as failure, not exception).
|
|
936
|
+
*
|
|
937
|
+
* @param txId - The transaction ID to check
|
|
938
|
+
* @param gateway - The gateway to check
|
|
939
|
+
* @param pathSuffix - Optional path suffix to append after txId
|
|
940
|
+
* @param signal - Optional external AbortSignal to cancel the check
|
|
941
|
+
* @returns true if the gateway can serve the transaction
|
|
942
|
+
*/
|
|
943
|
+
async checkGateway(e, t, s = "", a) {
|
|
944
|
+
const r = k(e, t, s), l = Date.now(), i = new AbortController(), c = setTimeout(() => i.abort(), this.timeout), n = () => i.abort();
|
|
945
|
+
a?.addEventListener("abort", n, { once: !0 });
|
|
946
|
+
try {
|
|
947
|
+
const y = await fetch(r, {
|
|
948
|
+
method: "HEAD",
|
|
949
|
+
mode: "cors",
|
|
950
|
+
signal: i.signal
|
|
951
|
+
});
|
|
952
|
+
clearTimeout(c), a?.removeEventListener("abort", n);
|
|
953
|
+
const w = Date.now() - l, o = y.ok || y.status >= 200 && y.status < 400;
|
|
954
|
+
return this.recordGatewayResponse(t.host, w, o), o;
|
|
955
|
+
} catch (y) {
|
|
956
|
+
clearTimeout(c), a?.removeEventListener("abort", n);
|
|
957
|
+
const w = Date.now() - l;
|
|
958
|
+
if (this.recordGatewayResponse(t.host, w, !1), y instanceof Error && y.name === "AbortError") {
|
|
959
|
+
const o = a?.aborted;
|
|
960
|
+
return this.logger.debug(o ? "Gateway check aborted by external signal" : "Gateway check timed out", {
|
|
961
|
+
txId: e,
|
|
962
|
+
gateway: t.host,
|
|
963
|
+
timeout: this.timeout
|
|
964
|
+
}), !1;
|
|
965
|
+
}
|
|
966
|
+
if (y instanceof TypeError) {
|
|
967
|
+
this.logger.debug("HEAD failed (likely CORS), retrying with no-cors", {
|
|
968
|
+
txId: e,
|
|
969
|
+
gateway: t.host,
|
|
970
|
+
error: y.message
|
|
971
|
+
});
|
|
972
|
+
try {
|
|
973
|
+
await fetch(r, {
|
|
974
|
+
method: "GET",
|
|
975
|
+
mode: "no-cors",
|
|
976
|
+
signal: i.signal
|
|
977
|
+
});
|
|
978
|
+
const o = Date.now() - l;
|
|
979
|
+
return this.recordGatewayResponse(t.host, o, !0), "maybe";
|
|
980
|
+
} catch {
|
|
981
|
+
const o = Date.now() - l;
|
|
982
|
+
return this.recordGatewayResponse(t.host, o, !1), !1;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return this.logger.debug("Gateway HEAD failed", {
|
|
986
|
+
txId: e,
|
|
987
|
+
gateway: t.host,
|
|
988
|
+
error: y instanceof Error ? y.message : String(y)
|
|
989
|
+
}), !1;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Get the cached working gateway for a transaction ID
|
|
994
|
+
*
|
|
995
|
+
* @param txId - The transaction ID to look up
|
|
996
|
+
* @returns The cached gateway config, or null if not cached or expired
|
|
997
|
+
*/
|
|
998
|
+
getCachedGateway(e) {
|
|
999
|
+
const t = this.cache.get(e);
|
|
1000
|
+
return t ? this.isCacheValid(t) ? t.workingGateway : (this.cache.delete(e), null) : null;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Cache a working gateway for a transaction ID
|
|
1004
|
+
*
|
|
1005
|
+
* @param txId - The transaction ID
|
|
1006
|
+
* @param gateway - The working gateway to cache
|
|
1007
|
+
*/
|
|
1008
|
+
setCache(e, t) {
|
|
1009
|
+
const s = {
|
|
1010
|
+
txId: e,
|
|
1011
|
+
workingGateway: t,
|
|
1012
|
+
timestamp: Date.now(),
|
|
1013
|
+
ttl: this.cacheTTL
|
|
1014
|
+
};
|
|
1015
|
+
this.cache.set(e, s), this.logger.debug("Cached working gateway", { txId: e, gateway: t.host });
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Clear all cached gateway entries
|
|
1019
|
+
*/
|
|
1020
|
+
clearCache() {
|
|
1021
|
+
const e = this.cache.size;
|
|
1022
|
+
this.cache.clear(), this.hitCount = 0, this.missCount = 0, this.logger.info("Cache cleared", { entriesRemoved: e });
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Invalidate the cached gateway for a specific URL's transaction ID.
|
|
1026
|
+
* Also clears the active gateway and persisted gateway so that resolveUrl()
|
|
1027
|
+
* runs the full fallback chain instead of short-circuiting.
|
|
1028
|
+
*/
|
|
1029
|
+
invalidateCacheForUrl(e) {
|
|
1030
|
+
if (!A(e)) return;
|
|
1031
|
+
const t = T(e);
|
|
1032
|
+
t && (this.cache.delete(t.txId), this.activeGateway = null, this.clearPersistedGateway());
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Prefetch and cache gateways for multiple URLs in parallel
|
|
1036
|
+
*
|
|
1037
|
+
* This is useful for warming up the cache at app startup with known model URLs.
|
|
1038
|
+
* URLs are resolved in parallel with configurable concurrency.
|
|
1039
|
+
*
|
|
1040
|
+
* @param urls - Array of URLs to prefetch (non-Arweave URLs are skipped)
|
|
1041
|
+
* @param options - Prefetch options
|
|
1042
|
+
* @returns Result summary with success/failure counts and details
|
|
1043
|
+
*
|
|
1044
|
+
* @example
|
|
1045
|
+
* ```ts
|
|
1046
|
+
* const result = await arweaveGatewayManager.prefetchUrls([
|
|
1047
|
+
* 'https://arweave.net/abc123.../model.json',
|
|
1048
|
+
* 'https://arweave.net/def456.../model.json',
|
|
1049
|
+
* ]);
|
|
1050
|
+
* console.log(`Prefetched ${result.succeeded}/${result.total} URLs`);
|
|
1051
|
+
* ```
|
|
1052
|
+
*/
|
|
1053
|
+
async prefetchUrls(e, t) {
|
|
1054
|
+
const s = t?.concurrency ?? 5, a = t?.continueOnError ?? !0, r = [];
|
|
1055
|
+
let l = 0, i = 0, c = 0;
|
|
1056
|
+
for (let n = 0; n < e.length; n += s) {
|
|
1057
|
+
const y = e.slice(n, n + s), w = await Promise.allSettled(
|
|
1058
|
+
y.map(async (o) => {
|
|
1059
|
+
if (!A(o))
|
|
1060
|
+
return {
|
|
1061
|
+
url: o,
|
|
1062
|
+
txId: "",
|
|
1063
|
+
success: !1,
|
|
1064
|
+
error: "Not an Arweave URL"
|
|
1065
|
+
};
|
|
1066
|
+
const g = T(o);
|
|
1067
|
+
if (!g)
|
|
1068
|
+
return {
|
|
1069
|
+
url: o,
|
|
1070
|
+
txId: "",
|
|
1071
|
+
success: !1,
|
|
1072
|
+
error: "Failed to parse Arweave URL"
|
|
1073
|
+
};
|
|
1074
|
+
const h = this.getCachedGateway(g.txId);
|
|
1075
|
+
if (h)
|
|
1076
|
+
return {
|
|
1077
|
+
url: o,
|
|
1078
|
+
txId: g.txId,
|
|
1079
|
+
success: !0,
|
|
1080
|
+
gateway: h
|
|
1081
|
+
};
|
|
1082
|
+
await this.resolveUrl(o);
|
|
1083
|
+
const u = this.getCachedGateway(g.txId);
|
|
1084
|
+
return u ? {
|
|
1085
|
+
url: o,
|
|
1086
|
+
txId: g.txId,
|
|
1087
|
+
success: !0,
|
|
1088
|
+
gateway: u
|
|
1089
|
+
} : {
|
|
1090
|
+
url: o,
|
|
1091
|
+
txId: g.txId,
|
|
1092
|
+
success: !1,
|
|
1093
|
+
error: "All gateways failed"
|
|
1094
|
+
};
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
for (const o of w)
|
|
1098
|
+
if (o.status === "fulfilled") {
|
|
1099
|
+
const g = o.value;
|
|
1100
|
+
r.push(g), g.error === "Not an Arweave URL" ? c++ : g.success ? l++ : i++;
|
|
1101
|
+
} else
|
|
1102
|
+
i++, r.push({
|
|
1103
|
+
url: "unknown",
|
|
1104
|
+
txId: "",
|
|
1105
|
+
success: !1,
|
|
1106
|
+
error: o.reason?.message ?? "Unknown error"
|
|
1107
|
+
});
|
|
1108
|
+
if (!a && i > 0) {
|
|
1109
|
+
this.logger.warn("Prefetch stopped due to failures", { failed: i, processed: r.length });
|
|
1110
|
+
break;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return this.logger.info("Prefetch complete", {
|
|
1114
|
+
total: e.length,
|
|
1115
|
+
succeeded: l,
|
|
1116
|
+
failed: i,
|
|
1117
|
+
skipped: c
|
|
1118
|
+
}), {
|
|
1119
|
+
total: e.length,
|
|
1120
|
+
succeeded: l,
|
|
1121
|
+
failed: i,
|
|
1122
|
+
skipped: c,
|
|
1123
|
+
entries: r
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Get statistics about the gateway cache
|
|
1128
|
+
*
|
|
1129
|
+
* @returns Cache statistics including size, entries, and hit/miss counts
|
|
1130
|
+
*
|
|
1131
|
+
* @example
|
|
1132
|
+
* ```ts
|
|
1133
|
+
* const stats = arweaveGatewayManager.getCacheStats();
|
|
1134
|
+
* console.log(`Cache has ${stats.size} entries, hit rate: ${stats.hitCount / (stats.hitCount + stats.missCount)}`);
|
|
1135
|
+
* ```
|
|
1136
|
+
*/
|
|
1137
|
+
getCacheStats() {
|
|
1138
|
+
const e = Array.from(this.cache.entries()), t = Date.now(), s = e.filter(([, a]) => t - a.timestamp < a.ttl).map(([a]) => a);
|
|
1139
|
+
return {
|
|
1140
|
+
size: s.length,
|
|
1141
|
+
txIds: s,
|
|
1142
|
+
hitCount: this.hitCount,
|
|
1143
|
+
missCount: this.missCount,
|
|
1144
|
+
ttl: this.cacheTTL
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Get all cached gateway entries (for debugging)
|
|
1149
|
+
*
|
|
1150
|
+
* @returns Array of all cache entries (including expired ones)
|
|
1151
|
+
*/
|
|
1152
|
+
getCacheEntries() {
|
|
1153
|
+
return Array.from(this.cache.values());
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Check if a cache entry is still valid
|
|
1157
|
+
*
|
|
1158
|
+
* @param cache - The cache entry to check
|
|
1159
|
+
* @returns true if the entry is still within its TTL
|
|
1160
|
+
*/
|
|
1161
|
+
isCacheValid(e) {
|
|
1162
|
+
return Date.now() - e.timestamp < e.ttl;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Get the average response time for a gateway from health data.
|
|
1166
|
+
* Returns 0 if no records exist (indicates no history).
|
|
1167
|
+
*
|
|
1168
|
+
* @param host - Gateway hostname
|
|
1169
|
+
* @returns Average response time in ms, or 0 if no data
|
|
1170
|
+
*/
|
|
1171
|
+
/**
|
|
1172
|
+
* Get the failure rate for a gateway based on health data.
|
|
1173
|
+
*
|
|
1174
|
+
* @param host - Gateway hostname
|
|
1175
|
+
* @returns Object with failure rate (0-1) and total check count
|
|
1176
|
+
*/
|
|
1177
|
+
getFailureRate(e) {
|
|
1178
|
+
const t = this.healthData.get(e);
|
|
1179
|
+
return !t || t.length === 0 ? { rate: 0, totalChecks: 0 } : { rate: t.filter((a) => !a.success).length / t.length, totalChecks: t.length };
|
|
1180
|
+
}
|
|
1181
|
+
getAverageResponseTime(e) {
|
|
1182
|
+
const t = this.healthData.get(e);
|
|
1183
|
+
return !t || t.length === 0 ? 0 : t.reduce((s, a) => s + a.responseTime, 0) / t.length;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Record a gateway response for health tracking
|
|
1187
|
+
*
|
|
1188
|
+
* @param host - Gateway hostname
|
|
1189
|
+
* @param responseTime - Response time in milliseconds
|
|
1190
|
+
* @param success - Whether the check was successful
|
|
1191
|
+
*/
|
|
1192
|
+
recordGatewayResponse(e, t, s) {
|
|
1193
|
+
this.healthData.has(e) || this.healthData.set(e, []);
|
|
1194
|
+
const a = this.healthData.get(e);
|
|
1195
|
+
a.push({
|
|
1196
|
+
responseTime: t,
|
|
1197
|
+
success: s,
|
|
1198
|
+
timestamp: Date.now()
|
|
1199
|
+
}), a.length > this.MAX_HEALTH_RECORDS && a.shift();
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Report real fetch timing data for a gateway.
|
|
1203
|
+
*
|
|
1204
|
+
* Callers invoke this after an actual data-transfer fetch (not just a HEAD check)
|
|
1205
|
+
* to feed real performance data into health tracking. This is separate from the
|
|
1206
|
+
* HEAD-based timing recorded by checkGateway(), and gives a more accurate picture
|
|
1207
|
+
* of actual download performance.
|
|
1208
|
+
*
|
|
1209
|
+
* @param host - Gateway hostname that served the fetch
|
|
1210
|
+
* @param timingMs - Time the full fetch took in milliseconds
|
|
1211
|
+
* @param success - Whether the fetch succeeded
|
|
1212
|
+
*/
|
|
1213
|
+
reportFetchTiming(e, t, s) {
|
|
1214
|
+
this.recordGatewayResponse(e, t, s), this.logger.debug("Fetch timing reported", { host: e, timingMs: t, success: s });
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Get health statistics for a specific gateway
|
|
1218
|
+
*
|
|
1219
|
+
* @param host - Gateway hostname
|
|
1220
|
+
* @returns Health statistics for the gateway, or undefined if not tracked
|
|
1221
|
+
*/
|
|
1222
|
+
getGatewayHealth(e) {
|
|
1223
|
+
const t = this.gateways.find((o) => o.host === e);
|
|
1224
|
+
if (!t)
|
|
1225
|
+
return;
|
|
1226
|
+
const s = this.healthData.get(e) || [], a = this.originalPriorities.get(e) ?? t.priority;
|
|
1227
|
+
if (s.length === 0)
|
|
1228
|
+
return {
|
|
1229
|
+
host: e,
|
|
1230
|
+
priority: t.priority,
|
|
1231
|
+
originalPriority: a,
|
|
1232
|
+
successCount: 0,
|
|
1233
|
+
failureCount: 0,
|
|
1234
|
+
totalChecks: 0,
|
|
1235
|
+
successRate: 0,
|
|
1236
|
+
averageResponseTime: 0,
|
|
1237
|
+
lastSuccess: null,
|
|
1238
|
+
lastFailure: null,
|
|
1239
|
+
isHealthy: !0
|
|
1240
|
+
// Assume healthy until we have data
|
|
1241
|
+
};
|
|
1242
|
+
const r = s.filter((o) => o.success).length, l = s.filter((o) => !o.success).length, i = r / s.length, c = Math.round(
|
|
1243
|
+
s.reduce((o, g) => o + g.responseTime, 0) / s.length
|
|
1244
|
+
), n = s.filter((o) => o.success).pop()?.timestamp ?? null, y = s.filter((o) => !o.success).pop()?.timestamp ?? null, w = i >= 0.5;
|
|
1245
|
+
return {
|
|
1246
|
+
host: e,
|
|
1247
|
+
priority: t.priority,
|
|
1248
|
+
originalPriority: a,
|
|
1249
|
+
successCount: r,
|
|
1250
|
+
failureCount: l,
|
|
1251
|
+
totalChecks: s.length,
|
|
1252
|
+
successRate: i,
|
|
1253
|
+
averageResponseTime: c,
|
|
1254
|
+
lastSuccess: n,
|
|
1255
|
+
lastFailure: y,
|
|
1256
|
+
isHealthy: w
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Get health statistics for all gateways
|
|
1261
|
+
*
|
|
1262
|
+
* @returns Array of health statistics for each gateway
|
|
1263
|
+
*/
|
|
1264
|
+
getAllGatewayHealth() {
|
|
1265
|
+
return this.gateways.map((e) => this.getGatewayHealth(e.host));
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Adjust gateway priorities based on health data
|
|
1269
|
+
*
|
|
1270
|
+
* This method reorders gateways based on:
|
|
1271
|
+
* 1. Health status (healthy gateways first)
|
|
1272
|
+
* 2. Average response time (faster gateways first among healthy)
|
|
1273
|
+
* 3. Success rate (higher success rate first)
|
|
1274
|
+
*
|
|
1275
|
+
* @param options - Options for priority adjustment
|
|
1276
|
+
* @returns The new gateway order after adjustment
|
|
1277
|
+
*/
|
|
1278
|
+
adjustGatewayPriorities(e) {
|
|
1279
|
+
const t = e?.minChecks ?? 3, s = e?.healthyThreshold ?? 0.5, r = this.getAllGatewayHealth().reduce((n, y) => n + y.totalChecks, 0);
|
|
1280
|
+
if (r < t)
|
|
1281
|
+
return this.logger.debug("Not enough health data to adjust priorities", {
|
|
1282
|
+
totalChecks: r,
|
|
1283
|
+
required: t
|
|
1284
|
+
}), [...this.gateways];
|
|
1285
|
+
const l = this.gateways.map((n, y) => ({ gateway: n, originalIndex: y }));
|
|
1286
|
+
l.sort((n, y) => {
|
|
1287
|
+
const w = this.getGatewayHealth(n.gateway.host), o = this.getGatewayHealth(y.gateway.host), g = w.successRate >= s || w.totalChecks === 0, h = o.successRate >= s || o.totalChecks === 0;
|
|
1288
|
+
if (g && !h) return -1;
|
|
1289
|
+
if (!g && h) return 1;
|
|
1290
|
+
if (w.totalChecks > 0 && o.totalChecks > 0) {
|
|
1291
|
+
const u = w.averageResponseTime - o.averageResponseTime;
|
|
1292
|
+
if (Math.abs(u) > 100)
|
|
1293
|
+
return u;
|
|
1294
|
+
}
|
|
1295
|
+
if (w.totalChecks > 0 && o.totalChecks > 0) {
|
|
1296
|
+
const u = o.successRate - w.successRate;
|
|
1297
|
+
if (Math.abs(u) > 0.1)
|
|
1298
|
+
return u;
|
|
1299
|
+
}
|
|
1300
|
+
return n.originalIndex - y.originalIndex;
|
|
1301
|
+
});
|
|
1302
|
+
const i = l.map((n) => n.gateway), c = Math.min(...Array.from(this.originalPriorities.values()));
|
|
1303
|
+
return i.forEach((n, y) => {
|
|
1304
|
+
n.priority = c + y;
|
|
1305
|
+
}), this.gateways = i, this.logger.info("Gateway priorities adjusted", {
|
|
1306
|
+
newOrder: this.gateways.map((n) => ({
|
|
1307
|
+
host: n.host,
|
|
1308
|
+
priority: n.priority,
|
|
1309
|
+
health: this.getGatewayHealth(n.host)
|
|
1310
|
+
}))
|
|
1311
|
+
}), [...this.gateways];
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Run a health check on all gateways
|
|
1315
|
+
*
|
|
1316
|
+
* Tests each gateway by making a HEAD request to a known transaction.
|
|
1317
|
+
* Optionally adjusts priorities based on results.
|
|
1318
|
+
*
|
|
1319
|
+
* @param options - Health check options
|
|
1320
|
+
* @returns Health check results
|
|
1321
|
+
*
|
|
1322
|
+
* @example
|
|
1323
|
+
* ```ts
|
|
1324
|
+
* const result = await arweaveGatewayManager.runHealthCheck({
|
|
1325
|
+
* adjustPriorities: true,
|
|
1326
|
+
* });
|
|
1327
|
+
* console.log(`Fastest gateway: ${result.fastestGateway}`);
|
|
1328
|
+
* console.log(`Healthy gateways: ${result.healthyGateways.join(', ')}`);
|
|
1329
|
+
* ```
|
|
1330
|
+
*/
|
|
1331
|
+
async runHealthCheck(e) {
|
|
1332
|
+
const t = Date.now(), s = e?.txId ?? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijk012345", a = e?.healthyThreshold ?? 0.5;
|
|
1333
|
+
e?.fastThreshold, this.logger.info("Starting gateway health check", {
|
|
1334
|
+
txId: s,
|
|
1335
|
+
gateways: this.gateways.map((h) => h.host)
|
|
1336
|
+
});
|
|
1337
|
+
const r = this.gateways.map(async (h) => {
|
|
1338
|
+
const u = Date.now();
|
|
1339
|
+
let d = !1, m = this.timeout;
|
|
1340
|
+
try {
|
|
1341
|
+
const p = k(s, h), R = new AbortController(), P = setTimeout(() => R.abort(), this.timeout), v = await fetch(p, {
|
|
1342
|
+
method: "HEAD",
|
|
1343
|
+
mode: "cors",
|
|
1344
|
+
signal: R.signal
|
|
1345
|
+
});
|
|
1346
|
+
clearTimeout(P), m = Date.now() - u, d = v.ok || v.status >= 200 && v.status < 400;
|
|
1347
|
+
} catch {
|
|
1348
|
+
m = Date.now() - u, d = !1;
|
|
1349
|
+
}
|
|
1350
|
+
return this.recordGatewayResponse(h.host, m, d), {
|
|
1351
|
+
host: h.host,
|
|
1352
|
+
success: d,
|
|
1353
|
+
responseTime: m
|
|
1354
|
+
};
|
|
1355
|
+
}), l = await Promise.all(r), i = this.getAllGatewayHealth(), c = [], n = [];
|
|
1356
|
+
let y = null, w = 1 / 0;
|
|
1357
|
+
for (const h of l) {
|
|
1358
|
+
const u = i.find((d) => d.host === h.host);
|
|
1359
|
+
h.success && h.responseTime < w && (w = h.responseTime, y = h.host), (u.successRate >= a || u.totalChecks === 1) && h.success ? c.push(h.host) : n.push(h.host);
|
|
1360
|
+
}
|
|
1361
|
+
const o = Date.now() - t;
|
|
1362
|
+
e?.adjustPriorities && this.adjustGatewayPriorities({
|
|
1363
|
+
minChecks: e.minChecksForAdjustment ?? 1,
|
|
1364
|
+
healthyThreshold: a
|
|
1365
|
+
});
|
|
1366
|
+
const g = {
|
|
1367
|
+
timestamp: t,
|
|
1368
|
+
gateways: i,
|
|
1369
|
+
healthyGateways: c,
|
|
1370
|
+
unhealthyGateways: n,
|
|
1371
|
+
fastestGateway: y,
|
|
1372
|
+
totalTime: o
|
|
1373
|
+
};
|
|
1374
|
+
return this.logger.info("Health check complete", {
|
|
1375
|
+
healthyGateways: c,
|
|
1376
|
+
unhealthyGateways: n,
|
|
1377
|
+
fastestGateway: y,
|
|
1378
|
+
totalTime: o
|
|
1379
|
+
}), g;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Reset gateway priorities to their original values
|
|
1383
|
+
*/
|
|
1384
|
+
resetGatewayPriorities() {
|
|
1385
|
+
this.gateways.forEach((e) => {
|
|
1386
|
+
const t = this.originalPriorities.get(e.host);
|
|
1387
|
+
t !== void 0 && (e.priority = t);
|
|
1388
|
+
}), this.gateways.sort((e, t) => {
|
|
1389
|
+
const s = this.originalPriorities.get(e.host) ?? e.priority, a = this.originalPriorities.get(t.host) ?? t.priority;
|
|
1390
|
+
return s - a;
|
|
1391
|
+
}), this.logger.info("Gateway priorities reset to original values", {
|
|
1392
|
+
gateways: this.gateways.map((e) => ({ host: e.host, priority: e.priority }))
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Clear all health tracking data
|
|
1397
|
+
*/
|
|
1398
|
+
clearHealthData() {
|
|
1399
|
+
this.healthData.clear(), this.logger.info("Health data cleared");
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
const X = new K();
|
|
1403
|
+
export {
|
|
1404
|
+
K as A,
|
|
1405
|
+
O as D,
|
|
1406
|
+
M as K,
|
|
1407
|
+
D as L,
|
|
1408
|
+
X as a,
|
|
1409
|
+
N as b,
|
|
1410
|
+
k as c,
|
|
1411
|
+
V as d,
|
|
1412
|
+
j as g,
|
|
1413
|
+
A as i,
|
|
1414
|
+
T as p
|
|
1415
|
+
};
|