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.
@@ -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
+ };