@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,350 @@
1
+ import type {
2
+ IFileUploader,
3
+ PickedFile,
4
+ UploadConfig,
5
+ UploadProgressInfo,
6
+ QueueStatus,
7
+ UploadResult,
8
+ UploadError,
9
+ CreateFileUploaderOptions,
10
+ } from '../types';
11
+ import { DEFAULT_UPLOAD_CONFIG } from '../constants';
12
+ import {
13
+ createUploadError,
14
+ shouldUseChunkedUpload,
15
+ generateId,
16
+ } from '../utils';
17
+ import { UploadQueue } from './UploadQueue';
18
+ import { ChunkedUploader } from './ChunkedUploader';
19
+
20
+ /**
21
+ * Web implementation of IFileUploader using XMLHttpRequest.
22
+ */
23
+ export class WebFileUploader implements IFileUploader {
24
+ private _queue: UploadQueue;
25
+ private _abortControllers = new Map<string, AbortController>();
26
+ private _xhrs = new Map<string, XMLHttpRequest>();
27
+ private _chunkedUploaders = new Map<string, ChunkedUploader>();
28
+ private _disposed = false;
29
+
30
+ constructor(options?: CreateFileUploaderOptions) {
31
+ this._queue = new UploadQueue(options?.concurrency || 3);
32
+ }
33
+
34
+ get queueStatus(): QueueStatus {
35
+ return this._queue.queueStatus;
36
+ }
37
+
38
+ get uploads(): Map<string, UploadProgressInfo> {
39
+ return this._queue.uploads;
40
+ }
41
+
42
+ add(files: PickedFile | PickedFile[], config: UploadConfig): string[] {
43
+ const finalConfig = { ...DEFAULT_UPLOAD_CONFIG, ...config } as UploadConfig;
44
+ return this._queue.add(files, finalConfig);
45
+ }
46
+
47
+ start(): void {
48
+ this._queue.start();
49
+ this._processQueue();
50
+ }
51
+
52
+ pause(): void {
53
+ this._queue.pause();
54
+
55
+ // Abort active uploads
56
+ for (const [id] of this._xhrs) {
57
+ this._abortUpload(id);
58
+ }
59
+ }
60
+
61
+ resume(): void {
62
+ this._queue.resume();
63
+ this._processQueue();
64
+ }
65
+
66
+ cancel(uploadId: string): void {
67
+ this._abortUpload(uploadId);
68
+ this._queue.cancel(uploadId);
69
+ }
70
+
71
+ cancelAll(): void {
72
+ for (const [id] of this._xhrs) {
73
+ this._abortUpload(id);
74
+ }
75
+ this._queue.cancelAll();
76
+ }
77
+
78
+ retry(uploadId: string): void {
79
+ this._queue.retry(uploadId);
80
+ this._processQueue();
81
+ }
82
+
83
+ retryAll(): void {
84
+ this._queue.retryAll();
85
+ this._processQueue();
86
+ }
87
+
88
+ remove(uploadId: string): void {
89
+ this._abortUpload(uploadId);
90
+ this._queue.remove(uploadId);
91
+ }
92
+
93
+ clearCompleted(): void {
94
+ this._queue.clearCompleted();
95
+ }
96
+
97
+ getUpload(uploadId: string): UploadProgressInfo | undefined {
98
+ return this._queue.getUpload(uploadId);
99
+ }
100
+
101
+ onQueueChange(callback: (status: QueueStatus) => void): () => void {
102
+ return this._queue.onQueueChange(callback);
103
+ }
104
+
105
+ onProgress(uploadId: string, callback: (progress: UploadProgressInfo) => void): () => void {
106
+ return this._queue.onProgress(uploadId, callback);
107
+ }
108
+
109
+ onComplete(callback: (result: UploadResult) => void): () => void {
110
+ return this._queue.onComplete(callback);
111
+ }
112
+
113
+ onError(callback: (error: UploadError, uploadId: string) => void): () => void {
114
+ return this._queue.onError(callback);
115
+ }
116
+
117
+ dispose(): void {
118
+ this._disposed = true;
119
+ this.cancelAll();
120
+ this._queue.dispose();
121
+ this._abortControllers.clear();
122
+ this._xhrs.clear();
123
+ this._chunkedUploaders.clear();
124
+ }
125
+
126
+ // ============================================
127
+ // PRIVATE METHODS
128
+ // ============================================
129
+
130
+ private _processQueue(): void {
131
+ if (this._disposed) return;
132
+
133
+ let nextUpload = this._queue.getNextUpload();
134
+
135
+ while (nextUpload) {
136
+ this._startUpload(nextUpload);
137
+ nextUpload = this._queue.getNextUpload();
138
+ }
139
+ }
140
+
141
+ private async _startUpload(upload: UploadProgressInfo): Promise<void> {
142
+ const { id, file, config } = upload;
143
+
144
+ this._queue.markStarted(id);
145
+
146
+ // Check if we should use chunked upload
147
+ if (shouldUseChunkedUpload(file.size, config)) {
148
+ await this._startChunkedUpload(upload);
149
+ } else {
150
+ await this._startSimpleUpload(upload);
151
+ }
152
+ }
153
+
154
+ private async _startSimpleUpload(upload: UploadProgressInfo): Promise<void> {
155
+ const { id, file, config } = upload;
156
+
157
+ const xhr = new XMLHttpRequest();
158
+ this._xhrs.set(id, xhr);
159
+
160
+ const abortController = new AbortController();
161
+ this._abortControllers.set(id, abortController);
162
+
163
+ return new Promise((resolve) => {
164
+ // Track upload progress
165
+ xhr.upload.onprogress = (event) => {
166
+ if (event.lengthComputable) {
167
+ this._queue.updateProgress(id, event.loaded);
168
+ }
169
+ };
170
+
171
+ // Handle completion
172
+ xhr.onload = async () => {
173
+ this._cleanup(id);
174
+
175
+ if (xhr.status >= 200 && xhr.status < 300) {
176
+ let response: unknown;
177
+ try {
178
+ response = JSON.parse(xhr.responseText);
179
+ } catch {
180
+ response = xhr.responseText;
181
+ }
182
+
183
+ this._queue.markCompleted(id, response, xhr.status);
184
+ } else {
185
+ this._queue.markFailed(
186
+ id,
187
+ createUploadError('SERVER_ERROR', `Server returned ${xhr.status}`, xhr.status)
188
+ );
189
+ }
190
+
191
+ this._processQueue();
192
+ resolve();
193
+ };
194
+
195
+ // Handle errors
196
+ xhr.onerror = () => {
197
+ this._cleanup(id);
198
+ this._queue.markFailed(id, createUploadError('NETWORK_ERROR', 'Network error occurred'));
199
+ this._processQueue();
200
+ resolve();
201
+ };
202
+
203
+ xhr.ontimeout = () => {
204
+ this._cleanup(id);
205
+ this._queue.markFailed(id, createUploadError('TIMEOUT', 'Upload timed out'));
206
+ this._processQueue();
207
+ resolve();
208
+ };
209
+
210
+ xhr.onabort = () => {
211
+ this._cleanup(id);
212
+ // Don't mark as failed - already handled by cancel/pause
213
+ resolve();
214
+ };
215
+
216
+ // Open connection
217
+ xhr.open(config.method, config.url, true);
218
+ xhr.timeout = config.timeout;
219
+
220
+ // Set headers
221
+ if (config.headers) {
222
+ for (const [key, value] of Object.entries(config.headers)) {
223
+ xhr.setRequestHeader(key, value);
224
+ }
225
+ }
226
+
227
+ // Build and send request
228
+ this._buildAndSendRequest(xhr, file, config);
229
+ });
230
+ }
231
+
232
+ private async _buildAndSendRequest(
233
+ xhr: XMLHttpRequest,
234
+ file: PickedFile,
235
+ config: UploadConfig
236
+ ): Promise<void> {
237
+ if (config.multipart) {
238
+ const formData = new FormData();
239
+
240
+ // Get file data
241
+ const data = await file.getData();
242
+ const blob = data instanceof Blob ? data : this._base64ToBlob(data as string, file.type);
243
+
244
+ formData.append(config.fieldName, blob, file.name);
245
+
246
+ // Add additional form data
247
+ if (config.formData) {
248
+ for (const [key, value] of Object.entries(config.formData)) {
249
+ formData.append(key, String(value));
250
+ }
251
+ }
252
+
253
+ // Apply custom transform if provided
254
+ if (config.transformRequest) {
255
+ const transformed = await config.transformRequest(file, formData);
256
+ xhr.send(transformed);
257
+ } else {
258
+ xhr.send(formData);
259
+ }
260
+ } else {
261
+ // Send raw file data
262
+ const data = await file.getData();
263
+ const blob = data instanceof Blob ? data : this._base64ToBlob(data as string, file.type);
264
+
265
+ xhr.setRequestHeader('Content-Type', file.type);
266
+ xhr.send(blob);
267
+ }
268
+ }
269
+
270
+ private async _startChunkedUpload(upload: UploadProgressInfo): Promise<void> {
271
+ const { id, file, config } = upload;
272
+
273
+ const chunkedUploader = new ChunkedUploader(config.chunkSize);
274
+ this._chunkedUploaders.set(id, chunkedUploader);
275
+
276
+ const abortController = new AbortController();
277
+ this._abortControllers.set(id, abortController);
278
+
279
+ const result = await chunkedUploader.uploadFile(
280
+ file,
281
+ {
282
+ ...config,
283
+ fileId: generateId('file'),
284
+ },
285
+ {
286
+ onProgress: (progress) => {
287
+ this._queue.updateProgress(id, progress.bytesUploaded, {
288
+ currentChunk: progress.currentChunk,
289
+ totalChunks: progress.totalChunks,
290
+ });
291
+ },
292
+ abortSignal: abortController.signal,
293
+ }
294
+ );
295
+
296
+ this._cleanup(id);
297
+
298
+ if (result.success) {
299
+ this._queue.markCompleted(id, result.response);
300
+ } else if (result.error) {
301
+ this._queue.markFailed(id, result.error);
302
+ }
303
+
304
+ this._processQueue();
305
+ }
306
+
307
+ private _abortUpload(uploadId: string): void {
308
+ // Abort XHR
309
+ const xhr = this._xhrs.get(uploadId);
310
+ if (xhr) {
311
+ xhr.abort();
312
+ }
313
+
314
+ // Abort controller
315
+ const controller = this._abortControllers.get(uploadId);
316
+ if (controller) {
317
+ controller.abort();
318
+ }
319
+
320
+ // Abort chunked uploader
321
+ const chunkedUploader = this._chunkedUploaders.get(uploadId);
322
+ if (chunkedUploader) {
323
+ chunkedUploader.abort();
324
+ }
325
+ }
326
+
327
+ private _cleanup(uploadId: string): void {
328
+ this._xhrs.delete(uploadId);
329
+ this._abortControllers.delete(uploadId);
330
+ this._chunkedUploaders.delete(uploadId);
331
+ }
332
+
333
+ private _base64ToBlob(base64: string, type: string): Blob {
334
+ const binaryString = atob(base64);
335
+ const bytes = new Uint8Array(binaryString.length);
336
+
337
+ for (let i = 0; i < binaryString.length; i++) {
338
+ bytes[i] = binaryString.charCodeAt(i);
339
+ }
340
+
341
+ return new Blob([bytes], { type });
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Create a new WebFileUploader instance.
347
+ */
348
+ export function createFileUploader(options?: CreateFileUploaderOptions): IFileUploader {
349
+ return new WebFileUploader(options);
350
+ }