@uploadista/client-browser 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-check.log +130 -0
- package/AUTO_CAPABILITIES.md +98 -0
- package/FRAMEWORK_INTEGRATION.md +407 -0
- package/LICENSE +21 -0
- package/README.md +795 -0
- package/SMART_CHUNKING.md +140 -0
- package/dist/client/create-uploadista-client.d.ts +182 -0
- package/dist/client/create-uploadista-client.d.ts.map +1 -0
- package/dist/client/create-uploadista-client.js +76 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +1 -0
- package/dist/framework-utils.d.ts +201 -0
- package/dist/framework-utils.d.ts.map +1 -0
- package/dist/framework-utils.js +282 -0
- package/dist/http-client.d.ts +44 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +489 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/services/abort-controller-factory.d.ts +30 -0
- package/dist/services/abort-controller-factory.d.ts.map +1 -0
- package/dist/services/abort-controller-factory.js +98 -0
- package/dist/services/checksum-service.d.ts +30 -0
- package/dist/services/checksum-service.d.ts.map +1 -0
- package/dist/services/checksum-service.js +44 -0
- package/dist/services/create-browser-services.d.ts +36 -0
- package/dist/services/create-browser-services.d.ts.map +1 -0
- package/dist/services/create-browser-services.js +56 -0
- package/dist/services/file-reader.d.ts +91 -0
- package/dist/services/file-reader.d.ts.map +1 -0
- package/dist/services/file-reader.js +251 -0
- package/dist/services/fingerprint-service.d.ts +41 -0
- package/dist/services/fingerprint-service.d.ts.map +1 -0
- package/dist/services/fingerprint-service.js +64 -0
- package/dist/services/id-generation/id-generation.d.ts +40 -0
- package/dist/services/id-generation/id-generation.d.ts.map +1 -0
- package/dist/services/id-generation/id-generation.js +58 -0
- package/dist/services/platform-service.d.ts +38 -0
- package/dist/services/platform-service.d.ts.map +1 -0
- package/dist/services/platform-service.js +221 -0
- package/dist/services/storage/local-storage-service.d.ts +55 -0
- package/dist/services/storage/local-storage-service.d.ts.map +1 -0
- package/dist/services/storage/local-storage-service.js +178 -0
- package/dist/services/storage/session-storage-service.d.ts +55 -0
- package/dist/services/storage/session-storage-service.d.ts.map +1 -0
- package/dist/services/storage/session-storage-service.js +179 -0
- package/dist/services/websocket-factory.d.ts +46 -0
- package/dist/services/websocket-factory.d.ts.map +1 -0
- package/dist/services/websocket-factory.js +196 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/upload-input.d.ts +26 -0
- package/dist/types/upload-input.d.ts.map +1 -0
- package/dist/types/upload-input.js +1 -0
- package/dist/utils/hash-util.d.ts +60 -0
- package/dist/utils/hash-util.d.ts.map +1 -0
- package/dist/utils/hash-util.js +75 -0
- package/package.json +32 -0
- package/src/client/create-uploadista-client.ts +150 -0
- package/src/client/index.ts +1 -0
- package/src/framework-utils.ts +446 -0
- package/src/http-client.ts +546 -0
- package/src/index.ts +8 -0
- package/src/services/abort-controller-factory.ts +108 -0
- package/src/services/checksum-service.ts +46 -0
- package/src/services/create-browser-services.ts +81 -0
- package/src/services/file-reader.ts +344 -0
- package/src/services/fingerprint-service.ts +67 -0
- package/src/services/id-generation/id-generation.ts +60 -0
- package/src/services/platform-service.ts +231 -0
- package/src/services/storage/local-storage-service.ts +187 -0
- package/src/services/storage/session-storage-service.ts +188 -0
- package/src/services/websocket-factory.ts +212 -0
- package/src/types/index.ts +1 -0
- package/src/types/upload-input.ts +25 -0
- package/src/utils/hash-util.ts +79 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConnectionHealth,
|
|
3
|
+
ConnectionMetrics,
|
|
4
|
+
ConnectionPoolConfig,
|
|
5
|
+
DetailedConnectionMetrics,
|
|
6
|
+
Http2Info,
|
|
7
|
+
HttpClient,
|
|
8
|
+
HttpRequestOptions,
|
|
9
|
+
HttpResponse,
|
|
10
|
+
} from "@uploadista/client-core";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a browser-optimized HTTP client using the Fetch API.
|
|
14
|
+
*
|
|
15
|
+
* This factory function returns an HttpClient implementation that uses the browser's
|
|
16
|
+
* native fetch() API with connection keep-alive headers for optimal performance.
|
|
17
|
+
* The client automatically manages connection pooling, tracks metrics, and provides
|
|
18
|
+
* connection health monitoring.
|
|
19
|
+
*
|
|
20
|
+
* @param config - Optional connection pooling configuration
|
|
21
|
+
* @returns A configured HTTP client ready for making requests
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { createHttpClient } from '@uploadista/client-browser';
|
|
26
|
+
*
|
|
27
|
+
* // Basic usage with defaults
|
|
28
|
+
* const client = createHttpClient();
|
|
29
|
+
*
|
|
30
|
+
* // With custom configuration
|
|
31
|
+
* const client = createHttpClient({
|
|
32
|
+
* maxConnectionsPerHost: 10,
|
|
33
|
+
* connectionTimeout: 60000,
|
|
34
|
+
* keepAliveTimeout: 120000,
|
|
35
|
+
* enableHttp2: true,
|
|
36
|
+
* retryOnConnectionError: true
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* // Make a request
|
|
40
|
+
* const response = await client.request('https://api.example.com/data', {
|
|
41
|
+
* method: 'POST',
|
|
42
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
* body: JSON.stringify({ key: 'value' })
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // Check connection health
|
|
47
|
+
* const metrics = client.getDetailedMetrics();
|
|
48
|
+
* console.log('Connection health:', metrics.health.status);
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @see {@link BrowserHttpClient} for implementation details
|
|
52
|
+
*/
|
|
53
|
+
export function createHttpClient(config?: ConnectionPoolConfig): HttpClient {
|
|
54
|
+
return new BrowserHttpClient(config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Browser-optimized HTTP client implementation using the Fetch API with connection keep-alive.
|
|
59
|
+
*
|
|
60
|
+
* This class implements the HttpClient interface and provides:
|
|
61
|
+
* - Connection pooling via keep-alive headers
|
|
62
|
+
* - Connection metrics tracking (reuse rate, latency, error rates)
|
|
63
|
+
* - HTTP/2 multiplexing detection and support
|
|
64
|
+
* - Connection health monitoring with actionable recommendations
|
|
65
|
+
* - Connection warmup capabilities
|
|
66
|
+
* - Automatic timeout handling
|
|
67
|
+
*
|
|
68
|
+
* The browser manages actual connection pooling, but this client optimizes for
|
|
69
|
+
* reuse through proper HTTP headers and provides visibility into connection performance.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const client = new BrowserHttpClient({
|
|
74
|
+
* maxConnectionsPerHost: 6,
|
|
75
|
+
* keepAliveTimeout: 60000,
|
|
76
|
+
* enableHttp2: true
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* // Monitor connection health
|
|
80
|
+
* setInterval(() => {
|
|
81
|
+
* const health = client.getDetailedMetrics().health;
|
|
82
|
+
* if (health.status === 'poor') {
|
|
83
|
+
* console.warn('Connection issues:', health.issues);
|
|
84
|
+
* console.log('Recommendations:', health.recommendations);
|
|
85
|
+
* }
|
|
86
|
+
* }, 30000);
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
class BrowserHttpClient implements HttpClient {
|
|
90
|
+
private config: Required<ConnectionPoolConfig>;
|
|
91
|
+
private metrics: ConnectionMetrics;
|
|
92
|
+
private connectionTimes: number[] = [];
|
|
93
|
+
private requestCount = 0;
|
|
94
|
+
private connectionCount = 0;
|
|
95
|
+
private errorCount = 0;
|
|
96
|
+
private timeoutCount = 0;
|
|
97
|
+
private retryCount = 0;
|
|
98
|
+
private startTime = Date.now();
|
|
99
|
+
private http2Info: Http2Info;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates a new browser HTTP client instance.
|
|
103
|
+
*
|
|
104
|
+
* @param config - Connection pooling configuration with optional overrides
|
|
105
|
+
*/
|
|
106
|
+
constructor(config: ConnectionPoolConfig = {}) {
|
|
107
|
+
this.config = {
|
|
108
|
+
maxConnectionsPerHost: config.maxConnectionsPerHost ?? 6,
|
|
109
|
+
connectionTimeout: config.connectionTimeout ?? 30000,
|
|
110
|
+
keepAliveTimeout: config.keepAliveTimeout ?? 60000,
|
|
111
|
+
enableHttp2: config.enableHttp2 ?? true,
|
|
112
|
+
retryOnConnectionError: config.retryOnConnectionError ?? true,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
this.metrics = {
|
|
116
|
+
activeConnections: 0,
|
|
117
|
+
totalConnections: 0,
|
|
118
|
+
reuseRate: 0,
|
|
119
|
+
averageConnectionTime: 0,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Initialize HTTP/2 detection
|
|
123
|
+
this.http2Info = this.detectHttp2Support();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Detects HTTP/2 support in the current browser environment.
|
|
128
|
+
*
|
|
129
|
+
* This method uses feature detection to determine if the browser supports
|
|
130
|
+
* HTTP/2 features like multiplexing. In browsers, we can't directly query
|
|
131
|
+
* the protocol version, so we infer support based on modern stream APIs.
|
|
132
|
+
*
|
|
133
|
+
* @returns HTTP/2 information with support status and detected features
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
private detectHttp2Support(): Http2Info {
|
|
137
|
+
// Check if the browser supports HTTP/2
|
|
138
|
+
const supported = "serviceWorker" in navigator && "fetch" in window;
|
|
139
|
+
|
|
140
|
+
// In browsers, we can't directly detect HTTP/2 protocol version
|
|
141
|
+
// but we can make educated guesses based on browser features
|
|
142
|
+
const hasModernFeatures =
|
|
143
|
+
"ReadableStream" in window &&
|
|
144
|
+
"WritableStream" in window &&
|
|
145
|
+
"TransformStream" in window;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
supported,
|
|
149
|
+
detected: false, // Will be updated during actual requests
|
|
150
|
+
version: hasModernFeatures ? "h2" : "h1.1",
|
|
151
|
+
multiplexingActive: hasModernFeatures && this.config.enableHttp2,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Makes an HTTP request using the Fetch API with optimized connection settings.
|
|
157
|
+
*
|
|
158
|
+
* This method automatically adds keep-alive headers for connection reuse,
|
|
159
|
+
* handles timeouts via AbortController, and tracks connection metrics.
|
|
160
|
+
*
|
|
161
|
+
* @param url - The URL to request
|
|
162
|
+
* @param options - Request options including method, headers, body, credentials, signal, and timeout
|
|
163
|
+
* @returns Promise resolving to the HTTP response
|
|
164
|
+
*
|
|
165
|
+
* @throws {Error} When the request fails or times out
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* // Simple GET request
|
|
170
|
+
* const response = await client.request('https://api.example.com/data');
|
|
171
|
+
* const data = await response.json();
|
|
172
|
+
*
|
|
173
|
+
* // POST with timeout
|
|
174
|
+
* const response = await client.request('https://api.example.com/upload', {
|
|
175
|
+
* method: 'POST',
|
|
176
|
+
* headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
* body: JSON.stringify({ file: 'data' }),
|
|
178
|
+
* timeout: 10000, // 10 seconds
|
|
179
|
+
* signal: abortController.signal
|
|
180
|
+
* });
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
async request(
|
|
184
|
+
url: string,
|
|
185
|
+
options: HttpRequestOptions = {},
|
|
186
|
+
): Promise<HttpResponse> {
|
|
187
|
+
this.requestCount++;
|
|
188
|
+
|
|
189
|
+
// Create optimized fetch options for connection reuse
|
|
190
|
+
const fetchOptions: RequestInit = {
|
|
191
|
+
method: options.method || "GET",
|
|
192
|
+
headers: {
|
|
193
|
+
// Add connection keep-alive headers for better reuse
|
|
194
|
+
Connection: "keep-alive",
|
|
195
|
+
"Keep-Alive": `timeout=${this.config.keepAliveTimeout / 1000}`,
|
|
196
|
+
...options.headers,
|
|
197
|
+
},
|
|
198
|
+
body: options.body as BodyInit | null | undefined,
|
|
199
|
+
credentials: options.credentials || "include",
|
|
200
|
+
signal: options.signal as AbortSignal | undefined,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Add timeout if specified
|
|
204
|
+
if (options.timeout) {
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout);
|
|
207
|
+
|
|
208
|
+
if (options.signal) {
|
|
209
|
+
options.signal.addEventListener("abort", () => controller.abort());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fetchOptions.signal = controller.signal;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const response = await this.makeRequest(url, fetchOptions);
|
|
216
|
+
clearTimeout(timeoutId);
|
|
217
|
+
return response;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
clearTimeout(timeoutId);
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return this.makeRequest(url, fetchOptions);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Internal method to execute the fetch request and track metrics.
|
|
229
|
+
*
|
|
230
|
+
* @param url - The URL to request
|
|
231
|
+
* @param options - Native fetch RequestInit options
|
|
232
|
+
* @returns Promise resolving to the HTTP response
|
|
233
|
+
* @throws {Error} When the fetch fails
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
private async makeRequest(
|
|
237
|
+
url: string,
|
|
238
|
+
options: RequestInit,
|
|
239
|
+
): Promise<HttpResponse> {
|
|
240
|
+
const startTime = Date.now();
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(url, options);
|
|
244
|
+
const connectionTime = Date.now() - startTime;
|
|
245
|
+
|
|
246
|
+
this.recordConnectionMetrics(connectionTime);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
status: response.status,
|
|
250
|
+
statusText: response.statusText,
|
|
251
|
+
headers: response.headers,
|
|
252
|
+
ok: response.ok,
|
|
253
|
+
json: () => response.json(),
|
|
254
|
+
text: () => response.text(),
|
|
255
|
+
arrayBuffer: () => response.arrayBuffer(),
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
// Record failed connection attempt
|
|
259
|
+
this.connectionCount++;
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Records connection timing metrics and updates statistics.
|
|
266
|
+
*
|
|
267
|
+
* Tracks connection times, calculates reuse rates based on latency patterns,
|
|
268
|
+
* and maintains a rolling window of the last 100 measurements.
|
|
269
|
+
*
|
|
270
|
+
* @param connectionTime - Time in milliseconds for this connection
|
|
271
|
+
* @private
|
|
272
|
+
*/
|
|
273
|
+
private recordConnectionMetrics(connectionTime: number): void {
|
|
274
|
+
this.connectionTimes.push(connectionTime);
|
|
275
|
+
this.connectionCount++;
|
|
276
|
+
|
|
277
|
+
// Keep only last 100 measurements for average calculation
|
|
278
|
+
if (this.connectionTimes.length > 100) {
|
|
279
|
+
this.connectionTimes.shift();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update metrics
|
|
283
|
+
this.metrics.totalConnections = this.connectionCount;
|
|
284
|
+
this.metrics.averageConnectionTime =
|
|
285
|
+
this.connectionTimes.reduce((sum, time) => sum + time, 0) /
|
|
286
|
+
this.connectionTimes.length;
|
|
287
|
+
|
|
288
|
+
// Estimate reuse rate based on connection time patterns
|
|
289
|
+
// Faster connections are likely reused connections
|
|
290
|
+
const fastConnections = this.connectionTimes.filter(
|
|
291
|
+
(time) => time < 100,
|
|
292
|
+
).length;
|
|
293
|
+
this.metrics.reuseRate = fastConnections / this.connectionTimes.length;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Retrieves basic connection metrics.
|
|
298
|
+
*
|
|
299
|
+
* @returns Current connection metrics including total connections, reuse rate, and average connection time
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```typescript
|
|
303
|
+
* const metrics = client.getMetrics();
|
|
304
|
+
* console.log(`Reuse rate: ${Math.round(metrics.reuseRate * 100)}%`);
|
|
305
|
+
* console.log(`Avg connection time: ${Math.round(metrics.averageConnectionTime)}ms`);
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
getMetrics(): ConnectionMetrics {
|
|
309
|
+
return { ...this.metrics };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Retrieves detailed connection metrics with health assessment.
|
|
314
|
+
*
|
|
315
|
+
* Provides comprehensive metrics including error rates, request throughput,
|
|
316
|
+
* connection health status with score, identified issues, and actionable
|
|
317
|
+
* recommendations for improving connection performance.
|
|
318
|
+
*
|
|
319
|
+
* @returns Detailed metrics with health information, request rates, and HTTP/2 info
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* const detailed = client.getDetailedMetrics();
|
|
324
|
+
*
|
|
325
|
+
* // Check health
|
|
326
|
+
* if (detailed.health.status === 'degraded') {
|
|
327
|
+
* console.warn('Issues:', detailed.health.issues);
|
|
328
|
+
* console.log('Try:', detailed.health.recommendations);
|
|
329
|
+
* }
|
|
330
|
+
*
|
|
331
|
+
* // Monitor performance
|
|
332
|
+
* console.log(`Requests/sec: ${detailed.requestsPerSecond.toFixed(2)}`);
|
|
333
|
+
* console.log(`Error rate: ${(detailed.errorRate * 100).toFixed(1)}%`);
|
|
334
|
+
* console.log(`Fast connections: ${detailed.fastConnections}/${detailed.fastConnections + detailed.slowConnections}`);
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
getDetailedMetrics(): DetailedConnectionMetrics {
|
|
338
|
+
const health = this.calculateConnectionHealth();
|
|
339
|
+
const elapsed = (Date.now() - this.startTime) / 1000; // seconds
|
|
340
|
+
const requestsPerSecond = elapsed > 0 ? this.requestCount / elapsed : 0;
|
|
341
|
+
const errorRate =
|
|
342
|
+
this.requestCount > 0 ? this.errorCount / this.requestCount : 0;
|
|
343
|
+
|
|
344
|
+
const fastConnections = this.connectionTimes.filter(
|
|
345
|
+
(time) => time < 100,
|
|
346
|
+
).length;
|
|
347
|
+
const slowConnections = this.connectionTimes.length - fastConnections;
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
...this.metrics,
|
|
351
|
+
health,
|
|
352
|
+
requestsPerSecond,
|
|
353
|
+
errorRate,
|
|
354
|
+
timeouts: this.timeoutCount,
|
|
355
|
+
retries: this.retryCount,
|
|
356
|
+
fastConnections,
|
|
357
|
+
slowConnections,
|
|
358
|
+
http2Info: this.http2Info,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Calculates connection health status based on current metrics.
|
|
364
|
+
*
|
|
365
|
+
* Analyzes reuse rates, error rates, and connection latency to determine
|
|
366
|
+
* overall health (healthy/degraded/poor), assigns a health score (0-100),
|
|
367
|
+
* and provides specific issues and recommendations.
|
|
368
|
+
*
|
|
369
|
+
* @returns Health assessment with status, score, issues, and recommendations
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
private calculateConnectionHealth(): ConnectionHealth {
|
|
373
|
+
const issues: string[] = [];
|
|
374
|
+
const recommendations: string[] = [];
|
|
375
|
+
let score = 100;
|
|
376
|
+
|
|
377
|
+
// Check reuse rate
|
|
378
|
+
if (this.metrics.reuseRate < 0.3) {
|
|
379
|
+
issues.push("Low connection reuse rate");
|
|
380
|
+
recommendations.push("Check if keep-alive headers are working");
|
|
381
|
+
score -= 30;
|
|
382
|
+
} else if (this.metrics.reuseRate < 0.7) {
|
|
383
|
+
issues.push("Moderate connection reuse rate");
|
|
384
|
+
recommendations.push("Consider adjusting keep-alive timeout");
|
|
385
|
+
score -= 15;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check error rate
|
|
389
|
+
const errorRate =
|
|
390
|
+
this.requestCount > 0 ? this.errorCount / this.requestCount : 0;
|
|
391
|
+
if (errorRate > 0.1) {
|
|
392
|
+
issues.push("High error rate");
|
|
393
|
+
recommendations.push("Check network stability and server configuration");
|
|
394
|
+
score -= 25;
|
|
395
|
+
} else if (errorRate > 0.05) {
|
|
396
|
+
issues.push("Moderate error rate");
|
|
397
|
+
recommendations.push("Monitor network conditions");
|
|
398
|
+
score -= 10;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check average connection time
|
|
402
|
+
if (this.metrics.averageConnectionTime > 1000) {
|
|
403
|
+
issues.push("Slow connection establishment");
|
|
404
|
+
recommendations.push("Check network latency and DNS resolution");
|
|
405
|
+
score -= 20;
|
|
406
|
+
} else if (this.metrics.averageConnectionTime > 500) {
|
|
407
|
+
issues.push("Moderate connection latency");
|
|
408
|
+
recommendations.push("Consider connection warming");
|
|
409
|
+
score -= 10;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Determine status
|
|
413
|
+
let status: "healthy" | "degraded" | "poor";
|
|
414
|
+
if (score >= 80) {
|
|
415
|
+
status = "healthy";
|
|
416
|
+
} else if (score >= 60) {
|
|
417
|
+
status = "degraded";
|
|
418
|
+
} else {
|
|
419
|
+
status = "poor";
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
status,
|
|
424
|
+
score: Math.max(0, score),
|
|
425
|
+
issues,
|
|
426
|
+
recommendations,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Warms up HTTP connections to specified URLs by making lightweight HEAD requests.
|
|
432
|
+
*
|
|
433
|
+
* This is useful for establishing connections before actual data transfer,
|
|
434
|
+
* reducing latency for subsequent requests. Particularly beneficial when
|
|
435
|
+
* uploading to multiple endpoints or when connection setup time is critical.
|
|
436
|
+
*
|
|
437
|
+
* @param urls - Array of URLs to warm up connections to
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```typescript
|
|
441
|
+
* // Warm up connections before uploading
|
|
442
|
+
* await client.warmupConnections([
|
|
443
|
+
* 'https://upload1.example.com',
|
|
444
|
+
* 'https://upload2.example.com',
|
|
445
|
+
* 'https://cdn.example.com'
|
|
446
|
+
* ]);
|
|
447
|
+
*
|
|
448
|
+
* // Now actual uploads will use pre-warmed connections
|
|
449
|
+
* await client.request('https://upload1.example.com/file', {
|
|
450
|
+
* method: 'PUT',
|
|
451
|
+
* body: fileData
|
|
452
|
+
* });
|
|
453
|
+
* ```
|
|
454
|
+
*/
|
|
455
|
+
async warmupConnections(urls: string[]): Promise<void> {
|
|
456
|
+
if (urls.length === 0) return;
|
|
457
|
+
|
|
458
|
+
console.log(`Warming up connections to ${urls.length} hosts...`);
|
|
459
|
+
|
|
460
|
+
// Make lightweight HEAD requests to warm up connections
|
|
461
|
+
const warmupPromises = urls.map(async (url) => {
|
|
462
|
+
try {
|
|
463
|
+
await this.request(url, {
|
|
464
|
+
method: "HEAD",
|
|
465
|
+
timeout: 5000, // 5 second timeout for warmup
|
|
466
|
+
});
|
|
467
|
+
} catch (error) {
|
|
468
|
+
// Ignore warmup failures - they're optional
|
|
469
|
+
console.warn(`Connection warmup failed for ${url}:`, error);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Wait for all warmup requests (with timeout)
|
|
474
|
+
await Promise.allSettled(warmupPromises);
|
|
475
|
+
console.log("Connection warmup completed");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Resets all connection metrics and statistics to initial state.
|
|
480
|
+
*
|
|
481
|
+
* Useful for clearing metrics after a long-running session or when
|
|
482
|
+
* you want to start fresh measurement without creating a new client instance.
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```typescript
|
|
486
|
+
* // Reset metrics after a batch of uploads
|
|
487
|
+
* client.reset();
|
|
488
|
+
*
|
|
489
|
+
* // Start fresh measurements
|
|
490
|
+
* const metrics = client.getMetrics(); // All counters back to zero
|
|
491
|
+
* ```
|
|
492
|
+
*/
|
|
493
|
+
reset(): void {
|
|
494
|
+
this.connectionTimes = [];
|
|
495
|
+
this.requestCount = 0;
|
|
496
|
+
this.connectionCount = 0;
|
|
497
|
+
this.errorCount = 0;
|
|
498
|
+
this.timeoutCount = 0;
|
|
499
|
+
this.retryCount = 0;
|
|
500
|
+
this.startTime = Date.now();
|
|
501
|
+
this.metrics = {
|
|
502
|
+
activeConnections: 0,
|
|
503
|
+
totalConnections: 0,
|
|
504
|
+
reuseRate: 0,
|
|
505
|
+
averageConnectionTime: 0,
|
|
506
|
+
};
|
|
507
|
+
this.http2Info = this.detectHttp2Support();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Gracefully shuts down the HTTP client and logs final statistics.
|
|
512
|
+
*
|
|
513
|
+
* In browser environments, connections are managed by the browser and cannot
|
|
514
|
+
* be explicitly closed. This method waits briefly for pending requests to complete,
|
|
515
|
+
* logs final connection metrics, and resets internal state.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```typescript
|
|
519
|
+
* // Clean shutdown with metrics logging
|
|
520
|
+
* await client.close();
|
|
521
|
+
* // Logs: "Total requests: 150, Connection reuse: 85%, Avg time: 45ms, Health: healthy"
|
|
522
|
+
* ```
|
|
523
|
+
*/
|
|
524
|
+
async close(): Promise<void> {
|
|
525
|
+
console.log("Gracefully shutting down HTTP client...");
|
|
526
|
+
|
|
527
|
+
// In browser environment, we can't explicitly close connections
|
|
528
|
+
// The browser manages connection pooling automatically
|
|
529
|
+
// But we can clean up our internal state
|
|
530
|
+
|
|
531
|
+
// Wait a short time for any pending requests to complete
|
|
532
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
533
|
+
|
|
534
|
+
// Log final statistics
|
|
535
|
+
const finalMetrics = this.getDetailedMetrics();
|
|
536
|
+
console.log("Final connection metrics:", {
|
|
537
|
+
totalRequests: this.requestCount,
|
|
538
|
+
connectionReuse: `${Math.round(finalMetrics.reuseRate * 100)}%`,
|
|
539
|
+
averageConnectionTime: `${Math.round(finalMetrics.averageConnectionTime)}ms`,
|
|
540
|
+
health: finalMetrics.health.status,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
this.reset();
|
|
544
|
+
console.log("HTTP client shutdown complete");
|
|
545
|
+
}
|
|
546
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Re-export everything from client-core
|
|
2
|
+
export * from "@uploadista/client-core";
|
|
3
|
+
export { createUploadistaClient, type UploadistaClientOptions } from "./client";
|
|
4
|
+
export * from "./framework-utils";
|
|
5
|
+
export { createHttpClient } from "./http-client";
|
|
6
|
+
export type { FileReader } from "./services/file-reader";
|
|
7
|
+
export { createBrowserFileReaderService } from "./services/file-reader";
|
|
8
|
+
export * from "./types";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AbortControllerFactory,
|
|
3
|
+
AbortControllerLike,
|
|
4
|
+
AbortSignalLike,
|
|
5
|
+
} from "@uploadista/client-core";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Browser implementation of AbortController that wraps the native AbortController API.
|
|
9
|
+
*
|
|
10
|
+
* This class provides a minimal wrapper around the browser's native AbortController
|
|
11
|
+
* to ensure compatibility with the Uploadista client's AbortControllerLike interface.
|
|
12
|
+
* It's used for canceling uploads and HTTP requests.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const controller = new BrowserAbortController();
|
|
17
|
+
*
|
|
18
|
+
* // Start an operation with the signal
|
|
19
|
+
* fetch('https://api.example.com/upload', {
|
|
20
|
+
* signal: controller.signal,
|
|
21
|
+
* method: 'POST',
|
|
22
|
+
* body: formData
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Cancel the operation
|
|
26
|
+
* controller.abort('User canceled');
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
class BrowserAbortController implements AbortControllerLike {
|
|
30
|
+
private native: AbortController;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates a new BrowserAbortController instance.
|
|
34
|
+
*
|
|
35
|
+
* Initializes a new native AbortController from the browser's API.
|
|
36
|
+
*/
|
|
37
|
+
constructor() {
|
|
38
|
+
this.native = new AbortController();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Gets the AbortSignal associated with this controller.
|
|
43
|
+
*
|
|
44
|
+
* This signal is passed to abortable operations (like fetch or upload)
|
|
45
|
+
* and will be triggered when abort() is called.
|
|
46
|
+
*
|
|
47
|
+
* @returns The abort signal that operations can listen to
|
|
48
|
+
*/
|
|
49
|
+
get signal(): AbortSignalLike {
|
|
50
|
+
return this.native.signal;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Aborts the operation associated with this controller.
|
|
55
|
+
*
|
|
56
|
+
* When called, this will trigger the abort signal, causing any operations
|
|
57
|
+
* listening to it (such as fetch requests or file uploads) to be canceled.
|
|
58
|
+
*
|
|
59
|
+
* @param reason - Optional reason for the abort, which will be available on the AbortSignal
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const controller = new BrowserAbortController();
|
|
64
|
+
*
|
|
65
|
+
* // Start upload
|
|
66
|
+
* const upload = client.upload(file, { signal: controller.signal });
|
|
67
|
+
*
|
|
68
|
+
* // Cancel upload after 5 seconds
|
|
69
|
+
* setTimeout(() => {
|
|
70
|
+
* controller.abort('Timeout exceeded');
|
|
71
|
+
* }, 5000);
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
abort(reason?: unknown): void {
|
|
75
|
+
this.native.abort(reason);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a factory for browser AbortController instances.
|
|
81
|
+
*
|
|
82
|
+
* This factory is used by the Uploadista client to create AbortController
|
|
83
|
+
* instances for canceling uploads and HTTP requests. It wraps the browser's
|
|
84
|
+
* native AbortController API.
|
|
85
|
+
*
|
|
86
|
+
* @returns An AbortControllerFactory that creates browser-compatible abort controllers
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* import { createBrowserAbortControllerFactory } from '@uploadista/client-browser';
|
|
91
|
+
*
|
|
92
|
+
* const factory = createBrowserAbortControllerFactory();
|
|
93
|
+
*
|
|
94
|
+
* // Create a controller
|
|
95
|
+
* const controller = factory.create();
|
|
96
|
+
*
|
|
97
|
+
* // Use it to cancel operations
|
|
98
|
+
* const upload = client.upload(file, {
|
|
99
|
+
* signal: controller.signal
|
|
100
|
+
* });
|
|
101
|
+
*
|
|
102
|
+
* // Later, cancel the upload
|
|
103
|
+
* controller.abort();
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export const createBrowserAbortControllerFactory = (): AbortControllerFactory => ({
|
|
107
|
+
create: (): AbortControllerLike => new BrowserAbortController(),
|
|
108
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ChecksumService } from "@uploadista/client-core";
|
|
2
|
+
import { computeblobSha256 } from "../utils/hash-util";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a checksum service for verifying data integrity using SHA-256 hashing.
|
|
6
|
+
*
|
|
7
|
+
* This service uses the browser's Web Crypto API to compute SHA-256 checksums
|
|
8
|
+
* of data chunks during upload. Checksums ensure that uploaded data hasn't been
|
|
9
|
+
* corrupted in transit and matches what was sent.
|
|
10
|
+
*
|
|
11
|
+
* The service is optimized for browser environments and leverages native crypto
|
|
12
|
+
* APIs for performance.
|
|
13
|
+
*
|
|
14
|
+
* @returns A ChecksumService that computes SHA-256 hashes for data chunks
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { createChecksumService } from '@uploadista/client-browser';
|
|
19
|
+
*
|
|
20
|
+
* const checksumService = createChecksumService();
|
|
21
|
+
*
|
|
22
|
+
* // Compute checksum for a data chunk
|
|
23
|
+
* const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
24
|
+
* const checksum = await checksumService.computeChecksum(data);
|
|
25
|
+
* console.log('SHA-256 checksum:', checksum);
|
|
26
|
+
* // Output: "74f81fe167d99b4cb41d6d0ccda82278caee9f3e2f25d5e5a3936ff3dcec60d0"
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @see {@link computeblobSha256} for the underlying hash implementation
|
|
30
|
+
*/
|
|
31
|
+
export function createChecksumService(): ChecksumService {
|
|
32
|
+
return {
|
|
33
|
+
/**
|
|
34
|
+
* Computes a SHA-256 checksum for the provided data.
|
|
35
|
+
*
|
|
36
|
+
* Converts the Uint8Array to a Blob and uses the Web Crypto API
|
|
37
|
+
* to calculate its SHA-256 hash, returned as a hex string.
|
|
38
|
+
*
|
|
39
|
+
* @param data - The data to checksum
|
|
40
|
+
* @returns Promise resolving to the hex-encoded SHA-256 checksum
|
|
41
|
+
*/
|
|
42
|
+
computeChecksum: async (data: Uint8Array<ArrayBuffer>) => {
|
|
43
|
+
return computeblobSha256(new Blob([data]));
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|