@knime/utils 1.1.1 → 1.1.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/fileExtensions.ts +68 -0
- package/src/index.ts +2 -0
- package/src/promise/retryPromise.ts +17 -7
- package/src/uploadManager/index.ts +23 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @knime/utils
|
|
2
2
|
|
|
3
|
+
## 1.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9fa8163: bugfix/improvements in uploads: retry only on server error, reset placeholder skeleton on prepare upload error
|
|
8
|
+
|
|
9
|
+
## 1.1.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 15a4479: add utilities for file extensions
|
|
14
|
+
|
|
3
15
|
## 1.1.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knime/utils",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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)",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const getFileExtension = (nameOrPath: string) => {
|
|
2
|
+
const basename = nameOrPath.split(/[\\/]/).pop();
|
|
3
|
+
const position = basename?.lastIndexOf(".") as number;
|
|
4
|
+
|
|
5
|
+
if (basename === "" || position < 1) {
|
|
6
|
+
return "";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return basename?.slice(position + 1);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Metadata for .knar files, which corresponds to exported KNIME Workflow Groups
|
|
14
|
+
* or Components
|
|
15
|
+
*/
|
|
16
|
+
const KNAR = Object.freeze({
|
|
17
|
+
extension: "knar",
|
|
18
|
+
mimeType: "application/vnd.knime.workflow-group+zip",
|
|
19
|
+
matches(file: File) {
|
|
20
|
+
const extension = getFileExtension(file.name);
|
|
21
|
+
return extension === this.extension;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Metadata for .knwf files, which corresponds to exported KNIME Workflows
|
|
27
|
+
*/
|
|
28
|
+
const KNWF = Object.freeze({
|
|
29
|
+
extension: "knwf",
|
|
30
|
+
mimeType: "application/vnd.knime.workflow+zip",
|
|
31
|
+
matches(file: File) {
|
|
32
|
+
const extension = getFileExtension(file.name);
|
|
33
|
+
return extension === this.extension;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const knimeFileFormats = { KNAR, KNWF };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets the mime type from a given file. Defaults to `application/octet-stream`
|
|
41
|
+
* if mimeType cannot be detected
|
|
42
|
+
* @param file File
|
|
43
|
+
* @returns mimeType
|
|
44
|
+
*/
|
|
45
|
+
export const getFileMimeType = (file: File): string => {
|
|
46
|
+
const DEFAULT_MIMETYPE = "application/octet-stream";
|
|
47
|
+
|
|
48
|
+
const extension = getFileExtension(file.name);
|
|
49
|
+
|
|
50
|
+
if (!extension) {
|
|
51
|
+
return DEFAULT_MIMETYPE;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (KNWF.matches(file)) {
|
|
55
|
+
return KNWF.mimeType;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (KNAR.matches(file)) {
|
|
59
|
+
return KNAR.mimeType;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// file has extension but unrecognizable mime type
|
|
63
|
+
if (!file.type) {
|
|
64
|
+
return DEFAULT_MIMETYPE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return file.type;
|
|
68
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable func-style */
|
|
2
2
|
import sleep from "../sleep";
|
|
3
3
|
|
|
4
|
-
const DEFAULT_RETRY_DELAY_MS = 0;
|
|
5
|
-
const DEFAULT_RETRY_COUNT = 5;
|
|
4
|
+
export const DEFAULT_RETRY_DELAY_MS = 0;
|
|
5
|
+
export const DEFAULT_RETRY_COUNT = 5;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Helper to add retry capability to an async function
|
|
@@ -11,12 +11,17 @@ const DEFAULT_RETRY_COUNT = 5;
|
|
|
11
11
|
* @param retryDelayMS delay in-between retries in millisecs. defaults to 0
|
|
12
12
|
* @param excludeError predicate fn to exclude specific errors from causing a retry
|
|
13
13
|
*/
|
|
14
|
-
export async function retryPromise<T>(
|
|
15
|
-
fn
|
|
14
|
+
export async function retryPromise<T>({
|
|
15
|
+
fn,
|
|
16
16
|
retryCount = DEFAULT_RETRY_COUNT,
|
|
17
17
|
retryDelayMS = DEFAULT_RETRY_DELAY_MS,
|
|
18
|
-
excludeError
|
|
19
|
-
|
|
18
|
+
excludeError,
|
|
19
|
+
}: {
|
|
20
|
+
fn: () => Promise<T>;
|
|
21
|
+
retryCount?: number;
|
|
22
|
+
retryDelayMS?: number;
|
|
23
|
+
excludeError?: (error: Error) => boolean;
|
|
24
|
+
}) {
|
|
20
25
|
try {
|
|
21
26
|
return await fn();
|
|
22
27
|
} catch (error) {
|
|
@@ -28,7 +33,12 @@ export async function retryPromise<T>(
|
|
|
28
33
|
await sleep(retryDelayMS);
|
|
29
34
|
|
|
30
35
|
// eslint-disable-next-line no-return-await
|
|
31
|
-
return await retryPromise(
|
|
36
|
+
return await retryPromise({
|
|
37
|
+
fn,
|
|
38
|
+
retryCount: retryCount - 1,
|
|
39
|
+
retryDelayMS,
|
|
40
|
+
excludeError,
|
|
41
|
+
});
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
throw error;
|
|
@@ -56,12 +56,22 @@ const uploadChunkWithProgress = (params: {
|
|
|
56
56
|
// https://datatracker.ietf.org/doc/html/rfc7232#section-2.3
|
|
57
57
|
resolve({ partId: etag.replace(/^(?:W\/)?"|"$/g, "") });
|
|
58
58
|
} else {
|
|
59
|
-
|
|
59
|
+
consola.error("Failed or unexpected XHR response", {
|
|
60
|
+
xhrStatus: xhr.status,
|
|
61
|
+
xhrResponse: xhr.response,
|
|
62
|
+
chunkIndex,
|
|
63
|
+
});
|
|
64
|
+
reject(new Error("Failed to upload file"));
|
|
60
65
|
}
|
|
61
66
|
};
|
|
62
67
|
|
|
63
|
-
xhr.onerror = () =>
|
|
64
|
-
|
|
68
|
+
xhr.onerror = (event) => {
|
|
69
|
+
consola.error("Failed to upload chunk", {
|
|
70
|
+
xhrErrorEvent: event,
|
|
71
|
+
chunkIndex,
|
|
72
|
+
});
|
|
73
|
+
reject(new Error("Failed to upload file"));
|
|
74
|
+
};
|
|
65
75
|
|
|
66
76
|
xhr.send(chunk);
|
|
67
77
|
});
|
|
@@ -251,11 +261,11 @@ export const createUploadManager = (config: UploaderManagerConfig) => {
|
|
|
251
261
|
const chunk = file.slice(start, end);
|
|
252
262
|
|
|
253
263
|
try {
|
|
254
|
-
const { method, url } = await retryPromise(
|
|
255
|
-
() => getUploadFilePartUrl(uploadId, chunkIndex + 1),
|
|
256
|
-
DEFAULT_RETRY_COUNT,
|
|
257
|
-
DEFAULT_RETRY_DELAY_MS,
|
|
258
|
-
);
|
|
264
|
+
const { method, url } = await retryPromise({
|
|
265
|
+
fn: () => getUploadFilePartUrl(uploadId, chunkIndex + 1),
|
|
266
|
+
retryCount: DEFAULT_RETRY_COUNT,
|
|
267
|
+
retryDelayMS: DEFAULT_RETRY_DELAY_MS,
|
|
268
|
+
});
|
|
259
269
|
|
|
260
270
|
consola.trace("File part upload url resolved", { method, url });
|
|
261
271
|
|
|
@@ -269,8 +279,8 @@ export const createUploadManager = (config: UploaderManagerConfig) => {
|
|
|
269
279
|
};
|
|
270
280
|
|
|
271
281
|
const filePartResponse = await runAbortablePromise(() =>
|
|
272
|
-
retryPromise(
|
|
273
|
-
() =>
|
|
282
|
+
retryPromise({
|
|
283
|
+
fn: () =>
|
|
274
284
|
uploadChunkWithProgress({
|
|
275
285
|
method,
|
|
276
286
|
url,
|
|
@@ -279,9 +289,9 @@ export const createUploadManager = (config: UploaderManagerConfig) => {
|
|
|
279
289
|
onProgress: onChunkUploadProgress,
|
|
280
290
|
abortSignal: abortController.signal,
|
|
281
291
|
}),
|
|
282
|
-
DEFAULT_RETRY_COUNT,
|
|
283
|
-
DEFAULT_RETRY_DELAY_MS,
|
|
284
|
-
),
|
|
292
|
+
retryCount: DEFAULT_RETRY_COUNT,
|
|
293
|
+
retryDelayMS: DEFAULT_RETRY_DELAY_MS,
|
|
294
|
+
}),
|
|
285
295
|
);
|
|
286
296
|
|
|
287
297
|
consola.trace(
|