@karpeleslab/klbfw 0.2.25 → 0.2.27

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 (3) hide show
  1. package/index.d.ts +25 -5
  2. package/package.json +1 -1
  3. package/upload.js +76 -12
package/index.d.ts CHANGED
@@ -61,6 +61,25 @@ interface RestResponse<T = any> {
61
61
  [key: string]: any;
62
62
  }
63
63
 
64
+ /**
65
+ * Context object for REST API calls.
66
+ * Keys are single characters representing different context dimensions.
67
+ */
68
+ interface Context {
69
+ /** Branch identifier */
70
+ b?: string;
71
+ /** Currency code (e.g., 'USD', 'EUR') */
72
+ c?: string;
73
+ /** Group identifier */
74
+ g?: string;
75
+ /** Language/locale code (e.g., 'en-US', 'ja-JP') */
76
+ l?: string;
77
+ /** Timezone identifier (e.g., 'Asia/Tokyo', 'America/New_York') */
78
+ t?: string;
79
+ /** User identifier */
80
+ u?: string;
81
+ }
82
+
64
83
  /** REST API error object (thrown on promise rejection) */
65
84
  interface RestError {
66
85
  /** Always 'error' for error responses */
@@ -172,7 +191,7 @@ interface Price extends PriceValue {
172
191
  tax_rate?: number;
173
192
  }
174
193
 
175
- declare function rest<T = any>(name: string, verb: string, params?: Record<string, any>, context?: Record<string, any>): Promise<RestResponse<T>>;
194
+ declare function rest<T = any>(name: string, verb: string, params?: Record<string, any>, context?: Context): Promise<RestResponse<T>>;
176
195
  declare function rest_get<T = any>(name: string, params?: Record<string, any>): Promise<RestResponse<T>>; // Backward compatibility
177
196
  declare function restGet<T = any>(name: string, params?: Record<string, any>): Promise<RestResponse<T>>;
178
197
 
@@ -218,7 +237,7 @@ interface SSESource {
218
237
  close(): void;
219
238
  }
220
239
 
221
- declare function restSSE(name: string, method?: string, params?: Record<string, any>, context?: Record<string, any>): SSESource;
240
+ declare function restSSE(name: string, method?: string, params?: Record<string, any>, context?: Context): SSESource;
222
241
 
223
242
  // Upload module types
224
243
 
@@ -266,7 +285,7 @@ interface UploadLegacyOptions {
266
285
  /** @deprecated Use uploadFile() instead */
267
286
  declare const upload: {
268
287
  init(path: string, params?: Record<string, any>, notify?: (status: any) => void): Promise<any> | ((files: any) => Promise<any>);
269
- append(path: string, file: File | object, params?: Record<string, any>, context?: Record<string, any>): Promise<any>;
288
+ append(path: string, file: File | object, params?: Record<string, any>, context?: Context): Promise<any>;
270
289
  run(): void;
271
290
  getStatus(): { queue: any[]; running: any[]; failed: any[] };
272
291
  resume(): void;
@@ -284,7 +303,7 @@ declare function uploadFile(
284
303
  buffer: UploadFileInput,
285
304
  method?: string,
286
305
  params?: Record<string, any>,
287
- context?: Record<string, any>,
306
+ context?: Context,
288
307
  options?: UploadFileOptions
289
308
  ): Promise<any>;
290
309
 
@@ -294,7 +313,7 @@ declare function uploadManyFiles(
294
313
  files: UploadFileInput[],
295
314
  method?: string,
296
315
  params?: Record<string, any>,
297
- context?: Record<string, any>,
316
+ context?: Context,
298
317
  options?: UploadManyFilesOptions
299
318
  ): Promise<any[]>;
300
319
 
@@ -334,6 +353,7 @@ export {
334
353
  uploadManyFiles,
335
354
  getI18N,
336
355
  trimPrefix,
356
+ Context,
337
357
  RestPaging,
338
358
  RestResponse,
339
359
  RestError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karpeleslab/klbfw",
3
- "version": "0.2.25",
3
+ "version": "0.2.27",
4
4
  "description": "Frontend Framework",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/upload.js CHANGED
@@ -13,6 +13,17 @@ const rest = require('./rest');
13
13
  const fwWrapper = require('./fw-wrapper');
14
14
  const { env, utils, awsReq, readChunkFromStream, readFileSlice } = require('./upload-internal');
15
15
 
16
+ /**
17
+ * Sleep for a specified duration with exponential backoff and jitter
18
+ * @private
19
+ */
20
+ function retryDelay(attempt, maxRetries) {
21
+ // Exponential backoff: 1s, 2s, 4s (capped at 4s) plus random jitter (0-500ms)
22
+ const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 4000);
23
+ const jitter = Math.random() * 500;
24
+ return new Promise(resolve => setTimeout(resolve, baseDelay + jitter));
25
+ }
26
+
16
27
  /**
17
28
  * Simple file upload function
18
29
  *
@@ -34,9 +45,10 @@ const { env, utils, awsReq, readChunkFromStream, readFileSlice } = require('./up
34
45
  * @param {Object} [context=null] - Request context (uses default context if not provided)
35
46
  * @param {Object} [options={}] - Upload options
36
47
  * @param {Function} [options.onProgress] - Progress callback(progress) where progress is 0-1
37
- * @param {Function} [options.onError] - Error callback(error, context). Can return a Promise
38
- * that, if resolved, will cause the failed operation to be retried. Context contains
39
- * { phase, blockNum, attempt } for block uploads or { phase, attempt } for other operations.
48
+ * @param {Function} [options.onError] - Error callback(error, context). Called only after 3
49
+ * automatic retries have failed. Can return a Promise that, if resolved, will reset the
50
+ * retry counter and attempt 3 more automatic retries. Context contains { phase, blockNum,
51
+ * attempt } for block uploads or { phase, attempt } for other operations.
40
52
  * @param {AbortSignal} [options.signal] - AbortSignal for cancellation. Use AbortController to cancel.
41
53
  * @returns {Promise<Object>} - Resolves with the full REST response. Rejects with AbortError if cancelled.
42
54
  *
@@ -49,18 +61,16 @@ const { env, utils, awsReq, readChunkFromStream, readFileSlice } = require('./up
49
61
  * });
50
62
  *
51
63
  * @example
52
- * // Upload with progress and error handling
64
+ * // Upload with progress - transient failures are automatically retried up to 3 times
53
65
  * const result = await uploadFile('Misc/Debug:testUpload', buffer, 'POST', {
54
66
  * filename: 'large-file.bin'
55
67
  * }, null, {
56
68
  * onProgress: (progress) => console.log(`${Math.round(progress * 100)}%`),
57
69
  * onError: async (error, ctx) => {
58
- * console.log(`Error in ${ctx.phase}, attempt ${ctx.attempt}:`, error.message);
59
- * if (ctx.attempt < 3) {
60
- * await new Promise(r => setTimeout(r, 1000)); // Wait 1s before retry
61
- * return; // Resolve to trigger retry
62
- * }
63
- * throw error; // Give up after 3 attempts
70
+ * // Called only after 3 automatic retries have failed
71
+ * console.log(`Error in ${ctx.phase} after ${ctx.attempt} attempts:`, error.message);
72
+ * // Resolve to reset counter and try 3 more times, or throw to give up
73
+ * throw error;
64
74
  * }
65
75
  * });
66
76
  *
@@ -338,9 +348,15 @@ async function doPutUpload(file, uploadInfo, context, options) {
338
348
  } catch (error) {
339
349
  // Check if aborted during completion
340
350
  checkAbort();
351
+ // Auto-retry up to 3 times before triggering onError
352
+ if (attempt < 3) {
353
+ await retryDelay(attempt);
354
+ continue;
355
+ }
341
356
  if (onError) {
342
357
  await onError(error, { phase: 'complete', attempt });
343
- // If onError resolves, retry
358
+ // If onError resolves, reset attempt counter and retry
359
+ attempt = 0;
344
360
  continue;
345
361
  }
346
362
  throw error;
@@ -388,8 +404,15 @@ async function uploadPutBlockWithDataAndRetry(uploadInfo, blockNum, startByte, d
388
404
  if (error.name === 'AbortError') {
389
405
  throw error;
390
406
  }
407
+ // Auto-retry up to 3 times before triggering onError
408
+ if (attempt < 3) {
409
+ await retryDelay(attempt);
410
+ continue;
411
+ }
391
412
  if (onError) {
392
413
  await onError(error, { phase: 'upload', blockNum, attempt });
414
+ // If onError resolves, reset attempt counter and retry
415
+ attempt = 0;
393
416
  continue;
394
417
  }
395
418
  throw error;
@@ -412,9 +435,15 @@ async function uploadPutBlockWithRetry(file, uploadInfo, blockNum, blockSize, on
412
435
  if (error.name === 'AbortError') {
413
436
  throw error;
414
437
  }
438
+ // Auto-retry up to 3 times before triggering onError
439
+ if (attempt < 3) {
440
+ await retryDelay(attempt);
441
+ continue;
442
+ }
415
443
  if (onError) {
416
444
  await onError(error, { phase: 'upload', blockNum, attempt });
417
- // If onError resolves, retry
445
+ // If onError resolves, reset attempt counter and retry
446
+ attempt = 0;
418
447
  continue;
419
448
  }
420
449
  throw error;
@@ -526,8 +555,15 @@ async function doAwsUpload(file, uploadInfo, context, options) {
526
555
  if (error.name === 'AbortError') {
527
556
  throw error;
528
557
  }
558
+ // Auto-retry up to 3 times before triggering onError
559
+ if (initAttempt < 3) {
560
+ await retryDelay(initAttempt);
561
+ continue;
562
+ }
529
563
  if (onError) {
530
564
  await onError(error, { phase: 'init', attempt: initAttempt });
565
+ // If onError resolves, reset attempt counter and retry
566
+ initAttempt = 0;
531
567
  continue;
532
568
  }
533
569
  throw error;
@@ -636,8 +672,15 @@ async function doAwsUpload(file, uploadInfo, context, options) {
636
672
  await abortMultipartUpload(uploadId);
637
673
  throw error;
638
674
  }
675
+ // Auto-retry up to 3 times before triggering onError
676
+ if (completeAttempt < 3) {
677
+ await retryDelay(completeAttempt);
678
+ continue;
679
+ }
639
680
  if (onError) {
640
681
  await onError(error, { phase: 'complete', attempt: completeAttempt });
682
+ // If onError resolves, reset attempt counter and retry
683
+ completeAttempt = 0;
641
684
  continue;
642
685
  }
643
686
  throw error;
@@ -662,8 +705,15 @@ async function doAwsUpload(file, uploadInfo, context, options) {
662
705
  } catch (error) {
663
706
  // Check if aborted during completion
664
707
  checkAbort();
708
+ // Auto-retry up to 3 times before triggering onError
709
+ if (handleAttempt < 3) {
710
+ await retryDelay(handleAttempt);
711
+ continue;
712
+ }
665
713
  if (onError) {
666
714
  await onError(error, { phase: 'handleComplete', attempt: handleAttempt });
715
+ // If onError resolves, reset attempt counter and retry
716
+ handleAttempt = 0;
667
717
  continue;
668
718
  }
669
719
  throw error;
@@ -703,8 +753,15 @@ async function uploadAwsBlockWithDataAndRetry(uploadInfo, uploadId, blockNum, da
703
753
  if (error.name === 'AbortError') {
704
754
  throw error;
705
755
  }
756
+ // Auto-retry up to 3 times before triggering onError
757
+ if (attempt < 3) {
758
+ await retryDelay(attempt);
759
+ continue;
760
+ }
706
761
  if (onError) {
707
762
  await onError(error, { phase: 'upload', blockNum, attempt });
763
+ // If onError resolves, reset attempt counter and retry
764
+ attempt = 0;
708
765
  continue;
709
766
  }
710
767
  throw error;
@@ -727,8 +784,15 @@ async function uploadAwsBlockWithRetry(file, uploadInfo, uploadId, blockNum, blo
727
784
  if (error.name === 'AbortError') {
728
785
  throw error;
729
786
  }
787
+ // Auto-retry up to 3 times before triggering onError
788
+ if (attempt < 3) {
789
+ await retryDelay(attempt);
790
+ continue;
791
+ }
730
792
  if (onError) {
731
793
  await onError(error, { phase: 'upload', blockNum, attempt });
794
+ // If onError resolves, reset attempt counter and retry
795
+ attempt = 0;
732
796
  continue;
733
797
  }
734
798
  throw error;