@knime/utils 1.0.17 → 1.1.1
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.
- package/CHANGELOG.md +17 -0
- package/package.json +5 -4
- package/src/index.ts +13 -0
- package/src/promise/createAbortablePromise.ts +56 -0
- package/src/promise/index.ts +2 -0
- package/src/promise/retryPromise.ts +36 -0
- package/src/uploadManager/index.ts +363 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @knime/utils
|
|
2
2
|
|
|
3
|
+
## 1.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @knime/styles@1.1.1
|
|
9
|
+
|
|
10
|
+
## 1.1.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- a8eebec: - Add `promise` utils
|
|
15
|
+
- `retryPromise` util function to retry async calls
|
|
16
|
+
- `createAbortablePromise` util function to leverage AbortControllers and
|
|
17
|
+
add abort behavior to an async call
|
|
18
|
+
- Add uploadManager util. Multi-file and multi-part upload handler
|
|
19
|
+
|
|
3
20
|
## 1.0.17
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knime/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Internal utility functions and constants shared across @knime packages.",
|
|
5
5
|
"homepage": "https://knime.github.io/webapps-common/",
|
|
6
6
|
"license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"consola": "3.2.3",
|
|
11
11
|
"date-fns": "2.30.0",
|
|
12
12
|
"date-fns-tz": "2.0.0",
|
|
13
|
-
"@knime/styles": "1.1.
|
|
13
|
+
"@knime/styles": "1.1.1"
|
|
14
14
|
},
|
|
15
15
|
"main": "src/index.ts",
|
|
16
16
|
"module": "src/index.ts",
|
|
17
17
|
"files": [
|
|
18
18
|
"src",
|
|
19
|
-
"!src
|
|
19
|
+
"!src/**/__tests__/*",
|
|
20
20
|
"test-utils",
|
|
21
21
|
"CHANGELOG.md"
|
|
22
22
|
],
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"flush-promises": "^1.0.2"
|
|
36
|
+
"flush-promises": "^1.0.2",
|
|
37
|
+
"mock-xmlhttprequest": "^8.3.0"
|
|
37
38
|
},
|
|
38
39
|
"scripts": {
|
|
39
40
|
"test:unit": "TZ='Europe/Berlin' vitest"
|
package/src/index.ts
CHANGED
|
@@ -22,10 +22,16 @@ import getWrappedAroundIndex from "./getWrappedAroundIndex";
|
|
|
22
22
|
import getLocalTimeZone from "./localTimezone";
|
|
23
23
|
import * as navigatorUtils from "./navigator";
|
|
24
24
|
import numIntegerDigits from "./numIntegerDigits";
|
|
25
|
+
import * as promise from "./promise";
|
|
25
26
|
import sleep from "./sleep";
|
|
26
27
|
import truncateString from "./truncateString";
|
|
27
28
|
import updateDate from "./updateDate";
|
|
28
29
|
import updateTime from "./updateTime";
|
|
30
|
+
import type {
|
|
31
|
+
UploaderManagerConfig as _UploadManagerConfig,
|
|
32
|
+
UploadStateName as _UploadStateName,
|
|
33
|
+
} from "./uploadManager";
|
|
34
|
+
import * as uploadManager from "./uploadManager";
|
|
29
35
|
|
|
30
36
|
const hotkeys = { formatHotkeys, formatHotkey, getDefaultSeparator };
|
|
31
37
|
|
|
@@ -34,7 +40,14 @@ export namespace HotkeysNS {
|
|
|
34
40
|
export type KnownHotkey = _KnownHotkey;
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
export namespace UploadManagerNS {
|
|
44
|
+
export type UploaderConfig = _UploadManagerConfig;
|
|
45
|
+
export type UploadStateName = _UploadStateName;
|
|
46
|
+
}
|
|
47
|
+
|
|
37
48
|
export {
|
|
49
|
+
uploadManager,
|
|
50
|
+
promise,
|
|
38
51
|
capitalize,
|
|
39
52
|
caseFormatter,
|
|
40
53
|
debounce,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class AbortError extends Error {}
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper to create an abortable promise. Returns an AbortController and a Function
|
|
5
|
+
* that you can pass your async handler to. Whenever the AbortController signal is
|
|
6
|
+
* aborted then the async handler's promise will reject with an AbortError
|
|
7
|
+
*
|
|
8
|
+
* Example:
|
|
9
|
+
* ```
|
|
10
|
+
* import { abortUtils } from "@knime/utils"
|
|
11
|
+
* const myFetchCall = async (params...) => {...};
|
|
12
|
+
*
|
|
13
|
+
* try {
|
|
14
|
+
* const { runAbortablePromise, abortController } = createAbortablePromise(() =>
|
|
15
|
+
* myFetchCall(params...),
|
|
16
|
+
* );
|
|
17
|
+
* } catch (e) {
|
|
18
|
+
* if (e instanceof abortUtils.AbortError) {
|
|
19
|
+
* console.info('ignore due to abort');
|
|
20
|
+
* return;
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // somewhere else (while request is in-flight)
|
|
25
|
+
* abortController.abort(new abortUtils.AbortError('reason here'))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const createAbortablePromise = () => {
|
|
29
|
+
const abortController: AbortController = new AbortController();
|
|
30
|
+
|
|
31
|
+
const runAbortablePromise = <T = unknown>(request: () => Promise<T>) => {
|
|
32
|
+
return new Promise<T>((resolve, reject) => {
|
|
33
|
+
// abort logic
|
|
34
|
+
const abortListener = ({ target }: Event) => {
|
|
35
|
+
abortController.signal.removeEventListener("abort", abortListener);
|
|
36
|
+
const reason = (target as AbortSignal).reason;
|
|
37
|
+
reject(reason instanceof AbortError ? reason : new AbortError(reason));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// the actual call
|
|
41
|
+
request()
|
|
42
|
+
.then(resolve)
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
if (error instanceof AbortError) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
reject(error instanceof Error ? error : new Error(error));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
abortController.signal.addEventListener("abort", abortListener);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return { abortController, runAbortablePromise };
|
|
56
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* eslint-disable func-style */
|
|
2
|
+
import sleep from "../sleep";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_RETRY_DELAY_MS = 0;
|
|
5
|
+
const DEFAULT_RETRY_COUNT = 5;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Helper to add retry capability to an async function
|
|
9
|
+
* @param fn async function
|
|
10
|
+
* @param retryCount max number of retries. defaults to 5
|
|
11
|
+
* @param retryDelayMS delay in-between retries in millisecs. defaults to 0
|
|
12
|
+
* @param excludeError predicate fn to exclude specific errors from causing a retry
|
|
13
|
+
*/
|
|
14
|
+
export async function retryPromise<T>(
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
retryCount = DEFAULT_RETRY_COUNT,
|
|
17
|
+
retryDelayMS = DEFAULT_RETRY_DELAY_MS,
|
|
18
|
+
excludeError?: (error: Error) => boolean,
|
|
19
|
+
) {
|
|
20
|
+
try {
|
|
21
|
+
return await fn();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (excludeError?.(error as Error)) {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (retryCount > 0) {
|
|
28
|
+
await sleep(retryDelayMS);
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line no-return-await
|
|
31
|
+
return await retryPromise(fn, retryCount - 1, retryDelayMS);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { AbortError, createAbortablePromise, retryPromise } from "../promise";
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line no-magic-numbers
|
|
4
|
+
const CHUNK_SIZE = 50 * 1024 * 1024; // 50 MB per chunk
|
|
5
|
+
|
|
6
|
+
const DEFAULT_RETRY_COUNT = 5;
|
|
7
|
+
const DEFAULT_RETRY_DELAY_MS = 50;
|
|
8
|
+
|
|
9
|
+
export type UploadStateName =
|
|
10
|
+
| "idle"
|
|
11
|
+
| "inprogress"
|
|
12
|
+
| "cancelled"
|
|
13
|
+
| "failed"
|
|
14
|
+
| "complete";
|
|
15
|
+
|
|
16
|
+
export type PartUploadResponse = {
|
|
17
|
+
partId: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const uploadChunkWithProgress = (params: {
|
|
21
|
+
method: string;
|
|
22
|
+
url: string;
|
|
23
|
+
chunk: Blob;
|
|
24
|
+
chunkIndex: number;
|
|
25
|
+
onProgress: (progress: number) => void;
|
|
26
|
+
abortSignal: AbortSignal;
|
|
27
|
+
}) => {
|
|
28
|
+
const { method, url, chunk, chunkIndex, onProgress, abortSignal } = params;
|
|
29
|
+
|
|
30
|
+
const OK = 200;
|
|
31
|
+
|
|
32
|
+
return new Promise<{ partId: string }>((resolve, reject) => {
|
|
33
|
+
const xhr = new XMLHttpRequest();
|
|
34
|
+
|
|
35
|
+
xhr.open(method, url, true);
|
|
36
|
+
|
|
37
|
+
abortSignal.addEventListener("abort", () => xhr.abort());
|
|
38
|
+
|
|
39
|
+
// Track the upload progress of the current chunk
|
|
40
|
+
xhr.upload.onprogress = (event) => {
|
|
41
|
+
if (event.lengthComputable) {
|
|
42
|
+
onProgress(event.loaded);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
xhr.onload = () => {
|
|
47
|
+
if (xhr.status === OK) {
|
|
48
|
+
const etag = xhr.getResponseHeader("ETag");
|
|
49
|
+
|
|
50
|
+
if (!etag) {
|
|
51
|
+
reject(new Error("Invalid part upload response"));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// remove possible doublequotes (") around the etag and/or weak identifier (if either is present)
|
|
56
|
+
// https://datatracker.ietf.org/doc/html/rfc7232#section-2.3
|
|
57
|
+
resolve({ partId: etag.replace(/^(?:W\/)?"|"$/g, "") });
|
|
58
|
+
} else {
|
|
59
|
+
reject(new Error(`Failed to upload chunk ${chunkIndex}`));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
xhr.onerror = () =>
|
|
64
|
+
reject(new Error(`Failed to upload chunk ${chunkIndex}`));
|
|
65
|
+
|
|
66
|
+
xhr.send(chunk);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type UploaderManagerConfig = {
|
|
71
|
+
/**
|
|
72
|
+
* Function to resolve the metadata for a request to upload a file part
|
|
73
|
+
* @param uploadId
|
|
74
|
+
* @param partNumber
|
|
75
|
+
* @returns
|
|
76
|
+
*/
|
|
77
|
+
resolveFilePartUploadURL: (
|
|
78
|
+
uploadId: string,
|
|
79
|
+
partNumber: number,
|
|
80
|
+
) => Promise<{ method: string; url: string }>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Callback that gets triggered when the whole file is uploaded.
|
|
84
|
+
* @param
|
|
85
|
+
* @returns
|
|
86
|
+
*/
|
|
87
|
+
onFileUploadComplete?: (payload: {
|
|
88
|
+
/**
|
|
89
|
+
* Identifier of the upload for this file (must be unique)
|
|
90
|
+
*/
|
|
91
|
+
uploadId: string;
|
|
92
|
+
/**
|
|
93
|
+
* Ids of all the uploaded file parts
|
|
94
|
+
*/
|
|
95
|
+
filePartIds: Record<number, string>;
|
|
96
|
+
}) => void;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Callback that gets triggered when an upload has failed.
|
|
100
|
+
* @param uploadId
|
|
101
|
+
* @param error the error that cause the failure if it exists
|
|
102
|
+
* @returns
|
|
103
|
+
*/
|
|
104
|
+
onFileUploadFailed?: (uploadId: string, error: Error) => void;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Callback that gets triggered when the progress of a file part upload
|
|
108
|
+
* for a specific upload id is updated
|
|
109
|
+
* @param uploadId
|
|
110
|
+
* @param progress
|
|
111
|
+
* @returns
|
|
112
|
+
*/
|
|
113
|
+
onProgress?: (uploadId: string, progress: number) => void;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type UploadState = {
|
|
117
|
+
uploadId: string;
|
|
118
|
+
file: File;
|
|
119
|
+
completedChunkCount: number;
|
|
120
|
+
uploadedByteCount: number;
|
|
121
|
+
state: UploadStateName;
|
|
122
|
+
abortController: AbortController;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates and returns an `uploadManager`, a comprehensive multi-file, multi-part upload handler.
|
|
127
|
+
*
|
|
128
|
+
* The `uploadManager` offers functionality for handling file uploads in parts and provides
|
|
129
|
+
* several built-in mechanisms to ensure the process is reliable and adaptable.
|
|
130
|
+
*
|
|
131
|
+
* Key features include:
|
|
132
|
+
*
|
|
133
|
+
* - **Chunked File Uploads**: Splits provided files into manageable chunks and initiates the upload
|
|
134
|
+
* for each part using the `resolveFilePartUploadURL` function provided in the configuration.
|
|
135
|
+
*
|
|
136
|
+
* - **Retry Mechanism**: Automatically retries failed upload attempts up to a maximum of 5 times
|
|
137
|
+
* per failed request.
|
|
138
|
+
*
|
|
139
|
+
* - **Upload Cancellation**: Allows cancellation of ongoing uploads, giving users control over the process.
|
|
140
|
+
*
|
|
141
|
+
* - **Progress Tracking**: Monitors the progress of each file part upload and triggers the `onProgress`
|
|
142
|
+
* callback to provide updates.
|
|
143
|
+
*
|
|
144
|
+
* - **Error Handling**: The returned `uploadFiles` function does not throw errors directly. Instead,
|
|
145
|
+
* it manages retries and stops any failed uploads after the retry limit is reached, marking them as failed.
|
|
146
|
+
* This ensures that the promise it returns completes gracefully without uncaught exceptions.
|
|
147
|
+
*
|
|
148
|
+
*/
|
|
149
|
+
export const createUploadManager = (config: UploaderManagerConfig) => {
|
|
150
|
+
const {
|
|
151
|
+
resolveFilePartUploadURL: getUploadFilePartUrl,
|
|
152
|
+
onFileUploadComplete,
|
|
153
|
+
onFileUploadFailed,
|
|
154
|
+
} = config;
|
|
155
|
+
const uploadStateMap = new Map<string, UploadState>();
|
|
156
|
+
|
|
157
|
+
const completedUploads = new Set<string>();
|
|
158
|
+
const cancelledUploads = new Set<string>();
|
|
159
|
+
const failedUploads = new Set<string>();
|
|
160
|
+
|
|
161
|
+
const assertState = (uploadId: string, state: UploadStateName) =>
|
|
162
|
+
uploadStateMap.has(uploadId) &&
|
|
163
|
+
uploadStateMap.get(uploadId)!.state === state;
|
|
164
|
+
|
|
165
|
+
const getUploadState = (uploadId: string) => uploadStateMap.get(uploadId);
|
|
166
|
+
const setUploadState = (uploadId: string, state: Partial<UploadState>) => {
|
|
167
|
+
uploadStateMap.set(uploadId, {
|
|
168
|
+
...uploadStateMap.get(uploadId)!,
|
|
169
|
+
...state,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const isFailed = (uploadId: string) => assertState(uploadId, "failed");
|
|
174
|
+
const isCancelled = (uploadId: string) => assertState(uploadId, "cancelled");
|
|
175
|
+
|
|
176
|
+
const getTotalFileParts = (file: File) => Math.ceil(file.size / CHUNK_SIZE);
|
|
177
|
+
|
|
178
|
+
const abortInFlightUpload = (uploadId: string, reason: string) => {
|
|
179
|
+
const currentState = uploadStateMap.get(uploadId);
|
|
180
|
+
|
|
181
|
+
if (!currentState) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
currentState.abortController.abort(
|
|
186
|
+
new AbortError(
|
|
187
|
+
`Upload (${currentState.uploadId}) for file "${currentState.file.name}" aborted. Reason: "${reason}"`,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const cancel = (uploadId: string) => {
|
|
193
|
+
if (!uploadStateMap.has(uploadId)) {
|
|
194
|
+
consola.error("cancel:: Invalid upload id");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setUploadState(uploadId, { state: "cancelled" });
|
|
199
|
+
|
|
200
|
+
abortInFlightUpload(uploadId, "upload cancelled");
|
|
201
|
+
cancelledUploads.add(uploadId);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const setFailed = (uploadId: string, error: Error) => {
|
|
205
|
+
if (!uploadStateMap.has(uploadId)) {
|
|
206
|
+
consola.error("failed:: Invalid upload id");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
setUploadState(uploadId, { state: "failed" });
|
|
211
|
+
onFileUploadFailed?.(uploadId, error);
|
|
212
|
+
failedUploads.add(uploadId);
|
|
213
|
+
|
|
214
|
+
if (completedUploads.has(uploadId)) {
|
|
215
|
+
completedUploads.delete(uploadId);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const uploadFileFromChunkIndex = async (
|
|
220
|
+
uploadId: string,
|
|
221
|
+
file: File,
|
|
222
|
+
initialChunkIndex = 0,
|
|
223
|
+
) => {
|
|
224
|
+
const totalChunks = getTotalFileParts(file);
|
|
225
|
+
const totalBytes = file.size;
|
|
226
|
+
const completedPartIds: Record<number, string> = {};
|
|
227
|
+
|
|
228
|
+
const { abortController, runAbortablePromise } = createAbortablePromise();
|
|
229
|
+
|
|
230
|
+
uploadStateMap.set(uploadId, {
|
|
231
|
+
uploadId,
|
|
232
|
+
file,
|
|
233
|
+
state: "inprogress",
|
|
234
|
+
uploadedByteCount: uploadStateMap.get(uploadId)?.uploadedByteCount ?? 0,
|
|
235
|
+
completedChunkCount:
|
|
236
|
+
uploadStateMap.get(uploadId)?.completedChunkCount ?? 0,
|
|
237
|
+
abortController,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
let chunkIndex = initialChunkIndex;
|
|
241
|
+
let uploadedBytes = uploadStateMap.get(uploadId)?.uploadedByteCount;
|
|
242
|
+
|
|
243
|
+
while (chunkIndex < totalChunks) {
|
|
244
|
+
if (isCancelled(uploadId) || isFailed(uploadId)) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const start = chunkIndex * CHUNK_SIZE;
|
|
249
|
+
const end = Math.min(start + CHUNK_SIZE, file.size);
|
|
250
|
+
|
|
251
|
+
const chunk = file.slice(start, end);
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const { method, url } = await retryPromise(
|
|
255
|
+
() => getUploadFilePartUrl(uploadId, chunkIndex + 1),
|
|
256
|
+
DEFAULT_RETRY_COUNT,
|
|
257
|
+
DEFAULT_RETRY_DELAY_MS,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
consola.trace("File part upload url resolved", { method, url });
|
|
261
|
+
|
|
262
|
+
const onChunkUploadProgress = (uploadedChunkBytes: number) => {
|
|
263
|
+
uploadedBytes = uploadedChunkBytes + start;
|
|
264
|
+
setUploadState(uploadId, { uploadedByteCount: uploadedBytes });
|
|
265
|
+
|
|
266
|
+
const progress = Math.round((uploadedBytes / totalBytes) * 100);
|
|
267
|
+
|
|
268
|
+
config.onProgress?.(uploadId, progress);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const filePartResponse = await runAbortablePromise(() =>
|
|
272
|
+
retryPromise(
|
|
273
|
+
() =>
|
|
274
|
+
uploadChunkWithProgress({
|
|
275
|
+
method,
|
|
276
|
+
url,
|
|
277
|
+
chunk,
|
|
278
|
+
chunkIndex,
|
|
279
|
+
onProgress: onChunkUploadProgress,
|
|
280
|
+
abortSignal: abortController.signal,
|
|
281
|
+
}),
|
|
282
|
+
DEFAULT_RETRY_COUNT,
|
|
283
|
+
DEFAULT_RETRY_DELAY_MS,
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
consola.trace(
|
|
288
|
+
`${file.name}: Chunk ${chunkIndex + 1} of ${totalChunks} uploaded successfully`,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
completedPartIds[chunkIndex + 1] = filePartResponse.partId;
|
|
292
|
+
|
|
293
|
+
setUploadState(uploadId, {
|
|
294
|
+
completedChunkCount:
|
|
295
|
+
uploadStateMap.get(uploadId)!.completedChunkCount + 1,
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error instanceof AbortError) {
|
|
299
|
+
consola.info("Aborted::", error);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
setFailed(
|
|
304
|
+
uploadId,
|
|
305
|
+
error instanceof Error ? error : new Error(error as any),
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
chunkIndex++;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (isCancelled(uploadId) || isFailed(uploadId)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
setUploadState(uploadId, {
|
|
318
|
+
completedChunkCount: totalChunks,
|
|
319
|
+
state: "complete",
|
|
320
|
+
});
|
|
321
|
+
completedUploads.add(uploadId);
|
|
322
|
+
onFileUploadComplete?.({ uploadId, filePartIds: completedPartIds });
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const uploadFiles = (files: Array<{ uploadId: string; file: File }>) => {
|
|
326
|
+
const result: Array<Promise<any>> = [];
|
|
327
|
+
|
|
328
|
+
for (const { uploadId, file } of files) {
|
|
329
|
+
result.push(uploadFileFromChunkIndex(uploadId, file));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return Promise.allSettled(result).then(() => {
|
|
333
|
+
// state cleanup
|
|
334
|
+
const completed = [...completedUploads.values()];
|
|
335
|
+
const cancelled = [...cancelledUploads.values()];
|
|
336
|
+
const failed = [...failedUploads.values()];
|
|
337
|
+
|
|
338
|
+
[...completed, ...cancelled, ...failed].forEach((uploadId) =>
|
|
339
|
+
uploadStateMap.delete(uploadId),
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
completedUploads.clear();
|
|
343
|
+
cancelledUploads.clear();
|
|
344
|
+
failedUploads.clear();
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
completed,
|
|
348
|
+
cancelled,
|
|
349
|
+
failed,
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
uploadFiles,
|
|
356
|
+
|
|
357
|
+
getUploadState,
|
|
358
|
+
isFailed,
|
|
359
|
+
isCancelled,
|
|
360
|
+
cancel,
|
|
361
|
+
setFailed,
|
|
362
|
+
};
|
|
363
|
+
};
|