@uploadista/client-core 0.0.13 → 0.0.14
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/dist/index.d.mts +972 -33
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -4
- package/src/index.ts +2 -0
- package/src/managers/__tests__/event-subscription-manager.test.ts +566 -0
- package/src/managers/__tests__/upload-manager.test.ts +588 -0
- package/src/managers/event-subscription-manager.ts +280 -0
- package/src/managers/flow-manager.ts +614 -0
- package/src/managers/index.ts +28 -0
- package/src/managers/upload-manager.ts +353 -0
- package/src/services/service-container.ts +213 -1
- package/src/testing/index.ts +16 -0
- package/src/testing/mock-service-container.ts +629 -0
- package/src/types/flow-upload-options.ts +29 -4
- package/src/types/index.ts +1 -0
- package/src/types/upload-metrics.ts +130 -0
- package/src/types/upload-options.ts +17 -1
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AbortControllerFactory,
|
|
3
|
+
AbortControllerLike,
|
|
4
|
+
AbortSignalLike,
|
|
5
|
+
} from "../services/abort-controller-service";
|
|
6
|
+
import type { ChecksumService } from "../services/checksum-service";
|
|
7
|
+
import type {
|
|
8
|
+
Base64Service,
|
|
9
|
+
FileReaderService,
|
|
10
|
+
FileSource,
|
|
11
|
+
SliceResult,
|
|
12
|
+
} from "../services/file-reader-service";
|
|
13
|
+
import type { FingerprintService } from "../services/fingerprint-service";
|
|
14
|
+
import type {
|
|
15
|
+
ConnectionMetrics,
|
|
16
|
+
DetailedConnectionMetrics,
|
|
17
|
+
HeadersLike,
|
|
18
|
+
HttpClient,
|
|
19
|
+
HttpRequestOptions,
|
|
20
|
+
HttpResponse,
|
|
21
|
+
} from "../services/http-client";
|
|
22
|
+
import type { IdGenerationService } from "../services/id-generation-service";
|
|
23
|
+
import type {
|
|
24
|
+
PlatformService,
|
|
25
|
+
Timeout,
|
|
26
|
+
} from "../services/platform-service";
|
|
27
|
+
import type { ServiceContainer } from "../services/service-container";
|
|
28
|
+
import type { StorageService } from "../services/storage-service";
|
|
29
|
+
import type {
|
|
30
|
+
WebSocketFactory,
|
|
31
|
+
WebSocketLike,
|
|
32
|
+
} from "../services/websocket-service";
|
|
33
|
+
|
|
34
|
+
// Platform globals polyfill for testing environments
|
|
35
|
+
declare function setTimeout(callback: () => void, ms: number): number;
|
|
36
|
+
declare function clearTimeout(id: number): void;
|
|
37
|
+
declare class TextEncoder {
|
|
38
|
+
encode(input?: string): Uint8Array;
|
|
39
|
+
}
|
|
40
|
+
declare class Blob {
|
|
41
|
+
readonly size: number;
|
|
42
|
+
readonly type: string;
|
|
43
|
+
slice(start?: number, end?: number, contentType?: string): Blob;
|
|
44
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
45
|
+
}
|
|
46
|
+
declare function btoa(data: string): string;
|
|
47
|
+
declare function atob(data: string): string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mock HTTP response configuration for testing
|
|
51
|
+
*/
|
|
52
|
+
export interface MockHttpResponseConfig {
|
|
53
|
+
status?: number;
|
|
54
|
+
statusText?: string;
|
|
55
|
+
headers?: Record<string, string>;
|
|
56
|
+
body?: unknown;
|
|
57
|
+
delay?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Mock HTTP client for testing upload logic without actual network calls
|
|
62
|
+
*
|
|
63
|
+
* Allows configuring responses for specific URLs and methods, with support
|
|
64
|
+
* for delays to simulate network latency.
|
|
65
|
+
*
|
|
66
|
+
* @example Basic usage
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const httpClient = new MockHttpClient();
|
|
69
|
+
* httpClient.mockResponse('https://api.example.com/upload', {
|
|
70
|
+
* status: 200,
|
|
71
|
+
* body: { uploadId: 'abc123' }
|
|
72
|
+
* });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export class MockHttpClient implements HttpClient {
|
|
76
|
+
private responses = new Map<string, MockHttpResponseConfig>();
|
|
77
|
+
private defaultResponse: MockHttpResponseConfig = {
|
|
78
|
+
status: 200,
|
|
79
|
+
statusText: "OK",
|
|
80
|
+
body: {},
|
|
81
|
+
};
|
|
82
|
+
private requestLog: Array<{ url: string; options?: HttpRequestOptions }> = [];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Configure a mock response for a specific URL
|
|
86
|
+
*/
|
|
87
|
+
mockResponse(url: string, config: MockHttpResponseConfig): void {
|
|
88
|
+
this.responses.set(url, config);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Set the default response for unmocked URLs
|
|
93
|
+
*/
|
|
94
|
+
setDefaultResponse(config: MockHttpResponseConfig): void {
|
|
95
|
+
this.defaultResponse = config;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get the log of all requests made
|
|
100
|
+
*/
|
|
101
|
+
getRequestLog(): Array<{ url: string; options?: HttpRequestOptions }> {
|
|
102
|
+
return [...this.requestLog];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear the request log
|
|
107
|
+
*/
|
|
108
|
+
clearRequestLog(): void {
|
|
109
|
+
this.requestLog = [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async request(
|
|
113
|
+
url: string,
|
|
114
|
+
options?: HttpRequestOptions,
|
|
115
|
+
): Promise<HttpResponse> {
|
|
116
|
+
// Log the request
|
|
117
|
+
this.requestLog.push({ url, options });
|
|
118
|
+
|
|
119
|
+
// Get the configured response or use default
|
|
120
|
+
const config = this.responses.get(url) || this.defaultResponse;
|
|
121
|
+
|
|
122
|
+
// Simulate network delay if configured
|
|
123
|
+
if (config.delay) {
|
|
124
|
+
await new Promise<void>((resolve) => setTimeout(() => resolve(), config.delay ?? 0));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create mock headers
|
|
128
|
+
const headers: HeadersLike = {
|
|
129
|
+
get: (name: string) => config.headers?.[name] ?? null,
|
|
130
|
+
has: (name: string) => config.headers?.[name] !== undefined,
|
|
131
|
+
forEach: (callback: (value: string, name: string) => void) => {
|
|
132
|
+
if (config.headers) {
|
|
133
|
+
for (const [key, value] of Object.entries(config.headers)) {
|
|
134
|
+
// Call the callback directly with value and name
|
|
135
|
+
(callback as (value: string, name: string) => void)(value, key);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Create mock response
|
|
142
|
+
const status = config.status ?? 200;
|
|
143
|
+
const response: HttpResponse = {
|
|
144
|
+
status,
|
|
145
|
+
statusText: config.statusText ?? "OK",
|
|
146
|
+
headers,
|
|
147
|
+
ok: status >= 200 && status < 300,
|
|
148
|
+
json: async () => config.body,
|
|
149
|
+
text: async () =>
|
|
150
|
+
typeof config.body === "string"
|
|
151
|
+
? config.body
|
|
152
|
+
: JSON.stringify(config.body),
|
|
153
|
+
arrayBuffer: async (): Promise<ArrayBuffer> => {
|
|
154
|
+
const text =
|
|
155
|
+
typeof config.body === "string"
|
|
156
|
+
? config.body
|
|
157
|
+
: JSON.stringify(config.body);
|
|
158
|
+
return new TextEncoder().encode(text).buffer as ArrayBuffer;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return response;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
getMetrics(): ConnectionMetrics {
|
|
166
|
+
return {
|
|
167
|
+
activeConnections: 0,
|
|
168
|
+
totalConnections: this.requestLog.length,
|
|
169
|
+
reuseRate: 0,
|
|
170
|
+
averageConnectionTime: 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getDetailedMetrics(): DetailedConnectionMetrics {
|
|
175
|
+
return {
|
|
176
|
+
activeConnections: 0,
|
|
177
|
+
totalConnections: this.requestLog.length,
|
|
178
|
+
reuseRate: 0,
|
|
179
|
+
averageConnectionTime: 0,
|
|
180
|
+
health: {
|
|
181
|
+
status: "healthy",
|
|
182
|
+
score: 100,
|
|
183
|
+
issues: [],
|
|
184
|
+
recommendations: [],
|
|
185
|
+
},
|
|
186
|
+
requestsPerSecond: 0,
|
|
187
|
+
errorRate: 0,
|
|
188
|
+
timeouts: 0,
|
|
189
|
+
retries: 0,
|
|
190
|
+
fastConnections: this.requestLog.length,
|
|
191
|
+
slowConnections: 0,
|
|
192
|
+
http2Info: {
|
|
193
|
+
supported: false,
|
|
194
|
+
detected: false,
|
|
195
|
+
version: "1.1",
|
|
196
|
+
multiplexingActive: false,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
reset(): void {
|
|
202
|
+
this.requestLog = [];
|
|
203
|
+
this.responses.clear();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async close(): Promise<void> {
|
|
207
|
+
// No-op for mock
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async warmupConnections(_urls: string[]): Promise<void> {
|
|
211
|
+
// No-op for mock
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Mock storage service for testing without actual persistent storage
|
|
217
|
+
*
|
|
218
|
+
* Uses in-memory storage that can be inspected and manipulated for testing.
|
|
219
|
+
*/
|
|
220
|
+
export class MockStorageService implements StorageService {
|
|
221
|
+
private storage = new Map<string, string>();
|
|
222
|
+
|
|
223
|
+
async getItem(key: string): Promise<string | null> {
|
|
224
|
+
return this.storage.get(key) ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
228
|
+
this.storage.set(key, value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async removeItem(key: string): Promise<void> {
|
|
232
|
+
this.storage.delete(key);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async findAll(): Promise<Record<string, string>> {
|
|
236
|
+
const result: Record<string, string> = {};
|
|
237
|
+
for (const [key, value] of this.storage.entries()) {
|
|
238
|
+
result[key] = value;
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async find(prefix: string): Promise<Record<string, string>> {
|
|
244
|
+
const result: Record<string, string> = {};
|
|
245
|
+
for (const [key, value] of this.storage.entries()) {
|
|
246
|
+
if (key.startsWith(prefix)) {
|
|
247
|
+
result[key] = value;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the current storage state for inspection
|
|
255
|
+
*/
|
|
256
|
+
getStorageState(): Map<string, string> {
|
|
257
|
+
return new Map(this.storage);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Clear all storage
|
|
262
|
+
*/
|
|
263
|
+
clear(): void {
|
|
264
|
+
this.storage.clear();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Mock file reader service for testing file operations
|
|
270
|
+
*
|
|
271
|
+
* Accepts either File/Blob objects or mock file data (Uint8Array).
|
|
272
|
+
*/
|
|
273
|
+
export class MockFileReaderService<UploadInput = unknown>
|
|
274
|
+
implements FileReaderService<UploadInput>
|
|
275
|
+
{
|
|
276
|
+
async openFile(input: UploadInput, _chunkSize: number): Promise<FileSource> {
|
|
277
|
+
// Handle File/Blob objects
|
|
278
|
+
if (
|
|
279
|
+
input instanceof Blob ||
|
|
280
|
+
(input && typeof input === "object" && "size" in input)
|
|
281
|
+
) {
|
|
282
|
+
const file = input as Blob & { name?: string; lastModified?: number };
|
|
283
|
+
return {
|
|
284
|
+
input,
|
|
285
|
+
size: file.size,
|
|
286
|
+
name:
|
|
287
|
+
"name" in file && typeof file.name === "string" ? file.name : null,
|
|
288
|
+
type: file.type || null,
|
|
289
|
+
lastModified:
|
|
290
|
+
"lastModified" in file && typeof file.lastModified === "number"
|
|
291
|
+
? file.lastModified
|
|
292
|
+
: null,
|
|
293
|
+
slice: async (start: number, end: number): Promise<SliceResult> => {
|
|
294
|
+
if (start >= file.size) {
|
|
295
|
+
return { done: true, value: null, size: null };
|
|
296
|
+
}
|
|
297
|
+
const blob = file.slice(start, end);
|
|
298
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
299
|
+
const chunk = new Uint8Array(arrayBuffer);
|
|
300
|
+
return { done: false, value: chunk, size: chunk.length };
|
|
301
|
+
},
|
|
302
|
+
close: () => {
|
|
303
|
+
// No-op for Blob
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Handle Uint8Array for testing
|
|
309
|
+
if (input instanceof Uint8Array) {
|
|
310
|
+
const data = input;
|
|
311
|
+
return {
|
|
312
|
+
input,
|
|
313
|
+
size: data.length,
|
|
314
|
+
name: "test-file.bin",
|
|
315
|
+
type: "application/octet-stream",
|
|
316
|
+
lastModified: Date.now(),
|
|
317
|
+
slice: async (start: number, end: number): Promise<SliceResult> => {
|
|
318
|
+
if (start >= data.length) {
|
|
319
|
+
return { done: true, value: null, size: null };
|
|
320
|
+
}
|
|
321
|
+
const chunk = data.slice(start, end);
|
|
322
|
+
return { done: false, value: chunk, size: chunk.length };
|
|
323
|
+
},
|
|
324
|
+
close: () => {
|
|
325
|
+
// No-op for Uint8Array
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Fallback for unknown types
|
|
331
|
+
throw new Error(
|
|
332
|
+
`MockFileReaderService: Unsupported input type: ${typeof input}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Mock abort controller for testing cancellation
|
|
339
|
+
*/
|
|
340
|
+
export class MockAbortController implements AbortControllerLike {
|
|
341
|
+
private _aborted = false;
|
|
342
|
+
private listeners: Array<() => void> = [];
|
|
343
|
+
|
|
344
|
+
get signal(): AbortSignalLike {
|
|
345
|
+
return {
|
|
346
|
+
aborted: this._aborted,
|
|
347
|
+
addEventListener: (_event: string, listener: () => void) => {
|
|
348
|
+
this.listeners.push(listener);
|
|
349
|
+
},
|
|
350
|
+
removeEventListener: (_event: string, listener: () => void) => {
|
|
351
|
+
const index = this.listeners.indexOf(listener);
|
|
352
|
+
if (index !== -1) {
|
|
353
|
+
this.listeners.splice(index, 1);
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
abort(): void {
|
|
360
|
+
this._aborted = true;
|
|
361
|
+
for (const listener of this.listeners) {
|
|
362
|
+
listener();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Mock abort controller factory
|
|
369
|
+
*/
|
|
370
|
+
export class MockAbortControllerFactory implements AbortControllerFactory {
|
|
371
|
+
create(): AbortControllerLike {
|
|
372
|
+
return new MockAbortController();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Mock WebSocket for testing real-time events
|
|
378
|
+
*/
|
|
379
|
+
export class MockWebSocket implements WebSocketLike {
|
|
380
|
+
readonly CONNECTING = 0;
|
|
381
|
+
readonly OPEN = 1;
|
|
382
|
+
readonly CLOSING = 2;
|
|
383
|
+
readonly CLOSED = 3;
|
|
384
|
+
|
|
385
|
+
readyState = 0; // CONNECTING
|
|
386
|
+
|
|
387
|
+
onopen: (() => void) | null = null;
|
|
388
|
+
onclose: ((event: { code: number; reason: string }) => void) | null = null;
|
|
389
|
+
onerror: ((event: { message: string }) => void) | null = null;
|
|
390
|
+
onmessage: ((event: { data: string }) => void) | null = null;
|
|
391
|
+
|
|
392
|
+
constructor(public url: string) {
|
|
393
|
+
// Simulate connection opening after a short delay
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
this.readyState = 1; // OPEN
|
|
396
|
+
if (this.onopen) {
|
|
397
|
+
this.onopen();
|
|
398
|
+
}
|
|
399
|
+
}, 10);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
send(_data: string | Uint8Array): void {
|
|
403
|
+
if (this.readyState !== 1) {
|
|
404
|
+
throw new Error("WebSocket is not open");
|
|
405
|
+
}
|
|
406
|
+
// Mock implementation - in tests, you can override this
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
close(code = 1000, reason = ""): void {
|
|
410
|
+
this.readyState = 3; // CLOSED
|
|
411
|
+
if (this.onclose) {
|
|
412
|
+
this.onclose({ code, reason });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Simulate receiving a message (for testing)
|
|
418
|
+
*/
|
|
419
|
+
simulateMessage(data: string): void {
|
|
420
|
+
if (this.readyState === 1 && this.onmessage) {
|
|
421
|
+
this.onmessage({ data });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Simulate an error (for testing)
|
|
427
|
+
*/
|
|
428
|
+
simulateError(message: string): void {
|
|
429
|
+
if (this.onerror) {
|
|
430
|
+
this.onerror({ message });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Mock WebSocket factory
|
|
437
|
+
*/
|
|
438
|
+
export class MockWebSocketFactory implements WebSocketFactory {
|
|
439
|
+
create(url: string): WebSocketLike {
|
|
440
|
+
return new MockWebSocket(url);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Mock platform service
|
|
446
|
+
*/
|
|
447
|
+
export class MockPlatformService implements PlatformService {
|
|
448
|
+
private timers = new Map<Timeout, ReturnType<typeof setTimeout>>();
|
|
449
|
+
private timerId = 0;
|
|
450
|
+
|
|
451
|
+
constructor(
|
|
452
|
+
private browser = true,
|
|
453
|
+
private online = true,
|
|
454
|
+
) {}
|
|
455
|
+
|
|
456
|
+
setTimeout(callback: () => void, ms: number | undefined): Timeout {
|
|
457
|
+
const id = ++this.timerId;
|
|
458
|
+
const timer = setTimeout(callback, ms ?? 0);
|
|
459
|
+
this.timers.set(id, timer);
|
|
460
|
+
return id;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
clearTimeout(id: Timeout): void {
|
|
464
|
+
const timer = this.timers.get(id);
|
|
465
|
+
if (timer !== undefined) {
|
|
466
|
+
clearTimeout(timer);
|
|
467
|
+
this.timers.delete(id);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
isBrowser(): boolean {
|
|
472
|
+
return this.browser;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
isOnline(): boolean {
|
|
476
|
+
return this.online;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
isFileLike(value: unknown): boolean {
|
|
480
|
+
if (typeof value !== "object" || value === null) return false;
|
|
481
|
+
// Check for File/Blob-like properties
|
|
482
|
+
return (
|
|
483
|
+
"size" in value &&
|
|
484
|
+
typeof (value as { size: unknown }).size === "number" &&
|
|
485
|
+
("slice" in value || "type" in value)
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
getFileName(file: unknown): string | undefined {
|
|
490
|
+
if (
|
|
491
|
+
typeof file === "object" &&
|
|
492
|
+
file !== null &&
|
|
493
|
+
"name" in file &&
|
|
494
|
+
typeof (file as { name: unknown }).name === "string"
|
|
495
|
+
) {
|
|
496
|
+
return (file as { name: string }).name;
|
|
497
|
+
}
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
getFileType(file: unknown): string | undefined {
|
|
502
|
+
if (
|
|
503
|
+
typeof file === "object" &&
|
|
504
|
+
file !== null &&
|
|
505
|
+
"type" in file &&
|
|
506
|
+
typeof (file as { type: unknown }).type === "string"
|
|
507
|
+
) {
|
|
508
|
+
return (file as { type: string }).type;
|
|
509
|
+
}
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
getFileSize(file: unknown): number | undefined {
|
|
514
|
+
if (
|
|
515
|
+
typeof file === "object" &&
|
|
516
|
+
file !== null &&
|
|
517
|
+
"size" in file &&
|
|
518
|
+
typeof (file as { size: unknown }).size === "number"
|
|
519
|
+
) {
|
|
520
|
+
return (file as { size: number }).size;
|
|
521
|
+
}
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
getFileLastModified(file: unknown): number | undefined {
|
|
526
|
+
if (
|
|
527
|
+
typeof file === "object" &&
|
|
528
|
+
file !== null &&
|
|
529
|
+
"lastModified" in file &&
|
|
530
|
+
typeof (file as { lastModified: unknown }).lastModified === "number"
|
|
531
|
+
) {
|
|
532
|
+
return (file as { lastModified: number }).lastModified;
|
|
533
|
+
}
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Set online status for testing
|
|
539
|
+
*/
|
|
540
|
+
setOnline(online: boolean): void {
|
|
541
|
+
this.online = online;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Mock checksum service
|
|
547
|
+
*/
|
|
548
|
+
export class MockChecksumService implements ChecksumService {
|
|
549
|
+
async computeChecksum(_data: Uint8Array): Promise<string> {
|
|
550
|
+
// Return a mock checksum
|
|
551
|
+
return `mock-checksum-${Math.random().toString(36).substring(7)}`;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Mock fingerprint service
|
|
557
|
+
*/
|
|
558
|
+
export class MockFingerprintService<UploadInput>
|
|
559
|
+
implements FingerprintService<UploadInput>
|
|
560
|
+
{
|
|
561
|
+
async computeFingerprint(_input: UploadInput): Promise<string> {
|
|
562
|
+
// Return a mock fingerprint
|
|
563
|
+
return `mock-fingerprint-${Math.random().toString(36).substring(7)}`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Mock ID generation service
|
|
569
|
+
*/
|
|
570
|
+
export class MockIdGenerationService implements IdGenerationService {
|
|
571
|
+
private counter = 0;
|
|
572
|
+
|
|
573
|
+
generate(): string {
|
|
574
|
+
return `mock-id-${++this.counter}`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Mock base64 service
|
|
580
|
+
*/
|
|
581
|
+
export class MockBase64Service implements Base64Service {
|
|
582
|
+
toBase64(data: ArrayBuffer): string {
|
|
583
|
+
// Simple mock implementation
|
|
584
|
+
const bytes = new Uint8Array(data);
|
|
585
|
+
let binary = "";
|
|
586
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
587
|
+
binary += String.fromCharCode(bytes[i] ?? 0);
|
|
588
|
+
}
|
|
589
|
+
return btoa(binary);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
fromBase64(data: string): ArrayBuffer {
|
|
593
|
+
const binary = atob(data);
|
|
594
|
+
const bytes = new Uint8Array(binary.length);
|
|
595
|
+
for (let i = 0; i < binary.length; i++) {
|
|
596
|
+
bytes[i] = binary.charCodeAt(i);
|
|
597
|
+
}
|
|
598
|
+
return bytes.buffer;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Create a complete mock service container for testing
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```typescript
|
|
607
|
+
* const services = createMockServiceContainer();
|
|
608
|
+
* const client = createUploadistaClient({
|
|
609
|
+
* apiUrl: 'https://api.example.com',
|
|
610
|
+
* services,
|
|
611
|
+
* });
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
export function createMockServiceContainer<
|
|
615
|
+
UploadInput = unknown,
|
|
616
|
+
>(): ServiceContainer<UploadInput> {
|
|
617
|
+
return {
|
|
618
|
+
storage: new MockStorageService(),
|
|
619
|
+
idGeneration: new MockIdGenerationService(),
|
|
620
|
+
httpClient: new MockHttpClient(),
|
|
621
|
+
fileReader: new MockFileReaderService<UploadInput>(),
|
|
622
|
+
base64: new MockBase64Service(),
|
|
623
|
+
websocket: new MockWebSocketFactory(),
|
|
624
|
+
abortController: new MockAbortControllerFactory(),
|
|
625
|
+
platform: new MockPlatformService(),
|
|
626
|
+
checksumService: new MockChecksumService(),
|
|
627
|
+
fingerprintService: new MockFingerprintService<UploadInput>(),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TypedOutput } from "@uploadista/core/flow";
|
|
1
2
|
import type { UploadFile } from "@uploadista/core/types";
|
|
2
3
|
import type { FlowUploadConfig } from "./flow-upload-config";
|
|
3
4
|
|
|
@@ -7,11 +8,21 @@ export interface FlowUploadOptions<TOutput = UploadFile> {
|
|
|
7
8
|
*/
|
|
8
9
|
flowConfig: FlowUploadConfig;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Called when the flow job starts
|
|
13
|
+
* @param jobId - The unique identifier for the flow job
|
|
14
|
+
*/
|
|
15
|
+
onJobStart?: (jobId: string) => void;
|
|
16
|
+
|
|
10
17
|
/**
|
|
11
18
|
* Called when upload progress updates
|
|
19
|
+
*
|
|
20
|
+
* @param uploadId - The unique identifier for this upload
|
|
21
|
+
* @param bytesUploaded - Number of bytes uploaded so far
|
|
22
|
+
* @param totalBytes - Total bytes to upload, null if unknown/deferred
|
|
12
23
|
*/
|
|
13
24
|
onProgress?: (
|
|
14
|
-
|
|
25
|
+
uploadId: string,
|
|
15
26
|
bytesUploaded: number,
|
|
16
27
|
totalBytes: number | null,
|
|
17
28
|
) => void;
|
|
@@ -27,10 +38,24 @@ export interface FlowUploadOptions<TOutput = UploadFile> {
|
|
|
27
38
|
|
|
28
39
|
/**
|
|
29
40
|
* Called when the flow completes successfully (receives full flow outputs)
|
|
30
|
-
* This is the recommended callback for multi-output flows
|
|
31
|
-
*
|
|
41
|
+
* This is the recommended callback for multi-output flows.
|
|
42
|
+
* Each output includes nodeId, optional nodeType, data, and timestamp.
|
|
43
|
+
*
|
|
44
|
+
* @param outputs - Array of typed outputs from all output nodes
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* onFlowComplete: (outputs) => {
|
|
49
|
+
* // Access all outputs with type information
|
|
50
|
+
* for (const output of outputs) {
|
|
51
|
+
* if (output.nodeType === 'storage-output-v1') {
|
|
52
|
+
* console.log('Storage output:', output.data);
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
32
57
|
*/
|
|
33
|
-
onFlowComplete?: (outputs:
|
|
58
|
+
onFlowComplete?: (outputs: TypedOutput[]) => void;
|
|
34
59
|
|
|
35
60
|
/**
|
|
36
61
|
* Called when upload succeeds (legacy, single-output flows)
|
package/src/types/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./multi-flow-upload-state";
|
|
|
8
8
|
export * from "./performance-insights";
|
|
9
9
|
export * from "./previous-upload";
|
|
10
10
|
export * from "./upload-options";
|
|
11
|
+
export * from "./upload-metrics";
|
|
11
12
|
export * from "./upload-response";
|
|
12
13
|
export * from "./upload-result";
|
|
13
14
|
export * from "./upload-session-metrics";
|