uploados 0.1.0

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 (66) hide show
  1. package/android/build.gradle +25 -0
  2. package/android/src/main/AndroidManifest.xml +15 -0
  3. package/android/src/main/java/expo/modules/uploados/UploadosModule.kt +121 -0
  4. package/android/src/main/java/expo/modules/uploados/upload/CompressionPipeline.kt +192 -0
  5. package/android/src/main/java/expo/modules/uploados/upload/FileStager.kt +125 -0
  6. package/android/src/main/java/expo/modules/uploados/upload/ProgressRequestBody.kt +36 -0
  7. package/android/src/main/java/expo/modules/uploados/upload/UploadManager.kt +857 -0
  8. package/android/src/main/java/expo/modules/uploados/upload/UploadModels.kt +209 -0
  9. package/android/src/main/java/expo/modules/uploados/upload/UploadNotificationHelper.kt +93 -0
  10. package/android/src/main/java/expo/modules/uploados/upload/UploadTaskStore.kt +224 -0
  11. package/android/src/main/java/expo/modules/uploados/upload/UploadWorker.kt +31 -0
  12. package/build/Uploados.types.d.ts +226 -0
  13. package/build/Uploados.types.d.ts.map +1 -0
  14. package/build/Uploados.types.js +2 -0
  15. package/build/Uploados.types.js.map +1 -0
  16. package/build/UploadosModule.d.ts +13 -0
  17. package/build/UploadosModule.d.ts.map +1 -0
  18. package/build/UploadosModule.js +3 -0
  19. package/build/UploadosModule.js.map +1 -0
  20. package/build/UploadosModule.web.d.ts +13 -0
  21. package/build/UploadosModule.web.d.ts.map +1 -0
  22. package/build/UploadosModule.web.js +33 -0
  23. package/build/UploadosModule.web.js.map +1 -0
  24. package/build/createUploader.d.ts +3 -0
  25. package/build/createUploader.d.ts.map +1 -0
  26. package/build/createUploader.js +108 -0
  27. package/build/createUploader.js.map +1 -0
  28. package/build/index.d.ts +5 -0
  29. package/build/index.d.ts.map +1 -0
  30. package/build/index.js +5 -0
  31. package/build/index.js.map +1 -0
  32. package/build/normalizeUploadOptions.d.ts +9 -0
  33. package/build/normalizeUploadOptions.d.ts.map +1 -0
  34. package/build/normalizeUploadOptions.js +81 -0
  35. package/build/normalizeUploadOptions.js.map +1 -0
  36. package/build/providers/defineUploadProvider.d.ts +26 -0
  37. package/build/providers/defineUploadProvider.d.ts.map +1 -0
  38. package/build/providers/defineUploadProvider.js +39 -0
  39. package/build/providers/defineUploadProvider.js.map +1 -0
  40. package/build/providers/multipartPlan.d.ts +10 -0
  41. package/build/providers/multipartPlan.d.ts.map +1 -0
  42. package/build/providers/multipartPlan.js +28 -0
  43. package/build/providers/multipartPlan.js.map +1 -0
  44. package/eslint.config.cjs +5 -0
  45. package/expo-module.config.json +10 -0
  46. package/ios/Upload/CompressionPipeline.swift +183 -0
  47. package/ios/Upload/FileStager.swift +67 -0
  48. package/ios/Upload/UploadManager.swift +813 -0
  49. package/ios/Upload/UploadModels.swift +305 -0
  50. package/ios/Upload/UploadSessionDelegate.swift +82 -0
  51. package/ios/Upload/UploadTaskStore.swift +92 -0
  52. package/ios/Upload/UploadosAppDelegate.swift +14 -0
  53. package/ios/Uploados.podspec +23 -0
  54. package/ios/UploadosModule.swift +87 -0
  55. package/jest.config.js +15 -0
  56. package/package.json +54 -0
  57. package/readme.md +169 -0
  58. package/src/Uploados.types.ts +260 -0
  59. package/src/UploadosModule.ts +18 -0
  60. package/src/UploadosModule.web.ts +49 -0
  61. package/src/createUploader.ts +146 -0
  62. package/src/index.ts +4 -0
  63. package/src/normalizeUploadOptions.ts +132 -0
  64. package/src/providers/defineUploadProvider.ts +75 -0
  65. package/src/providers/multipartPlan.ts +43 -0
  66. package/tsconfig.json +42 -0
package/readme.md ADDED
@@ -0,0 +1,169 @@
1
+ # uploados
2
+
3
+ Native signed-URL uploads for Expo and React Native apps.
4
+
5
+ uploados is an Expo native module that runs direct `PUT`/`POST` uploads from iOS and Android native code. The current release focuses on direct signed-URL uploads, provider-based presign, file staging, native JPEG compression presets, retry, progress events, and a persistent upload queue.
6
+
7
+ Native multipart uploads are not wired yet. Passing `multipart: true` currently throws a typed `MULTIPART_FAILED` error.
8
+
9
+ ## Current status
10
+
11
+ | Capability | Status |
12
+ |------------|--------|
13
+ | Direct signed-URL uploads | Shipped on iOS and Android |
14
+ | Provider-based direct presign (`getUploadUrl`) | Shipped |
15
+ | File staging for picker and content URIs | Shipped |
16
+ | Persistent queue and restore | Shipped |
17
+ | Native JPEG compression presets | Shipped |
18
+ | Android foreground upload notification | Shipped for background WorkManager uploads; customization is not exposed yet |
19
+ | iOS Live Activity / Dynamic Island | Not implemented; build app-owned progress UI from upload events |
20
+ | Native multipart engine | Deferred; use direct uploads for now |
21
+ | Web | Throws `UNSUPPORTED_PLATFORM` by design |
22
+
23
+ ## Installation
24
+
25
+ Use Expo to install the package so peer versions stay aligned with your SDK:
26
+
27
+ ```bash
28
+ npx expo install uploados
29
+ npx expo doctor
30
+ ```
31
+
32
+ Then rebuild native projects:
33
+
34
+ ```bash
35
+ npx expo prebuild
36
+ npx pod-install
37
+ npx expo run:ios
38
+ npx expo run:android
39
+ ```
40
+
41
+ Expo Go does not load custom native modules, so use a development build.
42
+
43
+ Current peer metadata targets Expo SDK 52 through 56:
44
+
45
+ - `expo >=52.0.0 <57.0.0`
46
+ - `react-native >=0.76.0 <0.86.0`
47
+ - `react >=18.3.1 <20.0.0`
48
+
49
+ ## Direct signed-URL upload
50
+
51
+ ```ts
52
+ import { createUploader } from 'uploados';
53
+
54
+ const uploader = createUploader({
55
+ background: true,
56
+ retry: { maxAttempts: 5 },
57
+ });
58
+
59
+ const task = await uploader.upload({
60
+ file: imageUri,
61
+ uploadUrl: signedPutUrl,
62
+ headers: { 'Content-Type': 'image/jpeg' },
63
+ compression: { enabled: true, preset: 'balanced' },
64
+ });
65
+
66
+ task.onProgress((progress) => {
67
+ console.log(progress.percentage);
68
+ });
69
+
70
+ task.onComplete((snapshot) => {
71
+ console.log(snapshot.id, snapshot.state);
72
+ });
73
+
74
+ task.onFailed((error) => {
75
+ console.log(error.code, error.message);
76
+ });
77
+ ```
78
+
79
+ ## Provider-based direct upload
80
+
81
+ Use `defineUploadProvider` when your backend accepts an object key and returns a presigned URL. Multipart callbacks are optional and unused until the native multipart path ships.
82
+
83
+ ```ts
84
+ import { createUploader, defineUploadProvider } from 'uploados';
85
+
86
+ const uploader = createUploader({
87
+ provider: defineUploadProvider({
88
+ getUploadUrl: async ({ key, contentType }) => {
89
+ const res = await fetch('https://your-api/presign', {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({ key, contentType }),
93
+ });
94
+ return res.json();
95
+ },
96
+ }),
97
+ });
98
+
99
+ await uploader.upload({
100
+ file: imageUri,
101
+ key: 'uploads/photo.jpg',
102
+ contentType: 'image/jpeg',
103
+ compression: { enabled: true, preset: 'inspection' },
104
+ });
105
+ ```
106
+
107
+ ## Compression
108
+
109
+ Compression runs natively before upload and leaves the original file unchanged. Output is JPEG, and `compressed` events include `CompressionStats` with `originalSize`, `optimizedSize`, `preset`, and `format: 'jpeg'`.
110
+
111
+ ```ts
112
+ compression: {
113
+ enabled: true,
114
+ preset: 'balanced',
115
+ }
116
+ ```
117
+
118
+ Presets:
119
+
120
+ - `balanced`: default size and quality tradeoff
121
+ - `inspection`: keeps more detail for inspection photos
122
+ - `avatar`: smaller profile image and thumbnail output
123
+
124
+ ## Queue and lifecycle
125
+
126
+ Each upload is stored in a native task queue. States use the exported `UploadState` union:
127
+
128
+ ```txt
129
+ created -> validating -> compressing -> queued -> connecting -> uploading
130
+ -> verifying -> completed
131
+ ```
132
+
133
+ Failures may enter `retrying` before `failed`. Cancelled tasks enter `cancelled`.
134
+
135
+ Native retries reuse the same `uploadUrl` and headers. For background uploads, issue presigned URLs with enough TTL for the retry window, or create a fresh upload after an `AUTH_ERROR` / `PROVIDER_ERROR`.
136
+
137
+ Use queue APIs to inspect or resume work:
138
+
139
+ ```ts
140
+ const snapshots = await uploader.restoreQueue();
141
+ const all = await uploader.getAllTasks();
142
+ const one = await uploader.getTask(taskId);
143
+ await uploader.cancel(taskId);
144
+ await uploader.retry(taskId);
145
+ ```
146
+
147
+ ## Platform notes
148
+
149
+ - iOS uses `URLSession`, including background sessions when `background: true`.
150
+ - Android uses WorkManager and OkHttp for deferred upload work.
151
+ - Android background uploads run as foreground WorkManager work with an ongoing upload notification. Apps targeting Android 13+ must request notification permission for the notification to be visible.
152
+ - Android notification customization is not exposed in the JS API yet.
153
+ - iOS Live Activity and Dynamic Island surfaces are app-owned. Subscribe to upload events and render your own progress UI if you need them.
154
+ - Web upload APIs intentionally throw `UNSUPPORTED_PLATFORM`.
155
+
156
+ ## Repository development
157
+
158
+ This repository uses Bun.
159
+
160
+ ```bash
161
+ bun install
162
+ EXPO_NONINTERACTIVE=1 bun run build
163
+ bun run lint
164
+ EXPO_NONINTERACTIVE=1 bun run test
165
+ ```
166
+
167
+ ## License
168
+
169
+ MIT
@@ -0,0 +1,260 @@
1
+ export type UploadState =
2
+ | 'created'
3
+ | 'validating'
4
+ | 'compressing'
5
+ | 'queued'
6
+ | 'connecting'
7
+ | 'uploading'
8
+ | 'verifying'
9
+ | 'retrying'
10
+ | 'completed'
11
+ | 'paused'
12
+ | 'failed'
13
+ | 'cancelled';
14
+
15
+ export type UploadHttpMethod = 'PUT' | 'POST';
16
+
17
+ export type UploadNetworkPolicy = 'wait' | 'failFast';
18
+
19
+ export type UploadErrorPhase =
20
+ | 'validating'
21
+ | 'compressing'
22
+ | 'connecting'
23
+ | 'uploading'
24
+ | 'verifying'
25
+ | 'retrying';
26
+
27
+ export type UploadProgress = {
28
+ bytesUploaded: number;
29
+ totalBytes: number;
30
+ percentage: number;
31
+ };
32
+
33
+ export type UploadErrorCode =
34
+ | 'FILE_NOT_FOUND'
35
+ | 'FILE_TOO_LARGE'
36
+ | 'NETWORK_ERROR'
37
+ | 'AUTH_ERROR'
38
+ | 'PROVIDER_ERROR'
39
+ | 'MULTIPART_FAILED'
40
+ | 'COMPRESSION_FAILED'
41
+ | 'BACKGROUND_RESTRICTED'
42
+ | 'CANCELLED'
43
+ | 'UNSUPPORTED_PLATFORM'
44
+ | 'UNKNOWN';
45
+
46
+ export type UploadErrorPayload = {
47
+ code: UploadErrorCode;
48
+ message: string;
49
+ retryable: boolean;
50
+ taskId: string;
51
+ phase?: UploadErrorPhase;
52
+ httpStatus?: number;
53
+ providerCode?: string;
54
+ providerMessage?: string;
55
+ nativeCause?: string;
56
+ };
57
+
58
+ export type UploadFileMetadata = {
59
+ originalUri: string;
60
+ uploadUri: string;
61
+ isStaged: boolean;
62
+ sizeBytes?: number;
63
+ };
64
+
65
+ export type CompressionStats = {
66
+ preset: CompressionPreset;
67
+ originalSize: number;
68
+ optimizedSize: number;
69
+ format: 'jpeg';
70
+ };
71
+
72
+ export type UploadTaskSnapshot = {
73
+ id: string;
74
+ localUri: string;
75
+ file?: UploadFileMetadata;
76
+ uploadUrl: string;
77
+ method: UploadHttpMethod;
78
+ state: UploadState;
79
+ progress: UploadProgress;
80
+ background: boolean;
81
+ networkPolicy: UploadNetworkPolicy;
82
+ attempt: number;
83
+ maxAttempts: number;
84
+ nextRetryAt?: number;
85
+ createdAt: number;
86
+ updatedAt: number;
87
+ originalSize?: number;
88
+ optimizedSize?: number;
89
+ compression?: CompressionStats;
90
+ error?: UploadErrorPayload;
91
+ };
92
+
93
+ export type UploadRetryOptions = {
94
+ maxAttempts?: number;
95
+ };
96
+
97
+ export type CompressionPreset = 'balanced' | 'inspection' | 'avatar';
98
+
99
+ export type CompressionOptions = {
100
+ enabled?: boolean;
101
+ preset?: CompressionPreset;
102
+ };
103
+
104
+ export type UploaderConfig = {
105
+ background?: boolean;
106
+ networkPolicy?: UploadNetworkPolicy;
107
+ retry?: UploadRetryOptions;
108
+ provider?: UploadProvider;
109
+ };
110
+
111
+ export type CreateMultipartInput = {
112
+ key: string;
113
+ contentType?: string;
114
+ metadata?: Record<string, string>;
115
+ };
116
+
117
+ export type CreateMultipartResult = {
118
+ uploadId: string;
119
+ key: string;
120
+ };
121
+
122
+ export type SignPartInput = {
123
+ key: string;
124
+ uploadId: string;
125
+ partNumber: number;
126
+ };
127
+
128
+ export type SignedPartResult = {
129
+ url: string;
130
+ method: UploadHttpMethod;
131
+ headers?: Record<string, string>;
132
+ partNumber: number;
133
+ };
134
+
135
+ export type CompletedPart = {
136
+ partNumber: number;
137
+ etag: string;
138
+ };
139
+
140
+ export type CompleteMultipartInput = {
141
+ key: string;
142
+ uploadId: string;
143
+ parts: CompletedPart[];
144
+ };
145
+
146
+ export type CompleteMultipartResult = {
147
+ key: string;
148
+ location?: string;
149
+ etag?: string;
150
+ };
151
+
152
+ export type AbortMultipartInput = {
153
+ key: string;
154
+ uploadId: string;
155
+ };
156
+
157
+ export type ProviderUploadUrlInput = {
158
+ key: string;
159
+ contentType?: string;
160
+ metadata?: Record<string, string>;
161
+ };
162
+
163
+ export type ProviderUploadUrlResult = {
164
+ url: string;
165
+ method?: UploadHttpMethod;
166
+ headers?: Record<string, string>;
167
+ };
168
+
169
+ export type UploadProvider = {
170
+ getUploadUrl?: (input: ProviderUploadUrlInput) => Promise<ProviderUploadUrlResult>;
171
+ createMultipartUpload?: (input: CreateMultipartInput) => Promise<CreateMultipartResult>;
172
+ signPart?: (input: SignPartInput) => Promise<SignedPartResult>;
173
+ completeMultipartUpload?: (input: CompleteMultipartInput) => Promise<CompleteMultipartResult>;
174
+ abortMultipartUpload?: (input: AbortMultipartInput) => Promise<void>;
175
+ };
176
+
177
+ /** Direct signed-URL upload supported by the current native baseline. */
178
+ export type DirectUploadOptions = {
179
+ file: string;
180
+ uploadUrl: string;
181
+ method?: UploadHttpMethod;
182
+ headers?: Record<string, string>;
183
+ background?: boolean;
184
+ networkPolicy?: UploadNetworkPolicy;
185
+ retry?: UploadRetryOptions;
186
+ /** Reserved for P1-C. Rejected when enabled until native compression ships. */
187
+ compression?: CompressionOptions;
188
+ /** Reserved for P1-E. Rejected when true until multipart ships. */
189
+ multipart?: boolean;
190
+ };
191
+
192
+ /** Provider-key upload reserved for the P1-D provider contract. */
193
+ export type ProviderUploadOptions = {
194
+ file: string;
195
+ key: string;
196
+ contentType?: string;
197
+ background?: boolean;
198
+ networkPolicy?: UploadNetworkPolicy;
199
+ retry?: UploadRetryOptions;
200
+ compression?: CompressionOptions;
201
+ multipart?: boolean;
202
+ };
203
+
204
+ export type UploadOptions = DirectUploadOptions | ProviderUploadOptions;
205
+
206
+ export type UploadTaskHandle = {
207
+ readonly id: string;
208
+ readonly snapshot: UploadTaskSnapshot;
209
+ cancel(): Promise<UploadTaskSnapshot>;
210
+ retry(): Promise<UploadTaskSnapshot>;
211
+ getSnapshot(): Promise<UploadTaskSnapshot | null>;
212
+ onProgress(listener: (progress: UploadProgress) => void): () => void;
213
+ onComplete(listener: (task: UploadTaskSnapshot) => void): () => void;
214
+ onFailed(listener: (error: UploadErrorPayload) => void): () => void;
215
+ onCancelled(listener: () => void): () => void;
216
+ };
217
+
218
+ export type Uploader = {
219
+ upload(options: UploadOptions): Promise<UploadTaskHandle>;
220
+ cancel(taskId: string): Promise<UploadTaskSnapshot>;
221
+ retry(taskId: string): Promise<UploadTaskSnapshot>;
222
+ getTask(taskId: string): Promise<UploadTaskSnapshot | null>;
223
+ getAllTasks(): Promise<UploadTaskSnapshot[]>;
224
+ restoreQueue(): Promise<UploadTaskSnapshot[]>;
225
+ subscribe(listener: (event: UploadEvent) => void): () => void;
226
+ };
227
+
228
+ export type CreateUploadOptions = {
229
+ localUri: string;
230
+ uploadUrl: string;
231
+ method?: UploadHttpMethod;
232
+ headers?: Record<string, string>;
233
+ background?: boolean;
234
+ networkPolicy?: UploadNetworkPolicy;
235
+ retry?: UploadRetryOptions;
236
+ compression?: CompressionOptions;
237
+ };
238
+
239
+ export type UploadEvent =
240
+ | { type: 'created'; task: UploadTaskSnapshot }
241
+ | { type: 'compressing'; taskId: string; task: UploadTaskSnapshot }
242
+ | { type: 'compressed'; taskId: string; task: UploadTaskSnapshot; stats: CompressionStats }
243
+ | { type: 'queued'; taskId: string }
244
+ | { type: 'connecting'; taskId: string; task: UploadTaskSnapshot }
245
+ | { type: 'progress'; taskId: string; progress: UploadProgress }
246
+ | { type: 'verifying'; taskId: string }
247
+ | {
248
+ type: 'retrying';
249
+ taskId: string;
250
+ attempt: number;
251
+ delayMs: number;
252
+ error: UploadErrorPayload;
253
+ }
254
+ | { type: 'completed'; taskId: string; task: UploadTaskSnapshot }
255
+ | { type: 'failed'; taskId: string; error: UploadErrorPayload }
256
+ | { type: 'cancelled'; taskId: string };
257
+
258
+ export type UploadosModuleEvents = {
259
+ onUploadEvent: (event: UploadEvent) => void;
260
+ };
@@ -0,0 +1,18 @@
1
+ import { NativeModule, requireNativeModule } from 'expo';
2
+
3
+ import type {
4
+ CreateUploadOptions,
5
+ UploadosModuleEvents,
6
+ UploadTaskSnapshot,
7
+ } from './Uploados.types';
8
+
9
+ declare class UploadosModule extends NativeModule<UploadosModuleEvents> {
10
+ createUploadAsync(options: CreateUploadOptions): Promise<UploadTaskSnapshot>;
11
+ cancelUploadAsync(taskId: string): Promise<UploadTaskSnapshot>;
12
+ retryUploadAsync(taskId: string): Promise<UploadTaskSnapshot>;
13
+ getTaskAsync(taskId: string): Promise<UploadTaskSnapshot | null>;
14
+ getAllTasksAsync(): Promise<UploadTaskSnapshot[]>;
15
+ restoreQueueAsync(): Promise<UploadTaskSnapshot[]>;
16
+ }
17
+
18
+ export default requireNativeModule<UploadosModule>('Uploados');
@@ -0,0 +1,49 @@
1
+ import { NativeModule, registerWebModule } from 'expo';
2
+
3
+ import type {
4
+ CreateUploadOptions,
5
+ UploadErrorPayload,
6
+ UploadosModuleEvents,
7
+ UploadTaskSnapshot,
8
+ } from './Uploados.types';
9
+
10
+ function unsupported(): never {
11
+ const error: UploadErrorPayload = {
12
+ code: 'UNSUPPORTED_PLATFORM',
13
+ message: 'Uploados native uploads are not available on web.',
14
+ retryable: false,
15
+ taskId: '',
16
+ };
17
+ throw error;
18
+ }
19
+
20
+ class Uploados extends NativeModule<UploadosModuleEvents> {
21
+ async createUploadAsync(_options: CreateUploadOptions): Promise<UploadTaskSnapshot> {
22
+ unsupported();
23
+ }
24
+
25
+ async cancelUploadAsync(_taskId: string): Promise<UploadTaskSnapshot> {
26
+ unsupported();
27
+ }
28
+
29
+ async retryUploadAsync(_taskId: string): Promise<UploadTaskSnapshot> {
30
+ unsupported();
31
+ }
32
+
33
+ async getTaskAsync(_taskId: string): Promise<UploadTaskSnapshot | null> {
34
+ unsupported();
35
+ }
36
+
37
+ async getAllTasksAsync(): Promise<UploadTaskSnapshot[]> {
38
+ unsupported();
39
+ }
40
+
41
+ async restoreQueueAsync(): Promise<UploadTaskSnapshot[]> {
42
+ unsupported();
43
+ }
44
+ }
45
+
46
+ type RegisterWebModuleCompat = (moduleImplementation: typeof Uploados) => Uploados;
47
+
48
+ // SDK 52 types only the one-argument form; newer SDKs support it at runtime.
49
+ export default (registerWebModule as unknown as RegisterWebModuleCompat)(Uploados);
@@ -0,0 +1,146 @@
1
+ import type {
2
+ DirectUploadOptions,
3
+ ProviderUploadOptions,
4
+ UploadErrorPayload,
5
+ UploadEvent,
6
+ UploadOptions,
7
+ UploadTaskHandle,
8
+ UploadTaskSnapshot,
9
+ Uploader,
10
+ UploaderConfig,
11
+ } from './Uploados.types';
12
+ import UploadosModule from './UploadosModule';
13
+ import {
14
+ normalizeDirectUploadOptions,
15
+ normalizeProviderUploadOptions,
16
+ } from './normalizeUploadOptions';
17
+ import {
18
+ assertProviderDirectUploadSupported,
19
+ assertUploadProvider,
20
+ } from './providers/defineUploadProvider';
21
+
22
+ function throwMultipartNotReady(): never {
23
+ const error: UploadErrorPayload = {
24
+ code: 'MULTIPART_FAILED',
25
+ message:
26
+ 'Multipart provider uploads require the P1-E native multipart engine. Use direct signed-URL uploads for now.',
27
+ retryable: false,
28
+ taskId: '',
29
+ };
30
+ throw error;
31
+ }
32
+
33
+ function isDirectUploadOptions(options: UploadOptions): options is DirectUploadOptions {
34
+ return 'uploadUrl' in options;
35
+ }
36
+
37
+ function isProviderUploadOptions(options: UploadOptions): options is ProviderUploadOptions {
38
+ return 'key' in options;
39
+ }
40
+
41
+ function getEventTaskId(event: UploadEvent): string | undefined {
42
+ if (event.type === 'created') {
43
+ return event.task.id;
44
+ }
45
+ return event.taskId;
46
+ }
47
+
48
+ function createTaskHandle(
49
+ snapshot: UploadTaskSnapshot,
50
+ subscribe: Uploader['subscribe'],
51
+ module: typeof UploadosModule
52
+ ): UploadTaskHandle {
53
+ const taskId = snapshot.id;
54
+
55
+ const onTaskEvent = <T extends UploadEvent['type']>(
56
+ type: T,
57
+ listener: (event: Extract<UploadEvent, { type: T }>) => void
58
+ ): (() => void) =>
59
+ subscribe((event) => {
60
+ if (getEventTaskId(event) === taskId && event.type === type) {
61
+ listener(event as Extract<UploadEvent, { type: T }>);
62
+ }
63
+ });
64
+
65
+ return {
66
+ id: taskId,
67
+ snapshot,
68
+ cancel: () => module.cancelUploadAsync(taskId),
69
+ retry: () => module.retryUploadAsync(taskId),
70
+ getSnapshot: () => module.getTaskAsync(taskId),
71
+ onProgress: (listener) => onTaskEvent('progress', (event) => listener(event.progress)),
72
+ onComplete: (listener) => onTaskEvent('completed', (event) => listener(event.task)),
73
+ onFailed: (listener) => onTaskEvent('failed', (event) => listener(event.error)),
74
+ onCancelled: (listener) => onTaskEvent('cancelled', () => listener()),
75
+ };
76
+ }
77
+
78
+ export function createUploader(config: UploaderConfig = {}): Uploader {
79
+ const listeners = new Set<(event: UploadEvent) => void>();
80
+ let moduleSubscription: { remove: () => void } | null = null;
81
+ const provider = config.provider;
82
+
83
+ const ensureModuleSubscription = () => {
84
+ if (moduleSubscription) {
85
+ return;
86
+ }
87
+ moduleSubscription = UploadosModule.addListener('onUploadEvent', (event: UploadEvent) => {
88
+ listeners.forEach((listener) => {
89
+ listener(event);
90
+ });
91
+ });
92
+ };
93
+
94
+ const subscribe = (listener: (event: UploadEvent) => void): (() => void) => {
95
+ ensureModuleSubscription();
96
+ listeners.add(listener);
97
+ return () => {
98
+ listeners.delete(listener);
99
+ if (listeners.size === 0 && moduleSubscription) {
100
+ moduleSubscription.remove();
101
+ moduleSubscription = null;
102
+ }
103
+ };
104
+ };
105
+
106
+ return {
107
+ async upload(options: UploadOptions): Promise<UploadTaskHandle> {
108
+ if (isProviderUploadOptions(options)) {
109
+ const resolvedProvider = assertUploadProvider(provider);
110
+ if (options.multipart === true) {
111
+ throwMultipartNotReady();
112
+ }
113
+ assertProviderDirectUploadSupported(resolvedProvider);
114
+ const signed = await resolvedProvider.getUploadUrl!({
115
+ key: options.key.trim(),
116
+ contentType: options.contentType,
117
+ });
118
+ const normalized = normalizeProviderUploadOptions(config, options, {
119
+ uploadUrl: signed.url,
120
+ method: signed.method,
121
+ headers: signed.headers,
122
+ });
123
+ const snapshot = await UploadosModule.createUploadAsync(normalized);
124
+ return createTaskHandle(snapshot, subscribe, UploadosModule);
125
+ }
126
+
127
+ if (!isDirectUploadOptions(options)) {
128
+ throw Object.assign(new Error('Invalid upload options.'), {
129
+ code: 'UNKNOWN',
130
+ retryable: false,
131
+ taskId: '',
132
+ });
133
+ }
134
+
135
+ const normalized = normalizeDirectUploadOptions(config, options);
136
+ const snapshot = await UploadosModule.createUploadAsync(normalized);
137
+ return createTaskHandle(snapshot, subscribe, UploadosModule);
138
+ },
139
+ cancel: (taskId) => UploadosModule.cancelUploadAsync(taskId),
140
+ retry: (taskId) => UploadosModule.retryUploadAsync(taskId),
141
+ getTask: (taskId) => UploadosModule.getTaskAsync(taskId),
142
+ getAllTasks: () => UploadosModule.getAllTasksAsync(),
143
+ restoreQueue: () => UploadosModule.restoreQueueAsync(),
144
+ subscribe,
145
+ };
146
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { createUploader } from './createUploader';
2
+ export { defineUploadProvider } from './providers/defineUploadProvider';
3
+ export { createMultipartUploadPlan, DEFAULT_MULTIPART_PART_SIZE } from './providers/multipartPlan';
4
+ export * from './Uploados.types';