@monygroupcorp/micro-web3 1.3.6 → 1.3.8

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.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * IPFS Service
3
- *
3
+ *
4
4
  * Light client for IPFS resolution with gateway rotation and fallback.
5
5
  * Handles both direct IPFS URIs and metadata fetching.
6
- *
6
+ *
7
7
  * No secrets, no backend - pure client-side IPFS gateway resolution.
8
8
  */
9
9
 
@@ -19,6 +19,265 @@ const IPFS_GATEWAYS = [
19
19
  'https://dweb.link/ipfs/'
20
20
  ];
21
21
 
22
+ /**
23
+ * CORS-friendly gateways that work well in development environments
24
+ */
25
+ const CORS_FRIENDLY_GATEWAYS = [
26
+ 'https://cloudflare-ipfs.com/ipfs/',
27
+ 'https://dweb.link/ipfs/',
28
+ 'https://w3s.link/ipfs/'
29
+ ];
30
+
31
+ /**
32
+ * Well-known CID for health checks (empty directory)
33
+ */
34
+ const HEALTH_CHECK_CID = 'bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi';
35
+
36
+ /**
37
+ * Internal GatewayManager class for intelligent gateway selection and rotation
38
+ */
39
+ class GatewayManager {
40
+ constructor() {
41
+ this.currentGateway = null;
42
+ this.gatewayLatencies = new Map();
43
+ this.failedGateways = new Set();
44
+ this.failureCounts = new Map();
45
+ this.lastHealthCheck = null;
46
+ this.healthCheckInterval = null;
47
+ this.initialized = false;
48
+ this.config = {
49
+ timeout: 5000,
50
+ maxRetries: 3,
51
+ healthCheckInterval: 60000,
52
+ parallelDiscovery: true,
53
+ corsGatewaysFirst: true
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Detect current environment (development vs production)
59
+ * @returns {'development'|'production'}
60
+ */
61
+ getEnvironment() {
62
+ if (typeof window === 'undefined') return 'production';
63
+ const hostname = window.location.hostname;
64
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local')
65
+ ? 'development'
66
+ : 'production';
67
+ }
68
+
69
+ /**
70
+ * Get gateways prioritized based on environment and performance
71
+ * @returns {string[]} Prioritized gateway list
72
+ */
73
+ getPrioritizedGateways() {
74
+ const custom = getCustomGateway();
75
+ const baseGateways = custom ? [custom, ...IPFS_GATEWAYS] : IPFS_GATEWAYS;
76
+
77
+ if (!this.config.corsGatewaysFirst || this.getEnvironment() !== 'development') {
78
+ return baseGateways;
79
+ }
80
+
81
+ // In dev, put CORS-friendly gateways first
82
+ const corsFirst = baseGateways.filter(g => CORS_FRIENDLY_GATEWAYS.some(c => g.includes(c)));
83
+ const others = baseGateways.filter(g => !CORS_FRIENDLY_GATEWAYS.some(c => g.includes(c)));
84
+ return [...corsFirst, ...others];
85
+ }
86
+
87
+ /**
88
+ * Record a successful request to a gateway
89
+ * @param {string} gateway - Gateway URL
90
+ * @param {number} latency - Request latency in ms
91
+ */
92
+ recordSuccess(gateway, latency) {
93
+ this.gatewayLatencies.set(gateway, latency);
94
+ this.failedGateways.delete(gateway);
95
+ this.failureCounts.delete(gateway);
96
+ if (!this.currentGateway || latency < (this.gatewayLatencies.get(this.currentGateway) || Infinity)) {
97
+ this.currentGateway = gateway;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Record a failed request to a gateway
103
+ * @param {string} gateway - Gateway URL
104
+ */
105
+ recordFailure(gateway) {
106
+ const count = (this.failureCounts.get(gateway) || 0) + 1;
107
+ this.failureCounts.set(gateway, count);
108
+ if (count >= this.config.maxRetries) {
109
+ this.failedGateways.add(gateway);
110
+ this.gatewayLatencies.set(gateway, Infinity);
111
+ if (this.currentGateway === gateway) {
112
+ this.currentGateway = this.getNextBestGateway();
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Get the next best gateway based on latency
119
+ * @returns {string|undefined} Next best gateway URL
120
+ */
121
+ getNextBestGateway() {
122
+ return this.getPrioritizedGateways()
123
+ .filter(g => !this.failedGateways.has(g))
124
+ .sort((a, b) =>
125
+ (this.gatewayLatencies.get(a) || Infinity) -
126
+ (this.gatewayLatencies.get(b) || Infinity)
127
+ )[0];
128
+ }
129
+
130
+ /**
131
+ * Select the best gateway by racing all gateways
132
+ * @param {string} testCid - CID to use for testing
133
+ * @returns {Promise<string>} Best gateway URL
134
+ */
135
+ async selectBestGateway(testCid = HEALTH_CHECK_CID) {
136
+ const gateways = this.getPrioritizedGateways();
137
+
138
+ const results = await Promise.allSettled(
139
+ gateways.map(async (gateway) => {
140
+ const start = Date.now();
141
+ const response = await fetch(`${gateway}${testCid}`, {
142
+ method: 'HEAD',
143
+ signal: AbortSignal.timeout(this.config.timeout)
144
+ });
145
+ if (!response.ok) throw new Error('Failed');
146
+ return { gateway, latency: Date.now() - start };
147
+ })
148
+ );
149
+
150
+ const successful = results
151
+ .filter(r => r.status === 'fulfilled')
152
+ .map(r => r.value)
153
+ .sort((a, b) => a.latency - b.latency);
154
+
155
+ // Record all successful results
156
+ successful.forEach(({ gateway, latency }) => this.recordSuccess(gateway, latency));
157
+
158
+ // Record failures
159
+ results.forEach((result, i) => {
160
+ if (result.status === 'rejected') {
161
+ this.recordFailure(gateways[i]);
162
+ }
163
+ });
164
+
165
+ this.currentGateway = successful[0]?.gateway || gateways[0];
166
+ this.initialized = true;
167
+ return this.currentGateway;
168
+ }
169
+
170
+ /**
171
+ * Fetch with intelligent gateway rotation
172
+ * @param {string} path - IPFS path to fetch
173
+ * @param {object} options - Fetch options
174
+ * @returns {Promise<object>} Parsed JSON response
175
+ */
176
+ async fetchWithRotation(path, options = {}) {
177
+ const { timeout = this.config.timeout, ...fetchOptions } = options;
178
+ const gateways = this.getPrioritizedGateways();
179
+ const errors = [];
180
+
181
+ // Start with current best or first gateway
182
+ let startIndex = this.currentGateway
183
+ ? gateways.indexOf(this.currentGateway)
184
+ : 0;
185
+ if (startIndex === -1) startIndex = 0;
186
+
187
+ for (let attempt = 0; attempt < gateways.length; attempt++) {
188
+ const index = (startIndex + attempt) % gateways.length;
189
+ const gateway = gateways[index];
190
+
191
+ if (this.failedGateways.has(gateway)) continue;
192
+
193
+ const controller = new AbortController();
194
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
195
+ const start = Date.now();
196
+
197
+ try {
198
+ const response = await fetch(`${gateway}${path}`, {
199
+ ...fetchOptions,
200
+ signal: controller.signal
201
+ });
202
+ clearTimeout(timeoutId);
203
+
204
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
205
+
206
+ const json = await response.json();
207
+ this.recordSuccess(gateway, Date.now() - start);
208
+ return json;
209
+ } catch (error) {
210
+ clearTimeout(timeoutId);
211
+ this.recordFailure(gateway);
212
+ errors.push(`${gateway}: ${error.message}`);
213
+ }
214
+ }
215
+
216
+ throw new Error(`All gateways failed:\n${errors.join('\n')}`);
217
+ }
218
+
219
+ /**
220
+ * Start background health monitoring
221
+ */
222
+ startHealthMonitoring() {
223
+ if (this.healthCheckInterval) return;
224
+ if (this.config.healthCheckInterval <= 0) return;
225
+
226
+ this.healthCheckInterval = setInterval(
227
+ () => this.runHealthCheck(),
228
+ this.config.healthCheckInterval
229
+ );
230
+ }
231
+
232
+ /**
233
+ * Stop background health monitoring
234
+ */
235
+ stopHealthMonitoring() {
236
+ if (this.healthCheckInterval) {
237
+ clearInterval(this.healthCheckInterval);
238
+ this.healthCheckInterval = null;
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Run a health check on all gateways
244
+ */
245
+ async runHealthCheck() {
246
+ const gateways = this.getPrioritizedGateways();
247
+ this.lastHealthCheck = Date.now();
248
+
249
+ for (const gateway of gateways) {
250
+ try {
251
+ const start = Date.now();
252
+ const response = await fetch(`${gateway}${HEALTH_CHECK_CID}`, {
253
+ method: 'HEAD',
254
+ signal: AbortSignal.timeout(3000)
255
+ });
256
+ if (response.ok) {
257
+ this.recordSuccess(gateway, Date.now() - start);
258
+ }
259
+ } catch {
260
+ // Don't mark as failed from health check, just update latency
261
+ this.gatewayLatencies.set(gateway, Infinity);
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Reset gateway manager state
268
+ */
269
+ reset() {
270
+ this.currentGateway = null;
271
+ this.gatewayLatencies.clear();
272
+ this.failedGateways.clear();
273
+ this.failureCounts.clear();
274
+ this.initialized = false;
275
+ }
276
+ }
277
+
278
+ // Singleton instance
279
+ const gatewayManager = new GatewayManager();
280
+
22
281
  /**
23
282
  * Get custom IPFS gateway from localStorage (user preference)
24
283
  * @returns {string|null} Custom gateway base URL or null
@@ -48,8 +307,7 @@ function getCustomGateway() {
48
307
  * @returns {string[]} Array of gateway base URLs
49
308
  */
50
309
  function getAllGateways() {
51
- const custom = getCustomGateway();
52
- return custom ? [custom, ...IPFS_GATEWAYS] : IPFS_GATEWAYS;
310
+ return gatewayManager.getPrioritizedGateways();
53
311
  }
54
312
 
55
313
  /**
@@ -85,22 +343,28 @@ export function normalizeIpfsPath(ipfsUri) {
85
343
  /**
86
344
  * Resolve IPFS URI to HTTP URL using a specific gateway
87
345
  * @param {string} ipfsUri - IPFS URI (e.g., ipfs://Qm...)
88
- * @param {number} gatewayIndex - Index of gateway to use (0-based)
346
+ * @param {number} [gatewayIndex] - Index of gateway to use (0-based). If omitted, uses intelligent selection.
89
347
  * @returns {string|null} HTTP URL or null if invalid
90
348
  */
91
- export function resolveIpfsToHttp(ipfsUri, gatewayIndex = 0) {
349
+ export function resolveIpfsToHttp(ipfsUri, gatewayIndex) {
92
350
  if (!isIpfsUri(ipfsUri)) {
93
351
  return ipfsUri; // Return as-is if not IPFS
94
352
  }
95
-
353
+
354
+ const normalizedPath = normalizeIpfsPath(ipfsUri);
355
+
356
+ // If no index provided, use intelligent gateway selection
357
+ if (gatewayIndex === undefined) {
358
+ const gateway = gatewayManager.currentGateway || gatewayManager.getPrioritizedGateways()[0];
359
+ return gateway + normalizedPath;
360
+ }
361
+
96
362
  const gateways = getAllGateways();
97
363
  if (gatewayIndex < 0 || gatewayIndex >= gateways.length) {
98
364
  return null;
99
365
  }
100
-
101
- const normalizedPath = normalizeIpfsPath(ipfsUri);
366
+
102
367
  const gateway = gateways[gatewayIndex];
103
-
104
368
  return gateway + normalizedPath;
105
369
  }
106
370
 
@@ -113,22 +377,22 @@ export function resolveIpfsToHttp(ipfsUri, gatewayIndex = 0) {
113
377
  */
114
378
  export async function fetchJsonWithIpfsSupport(urlOrIpfs, options = {}) {
115
379
  const { timeout = 10000, ...fetchOptions } = options;
116
-
380
+
117
381
  // If it's not an IPFS URI, use regular fetch
118
382
  if (!isIpfsUri(urlOrIpfs)) {
119
383
  const controller = new AbortController();
120
384
  const timeoutId = setTimeout(() => controller.abort(), timeout);
121
-
385
+
122
386
  try {
123
387
  const response = await fetch(urlOrIpfs, {
124
388
  ...fetchOptions,
125
389
  signal: controller.signal
126
390
  });
127
-
391
+
128
392
  if (!response.ok) {
129
393
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
130
394
  }
131
-
395
+
132
396
  const json = await response.json();
133
397
  clearTimeout(timeoutId);
134
398
  return json;
@@ -140,58 +404,10 @@ export async function fetchJsonWithIpfsSupport(urlOrIpfs, options = {}) {
140
404
  throw error;
141
405
  }
142
406
  }
143
-
144
- // IPFS URI - try gateways in order
145
- const gateways = getAllGateways();
407
+
408
+ // IPFS URI - use intelligent gateway rotation
146
409
  const normalizedPath = normalizeIpfsPath(urlOrIpfs);
147
- const errors = [];
148
-
149
- for (let i = 0; i < gateways.length; i++) {
150
- const gateway = gateways[i];
151
- const httpUrl = gateway + normalizedPath;
152
-
153
- const controller = new AbortController();
154
- const timeoutId = setTimeout(() => controller.abort(), timeout);
155
-
156
- try {
157
- const response = await fetch(httpUrl, {
158
- ...fetchOptions,
159
- signal: controller.signal
160
- });
161
-
162
- clearTimeout(timeoutId);
163
-
164
- if (!response.ok) {
165
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
166
- }
167
-
168
- const json = await response.json();
169
-
170
- // Success - log which gateway worked (for debugging)
171
- if (i > 0) {
172
- console.log(`[IpfsService] Resolved IPFS via gateway ${i + 1}/${gateways.length}: ${gateway}`);
173
- }
174
-
175
- return json;
176
- } catch (error) {
177
- clearTimeout(timeoutId);
178
-
179
- const errorMsg = error.name === 'AbortError'
180
- ? `Timeout after ${timeout}ms`
181
- : error.message || 'Unknown error';
182
-
183
- errors.push(`Gateway ${i + 1} (${gateway}): ${errorMsg}`);
184
-
185
- // Continue to next gateway
186
- continue;
187
- }
188
- }
189
-
190
- // All gateways failed
191
- throw new Error(
192
- `Failed to fetch IPFS content after trying ${gateways.length} gateways:\n` +
193
- errors.join('\n')
194
- );
410
+ return gatewayManager.fetchWithRotation(normalizedPath, { timeout, ...fetchOptions });
195
411
  }
196
412
 
197
413
  /**
@@ -242,8 +458,86 @@ export function setCustomGateway(gatewayBaseUrl) {
242
458
  } else {
243
459
  localStorage.removeItem('customIpfsGatewayBaseUrl');
244
460
  }
461
+ // Reset gateway manager state when custom gateway changes
462
+ gatewayManager.reset();
245
463
  } catch (error) {
246
464
  console.warn('[IpfsService] Failed to save custom gateway to localStorage:', error);
247
465
  }
248
466
  }
249
467
 
468
+ // ============================================================================
469
+ // New exports for intelligent gateway management
470
+ // ============================================================================
471
+
472
+ /**
473
+ * Get the current best gateway
474
+ * @returns {string} Current best gateway URL
475
+ */
476
+ export function getGateway() {
477
+ return gatewayManager.currentGateway || gatewayManager.getPrioritizedGateways()[0];
478
+ }
479
+
480
+ /**
481
+ * Resolve an IPFS URI to an HTTP URL using the best gateway
482
+ * Simpler alias for resolveIpfsToHttp without gateway index
483
+ * @param {string} ipfsUri - IPFS URI to resolve
484
+ * @returns {string} HTTP URL
485
+ */
486
+ export function resolveUrl(ipfsUri) {
487
+ if (!isIpfsUri(ipfsUri)) return ipfsUri;
488
+ return getGateway() + normalizeIpfsPath(ipfsUri);
489
+ }
490
+
491
+ /**
492
+ * Fetch JSON from IPFS with intelligent gateway rotation
493
+ * Cleaner API that assumes IPFS CID input
494
+ * @param {string} cid - IPFS CID or path
495
+ * @param {object} options - Fetch options
496
+ * @returns {Promise<object>} Parsed JSON
497
+ */
498
+ export function fetchJson(cid, options = {}) {
499
+ return gatewayManager.fetchWithRotation(cid, options);
500
+ }
501
+
502
+ /**
503
+ * Get gateway statistics
504
+ * @returns {object} Gateway stats including current, latencies, failed, lastHealthCheck, initialized
505
+ */
506
+ export function getGatewayStats() {
507
+ return {
508
+ current: gatewayManager.currentGateway,
509
+ latencies: Object.fromEntries(gatewayManager.gatewayLatencies),
510
+ failed: Array.from(gatewayManager.failedGateways),
511
+ lastHealthCheck: gatewayManager.lastHealthCheck,
512
+ initialized: gatewayManager.initialized
513
+ };
514
+ }
515
+
516
+ /**
517
+ * Initialize gateway discovery by racing all gateways
518
+ * @param {string} [testCid] - Optional CID to use for testing
519
+ * @returns {Promise<string>} Best gateway URL
520
+ */
521
+ export async function initializeGatewayDiscovery(testCid) {
522
+ return gatewayManager.selectBestGateway(testCid);
523
+ }
524
+
525
+ /**
526
+ * Configure the gateway manager
527
+ * @param {object} options - Configuration options
528
+ * @param {number} [options.timeout] - Request timeout in ms
529
+ * @param {number} [options.maxRetries] - Max retries before marking gateway as failed
530
+ * @param {number} [options.healthCheckInterval] - Health check interval in ms (0 to disable)
531
+ * @param {boolean} [options.parallelDiscovery] - Whether to race gateways in parallel
532
+ * @param {boolean} [options.corsGatewaysFirst] - Whether to prioritize CORS-friendly gateways in dev
533
+ */
534
+ export function configureGatewayManager(options) {
535
+ Object.assign(gatewayManager.config, options);
536
+ if (options.healthCheckInterval !== undefined) {
537
+ gatewayManager.stopHealthMonitoring();
538
+ if (options.healthCheckInterval > 0) {
539
+ gatewayManager.startHealthMonitoring();
540
+ }
541
+ }
542
+ }
543
+