@uploadista/client-browser 0.0.20 → 0.1.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,402 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { createHttpClient } from "./http-client";
3
+
4
+ describe("createHttpClient", () => {
5
+ // Mock fetch globally
6
+ const mockFetch = vi.fn();
7
+ const originalFetch = globalThis.fetch;
8
+
9
+ beforeEach(() => {
10
+ vi.useFakeTimers({ shouldAdvanceTime: true });
11
+ globalThis.fetch = mockFetch;
12
+ mockFetch.mockReset();
13
+ });
14
+
15
+ afterEach(() => {
16
+ vi.useRealTimers();
17
+ globalThis.fetch = originalFetch;
18
+ });
19
+
20
+ it("should create an HTTP client with default config", () => {
21
+ const client = createHttpClient();
22
+ expect(client).toBeDefined();
23
+ expect(client.request).toBeDefined();
24
+ expect(client.getMetrics).toBeDefined();
25
+ expect(client.getDetailedMetrics).toBeDefined();
26
+ expect(client.warmupConnections).toBeDefined();
27
+ expect(client.reset).toBeDefined();
28
+ expect(client.close).toBeDefined();
29
+ });
30
+
31
+ it("should create an HTTP client with custom config", () => {
32
+ const client = createHttpClient({
33
+ maxConnectionsPerHost: 10,
34
+ connectionTimeout: 60000,
35
+ keepAliveTimeout: 120000,
36
+ enableHttp2: true,
37
+ retryOnConnectionError: true,
38
+ });
39
+ expect(client).toBeDefined();
40
+ });
41
+
42
+ describe("request", () => {
43
+ it("should make a GET request", async () => {
44
+ const mockResponse = {
45
+ status: 200,
46
+ statusText: "OK",
47
+ ok: true,
48
+ headers: new Headers(),
49
+ json: vi.fn().mockResolvedValue({ data: "test" }),
50
+ text: vi.fn().mockResolvedValue("test"),
51
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
52
+ };
53
+ mockFetch.mockResolvedValue(mockResponse);
54
+
55
+ const client = createHttpClient();
56
+ const response = await client.request("https://api.example.com/data");
57
+
58
+ expect(mockFetch).toHaveBeenCalledWith(
59
+ "https://api.example.com/data",
60
+ expect.objectContaining({
61
+ method: "GET",
62
+ headers: expect.objectContaining({
63
+ Connection: "keep-alive",
64
+ }),
65
+ })
66
+ );
67
+ expect(response.status).toBe(200);
68
+ expect(response.ok).toBe(true);
69
+ });
70
+
71
+ it("should make a POST request with body", async () => {
72
+ const mockResponse = {
73
+ status: 201,
74
+ statusText: "Created",
75
+ ok: true,
76
+ headers: new Headers(),
77
+ json: vi.fn().mockResolvedValue({ id: 1 }),
78
+ text: vi.fn().mockResolvedValue(""),
79
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
80
+ };
81
+ mockFetch.mockResolvedValue(mockResponse);
82
+
83
+ const client = createHttpClient();
84
+ const body = JSON.stringify({ name: "test" });
85
+ const response = await client.request("https://api.example.com/data", {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json" },
88
+ body,
89
+ });
90
+
91
+ expect(mockFetch).toHaveBeenCalledWith(
92
+ "https://api.example.com/data",
93
+ expect.objectContaining({
94
+ method: "POST",
95
+ body,
96
+ })
97
+ );
98
+ expect(response.status).toBe(201);
99
+ });
100
+
101
+ it("should handle request timeout", async () => {
102
+ // Use real timers for this test since we need actual timing
103
+ vi.useRealTimers();
104
+
105
+ mockFetch.mockImplementation(
106
+ (_url: string, options?: RequestInit) =>
107
+ new Promise((resolve, reject) => {
108
+ const signal = options?.signal;
109
+ const timeoutId = setTimeout(() => {
110
+ resolve({
111
+ status: 200,
112
+ statusText: "OK",
113
+ ok: true,
114
+ headers: new Headers(),
115
+ json: vi.fn().mockResolvedValue({}),
116
+ text: vi.fn().mockResolvedValue(""),
117
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
118
+ });
119
+ }, 1000);
120
+
121
+ // Listen for abort signal
122
+ signal?.addEventListener("abort", () => {
123
+ clearTimeout(timeoutId);
124
+ reject(new DOMException("Aborted", "AbortError"));
125
+ });
126
+ })
127
+ );
128
+
129
+ const client = createHttpClient();
130
+
131
+ await expect(
132
+ client.request("https://api.example.com/slow", {
133
+ timeout: 50,
134
+ })
135
+ ).rejects.toThrow();
136
+
137
+ // Restore fake timers for other tests
138
+ vi.useFakeTimers({ shouldAdvanceTime: true });
139
+ });
140
+
141
+ it("should pass abort signal", async () => {
142
+ const mockResponse = {
143
+ status: 200,
144
+ statusText: "OK",
145
+ ok: true,
146
+ headers: new Headers(),
147
+ json: vi.fn().mockResolvedValue({}),
148
+ text: vi.fn().mockResolvedValue(""),
149
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
150
+ };
151
+ mockFetch.mockResolvedValue(mockResponse);
152
+
153
+ const client = createHttpClient();
154
+ const controller = new AbortController();
155
+
156
+ await client.request("https://api.example.com/data", {
157
+ signal: controller.signal,
158
+ });
159
+
160
+ expect(mockFetch).toHaveBeenCalledWith(
161
+ "https://api.example.com/data",
162
+ expect.objectContaining({
163
+ signal: expect.any(AbortSignal),
164
+ })
165
+ );
166
+ });
167
+
168
+ it("should include credentials by default", async () => {
169
+ const mockResponse = {
170
+ status: 200,
171
+ statusText: "OK",
172
+ ok: true,
173
+ headers: new Headers(),
174
+ json: vi.fn().mockResolvedValue({}),
175
+ text: vi.fn().mockResolvedValue(""),
176
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
177
+ };
178
+ mockFetch.mockResolvedValue(mockResponse);
179
+
180
+ const client = createHttpClient();
181
+ await client.request("https://api.example.com/data");
182
+
183
+ expect(mockFetch).toHaveBeenCalledWith(
184
+ "https://api.example.com/data",
185
+ expect.objectContaining({
186
+ credentials: "include",
187
+ })
188
+ );
189
+ });
190
+
191
+ it("should throw on fetch error", async () => {
192
+ mockFetch.mockRejectedValue(new Error("Network error"));
193
+
194
+ const client = createHttpClient();
195
+
196
+ await expect(
197
+ client.request("https://api.example.com/data")
198
+ ).rejects.toThrow("Network error");
199
+ });
200
+ });
201
+
202
+ describe("getMetrics", () => {
203
+ it("should return initial metrics", () => {
204
+ const client = createHttpClient();
205
+ const metrics = client.getMetrics();
206
+
207
+ expect(metrics).toEqual({
208
+ activeConnections: 0,
209
+ totalConnections: 0,
210
+ reuseRate: 0,
211
+ averageConnectionTime: 0,
212
+ });
213
+ });
214
+
215
+ it("should update metrics after requests", async () => {
216
+ const mockResponse = {
217
+ status: 200,
218
+ statusText: "OK",
219
+ ok: true,
220
+ headers: new Headers(),
221
+ json: vi.fn().mockResolvedValue({}),
222
+ text: vi.fn().mockResolvedValue(""),
223
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
224
+ };
225
+ mockFetch.mockResolvedValue(mockResponse);
226
+
227
+ const client = createHttpClient();
228
+
229
+ await client.request("https://api.example.com/data");
230
+
231
+ const metrics = client.getMetrics();
232
+ expect(metrics.totalConnections).toBe(1);
233
+ expect(metrics.averageConnectionTime).toBeGreaterThanOrEqual(0);
234
+ });
235
+ });
236
+
237
+ describe("getDetailedMetrics", () => {
238
+ it("should return detailed metrics", async () => {
239
+ const mockResponse = {
240
+ status: 200,
241
+ statusText: "OK",
242
+ ok: true,
243
+ headers: new Headers(),
244
+ json: vi.fn().mockResolvedValue({}),
245
+ text: vi.fn().mockResolvedValue(""),
246
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
247
+ };
248
+ mockFetch.mockResolvedValue(mockResponse);
249
+
250
+ const client = createHttpClient();
251
+ await client.request("https://api.example.com/data");
252
+
253
+ const detailed = client.getDetailedMetrics();
254
+
255
+ expect(detailed.health).toBeDefined();
256
+ expect(detailed.health.status).toBeDefined();
257
+ expect(detailed.health.score).toBeDefined();
258
+ expect(detailed.health.issues).toBeDefined();
259
+ expect(detailed.health.recommendations).toBeDefined();
260
+ expect(detailed.requestsPerSecond).toBeDefined();
261
+ expect(detailed.errorRate).toBeDefined();
262
+ expect(detailed.http2Info).toBeDefined();
263
+ });
264
+
265
+ it("should calculate health based on metrics", async () => {
266
+ const mockResponse = {
267
+ status: 200,
268
+ statusText: "OK",
269
+ ok: true,
270
+ headers: new Headers(),
271
+ json: vi.fn().mockResolvedValue({}),
272
+ text: vi.fn().mockResolvedValue(""),
273
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
274
+ };
275
+ mockFetch.mockResolvedValue(mockResponse);
276
+
277
+ const client = createHttpClient();
278
+
279
+ // Make several requests
280
+ for (let i = 0; i < 5; i++) {
281
+ await client.request("https://api.example.com/data");
282
+ }
283
+
284
+ const detailed = client.getDetailedMetrics();
285
+ expect(["healthy", "degraded", "poor"]).toContain(detailed.health.status);
286
+ expect(detailed.health.score).toBeGreaterThanOrEqual(0);
287
+ expect(detailed.health.score).toBeLessThanOrEqual(100);
288
+ });
289
+ });
290
+
291
+ describe("warmupConnections", () => {
292
+ it("should make HEAD requests to warm up connections", async () => {
293
+ const mockResponse = {
294
+ status: 200,
295
+ statusText: "OK",
296
+ ok: true,
297
+ headers: new Headers(),
298
+ json: vi.fn().mockResolvedValue({}),
299
+ text: vi.fn().mockResolvedValue(""),
300
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
301
+ };
302
+ mockFetch.mockResolvedValue(mockResponse);
303
+
304
+ const client = createHttpClient();
305
+ await client.warmupConnections([
306
+ "https://api1.example.com",
307
+ "https://api2.example.com",
308
+ ]);
309
+
310
+ expect(mockFetch).toHaveBeenCalledTimes(2);
311
+ expect(mockFetch).toHaveBeenCalledWith(
312
+ "https://api1.example.com",
313
+ expect.objectContaining({
314
+ method: "HEAD",
315
+ })
316
+ );
317
+ expect(mockFetch).toHaveBeenCalledWith(
318
+ "https://api2.example.com",
319
+ expect.objectContaining({
320
+ method: "HEAD",
321
+ })
322
+ );
323
+ });
324
+
325
+ it("should handle warmup failures gracefully", async () => {
326
+ mockFetch.mockRejectedValue(new Error("Connection failed"));
327
+
328
+ const client = createHttpClient();
329
+
330
+ // Should not throw
331
+ await expect(
332
+ client.warmupConnections(["https://api.example.com"])
333
+ ).resolves.not.toThrow();
334
+ });
335
+
336
+ it("should handle empty URL list", async () => {
337
+ const client = createHttpClient();
338
+
339
+ await expect(client.warmupConnections([])).resolves.not.toThrow();
340
+ expect(mockFetch).not.toHaveBeenCalled();
341
+ });
342
+ });
343
+
344
+ describe("reset", () => {
345
+ it("should reset all metrics", async () => {
346
+ const mockResponse = {
347
+ status: 200,
348
+ statusText: "OK",
349
+ ok: true,
350
+ headers: new Headers(),
351
+ json: vi.fn().mockResolvedValue({}),
352
+ text: vi.fn().mockResolvedValue(""),
353
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
354
+ };
355
+ mockFetch.mockResolvedValue(mockResponse);
356
+
357
+ const client = createHttpClient();
358
+
359
+ // Make some requests
360
+ await client.request("https://api.example.com/data");
361
+ await client.request("https://api.example.com/data");
362
+
363
+ // Verify metrics are populated
364
+ let metrics = client.getMetrics();
365
+ expect(metrics.totalConnections).toBe(2);
366
+
367
+ // Reset
368
+ client.reset();
369
+
370
+ // Verify metrics are cleared
371
+ metrics = client.getMetrics();
372
+ expect(metrics.totalConnections).toBe(0);
373
+ expect(metrics.reuseRate).toBe(0);
374
+ expect(metrics.averageConnectionTime).toBe(0);
375
+ });
376
+ });
377
+
378
+ describe("close", () => {
379
+ it("should close gracefully", async () => {
380
+ const mockResponse = {
381
+ status: 200,
382
+ statusText: "OK",
383
+ ok: true,
384
+ headers: new Headers(),
385
+ json: vi.fn().mockResolvedValue({}),
386
+ text: vi.fn().mockResolvedValue(""),
387
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(0)),
388
+ };
389
+ mockFetch.mockResolvedValue(mockResponse);
390
+
391
+ const client = createHttpClient();
392
+
393
+ await client.request("https://api.example.com/data");
394
+
395
+ await expect(client.close()).resolves.not.toThrow();
396
+
397
+ // Metrics should be reset after close
398
+ const metrics = client.getMetrics();
399
+ expect(metrics.totalConnections).toBe(0);
400
+ });
401
+ });
402
+ });
@@ -134,6 +134,17 @@ class BrowserHttpClient implements HttpClient {
134
134
  * @private
135
135
  */
136
136
  private detectHttp2Support(): Http2Info {
137
+ // Check if we're in a browser environment
138
+ if (typeof window === "undefined" || typeof navigator === "undefined") {
139
+ // SSR/Node.js environment - return safe defaults
140
+ return {
141
+ supported: false,
142
+ detected: false,
143
+ version: "h1.1",
144
+ multiplexingActive: false,
145
+ };
146
+ }
147
+
137
148
  // Check if the browser supports HTTP/2
138
149
  const supported = "serviceWorker" in navigator && "fetch" in window;
139
150
 
@@ -0,0 +1,163 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createBrowserAbortControllerFactory } from "./abort-controller-factory";
3
+
4
+ describe("createBrowserAbortControllerFactory", () => {
5
+ it("should create an AbortController factory", () => {
6
+ const factory = createBrowserAbortControllerFactory();
7
+ expect(factory).toBeDefined();
8
+ expect(factory.create).toBeDefined();
9
+ expect(typeof factory.create).toBe("function");
10
+ });
11
+
12
+ describe("create", () => {
13
+ it("should create an AbortController instance", () => {
14
+ const factory = createBrowserAbortControllerFactory();
15
+ const controller = factory.create();
16
+
17
+ expect(controller).toBeDefined();
18
+ expect(controller.signal).toBeDefined();
19
+ expect(controller.abort).toBeDefined();
20
+ expect(typeof controller.abort).toBe("function");
21
+ });
22
+
23
+ it("should create independent controllers", () => {
24
+ const factory = createBrowserAbortControllerFactory();
25
+ const controller1 = factory.create();
26
+ const controller2 = factory.create();
27
+
28
+ expect(controller1).not.toBe(controller2);
29
+ expect(controller1.signal).not.toBe(controller2.signal);
30
+ });
31
+ });
32
+
33
+ describe("signal", () => {
34
+ it("should have aborted property set to false initially", () => {
35
+ const factory = createBrowserAbortControllerFactory();
36
+ const controller = factory.create();
37
+
38
+ expect(controller.signal.aborted).toBe(false);
39
+ });
40
+
41
+ it("should support addEventListener", () => {
42
+ const factory = createBrowserAbortControllerFactory();
43
+ const controller = factory.create();
44
+
45
+ const listener = vi.fn();
46
+ controller.signal.addEventListener("abort", listener);
47
+
48
+ controller.abort();
49
+
50
+ expect(listener).toHaveBeenCalled();
51
+ });
52
+ });
53
+
54
+ describe("abort", () => {
55
+ it("should set aborted to true on signal", () => {
56
+ const factory = createBrowserAbortControllerFactory();
57
+ const controller = factory.create();
58
+
59
+ controller.abort();
60
+
61
+ expect(controller.signal.aborted).toBe(true);
62
+ });
63
+
64
+ it("should abort with reason", () => {
65
+ const factory = createBrowserAbortControllerFactory();
66
+ const controller = factory.create();
67
+
68
+ const reason = new Error("User cancelled");
69
+ controller.abort(reason);
70
+
71
+ expect(controller.signal.aborted).toBe(true);
72
+ expect(controller.signal.reason).toBe(reason);
73
+ });
74
+
75
+ it("should not throw when aborting multiple times", () => {
76
+ const factory = createBrowserAbortControllerFactory();
77
+ const controller = factory.create();
78
+
79
+ expect(() => {
80
+ controller.abort();
81
+ controller.abort();
82
+ controller.abort();
83
+ }).not.toThrow();
84
+
85
+ expect(controller.signal.aborted).toBe(true);
86
+ });
87
+
88
+ it("should trigger abort event", () => {
89
+ const factory = createBrowserAbortControllerFactory();
90
+ const controller = factory.create();
91
+
92
+ const abortHandler = vi.fn();
93
+ controller.signal.addEventListener("abort", abortHandler);
94
+
95
+ controller.abort();
96
+
97
+ expect(abortHandler).toHaveBeenCalledTimes(1);
98
+ });
99
+
100
+ it("should work with fetch-like API", async () => {
101
+ const factory = createBrowserAbortControllerFactory();
102
+ const controller = factory.create();
103
+
104
+ // Simulate an abortable operation
105
+ const operation = new Promise((resolve, reject) => {
106
+ const checkAbort = () => {
107
+ if (controller.signal.aborted) {
108
+ reject(new DOMException("Aborted", "AbortError"));
109
+ return true;
110
+ }
111
+ return false;
112
+ };
113
+
114
+ if (checkAbort()) return;
115
+
116
+ controller.signal.addEventListener("abort", () => {
117
+ reject(new DOMException("Aborted", "AbortError"));
118
+ });
119
+
120
+ // Simulate async work
121
+ setTimeout(() => {
122
+ if (!checkAbort()) {
123
+ resolve("completed");
124
+ }
125
+ }, 100);
126
+ });
127
+
128
+ // Abort immediately
129
+ controller.abort();
130
+
131
+ await expect(operation).rejects.toThrow("Aborted");
132
+ });
133
+ });
134
+
135
+ describe("integration", () => {
136
+ it("should work with multiple independent operations", () => {
137
+ const factory = createBrowserAbortControllerFactory();
138
+
139
+ const controller1 = factory.create();
140
+ const controller2 = factory.create();
141
+ const controller3 = factory.create();
142
+
143
+ const handler1 = vi.fn();
144
+ const handler2 = vi.fn();
145
+ const handler3 = vi.fn();
146
+
147
+ controller1.signal.addEventListener("abort", handler1);
148
+ controller2.signal.addEventListener("abort", handler2);
149
+ controller3.signal.addEventListener("abort", handler3);
150
+
151
+ // Abort only controller2
152
+ controller2.abort();
153
+
154
+ expect(controller1.signal.aborted).toBe(false);
155
+ expect(controller2.signal.aborted).toBe(true);
156
+ expect(controller3.signal.aborted).toBe(false);
157
+
158
+ expect(handler1).not.toHaveBeenCalled();
159
+ expect(handler2).toHaveBeenCalled();
160
+ expect(handler3).not.toHaveBeenCalled();
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createChecksumService } from "./checksum-service";
3
+
4
+ describe("createChecksumService", () => {
5
+ it("should create a checksum service", () => {
6
+ const service = createChecksumService();
7
+ expect(service).toBeDefined();
8
+ expect(service.computeChecksum).toBeDefined();
9
+ expect(typeof service.computeChecksum).toBe("function");
10
+ });
11
+
12
+ describe("computeChecksum", () => {
13
+ it("should compute checksum for Uint8Array data", async () => {
14
+ const service = createChecksumService();
15
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
16
+
17
+ const checksum = await service.computeChecksum(data);
18
+
19
+ // Should return a 64-character hex string (SHA-256)
20
+ expect(checksum).toHaveLength(64);
21
+ expect(checksum).toMatch(/^[a-f0-9]{64}$/);
22
+ });
23
+
24
+ it("should return same checksum for identical data", async () => {
25
+ const service = createChecksumService();
26
+ const data1 = new Uint8Array([1, 2, 3, 4, 5]);
27
+ const data2 = new Uint8Array([1, 2, 3, 4, 5]);
28
+
29
+ const checksum1 = await service.computeChecksum(data1);
30
+ const checksum2 = await service.computeChecksum(data2);
31
+
32
+ expect(checksum1).toBe(checksum2);
33
+ });
34
+
35
+ it("should return different checksum for different data", async () => {
36
+ const service = createChecksumService();
37
+ const data1 = new Uint8Array([1, 2, 3, 4, 5]);
38
+ const data2 = new Uint8Array([5, 4, 3, 2, 1]);
39
+
40
+ const checksum1 = await service.computeChecksum(data1);
41
+ const checksum2 = await service.computeChecksum(data2);
42
+
43
+ expect(checksum1).not.toBe(checksum2);
44
+ });
45
+
46
+ it("should handle empty data", async () => {
47
+ const service = createChecksumService();
48
+ const data = new Uint8Array([]);
49
+
50
+ const checksum = await service.computeChecksum(data);
51
+
52
+ expect(checksum).toHaveLength(64);
53
+ expect(checksum).toMatch(/^[a-f0-9]{64}$/);
54
+ });
55
+
56
+ it("should handle large data", async () => {
57
+ const service = createChecksumService();
58
+ // Create 1KB of data
59
+ const data = new Uint8Array(1024);
60
+ for (let i = 0; i < data.length; i++) {
61
+ data[i] = i % 256;
62
+ }
63
+
64
+ const checksum = await service.computeChecksum(data);
65
+
66
+ expect(checksum).toHaveLength(64);
67
+ expect(checksum).toMatch(/^[a-f0-9]{64}$/);
68
+ });
69
+ });
70
+ });