@agirails/sdk 2.0.3 → 2.2.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 (151) hide show
  1. package/README.md +536 -87
  2. package/dist/adapters/BaseAdapter.js +2 -2
  3. package/dist/adapters/BaseAdapter.js.map +1 -1
  4. package/dist/adapters/BasicAdapter.d.ts.map +1 -1
  5. package/dist/adapters/BasicAdapter.js +8 -0
  6. package/dist/adapters/BasicAdapter.js.map +1 -1
  7. package/dist/adapters/StandardAdapter.d.ts +10 -5
  8. package/dist/adapters/StandardAdapter.d.ts.map +1 -1
  9. package/dist/adapters/StandardAdapter.js +19 -6
  10. package/dist/adapters/StandardAdapter.js.map +1 -1
  11. package/dist/builders/QuoteBuilder.js +1 -1
  12. package/dist/builders/QuoteBuilder.js.map +1 -1
  13. package/dist/cli/commands/config.js +1 -1
  14. package/dist/cli/commands/config.js.map +1 -1
  15. package/dist/cli/commands/simulate.js.map +1 -1
  16. package/dist/cli/commands/time.d.ts.map +1 -1
  17. package/dist/cli/commands/time.js.map +1 -1
  18. package/dist/config/networks.d.ts +9 -0
  19. package/dist/config/networks.d.ts.map +1 -1
  20. package/dist/config/networks.js +25 -10
  21. package/dist/config/networks.js.map +1 -1
  22. package/dist/index.d.ts +6 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +31 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/level0/provide.d.ts.map +1 -1
  27. package/dist/level0/provide.js +2 -1
  28. package/dist/level0/provide.js.map +1 -1
  29. package/dist/level0/request.d.ts.map +1 -1
  30. package/dist/level0/request.js +1 -2
  31. package/dist/level0/request.js.map +1 -1
  32. package/dist/level1/Agent.d.ts.map +1 -1
  33. package/dist/level1/Agent.js +11 -3
  34. package/dist/level1/Agent.js.map +1 -1
  35. package/dist/level1/pricing/PriceCalculator.js +1 -1
  36. package/dist/level1/pricing/PriceCalculator.js.map +1 -1
  37. package/dist/level1/types/Options.d.ts.map +1 -1
  38. package/dist/protocol/ACTPKernel.d.ts.map +1 -1
  39. package/dist/protocol/ACTPKernel.js +7 -5
  40. package/dist/protocol/ACTPKernel.js.map +1 -1
  41. package/dist/protocol/DIDResolver.js +1 -1
  42. package/dist/protocol/DIDResolver.js.map +1 -1
  43. package/dist/protocol/EASHelper.d.ts.map +1 -1
  44. package/dist/protocol/EASHelper.js +2 -3
  45. package/dist/protocol/EASHelper.js.map +1 -1
  46. package/dist/protocol/MessageSigner.d.ts.map +1 -1
  47. package/dist/protocol/MessageSigner.js +9 -9
  48. package/dist/protocol/MessageSigner.js.map +1 -1
  49. package/dist/protocol/ProofGenerator.d.ts.map +1 -1
  50. package/dist/protocol/ProofGenerator.js +1 -0
  51. package/dist/protocol/ProofGenerator.js.map +1 -1
  52. package/dist/runtime/BlockchainRuntime.d.ts +10 -3
  53. package/dist/runtime/BlockchainRuntime.d.ts.map +1 -1
  54. package/dist/runtime/BlockchainRuntime.js +41 -25
  55. package/dist/runtime/BlockchainRuntime.js.map +1 -1
  56. package/dist/runtime/IACTPRuntime.d.ts +15 -0
  57. package/dist/runtime/IACTPRuntime.d.ts.map +1 -1
  58. package/dist/runtime/MockRuntime.d.ts +7 -0
  59. package/dist/runtime/MockRuntime.d.ts.map +1 -1
  60. package/dist/runtime/MockRuntime.js +15 -4
  61. package/dist/runtime/MockRuntime.js.map +1 -1
  62. package/dist/runtime/types/MockState.d.ts +5 -2
  63. package/dist/runtime/types/MockState.d.ts.map +1 -1
  64. package/dist/runtime/types/MockState.js.map +1 -1
  65. package/dist/storage/ArchiveBundleBuilder.d.ts +150 -0
  66. package/dist/storage/ArchiveBundleBuilder.d.ts.map +1 -0
  67. package/dist/storage/ArchiveBundleBuilder.js +468 -0
  68. package/dist/storage/ArchiveBundleBuilder.js.map +1 -0
  69. package/dist/storage/ArweaveClient.d.ts +271 -0
  70. package/dist/storage/ArweaveClient.d.ts.map +1 -0
  71. package/dist/storage/ArweaveClient.js +761 -0
  72. package/dist/storage/ArweaveClient.js.map +1 -0
  73. package/dist/storage/FilebaseClient.d.ts +193 -0
  74. package/dist/storage/FilebaseClient.d.ts.map +1 -0
  75. package/dist/storage/FilebaseClient.js +643 -0
  76. package/dist/storage/FilebaseClient.js.map +1 -0
  77. package/dist/storage/index.d.ts +47 -0
  78. package/dist/storage/index.d.ts.map +1 -0
  79. package/dist/storage/index.js +64 -0
  80. package/dist/storage/index.js.map +1 -0
  81. package/dist/storage/types.d.ts +291 -0
  82. package/dist/storage/types.d.ts.map +1 -0
  83. package/dist/storage/types.js +18 -0
  84. package/dist/storage/types.js.map +1 -0
  85. package/dist/types/state.d.ts +5 -4
  86. package/dist/types/state.d.ts.map +1 -1
  87. package/dist/types/state.js +10 -9
  88. package/dist/types/state.js.map +1 -1
  89. package/dist/utils/ErrorRecoveryGuide.d.ts.map +1 -1
  90. package/dist/utils/ErrorRecoveryGuide.js +1 -2
  91. package/dist/utils/ErrorRecoveryGuide.js.map +1 -1
  92. package/dist/utils/IPFSClient.d.ts.map +1 -1
  93. package/dist/utils/IPFSClient.js +5 -2
  94. package/dist/utils/IPFSClient.js.map +1 -1
  95. package/dist/utils/NonceManager.d.ts.map +1 -1
  96. package/dist/utils/NonceManager.js +3 -2
  97. package/dist/utils/NonceManager.js.map +1 -1
  98. package/dist/utils/UsedAttestationTracker.d.ts +1 -1
  99. package/dist/utils/UsedAttestationTracker.d.ts.map +1 -1
  100. package/dist/utils/UsedAttestationTracker.js +4 -4
  101. package/dist/utils/UsedAttestationTracker.js.map +1 -1
  102. package/dist/utils/circuitBreaker.d.ts +136 -0
  103. package/dist/utils/circuitBreaker.d.ts.map +1 -0
  104. package/dist/utils/circuitBreaker.js +253 -0
  105. package/dist/utils/circuitBreaker.js.map +1 -0
  106. package/dist/utils/retry.d.ts +120 -0
  107. package/dist/utils/retry.d.ts.map +1 -0
  108. package/dist/utils/retry.js +260 -0
  109. package/dist/utils/retry.js.map +1 -0
  110. package/dist/utils/validation.d.ts +100 -0
  111. package/dist/utils/validation.d.ts.map +1 -1
  112. package/dist/utils/validation.js +248 -1
  113. package/dist/utils/validation.js.map +1 -1
  114. package/package.json +14 -2
  115. package/src/adapters/BaseAdapter.ts +2 -2
  116. package/src/adapters/BasicAdapter.ts +12 -2
  117. package/src/adapters/StandardAdapter.ts +27 -7
  118. package/src/builders/QuoteBuilder.ts +1 -1
  119. package/src/cli/commands/config.ts +1 -1
  120. package/src/cli/commands/simulate.ts +1 -1
  121. package/src/cli/commands/time.ts +1 -2
  122. package/src/config/networks.ts +34 -10
  123. package/src/index.ts +54 -0
  124. package/src/level0/provide.ts +2 -1
  125. package/src/level0/request.ts +1 -2
  126. package/src/level1/Agent.ts +15 -5
  127. package/src/level1/pricing/PriceCalculator.ts +1 -1
  128. package/src/level1/types/Options.ts +1 -1
  129. package/src/protocol/ACTPKernel.ts +7 -5
  130. package/src/protocol/DIDResolver.ts +1 -1
  131. package/src/protocol/EASHelper.ts +2 -5
  132. package/src/protocol/MessageSigner.ts +9 -15
  133. package/src/protocol/ProofGenerator.ts +1 -0
  134. package/src/runtime/BlockchainRuntime.ts +42 -48
  135. package/src/runtime/IACTPRuntime.ts +16 -0
  136. package/src/runtime/MockRuntime.ts +16 -6
  137. package/src/runtime/types/MockState.ts +5 -2
  138. package/src/storage/ArchiveBundleBuilder.ts +563 -0
  139. package/src/storage/ArweaveClient.ts +945 -0
  140. package/src/storage/FilebaseClient.ts +790 -0
  141. package/src/storage/index.ts +96 -0
  142. package/src/storage/types.ts +348 -0
  143. package/src/types/state.ts +10 -9
  144. package/src/utils/ErrorRecoveryGuide.ts +1 -2
  145. package/src/utils/IPFSClient.ts +5 -4
  146. package/src/utils/NonceManager.ts +3 -2
  147. package/src/utils/UsedAttestationTracker.ts +4 -6
  148. package/src/utils/circuitBreaker.ts +324 -0
  149. package/src/utils/fsSafe.ts +5 -0
  150. package/src/utils/retry.ts +365 -0
  151. package/src/utils/validation.ts +295 -1
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Circuit Breaker - Gateway Health Tracking (Retry Amplification Protection)
3
+ *
4
+ * Implements the Circuit Breaker pattern for storage gateways:
5
+ * - Tracks consecutive failures per gateway
6
+ * - "Opens" circuit after threshold failures (blocks requests)
7
+ * - Auto-resets after cooldown period
8
+ *
9
+ * This prevents retry amplification attacks where a malicious/failing
10
+ * gateway causes excessive retries.
11
+ *
12
+ * @module utils/circuitBreaker
13
+ */
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Circuit state
21
+ */
22
+ export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
23
+
24
+ /**
25
+ * Gateway health record
26
+ */
27
+ interface GatewayHealth {
28
+ /** Consecutive failure count */
29
+ failures: number;
30
+ /** Timestamp of last failure */
31
+ lastFailure: number;
32
+ /** Current circuit state */
33
+ state: CircuitState;
34
+ /** Timestamp when circuit opened */
35
+ openedAt?: number;
36
+ }
37
+
38
+ /**
39
+ * Circuit breaker configuration
40
+ */
41
+ export interface CircuitBreakerConfig {
42
+ /** Number of failures before opening circuit (default: 5) */
43
+ failureThreshold?: number;
44
+
45
+ /** Cooldown period in ms before attempting reset (default: 60000 = 1 min) */
46
+ resetTimeoutMs?: number;
47
+
48
+ /** Time window in ms for counting failures (default: 300000 = 5 min) */
49
+ failureWindowMs?: number;
50
+
51
+ /** Number of successes in half-open to close circuit (default: 2) */
52
+ successThreshold?: number;
53
+ }
54
+
55
+ // ============================================================================
56
+ // Constants
57
+ // ============================================================================
58
+
59
+ const DEFAULT_FAILURE_THRESHOLD = 5;
60
+ const DEFAULT_RESET_TIMEOUT_MS = 60000; // 1 minute
61
+ const DEFAULT_FAILURE_WINDOW_MS = 300000; // 5 minutes
62
+ const DEFAULT_SUCCESS_THRESHOLD = 2;
63
+
64
+ // ============================================================================
65
+ // GatewayCircuitBreaker Class
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Circuit Breaker for gateway health management
70
+ *
71
+ * Usage:
72
+ * ```typescript
73
+ * const circuitBreaker = new GatewayCircuitBreaker();
74
+ *
75
+ * // Before making request
76
+ * if (!circuitBreaker.isHealthy(gatewayUrl)) {
77
+ * throw new Error('Gateway circuit open');
78
+ * }
79
+ *
80
+ * try {
81
+ * const result = await fetchFromGateway(gatewayUrl);
82
+ * circuitBreaker.recordSuccess(gatewayUrl);
83
+ * return result;
84
+ * } catch (error) {
85
+ * circuitBreaker.recordFailure(gatewayUrl);
86
+ * throw error;
87
+ * }
88
+ * ```
89
+ */
90
+ export class GatewayCircuitBreaker {
91
+ private readonly health = new Map<string, GatewayHealth>();
92
+ private readonly config: Required<CircuitBreakerConfig>;
93
+ private halfOpenSuccesses = new Map<string, number>();
94
+
95
+ constructor(config: CircuitBreakerConfig = {}) {
96
+ this.config = {
97
+ failureThreshold: config.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD,
98
+ resetTimeoutMs: config.resetTimeoutMs ?? DEFAULT_RESET_TIMEOUT_MS,
99
+ failureWindowMs: config.failureWindowMs ?? DEFAULT_FAILURE_WINDOW_MS,
100
+ successThreshold: config.successThreshold ?? DEFAULT_SUCCESS_THRESHOLD
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Check if gateway is healthy (circuit closed or half-open)
106
+ *
107
+ * @param url - Gateway URL
108
+ * @returns true if requests should be allowed
109
+ */
110
+ isHealthy(url: string): boolean {
111
+ const state = this.getState(url);
112
+ return state !== 'OPEN';
113
+ }
114
+
115
+ /**
116
+ * Get current circuit state for gateway
117
+ *
118
+ * @param url - Gateway URL
119
+ * @returns Circuit state
120
+ */
121
+ getState(url: string): CircuitState {
122
+ const health = this.health.get(url);
123
+
124
+ if (!health) {
125
+ return 'CLOSED';
126
+ }
127
+
128
+ // Check if circuit should transition from OPEN to HALF_OPEN
129
+ if (health.state === 'OPEN') {
130
+ const timeSinceOpen = Date.now() - (health.openedAt || 0);
131
+ if (timeSinceOpen >= this.config.resetTimeoutMs) {
132
+ // Transition to half-open
133
+ health.state = 'HALF_OPEN';
134
+ this.halfOpenSuccesses.set(url, 0);
135
+ }
136
+ }
137
+
138
+ // Check if failures have expired (outside window)
139
+ if (health.state === 'CLOSED') {
140
+ const timeSinceLastFailure = Date.now() - health.lastFailure;
141
+ if (timeSinceLastFailure >= this.config.failureWindowMs) {
142
+ // Reset failure count
143
+ health.failures = 0;
144
+ }
145
+ }
146
+
147
+ return health.state;
148
+ }
149
+
150
+ /**
151
+ * Record a successful request
152
+ *
153
+ * @param url - Gateway URL
154
+ */
155
+ recordSuccess(url: string): void {
156
+ const health = this.health.get(url);
157
+
158
+ if (!health) {
159
+ return; // No failures recorded, nothing to do
160
+ }
161
+
162
+ if (health.state === 'HALF_OPEN') {
163
+ // Count successes in half-open state
164
+ const successes = (this.halfOpenSuccesses.get(url) || 0) + 1;
165
+ this.halfOpenSuccesses.set(url, successes);
166
+
167
+ if (successes >= this.config.successThreshold) {
168
+ // Enough successes - close circuit
169
+ this.reset(url);
170
+ }
171
+ } else if (health.state === 'CLOSED') {
172
+ // Reset failure count on success
173
+ health.failures = 0;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Record a failed request
179
+ *
180
+ * @param url - Gateway URL
181
+ */
182
+ recordFailure(url: string): void {
183
+ const now = Date.now();
184
+ let health = this.health.get(url);
185
+
186
+ if (!health) {
187
+ health = {
188
+ failures: 0,
189
+ lastFailure: now,
190
+ state: 'CLOSED'
191
+ };
192
+ this.health.set(url, health);
193
+ }
194
+
195
+ // If in half-open state, any failure opens circuit again
196
+ if (health.state === 'HALF_OPEN') {
197
+ health.state = 'OPEN';
198
+ health.openedAt = now;
199
+ health.failures = this.config.failureThreshold; // Keep at threshold
200
+ this.halfOpenSuccesses.delete(url);
201
+ return;
202
+ }
203
+
204
+ // Check if previous failures have expired
205
+ const timeSinceLastFailure = now - health.lastFailure;
206
+ if (timeSinceLastFailure >= this.config.failureWindowMs) {
207
+ health.failures = 0;
208
+ }
209
+
210
+ // Increment failure count
211
+ health.failures++;
212
+ health.lastFailure = now;
213
+
214
+ // Check if threshold reached
215
+ if (health.failures >= this.config.failureThreshold) {
216
+ health.state = 'OPEN';
217
+ health.openedAt = now;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Reset circuit for gateway (force close)
223
+ *
224
+ * @param url - Gateway URL
225
+ */
226
+ reset(url: string): void {
227
+ this.health.delete(url);
228
+ this.halfOpenSuccesses.delete(url);
229
+ }
230
+
231
+ /**
232
+ * Reset all circuits
233
+ */
234
+ resetAll(): void {
235
+ this.health.clear();
236
+ this.halfOpenSuccesses.clear();
237
+ }
238
+
239
+ /**
240
+ * Get health status for all tracked gateways
241
+ *
242
+ * @returns Map of gateway URL to health info
243
+ */
244
+ getHealthStatus(): Map<string, { state: CircuitState; failures: number }> {
245
+ const status = new Map<string, { state: CircuitState; failures: number }>();
246
+
247
+ for (const [url, health] of this.health) {
248
+ status.set(url, {
249
+ state: this.getState(url), // This updates state if needed
250
+ failures: health.failures
251
+ });
252
+ }
253
+
254
+ return status;
255
+ }
256
+
257
+ /**
258
+ * Get failure count for gateway
259
+ *
260
+ * @param url - Gateway URL
261
+ * @returns Number of consecutive failures
262
+ */
263
+ getFailureCount(url: string): number {
264
+ return this.health.get(url)?.failures || 0;
265
+ }
266
+ }
267
+
268
+ // ============================================================================
269
+ // Singleton Instance
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Default global circuit breaker instance
274
+ *
275
+ * Use this for simple cases where a single circuit breaker is sufficient.
276
+ * For more control, create your own GatewayCircuitBreaker instance.
277
+ */
278
+ export const globalCircuitBreaker = new GatewayCircuitBreaker();
279
+
280
+ // ============================================================================
281
+ // Helper Functions
282
+ // ============================================================================
283
+
284
+ /**
285
+ * Wrap an async operation with circuit breaker protection
286
+ *
287
+ * @param url - Gateway URL to track
288
+ * @param operation - Async operation to execute
289
+ * @param circuitBreaker - Circuit breaker instance (default: global)
290
+ * @returns Operation result
291
+ * @throws Error if circuit is open, or operation error
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const result = await withCircuitBreaker(
296
+ * 'https://ipfs.filebase.io',
297
+ * () => fetch(url).then(r => r.json())
298
+ * );
299
+ * ```
300
+ */
301
+ export async function withCircuitBreaker<T>(
302
+ url: string,
303
+ operation: () => Promise<T>,
304
+ circuitBreaker: GatewayCircuitBreaker = globalCircuitBreaker
305
+ ): Promise<T> {
306
+ // Check if circuit allows request
307
+ if (!circuitBreaker.isHealthy(url)) {
308
+ const state = circuitBreaker.getState(url);
309
+ throw new Error(
310
+ `Circuit breaker OPEN for gateway: ${url}. ` +
311
+ `State: ${state}, Failures: ${circuitBreaker.getFailureCount(url)}. ` +
312
+ `Please wait for cooldown period before retrying.`
313
+ );
314
+ }
315
+
316
+ try {
317
+ const result = await operation();
318
+ circuitBreaker.recordSuccess(url);
319
+ return result;
320
+ } catch (error) {
321
+ circuitBreaker.recordFailure(url);
322
+ throw error;
323
+ }
324
+ }
@@ -73,3 +73,8 @@ export function ensureSafeFile(
73
73
 
74
74
 
75
75
 
76
+
77
+
78
+
79
+
80
+
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Retry Utility - Exponential Backoff with Jitter (P1-2)
3
+ *
4
+ * Implements retry logic for storage operations with:
5
+ * - Exponential backoff (doubles delay each attempt)
6
+ * - Random jitter (prevents thundering herd)
7
+ * - Maximum retry limit
8
+ * - Configurable retry conditions
9
+ *
10
+ * @module utils/retry
11
+ */
12
+
13
+ import { StorageRateLimitError } from '../errors';
14
+
15
+ // ============================================================================
16
+ // Types
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Retry configuration options
21
+ */
22
+ export interface RetryOptions {
23
+ /** Maximum number of retry attempts (default: 3) */
24
+ maxAttempts?: number;
25
+
26
+ /** Initial delay in milliseconds (default: 1000) */
27
+ initialDelayMs?: number;
28
+
29
+ /** Maximum delay in milliseconds (default: 30000) */
30
+ maxDelayMs?: number;
31
+
32
+ /** Multiplier for exponential backoff (default: 2) */
33
+ backoffMultiplier?: number;
34
+
35
+ /** Jitter factor 0-1 (default: 0.1 = ±10%) */
36
+ jitterFactor?: number;
37
+
38
+ /** Function to determine if error is retryable (default: built-in check) */
39
+ isRetryable?: (error: unknown) => boolean;
40
+
41
+ /** Callback for each retry attempt (for logging) */
42
+ onRetry?: (attempt: number, error: unknown, delayMs: number) => void;
43
+ }
44
+
45
+ /**
46
+ * Result of retry operation
47
+ */
48
+ export interface RetryResult<T> {
49
+ /** Operation result (if successful) */
50
+ result?: T;
51
+
52
+ /** Final error (if all retries failed) */
53
+ error?: unknown;
54
+
55
+ /** Number of attempts made */
56
+ attempts: number;
57
+
58
+ /** Total time spent (including delays) */
59
+ totalTimeMs: number;
60
+
61
+ /** Whether operation succeeded */
62
+ success: boolean;
63
+ }
64
+
65
+ // ============================================================================
66
+ // Constants
67
+ // ============================================================================
68
+
69
+ const DEFAULT_MAX_ATTEMPTS = 3;
70
+ const DEFAULT_INITIAL_DELAY_MS = 1000;
71
+ const DEFAULT_MAX_DELAY_MS = 10000; // Reduced from 30s to 10s (NEW-4: prevent long stalls)
72
+ const DEFAULT_BACKOFF_MULTIPLIER = 2;
73
+ const DEFAULT_JITTER_FACTOR = 0.1;
74
+
75
+ /**
76
+ * Default list of retryable HTTP status codes
77
+ */
78
+ const RETRYABLE_STATUS_CODES = new Set([
79
+ 408, // Request Timeout
80
+ 429, // Too Many Requests
81
+ 500, // Internal Server Error
82
+ 502, // Bad Gateway
83
+ 503, // Service Unavailable
84
+ 504 // Gateway Timeout
85
+ ]);
86
+
87
+ /**
88
+ * Default list of retryable error codes
89
+ */
90
+ const RETRYABLE_ERROR_CODES = new Set([
91
+ 'ECONNRESET',
92
+ 'ECONNREFUSED',
93
+ 'ETIMEDOUT',
94
+ 'ENOTFOUND',
95
+ 'ENETUNREACH',
96
+ 'EAI_AGAIN',
97
+ 'EPIPE',
98
+ 'EHOSTUNREACH'
99
+ ]);
100
+
101
+ // ============================================================================
102
+ // Functions
103
+ // ============================================================================
104
+
105
+ /**
106
+ * Default function to determine if error is retryable
107
+ *
108
+ * @param error - Error to check
109
+ * @returns True if error should trigger retry
110
+ */
111
+ export function isRetryableError(error: unknown): boolean {
112
+ if (!error) return false;
113
+
114
+ // Rate limit errors are always retryable
115
+ if (error instanceof StorageRateLimitError) {
116
+ return true;
117
+ }
118
+
119
+ const e = error as any;
120
+
121
+ // Check HTTP status codes
122
+ if (e.statusCode && RETRYABLE_STATUS_CODES.has(e.statusCode)) {
123
+ return true;
124
+ }
125
+
126
+ if (e.status && RETRYABLE_STATUS_CODES.has(e.status)) {
127
+ return true;
128
+ }
129
+
130
+ // Check error codes (network errors)
131
+ if (e.code && RETRYABLE_ERROR_CODES.has(e.code)) {
132
+ return true;
133
+ }
134
+
135
+ // Check for timeout errors
136
+ if (e.name === 'AbortError' || e.name === 'TimeoutError') {
137
+ return true;
138
+ }
139
+
140
+ // Check message for common retryable patterns
141
+ const message = String(e.message || e).toLowerCase();
142
+ if (
143
+ message.includes('timeout') ||
144
+ message.includes('rate limit') ||
145
+ message.includes('too many requests') ||
146
+ message.includes('service unavailable') ||
147
+ message.includes('temporarily unavailable')
148
+ ) {
149
+ return true;
150
+ }
151
+
152
+ return false;
153
+ }
154
+
155
+ /**
156
+ * Calculate delay with exponential backoff and jitter
157
+ *
158
+ * @param attempt - Current attempt number (1-based)
159
+ * @param options - Retry options
160
+ * @returns Delay in milliseconds
161
+ */
162
+ export function calculateBackoffDelay(
163
+ attempt: number,
164
+ options: RetryOptions = {}
165
+ ): number {
166
+ const {
167
+ initialDelayMs = DEFAULT_INITIAL_DELAY_MS,
168
+ maxDelayMs = DEFAULT_MAX_DELAY_MS,
169
+ backoffMultiplier = DEFAULT_BACKOFF_MULTIPLIER,
170
+ jitterFactor = DEFAULT_JITTER_FACTOR
171
+ } = options;
172
+
173
+ // Exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
174
+ const exponentialDelay = initialDelayMs * Math.pow(backoffMultiplier, attempt - 1);
175
+
176
+ // Cap at max delay
177
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
178
+
179
+ // Add jitter: ±jitterFactor
180
+ const jitter = cappedDelay * jitterFactor * (Math.random() * 2 - 1);
181
+ const finalDelay = Math.max(0, cappedDelay + jitter);
182
+
183
+ return Math.round(finalDelay);
184
+ }
185
+
186
+ /**
187
+ * Sleep for specified duration
188
+ *
189
+ * @param ms - Milliseconds to sleep
190
+ */
191
+ function sleep(ms: number): Promise<void> {
192
+ return new Promise(resolve => setTimeout(resolve, ms));
193
+ }
194
+
195
+ /**
196
+ * Execute async operation with retry logic
197
+ *
198
+ * Implements exponential backoff with jitter for handling transient failures.
199
+ * Particularly useful for storage operations that may fail due to:
200
+ * - Rate limiting
201
+ * - Network timeouts
202
+ * - Temporary service unavailability
203
+ *
204
+ * @param operation - Async function to execute
205
+ * @param options - Retry configuration
206
+ * @returns Promise resolving to operation result or throwing final error
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * // Basic usage
211
+ * const result = await withRetry(
212
+ * () => client.uploadJSON(data),
213
+ * { maxAttempts: 3 }
214
+ * );
215
+ *
216
+ * // With logging
217
+ * const result = await withRetry(
218
+ * () => client.downloadJSON(cid),
219
+ * {
220
+ * maxAttempts: 5,
221
+ * onRetry: (attempt, error, delay) => {
222
+ * console.log(`Retry ${attempt}, waiting ${delay}ms:`, error);
223
+ * }
224
+ * }
225
+ * );
226
+ * ```
227
+ */
228
+ export async function withRetry<T>(
229
+ operation: () => Promise<T>,
230
+ options: RetryOptions = {}
231
+ ): Promise<T> {
232
+ const {
233
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
234
+ isRetryable = isRetryableError,
235
+ onRetry
236
+ } = options;
237
+
238
+ let lastError: unknown;
239
+ const startTime = Date.now();
240
+
241
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
242
+ try {
243
+ return await operation();
244
+ } catch (error) {
245
+ lastError = error;
246
+
247
+ // Check if this is the last attempt
248
+ if (attempt >= maxAttempts) {
249
+ break;
250
+ }
251
+
252
+ // Check if error is retryable
253
+ if (!isRetryable(error)) {
254
+ break;
255
+ }
256
+
257
+ // Calculate delay
258
+ let delayMs = calculateBackoffDelay(attempt, options);
259
+
260
+ // If rate limit error with retry-after header, use that instead
261
+ if (error instanceof StorageRateLimitError && error.details?.retryAfter) {
262
+ delayMs = Math.max(delayMs, error.details.retryAfter * 1000);
263
+ }
264
+
265
+ // Notify callback
266
+ if (onRetry) {
267
+ onRetry(attempt, error, delayMs);
268
+ }
269
+
270
+ // Wait before retry
271
+ await sleep(delayMs);
272
+ }
273
+ }
274
+
275
+ // All retries failed
276
+ throw lastError;
277
+ }
278
+
279
+ /**
280
+ * Execute async operation with retry logic, returning detailed result
281
+ *
282
+ * Unlike `withRetry`, this function never throws - it returns a result object
283
+ * with success/failure status and metadata.
284
+ *
285
+ * @param operation - Async function to execute
286
+ * @param options - Retry configuration
287
+ * @returns Promise resolving to detailed result object
288
+ *
289
+ * @example
290
+ * ```typescript
291
+ * const result = await withRetryResult(
292
+ * () => client.uploadJSON(data),
293
+ * { maxAttempts: 3 }
294
+ * );
295
+ *
296
+ * if (result.success) {
297
+ * console.log('Uploaded:', result.result);
298
+ * } else {
299
+ * console.error(`Failed after ${result.attempts} attempts:`, result.error);
300
+ * }
301
+ * ```
302
+ */
303
+ export async function withRetryResult<T>(
304
+ operation: () => Promise<T>,
305
+ options: RetryOptions = {}
306
+ ): Promise<RetryResult<T>> {
307
+ const {
308
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
309
+ isRetryable = isRetryableError,
310
+ onRetry
311
+ } = options;
312
+
313
+ let lastError: unknown;
314
+ const startTime = Date.now();
315
+ let attempts = 0;
316
+
317
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
318
+ attempts = attempt;
319
+
320
+ try {
321
+ const result = await operation();
322
+ return {
323
+ result,
324
+ attempts,
325
+ totalTimeMs: Date.now() - startTime,
326
+ success: true
327
+ };
328
+ } catch (error) {
329
+ lastError = error;
330
+
331
+ // Check if this is the last attempt
332
+ if (attempt >= maxAttempts) {
333
+ break;
334
+ }
335
+
336
+ // Check if error is retryable
337
+ if (!isRetryable(error)) {
338
+ break;
339
+ }
340
+
341
+ // Calculate delay
342
+ let delayMs = calculateBackoffDelay(attempt, options);
343
+
344
+ // If rate limit error with retry-after header, use that instead
345
+ if (error instanceof StorageRateLimitError && error.details?.retryAfter) {
346
+ delayMs = Math.max(delayMs, error.details.retryAfter * 1000);
347
+ }
348
+
349
+ // Notify callback
350
+ if (onRetry) {
351
+ onRetry(attempt, error, delayMs);
352
+ }
353
+
354
+ // Wait before retry
355
+ await sleep(delayMs);
356
+ }
357
+ }
358
+
359
+ return {
360
+ error: lastError,
361
+ attempts,
362
+ totalTimeMs: Date.now() - startTime,
363
+ success: false
364
+ };
365
+ }