@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,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConnectionPoolConfig,
|
|
3
|
+
createClientStorage,
|
|
4
|
+
createLogger,
|
|
5
|
+
createUploadistaClient as createUploadistaClientCore,
|
|
6
|
+
type UploadistaClientOptions as UploadistaClientOptionsCore,
|
|
7
|
+
} from "@uploadista/client-core";
|
|
8
|
+
import { createBrowserServices } from "../services/create-browser-services";
|
|
9
|
+
import type { BrowserUploadInput } from "../types/upload-input";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration options for creating a browser-specific Uploadista client.
|
|
13
|
+
*
|
|
14
|
+
* This interface extends the core client options but omits browser-specific
|
|
15
|
+
* services that are automatically provided by the browser environment.
|
|
16
|
+
* These services include WebSocket factory, AbortController, ID generation,
|
|
17
|
+
* storage, logging, platform detection, fingerprinting, HTTP client, file reader,
|
|
18
|
+
* and checksum calculation.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { createUploadistaClient } from '@uploadista/client-browser';
|
|
23
|
+
*
|
|
24
|
+
* const client = createUploadistaClient({
|
|
25
|
+
* endpoint: 'https://api.uploadista.com/upload',
|
|
26
|
+
* connectionPooling: {
|
|
27
|
+
* maxConnectionsPerHost: 6,
|
|
28
|
+
* enableHttp2: true
|
|
29
|
+
* }
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export interface UploadistaClientOptions
|
|
34
|
+
extends Omit<
|
|
35
|
+
UploadistaClientOptionsCore<BrowserUploadInput>,
|
|
36
|
+
| "webSocketFactory"
|
|
37
|
+
| "abortControllerFactory"
|
|
38
|
+
| "generateId"
|
|
39
|
+
| "clientStorage"
|
|
40
|
+
| "logger"
|
|
41
|
+
| "platformService"
|
|
42
|
+
| "fingerprintService"
|
|
43
|
+
| "httpClient"
|
|
44
|
+
| "fileReader"
|
|
45
|
+
| "checksumService"
|
|
46
|
+
> {
|
|
47
|
+
/**
|
|
48
|
+
* Connection pooling configuration for the HTTP client.
|
|
49
|
+
*
|
|
50
|
+
* Controls how the browser manages HTTP connections for optimal performance.
|
|
51
|
+
* The browser's native fetch API with keep-alive headers is used under the hood.
|
|
52
|
+
*
|
|
53
|
+
* @default
|
|
54
|
+
* ```typescript
|
|
55
|
+
* {
|
|
56
|
+
* maxConnectionsPerHost: 6,
|
|
57
|
+
* connectionTimeout: 30000,
|
|
58
|
+
* keepAliveTimeout: 60000,
|
|
59
|
+
* enableHttp2: true,
|
|
60
|
+
* retryOnConnectionError: true
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* connectionPooling: {
|
|
67
|
+
* maxConnectionsPerHost: 10,
|
|
68
|
+
* enableHttp2: true,
|
|
69
|
+
* keepAliveTimeout: 120000
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
connectionPooling?: ConnectionPoolConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates a browser-optimized Uploadista client for file uploads and flow processing.
|
|
78
|
+
*
|
|
79
|
+
* This factory function automatically configures all browser-specific services including:
|
|
80
|
+
* - Fetch-based HTTP client with connection pooling
|
|
81
|
+
* - Native WebSocket support for real-time progress
|
|
82
|
+
* - localStorage for upload state persistence
|
|
83
|
+
* - Web Crypto API for checksums and fingerprints
|
|
84
|
+
* - File API for reading and chunking files
|
|
85
|
+
* - Browser platform detection and capabilities
|
|
86
|
+
*
|
|
87
|
+
* The created client can handle File and Blob objects from file inputs, drag-and-drop,
|
|
88
|
+
* or programmatically created content. It supports resumable uploads, progress tracking,
|
|
89
|
+
* and flow-based file processing.
|
|
90
|
+
*
|
|
91
|
+
* @param options - Configuration options for the browser client
|
|
92
|
+
* @returns A fully configured Uploadista client ready for browser use
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* import { createUploadistaClient } from '@uploadista/client-browser';
|
|
97
|
+
*
|
|
98
|
+
* // Basic usage
|
|
99
|
+
* const client = createUploadistaClient({
|
|
100
|
+
* endpoint: 'https://api.uploadista.com/upload'
|
|
101
|
+
* });
|
|
102
|
+
*
|
|
103
|
+
* // With custom configuration
|
|
104
|
+
* const client = createUploadistaClient({
|
|
105
|
+
* endpoint: 'https://api.uploadista.com/upload',
|
|
106
|
+
* connectionPooling: {
|
|
107
|
+
* maxConnectionsPerHost: 6,
|
|
108
|
+
* enableHttp2: true,
|
|
109
|
+
* keepAliveTimeout: 60000
|
|
110
|
+
* },
|
|
111
|
+
* chunkSize: 5 * 1024 * 1024, // 5MB chunks
|
|
112
|
+
* retryDelays: [1000, 3000, 5000],
|
|
113
|
+
* allowedMetaFields: ['userId', 'projectId']
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* // Upload a file
|
|
117
|
+
* const fileInput = document.querySelector('input[type="file"]');
|
|
118
|
+
* const file = fileInput.files[0];
|
|
119
|
+
*
|
|
120
|
+
* const upload = await client.upload(file, {
|
|
121
|
+
* onProgress: (event) => {
|
|
122
|
+
* console.log(`Progress: ${event.progress}%`);
|
|
123
|
+
* }
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* console.log('Upload complete:', upload.id);
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @see {@link UploadistaClientOptions} for available configuration options
|
|
130
|
+
* @see {@link BrowserUploadInput} for supported file input types
|
|
131
|
+
*/
|
|
132
|
+
export function createUploadistaClient(options: UploadistaClientOptions) {
|
|
133
|
+
const services = createBrowserServices({
|
|
134
|
+
connectionPooling: options.connectionPooling,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return createUploadistaClientCore<BrowserUploadInput>({
|
|
138
|
+
...options,
|
|
139
|
+
webSocketFactory: services.websocket,
|
|
140
|
+
abortControllerFactory: services.abortController,
|
|
141
|
+
platformService: services.platform,
|
|
142
|
+
httpClient: services.httpClient,
|
|
143
|
+
fileReader: services.fileReader,
|
|
144
|
+
generateId: services.idGeneration,
|
|
145
|
+
fingerprintService: services.fingerprintService,
|
|
146
|
+
checksumService: services.checksumService,
|
|
147
|
+
logger: createLogger(false, () => {}),
|
|
148
|
+
clientStorage: createClientStorage(services.storage),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./create-uploadista-client";
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework Integration Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides TypeScript utilities and helper types for building
|
|
5
|
+
* framework-specific wrappers around the Uploadista client.
|
|
6
|
+
*
|
|
7
|
+
* @module framework-utils
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FlowResult, UploadResult } from "@uploadista/client-core";
|
|
11
|
+
import type { FlowEvent } from "@uploadista/core/flow";
|
|
12
|
+
import type { UploadEvent, UploadFile } from "@uploadista/core/types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base upload state that framework wrappers should implement
|
|
16
|
+
*/
|
|
17
|
+
export interface BaseUploadState {
|
|
18
|
+
status: "idle" | "uploading" | "success" | "error" | "aborted";
|
|
19
|
+
progress: number;
|
|
20
|
+
bytesUploaded: number;
|
|
21
|
+
totalBytes: number;
|
|
22
|
+
error?: Error;
|
|
23
|
+
result?: UploadResult<UploadFile>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Base flow upload state
|
|
28
|
+
*/
|
|
29
|
+
export interface BaseFlowUploadState extends BaseUploadState {
|
|
30
|
+
jobId?: string;
|
|
31
|
+
flowStatus?: "pending" | "processing" | "completed" | "failed";
|
|
32
|
+
flowResult?: FlowResult<unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Progress callback signature
|
|
37
|
+
*/
|
|
38
|
+
export type ProgressCallback = (
|
|
39
|
+
uploadId: string,
|
|
40
|
+
bytesUploaded: number,
|
|
41
|
+
totalBytes: number,
|
|
42
|
+
) => void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Complete callback signature
|
|
46
|
+
*/
|
|
47
|
+
export type CompleteCallback = (uploadId: string, result: UploadResult) => void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Error callback signature
|
|
51
|
+
*/
|
|
52
|
+
export type ErrorCallback = (uploadId: string, error: Error) => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Abort callback signature
|
|
56
|
+
*/
|
|
57
|
+
export type AbortCallback = (uploadId: string) => void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Event handler signature for framework wrappers
|
|
61
|
+
*/
|
|
62
|
+
export type EventHandler<T = unknown> = (event: T) => void;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* WebSocket event handler signature
|
|
66
|
+
*/
|
|
67
|
+
export type WebSocketEventHandler = (event: UploadEvent | FlowEvent) => void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Framework state updater function signature
|
|
71
|
+
* @template T - The state type
|
|
72
|
+
*/
|
|
73
|
+
export type StateUpdater<T> = (updater: (prevState: T) => T) => void;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Cleanup function returned by setup functions
|
|
77
|
+
*/
|
|
78
|
+
export type CleanupFunction = () => void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Upload item for multi-upload tracking
|
|
82
|
+
*/
|
|
83
|
+
export interface UploadItem {
|
|
84
|
+
id: string;
|
|
85
|
+
file: File;
|
|
86
|
+
status: BaseUploadState["status"];
|
|
87
|
+
progress: number;
|
|
88
|
+
bytesUploaded: number;
|
|
89
|
+
totalBytes: number;
|
|
90
|
+
error?: Error;
|
|
91
|
+
result?: UploadResult;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Multi-upload aggregate statistics
|
|
96
|
+
*/
|
|
97
|
+
export interface MultiUploadStats {
|
|
98
|
+
totalFiles: number;
|
|
99
|
+
completedFiles: number;
|
|
100
|
+
failedFiles: number;
|
|
101
|
+
totalBytes: number;
|
|
102
|
+
uploadedBytes: number;
|
|
103
|
+
totalProgress: number;
|
|
104
|
+
allComplete: boolean;
|
|
105
|
+
hasErrors: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Drag and drop state
|
|
110
|
+
*/
|
|
111
|
+
export interface DragDropState {
|
|
112
|
+
isDragging: boolean;
|
|
113
|
+
isOver: boolean;
|
|
114
|
+
files: File[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* File validation result
|
|
119
|
+
*/
|
|
120
|
+
export interface FileValidationResult {
|
|
121
|
+
valid: boolean;
|
|
122
|
+
error?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* File validation function signature
|
|
127
|
+
*/
|
|
128
|
+
export type FileValidator = (file: File) => FileValidationResult;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Utility: Calculate aggregate upload statistics
|
|
132
|
+
*/
|
|
133
|
+
export function calculateMultiUploadStats(
|
|
134
|
+
uploads: UploadItem[],
|
|
135
|
+
): MultiUploadStats {
|
|
136
|
+
const totalFiles = uploads.length;
|
|
137
|
+
const completedFiles = uploads.filter((u) => u.status === "success").length;
|
|
138
|
+
const failedFiles = uploads.filter((u) => u.status === "error").length;
|
|
139
|
+
const totalBytes = uploads.reduce((sum, u) => sum + u.totalBytes, 0);
|
|
140
|
+
const uploadedBytes = uploads.reduce((sum, u) => sum + u.bytesUploaded, 0);
|
|
141
|
+
const totalProgress = totalBytes > 0 ? (uploadedBytes / totalBytes) * 100 : 0;
|
|
142
|
+
const allComplete = uploads.every((u) => u.status === "success");
|
|
143
|
+
const hasErrors = uploads.some((u) => u.status === "error");
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
totalFiles,
|
|
147
|
+
completedFiles,
|
|
148
|
+
failedFiles,
|
|
149
|
+
totalBytes,
|
|
150
|
+
uploadedBytes,
|
|
151
|
+
totalProgress,
|
|
152
|
+
allComplete,
|
|
153
|
+
hasErrors,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Utility: Format file size for display
|
|
159
|
+
*/
|
|
160
|
+
export function formatFileSize(bytes: number): string {
|
|
161
|
+
if (bytes === 0) return "0 Bytes";
|
|
162
|
+
|
|
163
|
+
const k = 1024;
|
|
164
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
165
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
166
|
+
|
|
167
|
+
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Utility: Format progress percentage
|
|
172
|
+
*/
|
|
173
|
+
export function formatProgress(progress: number): string {
|
|
174
|
+
return `${Math.round(progress)}%`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Utility: Get file extension
|
|
179
|
+
*/
|
|
180
|
+
export function getFileExtension(filename: string): string {
|
|
181
|
+
const lastDot = filename.lastIndexOf(".");
|
|
182
|
+
return lastDot === -1 ? "" : filename.slice(lastDot + 1).toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Utility: Check if file is an image
|
|
187
|
+
*/
|
|
188
|
+
export function isImageFile(file: File): boolean {
|
|
189
|
+
return file.type.startsWith("image/");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Utility: Check if file is a video
|
|
194
|
+
*/
|
|
195
|
+
export function isVideoFile(file: File): boolean {
|
|
196
|
+
return file.type.startsWith("video/");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Utility: Create file size validator
|
|
201
|
+
*/
|
|
202
|
+
export function createFileSizeValidator(maxSizeBytes: number): FileValidator {
|
|
203
|
+
return (file: File): FileValidationResult => {
|
|
204
|
+
if (file.size > maxSizeBytes) {
|
|
205
|
+
return {
|
|
206
|
+
valid: false,
|
|
207
|
+
error: `File size exceeds maximum of ${formatFileSize(maxSizeBytes)}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { valid: true };
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Utility: Create file type validator
|
|
216
|
+
*/
|
|
217
|
+
export function createFileTypeValidator(allowedTypes: string[]): FileValidator {
|
|
218
|
+
return (file: File): FileValidationResult => {
|
|
219
|
+
const fileType = file.type.toLowerCase();
|
|
220
|
+
const fileExt = getFileExtension(file.name);
|
|
221
|
+
|
|
222
|
+
const isAllowed = allowedTypes.some((type) => {
|
|
223
|
+
if (type.startsWith(".")) {
|
|
224
|
+
return type.slice(1) === fileExt;
|
|
225
|
+
}
|
|
226
|
+
if (type.includes("*")) {
|
|
227
|
+
const pattern = type.replace("*", "");
|
|
228
|
+
return fileType.startsWith(pattern);
|
|
229
|
+
}
|
|
230
|
+
return fileType === type;
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (!isAllowed) {
|
|
234
|
+
return {
|
|
235
|
+
valid: false,
|
|
236
|
+
error: `File type not allowed. Allowed types: ${allowedTypes.join(", ")}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return { valid: true };
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Utility: Compose multiple validators
|
|
245
|
+
*/
|
|
246
|
+
export function composeValidators(
|
|
247
|
+
...validators: FileValidator[]
|
|
248
|
+
): FileValidator {
|
|
249
|
+
return (file: File): FileValidationResult => {
|
|
250
|
+
for (const validator of validators) {
|
|
251
|
+
const result = validator(file);
|
|
252
|
+
if (!result.valid) {
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return { valid: true };
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Utility: Generate unique upload ID
|
|
262
|
+
*/
|
|
263
|
+
export function generateUploadId(): string {
|
|
264
|
+
return `upload-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Utility: Create delay promise for retry logic
|
|
269
|
+
*/
|
|
270
|
+
export function delay(ms: number): Promise<void> {
|
|
271
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Utility: Calculate exponential backoff delay
|
|
276
|
+
*/
|
|
277
|
+
export function calculateBackoff(
|
|
278
|
+
attempt: number,
|
|
279
|
+
baseDelay = 1000,
|
|
280
|
+
maxDelay = 30000,
|
|
281
|
+
): number {
|
|
282
|
+
const delay = Math.min(baseDelay * 2 ** attempt, maxDelay);
|
|
283
|
+
// Add jitter to prevent thundering herd
|
|
284
|
+
return delay + Math.random() * 1000;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Utility: Create retry wrapper for upload function
|
|
289
|
+
*/
|
|
290
|
+
export function createRetryWrapper<T>(
|
|
291
|
+
fn: () => Promise<T>,
|
|
292
|
+
maxAttempts = 3,
|
|
293
|
+
shouldRetry: (error: unknown) => boolean = () => true,
|
|
294
|
+
): () => Promise<T> {
|
|
295
|
+
return async () => {
|
|
296
|
+
let lastError: unknown;
|
|
297
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
298
|
+
try {
|
|
299
|
+
return await fn();
|
|
300
|
+
} catch (error) {
|
|
301
|
+
lastError = error;
|
|
302
|
+
if (attempt < maxAttempts - 1 && shouldRetry(error)) {
|
|
303
|
+
const delayMs = calculateBackoff(attempt);
|
|
304
|
+
await delay(delayMs);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw lastError;
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Type guard: Check if error is network-related (should retry)
|
|
316
|
+
*/
|
|
317
|
+
export function isNetworkError(error: unknown): boolean {
|
|
318
|
+
if (error instanceof Error) {
|
|
319
|
+
return (
|
|
320
|
+
error.message.includes("network") ||
|
|
321
|
+
error.message.includes("timeout") ||
|
|
322
|
+
error.message.includes("connection") ||
|
|
323
|
+
error.message.includes("ECONNREFUSED") ||
|
|
324
|
+
error.message.includes("ETIMEDOUT")
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Type guard: Check if error is abort-related (should not retry)
|
|
332
|
+
*/
|
|
333
|
+
export function isAbortError(error: unknown): boolean {
|
|
334
|
+
if (error instanceof Error) {
|
|
335
|
+
return error.name === "AbortError" || error.message.includes("abort");
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Format upload speed in human-readable format
|
|
342
|
+
*/
|
|
343
|
+
export function formatSpeed(bytesPerSecond: number): string {
|
|
344
|
+
if (bytesPerSecond === 0) return "0 B/s";
|
|
345
|
+
const k = 1024;
|
|
346
|
+
const sizes = ["B/s", "KB/s", "MB/s", "GB/s"];
|
|
347
|
+
const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k));
|
|
348
|
+
return `${parseFloat((bytesPerSecond / k ** i).toFixed(1))} ${sizes[i]}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Format duration in human-readable format
|
|
353
|
+
*/
|
|
354
|
+
export function formatDuration(milliseconds: number): string {
|
|
355
|
+
if (milliseconds < 1000) {
|
|
356
|
+
return `${Math.round(milliseconds)}ms`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (milliseconds < 60000) {
|
|
360
|
+
return `${Math.round(milliseconds / 1000)}s`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (milliseconds < 3600000) {
|
|
364
|
+
const minutes = Math.floor(milliseconds / 60000);
|
|
365
|
+
const seconds = Math.round((milliseconds % 60000) / 1000);
|
|
366
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const hours = Math.floor(milliseconds / 3600000);
|
|
370
|
+
const minutes = Math.round((milliseconds % 3600000) / 60000);
|
|
371
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Validate file type against accepted types
|
|
376
|
+
*/
|
|
377
|
+
export function validateFileType(file: File, accept: string[]): boolean {
|
|
378
|
+
if (!accept || accept.length === 0) return true;
|
|
379
|
+
|
|
380
|
+
return accept.some((acceptType) => {
|
|
381
|
+
if (acceptType.startsWith(".")) {
|
|
382
|
+
// File extension check
|
|
383
|
+
return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// MIME type check (supports wildcards like image/*)
|
|
387
|
+
if (acceptType.endsWith("/*")) {
|
|
388
|
+
const baseType = acceptType.slice(0, -2);
|
|
389
|
+
return file.type.startsWith(baseType);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return file.type === acceptType;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Check if a file is an audio file
|
|
398
|
+
*/
|
|
399
|
+
export function isAudioFile(file: File): boolean {
|
|
400
|
+
return file.type.startsWith("audio/");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Check if a file is a document
|
|
405
|
+
*/
|
|
406
|
+
export function isDocumentFile(file: File): boolean {
|
|
407
|
+
const documentTypes = [
|
|
408
|
+
"application/pdf",
|
|
409
|
+
"application/msword",
|
|
410
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
411
|
+
"application/vnd.ms-excel",
|
|
412
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
413
|
+
"application/vnd.ms-powerpoint",
|
|
414
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
415
|
+
"text/plain",
|
|
416
|
+
"text/csv",
|
|
417
|
+
"application/rtf",
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
return documentTypes.includes(file.type);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Create a preview URL for a file (if supported)
|
|
425
|
+
*/
|
|
426
|
+
export function createFilePreview(file: File): string | null {
|
|
427
|
+
if (isImageFile(file) || isVideoFile(file) || isAudioFile(file)) {
|
|
428
|
+
return URL.createObjectURL(file);
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Clean up a preview URL created with createFilePreview
|
|
435
|
+
*/
|
|
436
|
+
export function revokeFilePreview(previewUrl: string): void {
|
|
437
|
+
URL.revokeObjectURL(previewUrl);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Calculate progress percentage
|
|
442
|
+
*/
|
|
443
|
+
export function calculateProgress(current: number, total: number): number {
|
|
444
|
+
if (total === 0) return 0;
|
|
445
|
+
return Math.min(100, Math.max(0, Math.round((current / total) * 100)));
|
|
446
|
+
}
|