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