@uploadista/client-core 0.0.13 → 0.0.14

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.
@@ -0,0 +1,353 @@
1
+ import type { UploadFile } from "@uploadista/core/types";
2
+ import type { UploadOptions } from "../types/upload-options";
3
+
4
+ /**
5
+ * Upload status representing the current state of an upload
6
+ */
7
+ export type UploadStatus =
8
+ | "idle"
9
+ | "uploading"
10
+ | "success"
11
+ | "error"
12
+ | "aborted";
13
+
14
+ /**
15
+ * Complete upload state
16
+ */
17
+ export interface UploadState {
18
+ /** Current status of the upload */
19
+ status: UploadStatus;
20
+ /** Upload progress percentage (0-100) */
21
+ progress: number;
22
+ /** Number of bytes uploaded */
23
+ bytesUploaded: number;
24
+ /** Total bytes to upload, null if unknown/deferred */
25
+ totalBytes: number | null;
26
+ /** Error if upload failed */
27
+ error: Error | null;
28
+ /** Result if upload succeeded */
29
+ result: UploadFile | null;
30
+ }
31
+
32
+ /**
33
+ * Callbacks that UploadManager invokes during the upload lifecycle
34
+ */
35
+ export interface UploadManagerCallbacks {
36
+ /**
37
+ * Called when the upload state changes
38
+ */
39
+ onStateChange: (state: UploadState) => void;
40
+
41
+ /**
42
+ * Called when upload progress updates
43
+ * @param uploadId - The unique identifier for this upload
44
+ * @param bytesUploaded - Number of bytes uploaded
45
+ * @param totalBytes - Total bytes to upload, null if unknown
46
+ */
47
+ onProgress?: (
48
+ uploadId: string,
49
+ bytesUploaded: number,
50
+ totalBytes: number | null,
51
+ ) => void;
52
+
53
+ /**
54
+ * Called when a chunk completes
55
+ * @param chunkSize - Size of the completed chunk
56
+ * @param bytesAccepted - Total bytes accepted so far
57
+ * @param bytesTotal - Total bytes to upload, null if unknown
58
+ */
59
+ onChunkComplete?: (
60
+ chunkSize: number,
61
+ bytesAccepted: number,
62
+ bytesTotal: number | null,
63
+ ) => void;
64
+
65
+ /**
66
+ * Called when upload completes successfully
67
+ * @param result - The uploaded file result
68
+ */
69
+ onSuccess?: (result: UploadFile) => void;
70
+
71
+ /**
72
+ * Called when upload fails with an error
73
+ * @param error - The error that occurred
74
+ */
75
+ onError?: (error: Error) => void;
76
+
77
+ /**
78
+ * Called when upload is aborted
79
+ */
80
+ onAbort?: () => void;
81
+ }
82
+
83
+ /**
84
+ * Generic upload input type - can be any value that the upload client accepts
85
+ */
86
+ export type UploadInput = unknown;
87
+
88
+ /**
89
+ * Abort controller interface for canceling uploads
90
+ */
91
+ export interface UploadAbortController {
92
+ abort: () => void;
93
+ }
94
+
95
+ /**
96
+ * Upload function that performs the actual upload.
97
+ * Returns a promise that resolves to an abort controller.
98
+ */
99
+ export type UploadFunction<
100
+ TInput = UploadInput,
101
+ TOptions extends UploadOptions = UploadOptions,
102
+ > = (input: TInput, options: TOptions) => Promise<UploadAbortController>;
103
+
104
+ /**
105
+ * Initial state for a new upload
106
+ */
107
+ const initialState: UploadState = {
108
+ status: "idle",
109
+ progress: 0,
110
+ bytesUploaded: 0,
111
+ totalBytes: null,
112
+ error: null,
113
+ result: null,
114
+ };
115
+
116
+ /**
117
+ * Platform-agnostic upload manager that handles upload state machine,
118
+ * progress tracking, error handling, abort, reset, and retry logic.
119
+ *
120
+ * Framework packages (React, Vue, React Native) should wrap this manager
121
+ * with framework-specific hooks/composables.
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * const uploadFn = (input, options) => client.upload(input, options);
126
+ * const manager = new UploadManager(uploadFn, {
127
+ * onStateChange: (state) => setState(state),
128
+ * onProgress: (progress) => console.log(`${progress}%`),
129
+ * onSuccess: (result) => console.log('Upload complete:', result),
130
+ * onError: (error) => console.error('Upload failed:', error),
131
+ * });
132
+ *
133
+ * await manager.upload(file);
134
+ * ```
135
+ */
136
+ export class UploadManager<
137
+ TInput = UploadInput,
138
+ TOptions extends UploadOptions = UploadOptions,
139
+ > {
140
+ private state: UploadState;
141
+ private abortController: UploadAbortController | null = null;
142
+ private lastInput: TInput | null = null;
143
+ private uploadId: string | null = null;
144
+
145
+ /**
146
+ * Create a new UploadManager
147
+ *
148
+ * @param uploadFn - Upload function to use for uploads
149
+ * @param callbacks - Callbacks to invoke during upload lifecycle
150
+ * @param options - Upload configuration options
151
+ */
152
+ constructor(
153
+ private readonly uploadFn: UploadFunction<TInput, TOptions>,
154
+ private readonly callbacks: UploadManagerCallbacks,
155
+ private readonly options?: TOptions,
156
+ ) {
157
+ this.state = { ...initialState };
158
+ }
159
+
160
+ /**
161
+ * Get the current upload state
162
+ */
163
+ getState(): UploadState {
164
+ return { ...this.state };
165
+ }
166
+
167
+ /**
168
+ * Check if an upload is currently active
169
+ */
170
+ isUploading(): boolean {
171
+ return this.state.status === "uploading";
172
+ }
173
+
174
+ /**
175
+ * Check if the upload can be retried
176
+ */
177
+ canRetry(): boolean {
178
+ return (
179
+ (this.state.status === "error" || this.state.status === "aborted") &&
180
+ this.lastInput !== null
181
+ );
182
+ }
183
+
184
+ /**
185
+ * Update the internal state and notify callbacks
186
+ */
187
+ private updateState(update: Partial<UploadState>): void {
188
+ this.state = { ...this.state, ...update };
189
+ this.callbacks.onStateChange(this.state);
190
+ }
191
+
192
+ /**
193
+ * Start uploading a file or input
194
+ *
195
+ * @param input - File or input to upload (type depends on platform)
196
+ */
197
+ async upload(input: TInput): Promise<void> {
198
+ // Determine totalBytes from input if possible (File/Blob on browser platforms)
199
+ let totalBytes: number | null = null;
200
+ if (input && typeof input === "object") {
201
+ if ("size" in input && typeof input.size === "number") {
202
+ totalBytes = input.size;
203
+ }
204
+ }
205
+
206
+ // Reset state but keep reference for retries
207
+ this.updateState({
208
+ status: "uploading",
209
+ progress: 0,
210
+ bytesUploaded: 0,
211
+ totalBytes,
212
+ error: null,
213
+ result: null,
214
+ });
215
+
216
+ this.lastInput = input;
217
+
218
+ try {
219
+ // Build complete options with our callbacks
220
+ const uploadOptions = {
221
+ ...this.options,
222
+ onProgress: (
223
+ uploadId: string,
224
+ bytesUploaded: number,
225
+ bytes: number | null,
226
+ ) => {
227
+ // Store uploadId on first progress callback
228
+ if (!this.uploadId) {
229
+ this.uploadId = uploadId;
230
+ }
231
+
232
+ const progressPercent = bytes
233
+ ? Math.round((bytesUploaded / bytes) * 100)
234
+ : 0;
235
+
236
+ this.updateState({
237
+ progress: progressPercent,
238
+ bytesUploaded,
239
+ totalBytes: bytes,
240
+ });
241
+
242
+ this.callbacks.onProgress?.(uploadId, bytesUploaded, bytes);
243
+ this.options?.onProgress?.(uploadId, bytesUploaded, bytes);
244
+ },
245
+ onChunkComplete: (
246
+ chunkSize: number,
247
+ bytesAccepted: number,
248
+ bytesTotal: number | null,
249
+ ) => {
250
+ this.callbacks.onChunkComplete?.(
251
+ chunkSize,
252
+ bytesAccepted,
253
+ bytesTotal,
254
+ );
255
+ this.options?.onChunkComplete?.(chunkSize, bytesAccepted, bytesTotal);
256
+ },
257
+ onSuccess: (result: UploadFile) => {
258
+ this.updateState({
259
+ status: "success",
260
+ result,
261
+ progress: 100,
262
+ bytesUploaded: result.size || 0,
263
+ totalBytes: result.size || null,
264
+ });
265
+
266
+ this.callbacks.onSuccess?.(result);
267
+ this.options?.onSuccess?.(result);
268
+ this.abortController = null;
269
+ },
270
+ onError: (error: Error) => {
271
+ this.updateState({
272
+ status: "error",
273
+ error,
274
+ });
275
+
276
+ this.callbacks.onError?.(error);
277
+ this.options?.onError?.(error);
278
+ this.abortController = null;
279
+ },
280
+ onAbort: () => {
281
+ this.updateState({
282
+ status: "aborted",
283
+ });
284
+
285
+ this.callbacks.onAbort?.();
286
+ this.options?.onAbort?.();
287
+ this.abortController = null;
288
+ },
289
+ onShouldRetry: this.options?.onShouldRetry,
290
+ } as TOptions;
291
+
292
+ // Start the upload
293
+ this.abortController = await this.uploadFn(input, uploadOptions);
294
+ } catch (error) {
295
+ // Handle errors from upload initiation
296
+ const uploadError =
297
+ error instanceof Error ? error : new Error(String(error));
298
+ this.updateState({
299
+ status: "error",
300
+ error: uploadError,
301
+ });
302
+
303
+ this.callbacks.onError?.(uploadError);
304
+ this.options?.onError?.(uploadError);
305
+ this.abortController = null;
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Abort the current upload
311
+ */
312
+ abort(): void {
313
+ if (this.abortController) {
314
+ this.abortController.abort();
315
+ // Note: State update happens in onAbort callback
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Reset the upload state to idle
321
+ */
322
+ reset(): void {
323
+ if (this.abortController) {
324
+ this.abortController.abort();
325
+ this.abortController = null;
326
+ }
327
+
328
+ this.state = { ...initialState };
329
+ this.lastInput = null;
330
+ this.uploadId = null;
331
+ this.callbacks.onStateChange(this.state);
332
+ }
333
+
334
+ /**
335
+ * Retry the last failed or aborted upload
336
+ */
337
+ retry(): void {
338
+ if (this.canRetry() && this.lastInput !== null) {
339
+ this.upload(this.lastInput);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Clean up resources (call when disposing the manager)
345
+ */
346
+ cleanup(): void {
347
+ if (this.abortController) {
348
+ this.abortController.abort();
349
+ this.abortController = null;
350
+ }
351
+ this.uploadId = null;
352
+ }
353
+ }
@@ -7,18 +7,230 @@ import type { IdGenerationService } from "./id-generation-service";
7
7
  import type { PlatformService } from "./platform-service";
8
8
  import type { StorageService } from "./storage-service";
9
9
  import type { WebSocketFactory } from "./websocket-service";
10
+
10
11
  /**
11
- * Service container for dependency injection
12
+ * Service container for dependency injection in the Uploadista client.
13
+ *
14
+ * This container provides all platform-specific services needed by the upload client.
15
+ * Different platforms (browser, React Native, Node.js) provide their own implementations
16
+ * of these services to handle platform-specific APIs and behaviors.
17
+ *
18
+ * @template UploadInput - The type of input accepted by the file reader (e.g., File, Blob, string path)
19
+ *
20
+ * @example Browser implementation
21
+ * ```typescript
22
+ * const services: ServiceContainer<File | Blob> = {
23
+ * storage: new LocalStorageService(),
24
+ * idGeneration: new BrowserIdGenerationService(),
25
+ * httpClient: new FetchHttpClient(),
26
+ * fileReader: new BrowserFileReaderService(),
27
+ * base64: new BrowserBase64Service(),
28
+ * websocket: new BrowserWebSocketFactory(),
29
+ * abortController: new BrowserAbortControllerFactory(),
30
+ * platform: new BrowserPlatformService(),
31
+ * checksumService: new WebCryptoChecksumService(),
32
+ * fingerprintService: new BrowserFingerprintService(),
33
+ * };
34
+ * ```
35
+ *
36
+ * @example React Native implementation
37
+ * ```typescript
38
+ * const services: ServiceContainer<FilePickResult> = {
39
+ * storage: new AsyncStorageService(),
40
+ * idGeneration: new ReactNativeIdGenerationService(),
41
+ * httpClient: new FetchHttpClient(),
42
+ * fileReader: new ReactNativeFileReaderService(),
43
+ * websocket: new ReactNativeWebSocketFactory(),
44
+ * abortController: new ReactNativeAbortControllerFactory(),
45
+ * platform: new ReactNativePlatformService(),
46
+ * checksumService: new ReactNativeChecksumService(),
47
+ * fingerprintService: new ReactNativeFingerprintService(),
48
+ * };
49
+ * ```
12
50
  */
13
51
  export interface ServiceContainer<UploadInput> {
52
+ /**
53
+ * Storage service for persisting upload state and metadata.
54
+ *
55
+ * **Required**: Yes
56
+ *
57
+ * **Used for**:
58
+ * - Storing upload progress for resumption
59
+ * - Caching upload fingerprints
60
+ * - Persisting partial upload state across sessions
61
+ *
62
+ * **Platform implementations**:
63
+ * - Browser: `localStorage`, `IndexedDB`
64
+ * - React Native: `AsyncStorage`, `MMKV`
65
+ * - Node.js: File system, Redis
66
+ */
14
67
  storage: StorageService;
68
+
69
+ /**
70
+ * ID generation service for creating unique identifiers.
71
+ *
72
+ * **Required**: Yes
73
+ *
74
+ * **Used for**:
75
+ * - Generating upload IDs
76
+ * - Creating request correlation IDs
77
+ * - Generating chunk identifiers
78
+ *
79
+ * **Platform implementations**:
80
+ * - Browser: `crypto.randomUUID()` or fallback
81
+ * - React Native: UUID libraries
82
+ * - Node.js: `crypto.randomUUID()`
83
+ */
15
84
  idGeneration: IdGenerationService;
85
+
86
+ /**
87
+ * HTTP client for making upload requests.
88
+ *
89
+ * **Required**: Yes
90
+ *
91
+ * **Used for**:
92
+ * - Uploading file chunks
93
+ * - Making API calls to the upload server
94
+ * - Fetching upload metadata
95
+ *
96
+ * **Platform implementations**:
97
+ * - Browser: `fetch()` with connection pooling
98
+ * - React Native: `fetch()` or `XMLHttpRequest`
99
+ * - Node.js: `node:http`, `node:https`, or libraries like `undici`
100
+ *
101
+ * **Important**: Should support connection pooling for optimal performance
102
+ */
16
103
  httpClient: HttpClient;
104
+
105
+ /**
106
+ * File reader service for reading file contents.
107
+ *
108
+ * **Required**: Yes
109
+ *
110
+ * **Used for**:
111
+ * - Reading file data for upload
112
+ * - Slicing files into chunks
113
+ * - Computing file checksums and fingerprints
114
+ *
115
+ * **Platform implementations**:
116
+ * - Browser: `FileReader` API, `Blob.slice()`
117
+ * - React Native: Native file system modules, `react-native-fs`
118
+ * - Node.js: `fs.createReadStream()`, `fs.promises.open()`
119
+ *
120
+ * **Generic type**: Accepts platform-specific input types (File, Blob, path string)
121
+ */
17
122
  fileReader: FileReaderService<UploadInput>;
123
+
124
+ /**
125
+ * Base64 encoding/decoding service.
126
+ *
127
+ * **Required**: No (optional)
128
+ *
129
+ * **Used for**:
130
+ * - Encoding binary data for transport
131
+ * - Decoding server responses
132
+ * - Optional data transformations
133
+ *
134
+ * **Platform implementations**:
135
+ * - Browser: `btoa()`, `atob()`
136
+ * - React Native: `base64-js` or built-in
137
+ * - Node.js: `Buffer.from().toString('base64')`
138
+ *
139
+ * **Note**: Only needed for specific upload protocols that require base64 encoding
140
+ */
18
141
  base64?: Base64Service;
142
+
143
+ /**
144
+ * WebSocket factory for creating WebSocket connections.
145
+ *
146
+ * **Required**: Yes
147
+ *
148
+ * **Used for**:
149
+ * - Real-time upload progress updates
150
+ * - Server-side event notifications
151
+ * - Live upload status from server
152
+ *
153
+ * **Platform implementations**:
154
+ * - Browser: Native `WebSocket` API
155
+ * - React Native: `react-native-websocket` or polyfills
156
+ * - Node.js: `ws` library
157
+ *
158
+ * **Important**: Must support standard WebSocket protocol
159
+ */
19
160
  websocket: WebSocketFactory;
161
+
162
+ /**
163
+ * Abort controller factory for creating cancellation tokens.
164
+ *
165
+ * **Required**: Yes
166
+ *
167
+ * **Used for**:
168
+ * - Aborting in-flight upload requests
169
+ * - Cancelling file read operations
170
+ * - Implementing upload timeout logic
171
+ *
172
+ * **Platform implementations**:
173
+ * - Browser: Native `AbortController` API
174
+ * - React Native: `AbortController` polyfill
175
+ * - Node.js: Native `AbortController` (Node 15+)
176
+ */
20
177
  abortController: AbortControllerFactory;
178
+
179
+ /**
180
+ * Platform detection service.
181
+ *
182
+ * **Required**: Yes
183
+ *
184
+ * **Used for**:
185
+ * - Detecting runtime environment (browser, React Native, Node.js)
186
+ * - Applying platform-specific optimizations
187
+ * - Conditional feature availability
188
+ *
189
+ * **Platform implementations**:
190
+ * - Browser: Checks `window`, `document` availability
191
+ * - React Native: Checks `navigator.product === 'ReactNative'`
192
+ * - Node.js: Checks `process` availability
193
+ */
21
194
  platform: PlatformService;
195
+
196
+ /**
197
+ * Checksum service for computing file checksums.
198
+ *
199
+ * **Required**: Yes
200
+ *
201
+ * **Used for**:
202
+ * - Verifying upload integrity
203
+ * - Detecting corrupted uploads
204
+ * - Implementing upload deduplication
205
+ *
206
+ * **Platform implementations**:
207
+ * - Browser: `crypto.subtle` Web Crypto API
208
+ * - React Native: Native crypto modules or JavaScript implementations
209
+ * - Node.js: `crypto` module
210
+ *
211
+ * **Common algorithms**: SHA-256, MD5, CRC32
212
+ */
22
213
  checksumService: ChecksumService;
214
+
215
+ /**
216
+ * Fingerprint service for generating unique file identifiers.
217
+ *
218
+ * **Required**: Yes
219
+ *
220
+ * **Used for**:
221
+ * - Upload resumption (matching partial uploads)
222
+ * - Deduplication (detecting duplicate files)
223
+ * - Cache key generation
224
+ *
225
+ * **Platform implementations**:
226
+ * - Browser: Combines file metadata (size, name, modified date, first/last bytes)
227
+ * - React Native: Similar to browser but uses platform-specific file APIs
228
+ * - Node.js: File stat info + content sampling
229
+ *
230
+ * **Generic type**: Accepts platform-specific input types
231
+ *
232
+ * **Note**: Fingerprints should be stable (same file = same fingerprint) but
233
+ * fast to compute (avoid reading entire file if possible)
234
+ */
23
235
  fingerprintService: FingerprintService<UploadInput>;
24
236
  }
@@ -0,0 +1,16 @@
1
+ export {
2
+ MockAbortController,
3
+ MockAbortControllerFactory,
4
+ MockBase64Service,
5
+ MockChecksumService,
6
+ MockFileReaderService,
7
+ MockFingerprintService,
8
+ MockHttpClient,
9
+ type MockHttpResponseConfig,
10
+ MockIdGenerationService,
11
+ MockPlatformService,
12
+ MockStorageService,
13
+ MockWebSocket,
14
+ MockWebSocketFactory,
15
+ createMockServiceContainer,
16
+ } from "./mock-service-container";