@idealyst/files 1.2.96

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 (42) hide show
  1. package/package.json +94 -0
  2. package/src/components/DropZone.native.tsx +96 -0
  3. package/src/components/DropZone.styles.tsx +99 -0
  4. package/src/components/DropZone.web.tsx +178 -0
  5. package/src/components/FilePickerButton.native.tsx +82 -0
  6. package/src/components/FilePickerButton.styles.tsx +112 -0
  7. package/src/components/FilePickerButton.web.tsx +84 -0
  8. package/src/components/UploadProgress.native.tsx +203 -0
  9. package/src/components/UploadProgress.styles.tsx +90 -0
  10. package/src/components/UploadProgress.web.tsx +201 -0
  11. package/src/components/index.native.ts +8 -0
  12. package/src/components/index.ts +6 -0
  13. package/src/components/index.web.ts +8 -0
  14. package/src/constants.ts +336 -0
  15. package/src/examples/index.ts +181 -0
  16. package/src/hooks/createUseFilePickerHook.ts +169 -0
  17. package/src/hooks/createUseFileUploadHook.ts +173 -0
  18. package/src/hooks/index.native.ts +12 -0
  19. package/src/hooks/index.ts +12 -0
  20. package/src/hooks/index.web.ts +12 -0
  21. package/src/index.native.ts +142 -0
  22. package/src/index.ts +139 -0
  23. package/src/index.web.ts +142 -0
  24. package/src/permissions/index.native.ts +8 -0
  25. package/src/permissions/index.ts +8 -0
  26. package/src/permissions/index.web.ts +8 -0
  27. package/src/permissions/permissions.native.ts +177 -0
  28. package/src/permissions/permissions.web.ts +96 -0
  29. package/src/picker/FilePicker.native.ts +407 -0
  30. package/src/picker/FilePicker.web.ts +366 -0
  31. package/src/picker/index.native.ts +2 -0
  32. package/src/picker/index.ts +2 -0
  33. package/src/picker/index.web.ts +2 -0
  34. package/src/types.ts +990 -0
  35. package/src/uploader/ChunkedUploader.ts +312 -0
  36. package/src/uploader/FileUploader.native.ts +435 -0
  37. package/src/uploader/FileUploader.web.ts +350 -0
  38. package/src/uploader/UploadQueue.ts +519 -0
  39. package/src/uploader/index.native.ts +4 -0
  40. package/src/uploader/index.ts +4 -0
  41. package/src/uploader/index.web.ts +4 -0
  42. package/src/utils.ts +586 -0
@@ -0,0 +1,519 @@
1
+ import type {
2
+ PickedFile,
3
+ UploadConfig,
4
+ UploadProgressInfo,
5
+ UploadState,
6
+ UploadResult,
7
+ QueueStatus,
8
+ UploadError,
9
+ } from '../types';
10
+ import { INITIAL_QUEUE_STATUS, TIMING } from '../constants';
11
+ import {
12
+ generateId,
13
+ EventEmitter,
14
+ SpeedCalculator,
15
+ calculateETA,
16
+ calculateRetryDelay,
17
+ isRetryableError,
18
+ } from '../utils';
19
+
20
+ type QueueEvents = {
21
+ queueChange: [QueueStatus];
22
+ progress: [string, UploadProgressInfo];
23
+ complete: [UploadResult];
24
+ error: [UploadError, string];
25
+ };
26
+
27
+ /**
28
+ * Manages upload queue with concurrency control, retry logic, and progress tracking.
29
+ */
30
+ export class UploadQueue {
31
+ private _uploads = new Map<string, UploadProgressInfo>();
32
+ private _queue: string[] = [];
33
+ private _activeUploads = new Set<string>();
34
+ private _speedCalculators = new Map<string, SpeedCalculator>();
35
+ private _events = new EventEmitter<QueueEvents>();
36
+ private _status: QueueStatus = { ...INITIAL_QUEUE_STATUS };
37
+ private _concurrency: number;
38
+ private _isPaused = false;
39
+ private _isProcessing = false;
40
+ private _disposed = false;
41
+
42
+ constructor(concurrency = 3) {
43
+ this._concurrency = concurrency;
44
+ }
45
+
46
+ get queueStatus(): QueueStatus {
47
+ return { ...this._status };
48
+ }
49
+
50
+ get uploads(): Map<string, UploadProgressInfo> {
51
+ return new Map(this._uploads);
52
+ }
53
+
54
+ /**
55
+ * Add file(s) to the upload queue.
56
+ */
57
+ add(files: PickedFile | PickedFile[], config: UploadConfig): string[] {
58
+ const fileArray = Array.isArray(files) ? files : [files];
59
+ const ids: string[] = [];
60
+
61
+ for (const file of fileArray) {
62
+ const id = generateId('upload');
63
+
64
+ const progress: UploadProgressInfo = {
65
+ id,
66
+ file,
67
+ state: 'pending',
68
+ bytesUploaded: 0,
69
+ bytesTotal: file.size,
70
+ percentage: 0,
71
+ speed: 0,
72
+ estimatedTimeRemaining: 0,
73
+ retryCount: 0,
74
+ config,
75
+ };
76
+
77
+ this._uploads.set(id, progress);
78
+ this._queue.push(id);
79
+ this._speedCalculators.set(id, new SpeedCalculator(TIMING.SPEED_CALCULATION_WINDOW));
80
+ ids.push(id);
81
+ }
82
+
83
+ this._updateQueueStatus();
84
+ return ids;
85
+ }
86
+
87
+ /**
88
+ * Start processing the queue.
89
+ */
90
+ start(): void {
91
+ if (this._disposed || this._isProcessing) return;
92
+
93
+ this._isPaused = false;
94
+ this._isProcessing = true;
95
+ this._processQueue();
96
+ }
97
+
98
+ /**
99
+ * Pause all uploads.
100
+ */
101
+ pause(): void {
102
+ this._isPaused = true;
103
+
104
+ // Mark active uploads as paused
105
+ for (const id of this._activeUploads) {
106
+ this._updateUploadState(id, 'paused');
107
+ }
108
+
109
+ this._updateQueueStatus();
110
+ }
111
+
112
+ /**
113
+ * Resume paused uploads.
114
+ */
115
+ resume(): void {
116
+ this._isPaused = false;
117
+
118
+ // Mark paused uploads as pending to be reprocessed
119
+ for (const [id, upload] of this._uploads) {
120
+ if (upload.state === 'paused') {
121
+ this._updateUploadState(id, 'pending');
122
+ if (!this._queue.includes(id)) {
123
+ this._queue.push(id);
124
+ }
125
+ }
126
+ }
127
+
128
+ this._updateQueueStatus();
129
+ this._processQueue();
130
+ }
131
+
132
+ /**
133
+ * Cancel a specific upload.
134
+ */
135
+ cancel(uploadId: string): void {
136
+ const upload = this._uploads.get(uploadId);
137
+ if (!upload) return;
138
+
139
+ this._updateUploadState(uploadId, 'cancelled');
140
+ this._activeUploads.delete(uploadId);
141
+ this._queue = this._queue.filter(id => id !== uploadId);
142
+ this._speedCalculators.delete(uploadId);
143
+
144
+ this._updateQueueStatus();
145
+ this._processQueue();
146
+ }
147
+
148
+ /**
149
+ * Cancel all uploads.
150
+ */
151
+ cancelAll(): void {
152
+ for (const [id] of this._uploads) {
153
+ this._updateUploadState(id, 'cancelled');
154
+ }
155
+
156
+ this._activeUploads.clear();
157
+ this._queue = [];
158
+ this._speedCalculators.clear();
159
+
160
+ this._updateQueueStatus();
161
+ }
162
+
163
+ /**
164
+ * Retry a failed upload.
165
+ */
166
+ retry(uploadId: string): void {
167
+ const upload = this._uploads.get(uploadId);
168
+ if (!upload || upload.state !== 'failed') return;
169
+
170
+ // Reset upload state
171
+ this._uploads.set(uploadId, {
172
+ ...upload,
173
+ state: 'pending',
174
+ bytesUploaded: 0,
175
+ percentage: 0,
176
+ speed: 0,
177
+ estimatedTimeRemaining: 0,
178
+ error: undefined,
179
+ currentChunk: undefined,
180
+ });
181
+
182
+ this._speedCalculators.set(uploadId, new SpeedCalculator(TIMING.SPEED_CALCULATION_WINDOW));
183
+ this._queue.push(uploadId);
184
+
185
+ this._updateQueueStatus();
186
+ this._processQueue();
187
+ }
188
+
189
+ /**
190
+ * Retry all failed uploads.
191
+ */
192
+ retryAll(): void {
193
+ for (const [id, upload] of this._uploads) {
194
+ if (upload.state === 'failed') {
195
+ this.retry(id);
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Remove an upload from the queue.
202
+ */
203
+ remove(uploadId: string): void {
204
+ this._uploads.delete(uploadId);
205
+ this._activeUploads.delete(uploadId);
206
+ this._queue = this._queue.filter(id => id !== uploadId);
207
+ this._speedCalculators.delete(uploadId);
208
+
209
+ this._updateQueueStatus();
210
+ }
211
+
212
+ /**
213
+ * Clear completed uploads.
214
+ */
215
+ clearCompleted(): void {
216
+ for (const [id, upload] of this._uploads) {
217
+ if (upload.state === 'completed' || upload.state === 'cancelled') {
218
+ this._uploads.delete(id);
219
+ this._speedCalculators.delete(id);
220
+ }
221
+ }
222
+
223
+ this._updateQueueStatus();
224
+ }
225
+
226
+ /**
227
+ * Get a specific upload.
228
+ */
229
+ getUpload(uploadId: string): UploadProgressInfo | undefined {
230
+ return this._uploads.get(uploadId);
231
+ }
232
+
233
+ /**
234
+ * Update upload progress.
235
+ */
236
+ updateProgress(
237
+ uploadId: string,
238
+ bytesUploaded: number,
239
+ additionalData?: Partial<Pick<UploadProgressInfo, 'currentChunk' | 'totalChunks'>>
240
+ ): void {
241
+ const upload = this._uploads.get(uploadId);
242
+ if (!upload) return;
243
+
244
+ const speedCalc = this._speedCalculators.get(uploadId);
245
+ if (speedCalc) {
246
+ speedCalc.addSample(bytesUploaded);
247
+ }
248
+
249
+ const speed = speedCalc?.getSpeed() || 0;
250
+ const bytesRemaining = upload.bytesTotal - bytesUploaded;
251
+ const eta = calculateETA(bytesRemaining, speed);
252
+ const percentage = upload.bytesTotal > 0 ? (bytesUploaded / upload.bytesTotal) * 100 : 0;
253
+
254
+ this._uploads.set(uploadId, {
255
+ ...upload,
256
+ bytesUploaded,
257
+ percentage,
258
+ speed,
259
+ estimatedTimeRemaining: eta,
260
+ ...additionalData,
261
+ });
262
+
263
+ this._events.emit('progress', uploadId, this._uploads.get(uploadId)!);
264
+ this._updateQueueStatus();
265
+ }
266
+
267
+ /**
268
+ * Mark an upload as started.
269
+ */
270
+ markStarted(uploadId: string): void {
271
+ const upload = this._uploads.get(uploadId);
272
+ if (!upload) return;
273
+
274
+ this._uploads.set(uploadId, {
275
+ ...upload,
276
+ state: 'uploading',
277
+ startedAt: Date.now(),
278
+ });
279
+
280
+ this._activeUploads.add(uploadId);
281
+ this._events.emit('progress', uploadId, this._uploads.get(uploadId)!);
282
+ this._updateQueueStatus();
283
+ }
284
+
285
+ /**
286
+ * Mark an upload as completed.
287
+ */
288
+ markCompleted(uploadId: string, response?: unknown, statusCode?: number): void {
289
+ const upload = this._uploads.get(uploadId);
290
+ if (!upload) return;
291
+
292
+ const updatedUpload: UploadProgressInfo = {
293
+ ...upload,
294
+ state: 'completed',
295
+ bytesUploaded: upload.bytesTotal,
296
+ percentage: 100,
297
+ speed: 0,
298
+ estimatedTimeRemaining: 0,
299
+ completedAt: Date.now(),
300
+ };
301
+
302
+ this._uploads.set(uploadId, updatedUpload);
303
+ this._activeUploads.delete(uploadId);
304
+ this._speedCalculators.delete(uploadId);
305
+
306
+ const result: UploadResult = {
307
+ id: uploadId,
308
+ success: true,
309
+ response,
310
+ statusCode,
311
+ progress: updatedUpload,
312
+ };
313
+
314
+ this._events.emit('complete', result);
315
+ this._updateQueueStatus();
316
+ this._processQueue();
317
+ }
318
+
319
+ /**
320
+ * Mark an upload as failed.
321
+ */
322
+ markFailed(uploadId: string, error: UploadError): void {
323
+ const upload = this._uploads.get(uploadId);
324
+ if (!upload) return;
325
+
326
+ const shouldRetry = upload.config.retryEnabled &&
327
+ upload.retryCount < upload.config.maxRetries &&
328
+ isRetryableError(error);
329
+
330
+ if (shouldRetry) {
331
+ // Schedule retry
332
+ const delay = calculateRetryDelay(
333
+ upload.retryCount + 1,
334
+ upload.config.retryDelay,
335
+ upload.config.retryDelayMs
336
+ );
337
+
338
+ this._uploads.set(uploadId, {
339
+ ...upload,
340
+ state: 'pending',
341
+ retryCount: upload.retryCount + 1,
342
+ });
343
+
344
+ this._activeUploads.delete(uploadId);
345
+
346
+ // Add back to queue after delay
347
+ setTimeout(() => {
348
+ if (!this._disposed && this._uploads.has(uploadId)) {
349
+ this._queue.push(uploadId);
350
+ this._processQueue();
351
+ }
352
+ }, delay);
353
+ } else {
354
+ // Mark as failed permanently
355
+ this._uploads.set(uploadId, {
356
+ ...upload,
357
+ state: 'failed',
358
+ error,
359
+ });
360
+
361
+ this._activeUploads.delete(uploadId);
362
+ this._speedCalculators.delete(uploadId);
363
+
364
+ const result: UploadResult = {
365
+ id: uploadId,
366
+ success: false,
367
+ error,
368
+ statusCode: error.statusCode,
369
+ progress: this._uploads.get(uploadId)!,
370
+ };
371
+
372
+ this._events.emit('complete', result);
373
+ this._events.emit('error', error, uploadId);
374
+ }
375
+
376
+ this._updateQueueStatus();
377
+ this._processQueue();
378
+ }
379
+
380
+ /**
381
+ * Get the next upload to process.
382
+ */
383
+ getNextUpload(): UploadProgressInfo | undefined {
384
+ if (this._isPaused || this._disposed) return undefined;
385
+ if (this._activeUploads.size >= this._concurrency) return undefined;
386
+
387
+ while (this._queue.length > 0) {
388
+ const id = this._queue.shift()!;
389
+ const upload = this._uploads.get(id);
390
+
391
+ if (upload && upload.state === 'pending') {
392
+ return upload;
393
+ }
394
+ }
395
+
396
+ return undefined;
397
+ }
398
+
399
+ /**
400
+ * Subscribe to queue status changes.
401
+ */
402
+ onQueueChange(callback: (status: QueueStatus) => void): () => void {
403
+ return this._events.on('queueChange', callback);
404
+ }
405
+
406
+ /**
407
+ * Subscribe to individual upload progress.
408
+ */
409
+ onProgress(uploadId: string, callback: (progress: UploadProgressInfo) => void): () => void {
410
+ return this._events.on('progress', (id, progress) => {
411
+ if (id === uploadId) {
412
+ callback(progress);
413
+ }
414
+ });
415
+ }
416
+
417
+ /**
418
+ * Subscribe to upload completion.
419
+ */
420
+ onComplete(callback: (result: UploadResult) => void): () => void {
421
+ return this._events.on('complete', callback);
422
+ }
423
+
424
+ /**
425
+ * Subscribe to upload errors.
426
+ */
427
+ onError(callback: (error: UploadError, uploadId: string) => void): () => void {
428
+ return this._events.on('error', callback);
429
+ }
430
+
431
+ /**
432
+ * Clean up resources.
433
+ */
434
+ dispose(): void {
435
+ this._disposed = true;
436
+ this._uploads.clear();
437
+ this._queue = [];
438
+ this._activeUploads.clear();
439
+ this._speedCalculators.clear();
440
+ this._events.removeAllListeners();
441
+ }
442
+
443
+ // ============================================
444
+ // PROTECTED METHODS (for subclass extension)
445
+ // ============================================
446
+
447
+ /**
448
+ * Process the queue - to be called after state changes.
449
+ * Override in subclasses to implement actual upload logic.
450
+ */
451
+ protected _processQueue(): void {
452
+ // Base implementation just updates status
453
+ // Subclasses should override to implement actual upload processing
454
+ this._updateQueueStatus();
455
+ }
456
+
457
+ // ============================================
458
+ // PRIVATE METHODS
459
+ // ============================================
460
+
461
+ private _updateUploadState(uploadId: string, state: UploadState): void {
462
+ const upload = this._uploads.get(uploadId);
463
+ if (!upload) return;
464
+
465
+ this._uploads.set(uploadId, {
466
+ ...upload,
467
+ state,
468
+ });
469
+
470
+ this._events.emit('progress', uploadId, this._uploads.get(uploadId)!);
471
+ }
472
+
473
+ private _updateQueueStatus(): void {
474
+ let pending = 0;
475
+ let uploading = 0;
476
+ let completed = 0;
477
+ let failed = 0;
478
+ let totalBytes = 0;
479
+ let totalBytesUploaded = 0;
480
+
481
+ for (const [, upload] of this._uploads) {
482
+ totalBytes += upload.bytesTotal;
483
+ totalBytesUploaded += upload.bytesUploaded;
484
+
485
+ switch (upload.state) {
486
+ case 'pending':
487
+ pending++;
488
+ break;
489
+ case 'uploading':
490
+ uploading++;
491
+ break;
492
+ case 'completed':
493
+ completed++;
494
+ break;
495
+ case 'failed':
496
+ failed++;
497
+ break;
498
+ }
499
+ }
500
+
501
+ const total = this._uploads.size;
502
+ const overallProgress = totalBytes > 0 ? (totalBytesUploaded / totalBytes) * 100 : 0;
503
+
504
+ this._status = {
505
+ total,
506
+ pending,
507
+ uploading,
508
+ completed,
509
+ failed,
510
+ isProcessing: this._isProcessing && !this._isPaused && uploading > 0,
511
+ isPaused: this._isPaused,
512
+ overallProgress,
513
+ totalBytesUploaded,
514
+ totalBytes,
515
+ };
516
+
517
+ this._events.emit('queueChange', this._status);
518
+ }
519
+ }
@@ -0,0 +1,4 @@
1
+ export { UploadQueue } from './UploadQueue';
2
+ export { ChunkedUploader, createChunkedUploader } from './ChunkedUploader';
3
+ export { NativeFileUploader, createFileUploader } from './FileUploader.native';
4
+ export type { IFileUploader } from '../types';
@@ -0,0 +1,4 @@
1
+ export { UploadQueue } from './UploadQueue';
2
+ export { ChunkedUploader, createChunkedUploader } from './ChunkedUploader';
3
+ export { WebFileUploader, createFileUploader } from './FileUploader.web';
4
+ export type { IFileUploader } from '../types';
@@ -0,0 +1,4 @@
1
+ export { UploadQueue } from './UploadQueue';
2
+ export { ChunkedUploader, createChunkedUploader } from './ChunkedUploader';
3
+ export { WebFileUploader, createFileUploader } from './FileUploader.web';
4
+ export type { IFileUploader } from '../types';