@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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +130 -0
  3. package/AUTO_CAPABILITIES.md +98 -0
  4. package/FRAMEWORK_INTEGRATION.md +407 -0
  5. package/LICENSE +21 -0
  6. package/README.md +795 -0
  7. package/SMART_CHUNKING.md +140 -0
  8. package/dist/client/create-uploadista-client.d.ts +182 -0
  9. package/dist/client/create-uploadista-client.d.ts.map +1 -0
  10. package/dist/client/create-uploadista-client.js +76 -0
  11. package/dist/client/index.d.ts +2 -0
  12. package/dist/client/index.d.ts.map +1 -0
  13. package/dist/client/index.js +1 -0
  14. package/dist/framework-utils.d.ts +201 -0
  15. package/dist/framework-utils.d.ts.map +1 -0
  16. package/dist/framework-utils.js +282 -0
  17. package/dist/http-client.d.ts +44 -0
  18. package/dist/http-client.d.ts.map +1 -0
  19. package/dist/http-client.js +489 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +7 -0
  23. package/dist/services/abort-controller-factory.d.ts +30 -0
  24. package/dist/services/abort-controller-factory.d.ts.map +1 -0
  25. package/dist/services/abort-controller-factory.js +98 -0
  26. package/dist/services/checksum-service.d.ts +30 -0
  27. package/dist/services/checksum-service.d.ts.map +1 -0
  28. package/dist/services/checksum-service.js +44 -0
  29. package/dist/services/create-browser-services.d.ts +36 -0
  30. package/dist/services/create-browser-services.d.ts.map +1 -0
  31. package/dist/services/create-browser-services.js +56 -0
  32. package/dist/services/file-reader.d.ts +91 -0
  33. package/dist/services/file-reader.d.ts.map +1 -0
  34. package/dist/services/file-reader.js +251 -0
  35. package/dist/services/fingerprint-service.d.ts +41 -0
  36. package/dist/services/fingerprint-service.d.ts.map +1 -0
  37. package/dist/services/fingerprint-service.js +64 -0
  38. package/dist/services/id-generation/id-generation.d.ts +40 -0
  39. package/dist/services/id-generation/id-generation.d.ts.map +1 -0
  40. package/dist/services/id-generation/id-generation.js +58 -0
  41. package/dist/services/platform-service.d.ts +38 -0
  42. package/dist/services/platform-service.d.ts.map +1 -0
  43. package/dist/services/platform-service.js +221 -0
  44. package/dist/services/storage/local-storage-service.d.ts +55 -0
  45. package/dist/services/storage/local-storage-service.d.ts.map +1 -0
  46. package/dist/services/storage/local-storage-service.js +178 -0
  47. package/dist/services/storage/session-storage-service.d.ts +55 -0
  48. package/dist/services/storage/session-storage-service.d.ts.map +1 -0
  49. package/dist/services/storage/session-storage-service.js +179 -0
  50. package/dist/services/websocket-factory.d.ts +46 -0
  51. package/dist/services/websocket-factory.d.ts.map +1 -0
  52. package/dist/services/websocket-factory.js +196 -0
  53. package/dist/types/index.d.ts +2 -0
  54. package/dist/types/index.d.ts.map +1 -0
  55. package/dist/types/index.js +1 -0
  56. package/dist/types/upload-input.d.ts +26 -0
  57. package/dist/types/upload-input.d.ts.map +1 -0
  58. package/dist/types/upload-input.js +1 -0
  59. package/dist/utils/hash-util.d.ts +60 -0
  60. package/dist/utils/hash-util.d.ts.map +1 -0
  61. package/dist/utils/hash-util.js +75 -0
  62. package/package.json +32 -0
  63. package/src/client/create-uploadista-client.ts +150 -0
  64. package/src/client/index.ts +1 -0
  65. package/src/framework-utils.ts +446 -0
  66. package/src/http-client.ts +546 -0
  67. package/src/index.ts +8 -0
  68. package/src/services/abort-controller-factory.ts +108 -0
  69. package/src/services/checksum-service.ts +46 -0
  70. package/src/services/create-browser-services.ts +81 -0
  71. package/src/services/file-reader.ts +344 -0
  72. package/src/services/fingerprint-service.ts +67 -0
  73. package/src/services/id-generation/id-generation.ts +60 -0
  74. package/src/services/platform-service.ts +231 -0
  75. package/src/services/storage/local-storage-service.ts +187 -0
  76. package/src/services/storage/session-storage-service.ts +188 -0
  77. package/src/services/websocket-factory.ts +212 -0
  78. package/src/types/index.ts +1 -0
  79. package/src/types/upload-input.ts +25 -0
  80. package/src/utils/hash-util.ts +79 -0
  81. package/tsconfig.json +22 -0
  82. package/tsconfig.tsbuildinfo +1 -0
  83. 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
+ }