@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 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.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
@@ -45,6 +45,8 @@ export namespace UploadManagerNS {
45
45
  export type UploadStateName = _UploadStateName;
46
46
  }
47
47
 
48
+ export * from "./fileExtensions";
49
+
48
50
  export {
49
51
  uploadManager,
50
52
  promise,
@@ -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: () => Promise<T>,
14
+ export async function retryPromise<T>({
15
+ fn,
16
16
  retryCount = DEFAULT_RETRY_COUNT,
17
17
  retryDelayMS = DEFAULT_RETRY_DELAY_MS,
18
- excludeError?: (error: Error) => boolean,
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(fn, retryCount - 1, retryDelayMS);
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
- reject(new Error(`Failed to upload chunk ${chunkIndex}`));
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
- reject(new Error(`Failed to upload chunk ${chunkIndex}`));
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(