@uploadista/client-core 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 (235) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +100 -0
  4. package/dist/auth/auth-http-client.d.ts +50 -0
  5. package/dist/auth/auth-http-client.d.ts.map +1 -0
  6. package/dist/auth/auth-http-client.js +110 -0
  7. package/dist/auth/direct-auth.d.ts +38 -0
  8. package/dist/auth/direct-auth.d.ts.map +1 -0
  9. package/dist/auth/direct-auth.js +95 -0
  10. package/dist/auth/index.d.ts +6 -0
  11. package/dist/auth/index.d.ts.map +1 -0
  12. package/dist/auth/index.js +5 -0
  13. package/dist/auth/no-auth.d.ts +26 -0
  14. package/dist/auth/no-auth.d.ts.map +1 -0
  15. package/dist/auth/no-auth.js +33 -0
  16. package/dist/auth/saas-auth.d.ts +80 -0
  17. package/dist/auth/saas-auth.d.ts.map +1 -0
  18. package/dist/auth/saas-auth.js +167 -0
  19. package/dist/auth/types.d.ts +101 -0
  20. package/dist/auth/types.d.ts.map +1 -0
  21. package/dist/auth/types.js +8 -0
  22. package/dist/chunk-buffer.d.ts +209 -0
  23. package/dist/chunk-buffer.d.ts.map +1 -0
  24. package/dist/chunk-buffer.js +236 -0
  25. package/dist/client/create-uploadista-client.d.ts +369 -0
  26. package/dist/client/create-uploadista-client.d.ts.map +1 -0
  27. package/dist/client/create-uploadista-client.js +518 -0
  28. package/dist/client/index.d.ts +4 -0
  29. package/dist/client/index.d.ts.map +1 -0
  30. package/dist/client/index.js +3 -0
  31. package/dist/client/uploadista-api.d.ts +284 -0
  32. package/dist/client/uploadista-api.d.ts.map +1 -0
  33. package/dist/client/uploadista-api.js +444 -0
  34. package/dist/client/uploadista-websocket-manager.d.ts +110 -0
  35. package/dist/client/uploadista-websocket-manager.d.ts.map +1 -0
  36. package/dist/client/uploadista-websocket-manager.js +207 -0
  37. package/dist/error.d.ts +106 -0
  38. package/dist/error.d.ts.map +1 -0
  39. package/dist/error.js +69 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +12 -0
  43. package/dist/logger.d.ts +70 -0
  44. package/dist/logger.d.ts.map +1 -0
  45. package/dist/logger.js +59 -0
  46. package/dist/mock-data-store.d.ts +30 -0
  47. package/dist/mock-data-store.d.ts.map +1 -0
  48. package/dist/mock-data-store.js +88 -0
  49. package/dist/network-monitor.d.ts +262 -0
  50. package/dist/network-monitor.d.ts.map +1 -0
  51. package/dist/network-monitor.js +291 -0
  52. package/dist/services/abort-controller-service.d.ts +19 -0
  53. package/dist/services/abort-controller-service.d.ts.map +1 -0
  54. package/dist/services/abort-controller-service.js +4 -0
  55. package/dist/services/checksum-service.d.ts +4 -0
  56. package/dist/services/checksum-service.d.ts.map +1 -0
  57. package/dist/services/checksum-service.js +1 -0
  58. package/dist/services/file-reader-service.d.ts +38 -0
  59. package/dist/services/file-reader-service.d.ts.map +1 -0
  60. package/dist/services/file-reader-service.js +4 -0
  61. package/dist/services/fingerprint-service.d.ts +4 -0
  62. package/dist/services/fingerprint-service.d.ts.map +1 -0
  63. package/dist/services/fingerprint-service.js +1 -0
  64. package/dist/services/http-client.d.ts +182 -0
  65. package/dist/services/http-client.d.ts.map +1 -0
  66. package/dist/services/http-client.js +1 -0
  67. package/dist/services/id-generation-service.d.ts +10 -0
  68. package/dist/services/id-generation-service.d.ts.map +1 -0
  69. package/dist/services/id-generation-service.js +1 -0
  70. package/dist/services/index.d.ts +11 -0
  71. package/dist/services/index.d.ts.map +1 -0
  72. package/dist/services/index.js +10 -0
  73. package/dist/services/platform-service.d.ts +48 -0
  74. package/dist/services/platform-service.d.ts.map +1 -0
  75. package/dist/services/platform-service.js +10 -0
  76. package/dist/services/service-container.d.ts +25 -0
  77. package/dist/services/service-container.d.ts.map +1 -0
  78. package/dist/services/service-container.js +1 -0
  79. package/dist/services/storage-service.d.ts +26 -0
  80. package/dist/services/storage-service.d.ts.map +1 -0
  81. package/dist/services/storage-service.js +1 -0
  82. package/dist/services/websocket-service.d.ts +36 -0
  83. package/dist/services/websocket-service.d.ts.map +1 -0
  84. package/dist/services/websocket-service.js +4 -0
  85. package/dist/smart-chunker.d.ts +72 -0
  86. package/dist/smart-chunker.d.ts.map +1 -0
  87. package/dist/smart-chunker.js +317 -0
  88. package/dist/storage/client-storage.d.ts +148 -0
  89. package/dist/storage/client-storage.d.ts.map +1 -0
  90. package/dist/storage/client-storage.js +62 -0
  91. package/dist/storage/in-memory-storage-service.d.ts +7 -0
  92. package/dist/storage/in-memory-storage-service.d.ts.map +1 -0
  93. package/dist/storage/in-memory-storage-service.js +24 -0
  94. package/dist/storage/index.d.ts +3 -0
  95. package/dist/storage/index.d.ts.map +1 -0
  96. package/dist/storage/index.js +2 -0
  97. package/dist/types/buffered-chunk.d.ts +6 -0
  98. package/dist/types/buffered-chunk.d.ts.map +1 -0
  99. package/dist/types/buffered-chunk.js +1 -0
  100. package/dist/types/chunk-metrics.d.ts +12 -0
  101. package/dist/types/chunk-metrics.d.ts.map +1 -0
  102. package/dist/types/chunk-metrics.js +1 -0
  103. package/dist/types/flow-result.d.ts +11 -0
  104. package/dist/types/flow-result.d.ts.map +1 -0
  105. package/dist/types/flow-result.js +1 -0
  106. package/dist/types/flow-upload-config.d.ts +54 -0
  107. package/dist/types/flow-upload-config.d.ts.map +1 -0
  108. package/dist/types/flow-upload-config.js +1 -0
  109. package/dist/types/flow-upload-item.d.ts +16 -0
  110. package/dist/types/flow-upload-item.d.ts.map +1 -0
  111. package/dist/types/flow-upload-item.js +1 -0
  112. package/dist/types/flow-upload-options.d.ts +41 -0
  113. package/dist/types/flow-upload-options.d.ts.map +1 -0
  114. package/dist/types/flow-upload-options.js +1 -0
  115. package/dist/types/index.d.ts +14 -0
  116. package/dist/types/index.d.ts.map +1 -0
  117. package/dist/types/index.js +13 -0
  118. package/dist/types/multi-flow-upload-options.d.ts +33 -0
  119. package/dist/types/multi-flow-upload-options.d.ts.map +1 -0
  120. package/dist/types/multi-flow-upload-options.js +1 -0
  121. package/dist/types/multi-flow-upload-state.d.ts +9 -0
  122. package/dist/types/multi-flow-upload-state.d.ts.map +1 -0
  123. package/dist/types/multi-flow-upload-state.js +1 -0
  124. package/dist/types/performance-insights.d.ts +11 -0
  125. package/dist/types/performance-insights.d.ts.map +1 -0
  126. package/dist/types/performance-insights.js +1 -0
  127. package/dist/types/previous-upload.d.ts +20 -0
  128. package/dist/types/previous-upload.d.ts.map +1 -0
  129. package/dist/types/previous-upload.js +9 -0
  130. package/dist/types/upload-options.d.ts +40 -0
  131. package/dist/types/upload-options.d.ts.map +1 -0
  132. package/dist/types/upload-options.js +1 -0
  133. package/dist/types/upload-response.d.ts +6 -0
  134. package/dist/types/upload-response.d.ts.map +1 -0
  135. package/dist/types/upload-response.js +1 -0
  136. package/dist/types/upload-result.d.ts +57 -0
  137. package/dist/types/upload-result.d.ts.map +1 -0
  138. package/dist/types/upload-result.js +1 -0
  139. package/dist/types/upload-session-metrics.d.ts +16 -0
  140. package/dist/types/upload-session-metrics.d.ts.map +1 -0
  141. package/dist/types/upload-session-metrics.js +1 -0
  142. package/dist/upload/chunk-upload.d.ts +40 -0
  143. package/dist/upload/chunk-upload.d.ts.map +1 -0
  144. package/dist/upload/chunk-upload.js +82 -0
  145. package/dist/upload/flow-upload.d.ts +48 -0
  146. package/dist/upload/flow-upload.d.ts.map +1 -0
  147. package/dist/upload/flow-upload.js +240 -0
  148. package/dist/upload/index.d.ts +3 -0
  149. package/dist/upload/index.d.ts.map +1 -0
  150. package/dist/upload/index.js +2 -0
  151. package/dist/upload/parallel-upload.d.ts +65 -0
  152. package/dist/upload/parallel-upload.d.ts.map +1 -0
  153. package/dist/upload/parallel-upload.js +231 -0
  154. package/dist/upload/single-upload.d.ts +118 -0
  155. package/dist/upload/single-upload.d.ts.map +1 -0
  156. package/dist/upload/single-upload.js +332 -0
  157. package/dist/upload/upload-manager.d.ts +30 -0
  158. package/dist/upload/upload-manager.d.ts.map +1 -0
  159. package/dist/upload/upload-manager.js +57 -0
  160. package/dist/upload/upload-metrics.d.ts +37 -0
  161. package/dist/upload/upload-metrics.d.ts.map +1 -0
  162. package/dist/upload/upload-metrics.js +236 -0
  163. package/dist/upload/upload-storage.d.ts +32 -0
  164. package/dist/upload/upload-storage.d.ts.map +1 -0
  165. package/dist/upload/upload-storage.js +46 -0
  166. package/dist/upload/upload-strategy.d.ts +66 -0
  167. package/dist/upload/upload-strategy.d.ts.map +1 -0
  168. package/dist/upload/upload-strategy.js +171 -0
  169. package/dist/upload/upload-utils.d.ts +26 -0
  170. package/dist/upload/upload-utils.d.ts.map +1 -0
  171. package/dist/upload/upload-utils.js +80 -0
  172. package/package.json +29 -0
  173. package/src/__tests__/smart-chunking.test.ts +399 -0
  174. package/src/auth/__tests__/auth-http-client.test.ts +327 -0
  175. package/src/auth/__tests__/direct-auth.test.ts +135 -0
  176. package/src/auth/__tests__/no-auth.test.ts +40 -0
  177. package/src/auth/__tests__/saas-auth.test.ts +337 -0
  178. package/src/auth/auth-http-client.ts +150 -0
  179. package/src/auth/direct-auth.ts +121 -0
  180. package/src/auth/index.ts +5 -0
  181. package/src/auth/no-auth.ts +39 -0
  182. package/src/auth/saas-auth.ts +218 -0
  183. package/src/auth/types.ts +105 -0
  184. package/src/chunk-buffer.ts +287 -0
  185. package/src/client/create-uploadista-client.ts +901 -0
  186. package/src/client/index.ts +3 -0
  187. package/src/client/uploadista-api.ts +857 -0
  188. package/src/client/uploadista-websocket-manager.ts +275 -0
  189. package/src/error.ts +149 -0
  190. package/src/index.ts +13 -0
  191. package/src/logger.ts +104 -0
  192. package/src/mock-data-store.ts +97 -0
  193. package/src/network-monitor.ts +445 -0
  194. package/src/services/abort-controller-service.ts +21 -0
  195. package/src/services/checksum-service.ts +3 -0
  196. package/src/services/file-reader-service.ts +44 -0
  197. package/src/services/fingerprint-service.ts +6 -0
  198. package/src/services/http-client.ts +229 -0
  199. package/src/services/id-generation-service.ts +9 -0
  200. package/src/services/index.ts +10 -0
  201. package/src/services/platform-service.ts +65 -0
  202. package/src/services/service-container.ts +24 -0
  203. package/src/services/storage-service.ts +29 -0
  204. package/src/services/websocket-service.ts +33 -0
  205. package/src/smart-chunker.ts +451 -0
  206. package/src/storage/client-storage.ts +186 -0
  207. package/src/storage/in-memory-storage-service.ts +33 -0
  208. package/src/storage/index.ts +2 -0
  209. package/src/types/buffered-chunk.ts +5 -0
  210. package/src/types/chunk-metrics.ts +11 -0
  211. package/src/types/flow-result.ts +14 -0
  212. package/src/types/flow-upload-config.ts +56 -0
  213. package/src/types/flow-upload-item.ts +16 -0
  214. package/src/types/flow-upload-options.ts +56 -0
  215. package/src/types/index.ts +13 -0
  216. package/src/types/multi-flow-upload-options.ts +39 -0
  217. package/src/types/multi-flow-upload-state.ts +9 -0
  218. package/src/types/performance-insights.ts +7 -0
  219. package/src/types/previous-upload.ts +22 -0
  220. package/src/types/upload-options.ts +56 -0
  221. package/src/types/upload-response.ts +6 -0
  222. package/src/types/upload-result.ts +60 -0
  223. package/src/types/upload-session-metrics.ts +15 -0
  224. package/src/upload/chunk-upload.ts +151 -0
  225. package/src/upload/flow-upload.ts +367 -0
  226. package/src/upload/index.ts +2 -0
  227. package/src/upload/parallel-upload.ts +387 -0
  228. package/src/upload/single-upload.ts +554 -0
  229. package/src/upload/upload-manager.ts +106 -0
  230. package/src/upload/upload-metrics.ts +340 -0
  231. package/src/upload/upload-storage.ts +87 -0
  232. package/src/upload/upload-strategy.ts +296 -0
  233. package/src/upload/upload-utils.ts +114 -0
  234. package/tsconfig.json +23 -0
  235. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,554 @@
1
+ import type { InputFile, UploadFile } from "@uploadista/core/types";
2
+ import type { UploadistaApi } from "../client/uploadista-api";
3
+ import { UploadistaError } from "../error";
4
+ import type { Logger } from "../logger";
5
+ import type { AbortControllerLike } from "../services/abort-controller-service";
6
+ import type { ChecksumService } from "../services/checksum-service";
7
+ import type { FileSource } from "../services/file-reader-service";
8
+ import type { IdGenerationService } from "../services/id-generation-service";
9
+ import type { PlatformService, Timeout } from "../services/platform-service";
10
+ import type { WebSocketLike } from "../services/websocket-service";
11
+ import type { SmartChunker, SmartChunkerConfig } from "../smart-chunker";
12
+ import type { ClientStorage } from "../storage/client-storage";
13
+ import {
14
+ type OnProgress,
15
+ type OnShouldRetry,
16
+ shouldRetry,
17
+ uploadChunk,
18
+ } from "./chunk-upload";
19
+ import type { UploadMetrics } from "./upload-metrics";
20
+ import {
21
+ removeFromClientStorage,
22
+ saveUploadInClientStorage,
23
+ } from "./upload-storage";
24
+ import { encodeMetadata, inStatusCategory } from "./upload-utils";
25
+
26
+ export type Callbacks = {
27
+ onProgress?: OnProgress;
28
+ onChunkComplete?: (
29
+ chunkSize: number,
30
+ bytesAccepted: number,
31
+ bytesTotal: number | null,
32
+ ) => void;
33
+ onSuccess?: (payload: UploadFile) => void;
34
+ onError?: (error: Error | UploadistaError) => void;
35
+ onStart?: (file: { uploadId: string; size: number | null }) => void;
36
+ onJobStart?: (jobId: string) => void;
37
+ onShouldRetry?: OnShouldRetry;
38
+ };
39
+
40
+ export type SingleUploadResult = {
41
+ uploadIdStorageKey: string | undefined;
42
+ uploadId: string;
43
+ offset: number;
44
+ };
45
+
46
+ /**
47
+ * Start uploading the file using PATCH requests. The file will be divided
48
+ * into chunks as specified in the chunkSize option. During the upload
49
+ * the onProgress event handler may be invoked multiple times.
50
+ */
51
+ export async function performUpload({
52
+ uploadId,
53
+ offset,
54
+ source,
55
+ uploadLengthDeferred,
56
+ retryAttempt = 0,
57
+ abortController,
58
+ retryDelays,
59
+ smartChunker,
60
+ uploadistaApi,
61
+ logger,
62
+ smartChunking,
63
+ metrics,
64
+ platformService,
65
+ onRetry,
66
+ ...callbacks
67
+ }: {
68
+ uploadId: string;
69
+ offset: number;
70
+ retryAttempt?: number;
71
+ source: FileSource;
72
+ abortController: AbortControllerLike;
73
+ uploadLengthDeferred: boolean | undefined;
74
+ retryDelays: number[] | undefined;
75
+ smartChunker: SmartChunker;
76
+ uploadistaApi: UploadistaApi;
77
+ logger: Logger;
78
+ smartChunking?: SmartChunkerConfig;
79
+ metrics: UploadMetrics;
80
+ platformService: PlatformService;
81
+ onRetry?: (timeout: Timeout) => void;
82
+ } & Callbacks): Promise<void> {
83
+ let offsetBeforeRetry = offset;
84
+ let currentOffset = offset;
85
+
86
+ try {
87
+ const res = await uploadChunk({
88
+ uploadId,
89
+ source,
90
+ offset,
91
+ uploadLengthDeferred,
92
+ onProgress: callbacks.onProgress,
93
+ abortController,
94
+ smartChunker,
95
+ uploadistaApi,
96
+ logger,
97
+ });
98
+
99
+ if (!inStatusCategory(res.status, 200) || res.upload == null) {
100
+ throw new UploadistaError({
101
+ name: "NETWORK_UNEXPECTED_RESPONSE",
102
+ message: "Unexpected response while uploading chunk",
103
+ });
104
+ }
105
+
106
+ currentOffset = res.upload.offset;
107
+
108
+ callbacks.onProgress?.(uploadId, currentOffset, res.upload.size ?? 0);
109
+ callbacks.onChunkComplete?.(
110
+ currentOffset - offset,
111
+ offset,
112
+ res.upload?.size ?? 0,
113
+ );
114
+
115
+ // Record detailed chunk metrics
116
+ if (smartChunking?.enabled !== false) {
117
+ const chunkIndex = Math.floor(offset / (currentOffset - offset || 1));
118
+ const chunkSize = currentOffset - offset;
119
+ const chunkDuration = Date.now() - (Date.now() - 100); // Approximate, real timing is in uploadChunk
120
+ const lastDecision = smartChunker.getLastDecision();
121
+
122
+ metrics.recordChunk({
123
+ chunkIndex,
124
+ size: chunkSize,
125
+ duration: chunkDuration,
126
+ speed: chunkSize / (chunkDuration / 1000),
127
+ success: true,
128
+ retryCount: retryAttempt,
129
+ networkCondition: lastDecision?.networkCondition?.type,
130
+ chunkingStrategy: lastDecision?.strategy,
131
+ });
132
+
133
+ // Update smart chunker with connection metrics for pooling optimization
134
+ const connectionMetrics = uploadistaApi.getConnectionMetrics();
135
+ smartChunker.updateConnectionMetrics(connectionMetrics);
136
+ }
137
+
138
+ if (currentOffset >= (source.size ?? 0)) {
139
+ if (source) source.close();
140
+
141
+ // Complete metrics session
142
+ if (smartChunking?.enabled !== false) {
143
+ const sessionMetrics = metrics.endSession();
144
+ if (sessionMetrics) {
145
+ logger.log(
146
+ `Upload completed: ${sessionMetrics.totalSize} bytes in ${sessionMetrics.totalDuration}ms, avg speed: ${Math.round(sessionMetrics.averageSpeed / 1024)}KB/s`,
147
+ );
148
+ }
149
+ }
150
+
151
+ callbacks.onSuccess?.(res.upload);
152
+ return;
153
+ }
154
+
155
+ await performUpload({
156
+ uploadId,
157
+ offset: currentOffset,
158
+ source,
159
+ uploadLengthDeferred,
160
+ retryDelays,
161
+ smartChunker,
162
+ platformService,
163
+ uploadistaApi,
164
+ logger,
165
+ smartChunking,
166
+ metrics,
167
+ onRetry,
168
+ abortController,
169
+ ...callbacks,
170
+ });
171
+ } catch (err) {
172
+ // Check if we should retry, when enabled, before sending the error to the user.
173
+ if (retryDelays != null) {
174
+ // We will reset the attempt counter if
175
+ // - we were already able to connect to the server (offset != null) and
176
+ // - we were able to upload a small chunk of data to the server
177
+ const shouldResetDelays =
178
+ offset != null && currentOffset > offsetBeforeRetry;
179
+ if (shouldResetDelays) {
180
+ retryAttempt = 0;
181
+ }
182
+
183
+ const castedErr = !(err instanceof UploadistaError)
184
+ ? new UploadistaError({
185
+ name: "NETWORK_ERROR",
186
+ message: "Network error",
187
+ cause: err as Error,
188
+ })
189
+ : err;
190
+
191
+ if (
192
+ shouldRetry(
193
+ platformService,
194
+ castedErr,
195
+ retryAttempt,
196
+ retryDelays,
197
+ callbacks.onShouldRetry,
198
+ )
199
+ ) {
200
+ const delay = retryDelays[retryAttempt];
201
+
202
+ offsetBeforeRetry = offset;
203
+
204
+ const timeout = platformService.setTimeout(async () => {
205
+ await performUpload({
206
+ uploadId,
207
+ offset,
208
+ source,
209
+ retryAttempt: retryAttempt + 1,
210
+ uploadLengthDeferred,
211
+ retryDelays,
212
+ smartChunker,
213
+ platformService,
214
+ uploadistaApi,
215
+ logger,
216
+ smartChunking,
217
+ metrics,
218
+ onRetry,
219
+ abortController,
220
+ ...callbacks,
221
+ });
222
+ }, delay);
223
+ onRetry?.(timeout);
224
+ } else {
225
+ throw new UploadistaError({
226
+ name: "UPLOAD_CHUNK_FAILED",
227
+ message: `failed to upload chunk for ${uploadId} at offset ${offset}`,
228
+ cause: err as Error,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Create a new upload using the creation extension by sending a POST
237
+ * request to the endpoint. After successful creation the file will be
238
+ * uploaded
239
+ */
240
+ export async function createUpload({
241
+ fingerprint,
242
+ storageId,
243
+ source,
244
+ uploadLengthDeferred,
245
+ metadata,
246
+ uploadistaApi,
247
+ logger,
248
+ checksumService,
249
+ clientStorage,
250
+ generateId,
251
+ storeFingerprintForResuming,
252
+ openWebSocket,
253
+ closeWebSocket,
254
+ computeChecksum = true,
255
+ checksumAlgorithm = "sha256",
256
+ platformService,
257
+ ...callbacks
258
+ }: {
259
+ fingerprint: string;
260
+ storageId: string;
261
+ source: FileSource;
262
+ uploadLengthDeferred: boolean | undefined;
263
+ metadata: Record<string, string>;
264
+ uploadistaApi: UploadistaApi;
265
+ logger: Logger;
266
+ clientStorage: ClientStorage;
267
+ generateId: IdGenerationService;
268
+ storeFingerprintForResuming: boolean;
269
+ openWebSocket: (uploadId: string) => WebSocketLike;
270
+ closeWebSocket: (uploadId: string) => void;
271
+ checksumService: ChecksumService;
272
+ computeChecksum?: boolean;
273
+ checksumAlgorithm?: string;
274
+ platformService: PlatformService;
275
+ } & Callbacks): Promise<SingleUploadResult | undefined> {
276
+ if (!uploadLengthDeferred && source.size == null) {
277
+ const error = new UploadistaError({
278
+ name: "UPLOAD_SIZE_NOT_SPECIFIED",
279
+ message: "expected size to be set",
280
+ });
281
+ callbacks.onError?.(error);
282
+ throw error;
283
+ }
284
+
285
+ // Compute checksum if enabled and file is a File object
286
+ let checksum: string | undefined;
287
+ if (computeChecksum && platformService.isFileLike(source.input)) {
288
+ try {
289
+ logger.log("Computing file checksum...");
290
+ checksum = await checksumService.computeChecksum(
291
+ new Uint8Array(source.input as any),
292
+ );
293
+ logger.log(`Checksum computed: ${checksum}`);
294
+ } catch (error) {
295
+ logger.log(
296
+ `Warning: Failed to compute checksum: ${error instanceof Error ? error.message : "Unknown error"}`,
297
+ );
298
+ // Continue without checksum if computation fails
299
+ }
300
+ }
301
+
302
+ const createUploadData: InputFile = {
303
+ uploadLengthDeferred,
304
+ storageId,
305
+ size: source.size ?? 0,
306
+ metadata: metadata ? encodeMetadata(metadata) : undefined,
307
+ fileName: source.name ?? undefined,
308
+ type: source.type ?? "",
309
+ lastModified: source.lastModified ?? undefined,
310
+ checksum,
311
+ checksumAlgorithm: checksum ? checksumAlgorithm : undefined,
312
+ };
313
+
314
+ const { upload, status } = await uploadistaApi.createUpload(createUploadData);
315
+
316
+ if (!inStatusCategory(status, 200) || upload == null) {
317
+ const error = new UploadistaError({
318
+ name: "NETWORK_UNEXPECTED_RESPONSE",
319
+ message: "Unexpected response while creating upload",
320
+ });
321
+ callbacks.onError?.(error);
322
+ throw error;
323
+ }
324
+
325
+ logger.log(`Created upload ${upload.id}`);
326
+
327
+ openWebSocket(upload.id);
328
+
329
+ if (upload.size === 0) {
330
+ // Nothing to upload and file was successfully created
331
+ callbacks.onSuccess?.(upload);
332
+ if (source) source.close();
333
+ closeWebSocket(upload.id);
334
+ return;
335
+ }
336
+
337
+ const uploadIdStorageKey = await saveUploadInClientStorage({
338
+ clientStorage,
339
+ fingerprint,
340
+ size: upload.size ?? 0,
341
+ metadata: upload.metadata ?? {},
342
+ clientStorageKey: null,
343
+ storeFingerprintForResuming,
344
+ generateId,
345
+ });
346
+
347
+ callbacks.onStart?.({
348
+ uploadId: upload.id,
349
+ size: upload.size ?? null,
350
+ });
351
+
352
+ return {
353
+ uploadIdStorageKey,
354
+ uploadId: upload.id,
355
+ offset: upload.offset,
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Try to resume an existing upload. First a HEAD request will be sent
361
+ * to retrieve the offset. If the request fails a new upload will be
362
+ * created. In the case of a successful response the file will be uploaded.
363
+ */
364
+ export async function resumeUpload({
365
+ uploadId,
366
+ storageId,
367
+ uploadIdStorageKey,
368
+ fingerprint,
369
+ source,
370
+ uploadLengthDeferred,
371
+ uploadistaApi,
372
+ logger,
373
+ platformService,
374
+ checksumService,
375
+ clientStorage,
376
+ generateId,
377
+ storeFingerprintForResuming,
378
+ openWebSocket,
379
+ ...callbacks
380
+ }: {
381
+ uploadId: string;
382
+ storageId: string;
383
+ uploadIdStorageKey: string;
384
+ fingerprint: string;
385
+ platformService: PlatformService;
386
+ source: FileSource;
387
+ uploadLengthDeferred: boolean | undefined;
388
+ uploadistaApi: UploadistaApi;
389
+ checksumService: ChecksumService;
390
+ logger: Logger;
391
+ clientStorage: ClientStorage;
392
+ generateId: IdGenerationService;
393
+ storeFingerprintForResuming: boolean;
394
+ openWebSocket: (uploadId: string) => WebSocketLike;
395
+ } & Callbacks): Promise<SingleUploadResult | undefined> {
396
+ const res = await uploadistaApi.getUpload(uploadId);
397
+ const status = res.status;
398
+
399
+ if (!inStatusCategory(status, 200)) {
400
+ // If the upload is locked (indicated by the 423 Locked status code), we
401
+ // emit an error instead of directly starting a new upload. This way the
402
+ // retry logic can catch the error and will retry the upload. An upload
403
+ // is usually locked for a short period of time and will be available
404
+ // afterwards.
405
+ if (status === 423) {
406
+ const error = new UploadistaError({
407
+ name: "UPLOAD_LOCKED",
408
+ message: "upload is currently locked; retry later",
409
+ });
410
+ callbacks.onError?.(error);
411
+ throw error;
412
+ }
413
+
414
+ if (inStatusCategory(status, 400)) {
415
+ // Remove stored fingerprint and corresponding endpoint,
416
+ // on client errors since the file can not be found
417
+ await removeFromClientStorage(clientStorage, uploadIdStorageKey);
418
+ }
419
+
420
+ // Try to create a new upload
421
+ return await createUpload({
422
+ platformService,
423
+ fingerprint,
424
+ storageId,
425
+ source,
426
+ uploadLengthDeferred,
427
+ metadata: {},
428
+ uploadistaApi,
429
+ logger,
430
+ checksumService,
431
+ clientStorage,
432
+ generateId,
433
+ storeFingerprintForResuming,
434
+ openWebSocket,
435
+ closeWebSocket: () => {}, // Placeholder, will be provided by caller
436
+ ...callbacks,
437
+ });
438
+ }
439
+
440
+ const upload = res.upload;
441
+ if (upload == null) {
442
+ const error = new UploadistaError({
443
+ name: "NETWORK_UNEXPECTED_RESPONSE",
444
+ message: "Unexpected response while resuming upload",
445
+ });
446
+ callbacks.onError?.(error);
447
+ throw error;
448
+ }
449
+
450
+ await saveUploadInClientStorage({
451
+ clientStorage,
452
+ fingerprint,
453
+ size: upload.size ?? 0,
454
+ metadata: upload.metadata ?? {},
455
+ clientStorageKey: uploadIdStorageKey,
456
+ storeFingerprintForResuming,
457
+ generateId,
458
+ });
459
+
460
+ // Upload has already been completed and we do not need to send additional
461
+ // data to the server
462
+ if (upload.offset === upload.size) {
463
+ return undefined;
464
+ }
465
+
466
+ openWebSocket(upload.id);
467
+
468
+ return {
469
+ uploadId,
470
+ uploadIdStorageKey,
471
+ offset: upload.offset,
472
+ };
473
+ }
474
+
475
+ /**
476
+ * Initiate the uploading procedure for a non-parallel upload. Here the entire file is
477
+ * uploaded in a sequential matter.
478
+ */
479
+ export async function startSingleUpload({
480
+ source,
481
+ uploadId,
482
+ uploadIdStorageKey,
483
+ storageId,
484
+ fingerprint,
485
+ platformService,
486
+ uploadLengthDeferred,
487
+ uploadistaApi,
488
+ checksumService,
489
+ logger,
490
+ clientStorage,
491
+ generateId,
492
+ storeFingerprintForResuming,
493
+ openWebSocket,
494
+ closeWebSocket,
495
+ ...callbacks
496
+ }: {
497
+ source: FileSource;
498
+ uploadId: string | null;
499
+ uploadIdStorageKey: string | null;
500
+ storageId: string;
501
+ fingerprint: string;
502
+ platformService: PlatformService;
503
+ uploadLengthDeferred: boolean | undefined;
504
+ uploadistaApi: UploadistaApi;
505
+ checksumService: ChecksumService;
506
+ logger: Logger;
507
+ clientStorage: ClientStorage;
508
+ generateId: IdGenerationService;
509
+ storeFingerprintForResuming: boolean;
510
+ openWebSocket: (uploadId: string) => WebSocketLike;
511
+ closeWebSocket: (uploadId: string) => void;
512
+ } & Callbacks): Promise<SingleUploadResult | undefined> {
513
+ // The upload had been started previously and we should reuse this URL.
514
+ if (uploadId != null && uploadIdStorageKey != null) {
515
+ logger.log(`Resuming upload from previous id: ${uploadId}`);
516
+ return await resumeUpload({
517
+ uploadId,
518
+ uploadIdStorageKey,
519
+ storageId,
520
+ fingerprint,
521
+ source,
522
+ checksumService,
523
+ uploadLengthDeferred,
524
+ uploadistaApi,
525
+ logger,
526
+ platformService,
527
+ clientStorage,
528
+ generateId,
529
+ storeFingerprintForResuming,
530
+ openWebSocket,
531
+ ...callbacks,
532
+ });
533
+ }
534
+
535
+ // An upload has not started for the file yet, so we start a new one
536
+ logger.log("Creating a new upload");
537
+ return await createUpload({
538
+ fingerprint,
539
+ storageId,
540
+ source,
541
+ uploadLengthDeferred,
542
+ metadata: {},
543
+ uploadistaApi,
544
+ logger,
545
+ checksumService,
546
+ platformService,
547
+ clientStorage,
548
+ generateId,
549
+ storeFingerprintForResuming,
550
+ openWebSocket,
551
+ closeWebSocket,
552
+ ...callbacks,
553
+ });
554
+ }
@@ -0,0 +1,106 @@
1
+ import type { UploadistaApi } from "../client/uploadista-api";
2
+ import { UploadistaError } from "../error";
3
+ import type { AbortControllerLike } from "../services/abort-controller-service";
4
+ import {
5
+ type PlatformService,
6
+ type Timeout,
7
+ wait,
8
+ } from "../services/platform-service";
9
+ import type { ClientStorage } from "../storage/client-storage";
10
+ import { shouldRetry } from "./chunk-upload";
11
+ import { removeFromClientStorage } from "./upload-storage";
12
+
13
+ /**
14
+ * Use the Termination extension to delete an upload from the server by sending a DELETE
15
+ * request to the specified upload URL. This is only possible if the server supports the
16
+ * Termination extension. If the `retryDelays` property is set, the method will
17
+ * also retry if an error occurs.
18
+ */
19
+ export async function terminate(
20
+ uploadId: string,
21
+ uploadistaApi: UploadistaApi,
22
+ platformService: PlatformService,
23
+ retryDelays: number[] | undefined,
24
+ retryAttempt = 0,
25
+ ): Promise<void> {
26
+ try {
27
+ const res = await uploadistaApi.deleteUpload(uploadId);
28
+ // A 204 response indicates a successful request
29
+ if (res.status === 204) {
30
+ return;
31
+ }
32
+
33
+ throw new UploadistaError({
34
+ name: "NETWORK_UNEXPECTED_RESPONSE",
35
+ message: "Unexpected response while terminating upload",
36
+ });
37
+ } catch (err) {
38
+ const error = err as UploadistaError;
39
+
40
+ if (!shouldRetry(platformService, error, retryAttempt, retryDelays)) {
41
+ throw err;
42
+ }
43
+
44
+ // Instead of keeping track of the retry attempts, we remove the first element from the delays
45
+ // array. If the array is empty, all retry attempts are used up and we will bubble up the error.
46
+ // We recursively call the terminate function will removing elements from the retryDelays array.
47
+ const delay = retryDelays?.[retryAttempt] ?? 0;
48
+
49
+ await wait(platformService, delay);
50
+
51
+ return await terminate(
52
+ uploadId,
53
+ uploadistaApi,
54
+ platformService,
55
+ retryDelays,
56
+ retryAttempt + 1,
57
+ );
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Abort any running request and stop the current upload. After abort is called, no event
63
+ * handler will be invoked anymore. You can use the `start` method to resume the upload
64
+ * again.
65
+ * If `shouldTerminate` is true, the `terminate` function will be called to remove the
66
+ * current upload from the server.
67
+ */
68
+ export async function abort({
69
+ uploadId,
70
+ uploadIdStorageKey,
71
+ retryTimeout,
72
+ shouldTerminate,
73
+ abortController,
74
+ uploadistaApi,
75
+ platformService,
76
+ retryDelays,
77
+ clientStorage,
78
+ }: {
79
+ uploadId: string;
80
+ uploadIdStorageKey: string | undefined;
81
+ retryTimeout: Timeout | null;
82
+ shouldTerminate: boolean;
83
+ abortController: AbortControllerLike;
84
+ uploadistaApi: UploadistaApi;
85
+ platformService: PlatformService;
86
+ retryDelays?: number[];
87
+ clientStorage: ClientStorage;
88
+ }): Promise<void> {
89
+ // Stop any current running request.
90
+ abortController.abort();
91
+
92
+ // Stop any timeout used for initiating a retry.
93
+ if (retryTimeout != null) {
94
+ platformService.clearTimeout(retryTimeout);
95
+ }
96
+
97
+ if (!shouldTerminate || uploadId == null) {
98
+ return;
99
+ }
100
+
101
+ await terminate(uploadId, uploadistaApi, platformService, retryDelays);
102
+
103
+ if (uploadIdStorageKey != null) {
104
+ return removeFromClientStorage(clientStorage, uploadIdStorageKey);
105
+ }
106
+ }