@koolbase/core 12.20.0 → 12.22.0
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/dist/cjs/index.d.ts +2 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/react/index.d.ts +1 -0
- package/dist/cjs/react/index.js +3 -1
- package/dist/cjs/react/use-upload-queue.d.ts +18 -0
- package/dist/cjs/react/use-upload-queue.js +31 -0
- package/dist/cjs/storage-errors.d.ts +16 -0
- package/dist/cjs/storage-errors.js +33 -1
- package/dist/cjs/upload-queue.d.ts +81 -0
- package/dist/cjs/upload-queue.js +190 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/react/index.d.ts +1 -0
- package/dist/esm/react/index.js +1 -0
- package/dist/esm/react/use-upload-queue.d.ts +18 -0
- package/dist/esm/react/use-upload-queue.js +28 -0
- package/dist/esm/storage-errors.d.ts +16 -0
- package/dist/esm/storage-errors.js +30 -0
- package/dist/esm/upload-queue.d.ts +81 -0
- package/dist/esm/upload-queue.js +186 -0
- package/package.json +1 -1
package/dist/cjs/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export { KoolbaseFilesController, initialFilesState, filesQueryKey } from './fil
|
|
|
27
27
|
export type { FilesQuery, FilesState, FilesStatus } from './files-controller.js';
|
|
28
28
|
export { KoolbaseFileController, initialFileState } from './file-controller.js';
|
|
29
29
|
export type { FileState, FileStatus } from './file-controller.js';
|
|
30
|
+
export { KoolbaseUploadQueue, initialUploadQueueState } from './upload-queue.js';
|
|
31
|
+
export type { KoolbaseUploader, QueuedUpload, QueuedUploadStatus, QueueFile, UploadQueueState } from './upload-queue.js';
|
|
30
32
|
export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
|
|
31
33
|
export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
|
|
32
34
|
export type { FiscalIntentResult, FiscalStatus } from './fiscal.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
19
19
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
20
|
};
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.memoryPlatform = exports.getPlatform = exports.setPlatform = exports.RestoreResult = exports.koolbaseSdkVersion = exports.getOrCreateDeviceId = exports.KoolbaseMessaging = exports.isPending = exports.isFiscalized = exports.decodeFiscalResponse = exports.KoolbaseFiscalError = exports.KoolbaseFiscal = exports.disabledAnalytics = exports.KoolbaseAnalytics = exports.initialFileState = exports.KoolbaseFileController = exports.filesQueryKey = exports.initialFilesState = exports.KoolbaseFilesController = exports.KoolbaseUploadTask = exports.KoolbaseStorage = exports.KoolbaseConnectivity = exports.KoolbaseRealtime = exports.KoolbaseFunctions = exports.KoolbaseFlags = exports.initialRecordState = exports.KoolbaseRecordController = exports.initialCollectionState = exports.collectionQueryKey = exports.KoolbaseCollectionController = exports.KoolbaseDatabase = exports.KoolbaseAuth = void 0;
|
|
22
|
+
exports.memoryPlatform = exports.getPlatform = exports.setPlatform = exports.RestoreResult = exports.koolbaseSdkVersion = exports.getOrCreateDeviceId = exports.KoolbaseMessaging = exports.isPending = exports.isFiscalized = exports.decodeFiscalResponse = exports.KoolbaseFiscalError = exports.KoolbaseFiscal = exports.disabledAnalytics = exports.KoolbaseAnalytics = exports.initialUploadQueueState = exports.KoolbaseUploadQueue = exports.initialFileState = exports.KoolbaseFileController = exports.filesQueryKey = exports.initialFilesState = exports.KoolbaseFilesController = exports.KoolbaseUploadTask = exports.KoolbaseStorage = exports.KoolbaseConnectivity = exports.KoolbaseRealtime = exports.KoolbaseFunctions = exports.KoolbaseFlags = exports.initialRecordState = exports.KoolbaseRecordController = exports.initialCollectionState = exports.collectionQueryKey = exports.KoolbaseCollectionController = exports.KoolbaseDatabase = exports.KoolbaseAuth = void 0;
|
|
23
23
|
__exportStar(require("./types.js"), exports);
|
|
24
24
|
__exportStar(require("./errors.js"), exports);
|
|
25
25
|
__exportStar(require("./network.js"), exports);
|
|
@@ -62,6 +62,10 @@ Object.defineProperty(exports, "filesQueryKey", { enumerable: true, get: functio
|
|
|
62
62
|
var file_controller_js_1 = require("./file-controller.js");
|
|
63
63
|
Object.defineProperty(exports, "KoolbaseFileController", { enumerable: true, get: function () { return file_controller_js_1.KoolbaseFileController; } });
|
|
64
64
|
Object.defineProperty(exports, "initialFileState", { enumerable: true, get: function () { return file_controller_js_1.initialFileState; } });
|
|
65
|
+
// Several uploads in a queue (storage S4).
|
|
66
|
+
var upload_queue_js_1 = require("./upload-queue.js");
|
|
67
|
+
Object.defineProperty(exports, "KoolbaseUploadQueue", { enumerable: true, get: function () { return upload_queue_js_1.KoolbaseUploadQueue; } });
|
|
68
|
+
Object.defineProperty(exports, "initialUploadQueueState", { enumerable: true, get: function () { return upload_queue_js_1.initialUploadQueueState; } });
|
|
65
69
|
var analytics_js_1 = require("./analytics.js");
|
|
66
70
|
Object.defineProperty(exports, "KoolbaseAnalytics", { enumerable: true, get: function () { return analytics_js_1.KoolbaseAnalytics; } });
|
|
67
71
|
Object.defineProperty(exports, "disabledAnalytics", { enumerable: true, get: function () { return analytics_js_1.disabledAnalytics; } });
|
|
@@ -2,3 +2,4 @@ export { createUseCollection, type UseCollectionResult } from './use-collection.
|
|
|
2
2
|
export { createUseRecord, type UseRecordResult } from './use-record.js';
|
|
3
3
|
export { createUseFiles, type UseFilesResult } from './use-files.js';
|
|
4
4
|
export { createUseFile, type UseFileResult } from './use-file.js';
|
|
5
|
+
export { createUseUploadQueue, type UseUploadQueueResult } from './use-upload-queue.js';
|
package/dist/cjs/react/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createUseFile = exports.createUseFiles = exports.createUseRecord = exports.createUseCollection = void 0;
|
|
3
|
+
exports.createUseUploadQueue = exports.createUseFile = exports.createUseFiles = exports.createUseRecord = exports.createUseCollection = void 0;
|
|
4
4
|
// React hooks over core's controllers: ONE implementation, exported to apps by
|
|
5
5
|
// @koolbase/react-native (from its root, as always) and @koolbase/js/react.
|
|
6
6
|
// React only -- nothing native, nothing DOM. Deliberately not exported from
|
|
@@ -13,3 +13,5 @@ var use_files_js_1 = require("./use-files.js");
|
|
|
13
13
|
Object.defineProperty(exports, "createUseFiles", { enumerable: true, get: function () { return use_files_js_1.createUseFiles; } });
|
|
14
14
|
var use_file_js_1 = require("./use-file.js");
|
|
15
15
|
Object.defineProperty(exports, "createUseFile", { enumerable: true, get: function () { return use_file_js_1.createUseFile; } });
|
|
16
|
+
var use_upload_queue_js_1 = require("./use-upload-queue.js");
|
|
17
|
+
Object.defineProperty(exports, "createUseUploadQueue", { enumerable: true, get: function () { return use_upload_queue_js_1.createUseUploadQueue; } });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type KoolbaseUploader, type QueueFile, type UploadQueueState } from '../index.js';
|
|
2
|
+
export interface UseUploadQueueResult extends UploadQueueState {
|
|
3
|
+
/** Adds files and starts as many as may run; returns their ids. */
|
|
4
|
+
add: (files: readonly QueueFile[]) => string[];
|
|
5
|
+
/** Stops one file. */
|
|
6
|
+
cancel: (id: string) => boolean;
|
|
7
|
+
/** Stops every file that can still be stopped. */
|
|
8
|
+
cancelAll: () => void;
|
|
9
|
+
/** Tries a failed or cancelled file again. */
|
|
10
|
+
retry: (id: string) => boolean;
|
|
11
|
+
/** Removes the files that have finished. */
|
|
12
|
+
clearFinished: () => void;
|
|
13
|
+
/** Resolves once nothing is running. */
|
|
14
|
+
whenIdle: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createUseUploadQueue(getStorage: () => KoolbaseUploader): (options?: {
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
}) => UseUploadQueueResult;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// useUploadQueue: several uploads as React state (storage S4).
|
|
3
|
+
//
|
|
4
|
+
// A thin wrapper. Everything about the queue lives in @koolbase/core's
|
|
5
|
+
// KoolbaseUploadQueue, which is tested there; this file only gives a
|
|
6
|
+
// component one queue for its lifetime. Uploads still running when the
|
|
7
|
+
// component goes finish on their own.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createUseUploadQueue = createUseUploadQueue;
|
|
10
|
+
const react_1 = require("react");
|
|
11
|
+
const index_js_1 = require("../index.js");
|
|
12
|
+
function createUseUploadQueue(getStorage) {
|
|
13
|
+
return function useUploadQueue(options = {}) {
|
|
14
|
+
const ref = (0, react_1.useRef)(null);
|
|
15
|
+
if (ref.current === null)
|
|
16
|
+
ref.current = new index_js_1.KoolbaseUploadQueue(getStorage(), options);
|
|
17
|
+
const queue = ref.current;
|
|
18
|
+
const subscribe = (0, react_1.useCallback)((onChange) => queue.subscribe(onChange), [queue]);
|
|
19
|
+
const getSnapshot = (0, react_1.useCallback)(() => queue.getState(), [queue]);
|
|
20
|
+
const state = (0, react_1.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
21
|
+
return {
|
|
22
|
+
...state,
|
|
23
|
+
add: (0, react_1.useCallback)((files) => queue.add(files), [queue]),
|
|
24
|
+
cancel: (0, react_1.useCallback)((id) => queue.cancel(id), [queue]),
|
|
25
|
+
cancelAll: (0, react_1.useCallback)(() => queue.cancelAll(), [queue]),
|
|
26
|
+
retry: (0, react_1.useCallback)((id) => queue.retry(id), [queue]),
|
|
27
|
+
clearFinished: (0, react_1.useCallback)(() => queue.clearFinished(), [queue]),
|
|
28
|
+
whenIdle: (0, react_1.useCallback)(() => queue.whenIdle(), [queue]),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -258,6 +258,22 @@ export declare class KoolbaseStorageFileInUseError extends KoolbaseStorageError
|
|
|
258
258
|
export declare class KoolbaseStorageNameHasHistoryError extends KoolbaseStorageError {
|
|
259
259
|
constructor(message?: string);
|
|
260
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* An upload refused because malware was found in it (storage S6). Nothing of
|
|
263
|
+
* it was kept; the message names what was found. Trying again with the same
|
|
264
|
+
* file will be refused again. Code: `malware_found`.
|
|
265
|
+
*/
|
|
266
|
+
export declare class KoolbaseStorageMalwareError extends KoolbaseStorageError {
|
|
267
|
+
constructor(message?: string);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A file found to contain malware after it was stored (storage S6): it is
|
|
271
|
+
* quarantined, and no link to it is given. The server answers 423. Code:
|
|
272
|
+
* `quarantined`.
|
|
273
|
+
*/
|
|
274
|
+
export declare class KoolbaseStorageQuarantinedError extends KoolbaseStorageError {
|
|
275
|
+
constructor(message?: string);
|
|
276
|
+
}
|
|
261
277
|
/**
|
|
262
278
|
* Maps a non-2xx storage-layer response to a typed
|
|
263
279
|
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.KoolbaseStorageNameHasHistoryError = exports.KoolbaseStorageFileInUseError = exports.KoolbaseDownloadCancelledError = exports.KoolbaseUploadIncompleteError = exports.KoolbaseStorageSignInRequiredError = exports.KoolbaseStorageInvalidCursorError = exports.KoolbaseStorageBusyError = exports.KoolbaseUploadNotFoundError = exports.KoolbaseUploadCancelledError = exports.KoolbaseStorageMetadataInvalidError = exports.KoolbaseStorageMimeTypeError = exports.KoolbaseStorageFileTooLargeError = exports.KoolbaseStorageQuotaError = exports.KoolbaseCapBelowUsageError = exports.KoolbaseUploadExpiredError = exports.KoolbaseUploadURLFailedError = exports.KoolbaseStoragePermissionError = exports.KoolbaseStorageValidationError = exports.KoolbaseStorageNotFoundError = exports.KoolbaseStorageConflictError = exports.KoolbaseStorageProjectIdentityError = exports.KoolbaseStorageError = void 0;
|
|
3
|
+
exports.KoolbaseStorageQuarantinedError = exports.KoolbaseStorageMalwareError = exports.KoolbaseStorageNameHasHistoryError = exports.KoolbaseStorageFileInUseError = exports.KoolbaseDownloadCancelledError = exports.KoolbaseUploadIncompleteError = exports.KoolbaseStorageSignInRequiredError = exports.KoolbaseStorageInvalidCursorError = exports.KoolbaseStorageBusyError = exports.KoolbaseUploadNotFoundError = exports.KoolbaseUploadCancelledError = exports.KoolbaseStorageMetadataInvalidError = exports.KoolbaseStorageMimeTypeError = exports.KoolbaseStorageFileTooLargeError = exports.KoolbaseStorageQuotaError = exports.KoolbaseCapBelowUsageError = exports.KoolbaseUploadExpiredError = exports.KoolbaseUploadURLFailedError = exports.KoolbaseStoragePermissionError = exports.KoolbaseStorageValidationError = exports.KoolbaseStorageNotFoundError = exports.KoolbaseStorageConflictError = exports.KoolbaseStorageProjectIdentityError = exports.KoolbaseStorageError = void 0;
|
|
4
4
|
exports.koolbaseStorageError = koolbaseStorageError;
|
|
5
5
|
exports.koolbaseStorageErrorFromResponse = koolbaseStorageErrorFromResponse;
|
|
6
6
|
const errors_js_1 = require("./errors.js");
|
|
@@ -368,6 +368,32 @@ class KoolbaseStorageNameHasHistoryError extends KoolbaseStorageError {
|
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
370
|
exports.KoolbaseStorageNameHasHistoryError = KoolbaseStorageNameHasHistoryError;
|
|
371
|
+
/**
|
|
372
|
+
* An upload refused because malware was found in it (storage S6). Nothing of
|
|
373
|
+
* it was kept; the message names what was found. Trying again with the same
|
|
374
|
+
* file will be refused again. Code: `malware_found`.
|
|
375
|
+
*/
|
|
376
|
+
class KoolbaseStorageMalwareError extends KoolbaseStorageError {
|
|
377
|
+
constructor(message) {
|
|
378
|
+
super(message ?? 'This file contains malware and was not uploaded', 'malware_found');
|
|
379
|
+
this.name = 'KoolbaseStorageMalwareError';
|
|
380
|
+
Object.setPrototypeOf(this, KoolbaseStorageMalwareError.prototype);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
exports.KoolbaseStorageMalwareError = KoolbaseStorageMalwareError;
|
|
384
|
+
/**
|
|
385
|
+
* A file found to contain malware after it was stored (storage S6): it is
|
|
386
|
+
* quarantined, and no link to it is given. The server answers 423. Code:
|
|
387
|
+
* `quarantined`.
|
|
388
|
+
*/
|
|
389
|
+
class KoolbaseStorageQuarantinedError extends KoolbaseStorageError {
|
|
390
|
+
constructor(message) {
|
|
391
|
+
super(message ?? 'This file is quarantined: malware was found in it', 'quarantined');
|
|
392
|
+
this.name = 'KoolbaseStorageQuarantinedError';
|
|
393
|
+
Object.setPrototypeOf(this, KoolbaseStorageQuarantinedError.prototype);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
exports.KoolbaseStorageQuarantinedError = KoolbaseStorageQuarantinedError;
|
|
371
397
|
/**
|
|
372
398
|
* Maps a non-2xx storage-layer response to a typed
|
|
373
399
|
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
|
@@ -421,6 +447,10 @@ function koolbaseStorageError(status, body, fallbackMessage = 'Storage request f
|
|
|
421
447
|
}
|
|
422
448
|
case 'name_has_history':
|
|
423
449
|
return new KoolbaseStorageNameHasHistoryError(message);
|
|
450
|
+
case 'malware_found':
|
|
451
|
+
return new KoolbaseStorageMalwareError(message);
|
|
452
|
+
case 'quarantined':
|
|
453
|
+
return new KoolbaseStorageQuarantinedError(message);
|
|
424
454
|
case 'upload_incomplete': {
|
|
425
455
|
const d = (body?.details ?? {});
|
|
426
456
|
return new KoolbaseUploadIncompleteError(message, Array.isArray(d.missing) ? d.missing : []);
|
|
@@ -434,6 +464,8 @@ function koolbaseStorageError(status, body, fallbackMessage = 'Storage request f
|
|
|
434
464
|
return new KoolbaseStorageFileTooLargeError(message);
|
|
435
465
|
case 415:
|
|
436
466
|
return new KoolbaseStorageMimeTypeError(message);
|
|
467
|
+
case 423:
|
|
468
|
+
return new KoolbaseStorageQuarantinedError(message);
|
|
437
469
|
case 404:
|
|
438
470
|
return new KoolbaseStorageNotFoundError(message);
|
|
439
471
|
case 401:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { KoolbaseUploadTask } from './storage-transfer.js';
|
|
2
|
+
import type { UploadOptions } from './types.js';
|
|
3
|
+
export type QueuedUploadStatus = 'queued' | 'uploading' | 'verifying' | 'done' | 'failed' | 'cancelled';
|
|
4
|
+
/** One file in the queue. */
|
|
5
|
+
export interface QueuedUpload {
|
|
6
|
+
/** The queue's own id for it: what cancel and retry take. */
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly bucket: string;
|
|
9
|
+
/** Where it goes in the bucket. */
|
|
10
|
+
readonly path: string;
|
|
11
|
+
/** What a person calls it: the name given, else the path's last part. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly status: QueuedUploadStatus;
|
|
14
|
+
/** 0 to 1: the share of its bytes sent; 1 once verifying. */
|
|
15
|
+
readonly progress: number;
|
|
16
|
+
/** Why it failed, while status is 'failed'. */
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface UploadQueueState {
|
|
20
|
+
readonly items: readonly QueuedUpload[];
|
|
21
|
+
/** 0 to 1: the files' average progress, a cancelled one not counted. */
|
|
22
|
+
readonly progress: number;
|
|
23
|
+
/** Whether any file is still queued, uploading or verifying. */
|
|
24
|
+
readonly running: boolean;
|
|
25
|
+
/** How many have landed, and how many failed. */
|
|
26
|
+
readonly done: number;
|
|
27
|
+
readonly failed: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const initialUploadQueueState: UploadQueueState;
|
|
30
|
+
/** A file to add: where it goes, and the bytes as startUpload takes them. */
|
|
31
|
+
export interface QueueFile {
|
|
32
|
+
bucket: string;
|
|
33
|
+
path: string;
|
|
34
|
+
file: UploadOptions['file'];
|
|
35
|
+
/** Shown for it; the path's last part unless set. */
|
|
36
|
+
name?: string;
|
|
37
|
+
overwrite?: boolean;
|
|
38
|
+
metadata?: UploadOptions['metadata'];
|
|
39
|
+
}
|
|
40
|
+
type QueueTask = Pick<KoolbaseUploadTask, 'onProgress' | 'cancel' | 'result'>;
|
|
41
|
+
/** What the queue needs from storage: startUpload. */
|
|
42
|
+
export type KoolbaseUploader = {
|
|
43
|
+
startUpload(options: UploadOptions): QueueTask;
|
|
44
|
+
};
|
|
45
|
+
export declare class KoolbaseUploadQueue {
|
|
46
|
+
readonly concurrency: number;
|
|
47
|
+
private readonly storage;
|
|
48
|
+
private entries;
|
|
49
|
+
private state;
|
|
50
|
+
private readonly listeners;
|
|
51
|
+
private idleWaiters;
|
|
52
|
+
private nextId;
|
|
53
|
+
private disposed;
|
|
54
|
+
constructor(storage: KoolbaseUploader, options?: {
|
|
55
|
+
concurrency?: number;
|
|
56
|
+
});
|
|
57
|
+
getState(): UploadQueueState;
|
|
58
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
59
|
+
subscribe(listener: () => void): () => void;
|
|
60
|
+
/** Adds files to the end of the queue and starts as many as may run. Returns their ids. */
|
|
61
|
+
add(files: readonly QueueFile[]): string[];
|
|
62
|
+
/** Stops one file: a queued one never starts; one uploading stops. Returns whether it was stopped. */
|
|
63
|
+
cancel(id: string): boolean;
|
|
64
|
+
/** Stops every file that can still be stopped. */
|
|
65
|
+
cancelAll(): void;
|
|
66
|
+
/** Tries a failed or cancelled file again, from the start. Returns whether it was queued. */
|
|
67
|
+
retry(id: string): boolean;
|
|
68
|
+
/** Removes the files that have finished: landed, failed or cancelled. */
|
|
69
|
+
clearFinished(): void;
|
|
70
|
+
/** Resolves once nothing is queued, uploading or verifying. */
|
|
71
|
+
whenIdle(): Promise<void>;
|
|
72
|
+
/** Stops reporting. Uploads already running finish on their own. */
|
|
73
|
+
dispose(): void;
|
|
74
|
+
private active;
|
|
75
|
+
private pump;
|
|
76
|
+
private start;
|
|
77
|
+
private finish;
|
|
78
|
+
private patch;
|
|
79
|
+
private publish;
|
|
80
|
+
}
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Several uploads in a queue, framework-free (storage S4).
|
|
3
|
+
//
|
|
4
|
+
// Files added together go a few at a time (three unless set), each its own
|
|
5
|
+
// SDK upload task -- so a large one still goes in parts, and a bucket's
|
|
6
|
+
// file lists still reload as each lands. Every file has its own state and
|
|
7
|
+
// progress, and can be cancelled or tried again on its own. No React here,
|
|
8
|
+
// so it is tested in this package; useUploadQueue in @koolbase/react-native
|
|
9
|
+
// and @koolbase/js/react is a thin wrapper. Mirrors the Flutter SDK's
|
|
10
|
+
// KoolbaseUploadQueue.
|
|
11
|
+
//
|
|
12
|
+
// - queued -> uploading -> verifying -> done, or failed / cancelled
|
|
13
|
+
// - a queued file cancelled never starts; one uploading stops (once the
|
|
14
|
+
// server is verifying it, it lands or fails on its own)
|
|
15
|
+
// - a failed or cancelled file can be tried again: it queues once more,
|
|
16
|
+
// from the start, under the same path
|
|
17
|
+
// - a file that fails never stops the others
|
|
18
|
+
// - progress is the files' average, a cancelled one not counted
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.KoolbaseUploadQueue = exports.initialUploadQueueState = void 0;
|
|
21
|
+
const storage_errors_js_1 = require("./storage-errors.js");
|
|
22
|
+
exports.initialUploadQueueState = Object.freeze({
|
|
23
|
+
items: Object.freeze([]),
|
|
24
|
+
progress: 0,
|
|
25
|
+
running: false,
|
|
26
|
+
done: 0,
|
|
27
|
+
failed: 0,
|
|
28
|
+
});
|
|
29
|
+
class KoolbaseUploadQueue {
|
|
30
|
+
constructor(storage, options = {}) {
|
|
31
|
+
this.entries = [];
|
|
32
|
+
this.state = exports.initialUploadQueueState;
|
|
33
|
+
this.listeners = new Set();
|
|
34
|
+
this.idleWaiters = [];
|
|
35
|
+
this.nextId = 0;
|
|
36
|
+
this.disposed = false;
|
|
37
|
+
this.storage = storage;
|
|
38
|
+
this.concurrency = Math.max(1, Math.floor(options.concurrency ?? 3));
|
|
39
|
+
}
|
|
40
|
+
getState() {
|
|
41
|
+
return this.state;
|
|
42
|
+
}
|
|
43
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
44
|
+
subscribe(listener) {
|
|
45
|
+
this.listeners.add(listener);
|
|
46
|
+
return () => {
|
|
47
|
+
this.listeners.delete(listener);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Adds files to the end of the queue and starts as many as may run. Returns their ids. */
|
|
51
|
+
add(files) {
|
|
52
|
+
if (this.disposed)
|
|
53
|
+
return [];
|
|
54
|
+
const ids = [];
|
|
55
|
+
for (const source of files) {
|
|
56
|
+
const id = `u${++this.nextId}`;
|
|
57
|
+
ids.push(id);
|
|
58
|
+
this.entries.push({
|
|
59
|
+
item: { id, bucket: source.bucket, path: source.path, name: source.name || source.path.split('/').pop() || source.path, status: 'queued', progress: 0, error: null },
|
|
60
|
+
source,
|
|
61
|
+
task: null,
|
|
62
|
+
run: 0,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
this.pump();
|
|
66
|
+
this.publish();
|
|
67
|
+
return ids;
|
|
68
|
+
}
|
|
69
|
+
/** Stops one file: a queued one never starts; one uploading stops. Returns whether it was stopped. */
|
|
70
|
+
cancel(id) {
|
|
71
|
+
const e = this.entries.find((x) => x.item.id === id);
|
|
72
|
+
if (!e)
|
|
73
|
+
return false;
|
|
74
|
+
if (e.item.status === 'queued') {
|
|
75
|
+
this.patch(e, { status: 'cancelled', progress: 0 });
|
|
76
|
+
this.publish();
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (e.item.status === 'uploading')
|
|
80
|
+
return e.task?.cancel() ?? false;
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/** Stops every file that can still be stopped. */
|
|
84
|
+
cancelAll() {
|
|
85
|
+
for (const e of [...this.entries])
|
|
86
|
+
this.cancel(e.item.id);
|
|
87
|
+
}
|
|
88
|
+
/** Tries a failed or cancelled file again, from the start. Returns whether it was queued. */
|
|
89
|
+
retry(id) {
|
|
90
|
+
const e = this.entries.find((x) => x.item.id === id);
|
|
91
|
+
if (!e || (e.item.status !== 'failed' && e.item.status !== 'cancelled') || this.disposed)
|
|
92
|
+
return false;
|
|
93
|
+
e.task = null;
|
|
94
|
+
this.patch(e, { status: 'queued', progress: 0, error: null });
|
|
95
|
+
this.pump();
|
|
96
|
+
this.publish();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/** Removes the files that have finished: landed, failed or cancelled. */
|
|
100
|
+
clearFinished() {
|
|
101
|
+
const before = this.entries.length;
|
|
102
|
+
this.entries = this.entries.filter((e) => e.item.status === 'queued' || e.item.status === 'uploading' || e.item.status === 'verifying');
|
|
103
|
+
if (this.entries.length !== before)
|
|
104
|
+
this.publish();
|
|
105
|
+
}
|
|
106
|
+
/** Resolves once nothing is queued, uploading or verifying. */
|
|
107
|
+
whenIdle() {
|
|
108
|
+
if (!this.state.running)
|
|
109
|
+
return Promise.resolve();
|
|
110
|
+
return new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
111
|
+
}
|
|
112
|
+
/** Stops reporting. Uploads already running finish on their own. */
|
|
113
|
+
dispose() {
|
|
114
|
+
this.disposed = true;
|
|
115
|
+
this.listeners.clear();
|
|
116
|
+
}
|
|
117
|
+
active() {
|
|
118
|
+
return this.entries.filter((e) => e.item.status === 'uploading' || e.item.status === 'verifying').length;
|
|
119
|
+
}
|
|
120
|
+
pump() {
|
|
121
|
+
if (this.disposed)
|
|
122
|
+
return;
|
|
123
|
+
let free = this.concurrency - this.active();
|
|
124
|
+
for (const e of this.entries) {
|
|
125
|
+
if (free <= 0)
|
|
126
|
+
break;
|
|
127
|
+
if (e.item.status !== 'queued')
|
|
128
|
+
continue;
|
|
129
|
+
free--;
|
|
130
|
+
this.start(e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
start(e) {
|
|
134
|
+
const run = ++e.run;
|
|
135
|
+
this.patch(e, { status: 'uploading', progress: 0, error: null });
|
|
136
|
+
let task;
|
|
137
|
+
try {
|
|
138
|
+
task = this.storage.startUpload({ bucket: e.source.bucket, path: e.source.path, file: e.source.file, overwrite: e.source.overwrite, metadata: e.source.metadata });
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.finish(e, run, 'failed', err);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
e.task = task;
|
|
145
|
+
task.onProgress((p) => {
|
|
146
|
+
if (e.run !== run || e.item.status === 'cancelled' || p.isDone)
|
|
147
|
+
return;
|
|
148
|
+
const status = p.state === 'verifying' ? 'verifying' : 'uploading';
|
|
149
|
+
if (status === e.item.status && p.fraction === e.item.progress)
|
|
150
|
+
return;
|
|
151
|
+
this.patch(e, { status, progress: p.fraction });
|
|
152
|
+
this.publish();
|
|
153
|
+
});
|
|
154
|
+
task.result.then(() => this.finish(e, run, 'done', null), (err) => this.finish(e, run, err instanceof storage_errors_js_1.KoolbaseUploadCancelledError ? 'cancelled' : 'failed', err));
|
|
155
|
+
}
|
|
156
|
+
finish(e, run, status, err) {
|
|
157
|
+
if (e.run !== run)
|
|
158
|
+
return;
|
|
159
|
+
e.task = null;
|
|
160
|
+
this.patch(e, { status, progress: status === 'done' ? 1 : status === 'cancelled' ? 0 : e.item.progress, error: status === 'failed' ? err : null });
|
|
161
|
+
this.pump();
|
|
162
|
+
this.publish();
|
|
163
|
+
}
|
|
164
|
+
patch(e, change) {
|
|
165
|
+
e.item = Object.freeze({ ...e.item, ...change });
|
|
166
|
+
}
|
|
167
|
+
publish() {
|
|
168
|
+
const items = Object.freeze(this.entries.map((e) => e.item));
|
|
169
|
+
const counted = items.filter((i) => i.status !== 'cancelled');
|
|
170
|
+
const running = items.some((i) => i.status === 'queued' || i.status === 'uploading' || i.status === 'verifying');
|
|
171
|
+
this.state = Object.freeze({
|
|
172
|
+
items,
|
|
173
|
+
progress: counted.length ? counted.reduce((s, i) => s + i.progress, 0) / counted.length : 0,
|
|
174
|
+
running,
|
|
175
|
+
done: items.filter((i) => i.status === 'done').length,
|
|
176
|
+
failed: items.filter((i) => i.status === 'failed').length,
|
|
177
|
+
});
|
|
178
|
+
if (!running && this.idleWaiters.length) {
|
|
179
|
+
const waiters = this.idleWaiters;
|
|
180
|
+
this.idleWaiters = [];
|
|
181
|
+
for (const w of waiters)
|
|
182
|
+
w();
|
|
183
|
+
}
|
|
184
|
+
if (this.disposed)
|
|
185
|
+
return;
|
|
186
|
+
for (const l of [...this.listeners])
|
|
187
|
+
l();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.KoolbaseUploadQueue = KoolbaseUploadQueue;
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export { KoolbaseFilesController, initialFilesState, filesQueryKey } from './fil
|
|
|
27
27
|
export type { FilesQuery, FilesState, FilesStatus } from './files-controller.js';
|
|
28
28
|
export { KoolbaseFileController, initialFileState } from './file-controller.js';
|
|
29
29
|
export type { FileState, FileStatus } from './file-controller.js';
|
|
30
|
+
export { KoolbaseUploadQueue, initialUploadQueueState } from './upload-queue.js';
|
|
31
|
+
export type { KoolbaseUploader, QueuedUpload, QueuedUploadStatus, QueueFile, UploadQueueState } from './upload-queue.js';
|
|
30
32
|
export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
|
|
31
33
|
export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
|
|
32
34
|
export type { FiscalIntentResult, FiscalStatus } from './fiscal.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -27,6 +27,8 @@ export { KoolbaseUploadTask } from './storage-transfer.js';
|
|
|
27
27
|
export { KoolbaseFilesController, initialFilesState, filesQueryKey } from './files-controller.js';
|
|
28
28
|
// One file's details as state (storage S4).
|
|
29
29
|
export { KoolbaseFileController, initialFileState } from './file-controller.js';
|
|
30
|
+
// Several uploads in a queue (storage S4).
|
|
31
|
+
export { KoolbaseUploadQueue, initialUploadQueueState } from './upload-queue.js';
|
|
30
32
|
export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
|
|
31
33
|
export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
|
|
32
34
|
export { KoolbaseMessaging } from './messaging.js';
|
|
@@ -2,3 +2,4 @@ export { createUseCollection, type UseCollectionResult } from './use-collection.
|
|
|
2
2
|
export { createUseRecord, type UseRecordResult } from './use-record.js';
|
|
3
3
|
export { createUseFiles, type UseFilesResult } from './use-files.js';
|
|
4
4
|
export { createUseFile, type UseFileResult } from './use-file.js';
|
|
5
|
+
export { createUseUploadQueue, type UseUploadQueueResult } from './use-upload-queue.js';
|
package/dist/esm/react/index.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type KoolbaseUploader, type QueueFile, type UploadQueueState } from '../index.js';
|
|
2
|
+
export interface UseUploadQueueResult extends UploadQueueState {
|
|
3
|
+
/** Adds files and starts as many as may run; returns their ids. */
|
|
4
|
+
add: (files: readonly QueueFile[]) => string[];
|
|
5
|
+
/** Stops one file. */
|
|
6
|
+
cancel: (id: string) => boolean;
|
|
7
|
+
/** Stops every file that can still be stopped. */
|
|
8
|
+
cancelAll: () => void;
|
|
9
|
+
/** Tries a failed or cancelled file again. */
|
|
10
|
+
retry: (id: string) => boolean;
|
|
11
|
+
/** Removes the files that have finished. */
|
|
12
|
+
clearFinished: () => void;
|
|
13
|
+
/** Resolves once nothing is running. */
|
|
14
|
+
whenIdle: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createUseUploadQueue(getStorage: () => KoolbaseUploader): (options?: {
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
}) => UseUploadQueueResult;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// useUploadQueue: several uploads as React state (storage S4).
|
|
2
|
+
//
|
|
3
|
+
// A thin wrapper. Everything about the queue lives in @koolbase/core's
|
|
4
|
+
// KoolbaseUploadQueue, which is tested there; this file only gives a
|
|
5
|
+
// component one queue for its lifetime. Uploads still running when the
|
|
6
|
+
// component goes finish on their own.
|
|
7
|
+
import { useCallback, useRef, useSyncExternalStore } from 'react';
|
|
8
|
+
import { KoolbaseUploadQueue } from '../index.js';
|
|
9
|
+
export function createUseUploadQueue(getStorage) {
|
|
10
|
+
return function useUploadQueue(options = {}) {
|
|
11
|
+
const ref = useRef(null);
|
|
12
|
+
if (ref.current === null)
|
|
13
|
+
ref.current = new KoolbaseUploadQueue(getStorage(), options);
|
|
14
|
+
const queue = ref.current;
|
|
15
|
+
const subscribe = useCallback((onChange) => queue.subscribe(onChange), [queue]);
|
|
16
|
+
const getSnapshot = useCallback(() => queue.getState(), [queue]);
|
|
17
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
18
|
+
return {
|
|
19
|
+
...state,
|
|
20
|
+
add: useCallback((files) => queue.add(files), [queue]),
|
|
21
|
+
cancel: useCallback((id) => queue.cancel(id), [queue]),
|
|
22
|
+
cancelAll: useCallback(() => queue.cancelAll(), [queue]),
|
|
23
|
+
retry: useCallback((id) => queue.retry(id), [queue]),
|
|
24
|
+
clearFinished: useCallback(() => queue.clearFinished(), [queue]),
|
|
25
|
+
whenIdle: useCallback(() => queue.whenIdle(), [queue]),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -258,6 +258,22 @@ export declare class KoolbaseStorageFileInUseError extends KoolbaseStorageError
|
|
|
258
258
|
export declare class KoolbaseStorageNameHasHistoryError extends KoolbaseStorageError {
|
|
259
259
|
constructor(message?: string);
|
|
260
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* An upload refused because malware was found in it (storage S6). Nothing of
|
|
263
|
+
* it was kept; the message names what was found. Trying again with the same
|
|
264
|
+
* file will be refused again. Code: `malware_found`.
|
|
265
|
+
*/
|
|
266
|
+
export declare class KoolbaseStorageMalwareError extends KoolbaseStorageError {
|
|
267
|
+
constructor(message?: string);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* A file found to contain malware after it was stored (storage S6): it is
|
|
271
|
+
* quarantined, and no link to it is given. The server answers 423. Code:
|
|
272
|
+
* `quarantined`.
|
|
273
|
+
*/
|
|
274
|
+
export declare class KoolbaseStorageQuarantinedError extends KoolbaseStorageError {
|
|
275
|
+
constructor(message?: string);
|
|
276
|
+
}
|
|
261
277
|
/**
|
|
262
278
|
* Maps a non-2xx storage-layer response to a typed
|
|
263
279
|
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
|
@@ -341,6 +341,30 @@ export class KoolbaseStorageNameHasHistoryError extends KoolbaseStorageError {
|
|
|
341
341
|
Object.setPrototypeOf(this, KoolbaseStorageNameHasHistoryError.prototype);
|
|
342
342
|
}
|
|
343
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* An upload refused because malware was found in it (storage S6). Nothing of
|
|
346
|
+
* it was kept; the message names what was found. Trying again with the same
|
|
347
|
+
* file will be refused again. Code: `malware_found`.
|
|
348
|
+
*/
|
|
349
|
+
export class KoolbaseStorageMalwareError extends KoolbaseStorageError {
|
|
350
|
+
constructor(message) {
|
|
351
|
+
super(message ?? 'This file contains malware and was not uploaded', 'malware_found');
|
|
352
|
+
this.name = 'KoolbaseStorageMalwareError';
|
|
353
|
+
Object.setPrototypeOf(this, KoolbaseStorageMalwareError.prototype);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* A file found to contain malware after it was stored (storage S6): it is
|
|
358
|
+
* quarantined, and no link to it is given. The server answers 423. Code:
|
|
359
|
+
* `quarantined`.
|
|
360
|
+
*/
|
|
361
|
+
export class KoolbaseStorageQuarantinedError extends KoolbaseStorageError {
|
|
362
|
+
constructor(message) {
|
|
363
|
+
super(message ?? 'This file is quarantined: malware was found in it', 'quarantined');
|
|
364
|
+
this.name = 'KoolbaseStorageQuarantinedError';
|
|
365
|
+
Object.setPrototypeOf(this, KoolbaseStorageQuarantinedError.prototype);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
344
368
|
/**
|
|
345
369
|
* Maps a non-2xx storage-layer response to a typed
|
|
346
370
|
* {@link KoolbaseStorageError}, preferring the server's stable `code` and
|
|
@@ -394,6 +418,10 @@ export function koolbaseStorageError(status, body, fallbackMessage = 'Storage re
|
|
|
394
418
|
}
|
|
395
419
|
case 'name_has_history':
|
|
396
420
|
return new KoolbaseStorageNameHasHistoryError(message);
|
|
421
|
+
case 'malware_found':
|
|
422
|
+
return new KoolbaseStorageMalwareError(message);
|
|
423
|
+
case 'quarantined':
|
|
424
|
+
return new KoolbaseStorageQuarantinedError(message);
|
|
397
425
|
case 'upload_incomplete': {
|
|
398
426
|
const d = (body?.details ?? {});
|
|
399
427
|
return new KoolbaseUploadIncompleteError(message, Array.isArray(d.missing) ? d.missing : []);
|
|
@@ -407,6 +435,8 @@ export function koolbaseStorageError(status, body, fallbackMessage = 'Storage re
|
|
|
407
435
|
return new KoolbaseStorageFileTooLargeError(message);
|
|
408
436
|
case 415:
|
|
409
437
|
return new KoolbaseStorageMimeTypeError(message);
|
|
438
|
+
case 423:
|
|
439
|
+
return new KoolbaseStorageQuarantinedError(message);
|
|
410
440
|
case 404:
|
|
411
441
|
return new KoolbaseStorageNotFoundError(message);
|
|
412
442
|
case 401:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { KoolbaseUploadTask } from './storage-transfer.js';
|
|
2
|
+
import type { UploadOptions } from './types.js';
|
|
3
|
+
export type QueuedUploadStatus = 'queued' | 'uploading' | 'verifying' | 'done' | 'failed' | 'cancelled';
|
|
4
|
+
/** One file in the queue. */
|
|
5
|
+
export interface QueuedUpload {
|
|
6
|
+
/** The queue's own id for it: what cancel and retry take. */
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly bucket: string;
|
|
9
|
+
/** Where it goes in the bucket. */
|
|
10
|
+
readonly path: string;
|
|
11
|
+
/** What a person calls it: the name given, else the path's last part. */
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly status: QueuedUploadStatus;
|
|
14
|
+
/** 0 to 1: the share of its bytes sent; 1 once verifying. */
|
|
15
|
+
readonly progress: number;
|
|
16
|
+
/** Why it failed, while status is 'failed'. */
|
|
17
|
+
readonly error: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface UploadQueueState {
|
|
20
|
+
readonly items: readonly QueuedUpload[];
|
|
21
|
+
/** 0 to 1: the files' average progress, a cancelled one not counted. */
|
|
22
|
+
readonly progress: number;
|
|
23
|
+
/** Whether any file is still queued, uploading or verifying. */
|
|
24
|
+
readonly running: boolean;
|
|
25
|
+
/** How many have landed, and how many failed. */
|
|
26
|
+
readonly done: number;
|
|
27
|
+
readonly failed: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const initialUploadQueueState: UploadQueueState;
|
|
30
|
+
/** A file to add: where it goes, and the bytes as startUpload takes them. */
|
|
31
|
+
export interface QueueFile {
|
|
32
|
+
bucket: string;
|
|
33
|
+
path: string;
|
|
34
|
+
file: UploadOptions['file'];
|
|
35
|
+
/** Shown for it; the path's last part unless set. */
|
|
36
|
+
name?: string;
|
|
37
|
+
overwrite?: boolean;
|
|
38
|
+
metadata?: UploadOptions['metadata'];
|
|
39
|
+
}
|
|
40
|
+
type QueueTask = Pick<KoolbaseUploadTask, 'onProgress' | 'cancel' | 'result'>;
|
|
41
|
+
/** What the queue needs from storage: startUpload. */
|
|
42
|
+
export type KoolbaseUploader = {
|
|
43
|
+
startUpload(options: UploadOptions): QueueTask;
|
|
44
|
+
};
|
|
45
|
+
export declare class KoolbaseUploadQueue {
|
|
46
|
+
readonly concurrency: number;
|
|
47
|
+
private readonly storage;
|
|
48
|
+
private entries;
|
|
49
|
+
private state;
|
|
50
|
+
private readonly listeners;
|
|
51
|
+
private idleWaiters;
|
|
52
|
+
private nextId;
|
|
53
|
+
private disposed;
|
|
54
|
+
constructor(storage: KoolbaseUploader, options?: {
|
|
55
|
+
concurrency?: number;
|
|
56
|
+
});
|
|
57
|
+
getState(): UploadQueueState;
|
|
58
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
59
|
+
subscribe(listener: () => void): () => void;
|
|
60
|
+
/** Adds files to the end of the queue and starts as many as may run. Returns their ids. */
|
|
61
|
+
add(files: readonly QueueFile[]): string[];
|
|
62
|
+
/** Stops one file: a queued one never starts; one uploading stops. Returns whether it was stopped. */
|
|
63
|
+
cancel(id: string): boolean;
|
|
64
|
+
/** Stops every file that can still be stopped. */
|
|
65
|
+
cancelAll(): void;
|
|
66
|
+
/** Tries a failed or cancelled file again, from the start. Returns whether it was queued. */
|
|
67
|
+
retry(id: string): boolean;
|
|
68
|
+
/** Removes the files that have finished: landed, failed or cancelled. */
|
|
69
|
+
clearFinished(): void;
|
|
70
|
+
/** Resolves once nothing is queued, uploading or verifying. */
|
|
71
|
+
whenIdle(): Promise<void>;
|
|
72
|
+
/** Stops reporting. Uploads already running finish on their own. */
|
|
73
|
+
dispose(): void;
|
|
74
|
+
private active;
|
|
75
|
+
private pump;
|
|
76
|
+
private start;
|
|
77
|
+
private finish;
|
|
78
|
+
private patch;
|
|
79
|
+
private publish;
|
|
80
|
+
}
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Several uploads in a queue, framework-free (storage S4).
|
|
2
|
+
//
|
|
3
|
+
// Files added together go a few at a time (three unless set), each its own
|
|
4
|
+
// SDK upload task -- so a large one still goes in parts, and a bucket's
|
|
5
|
+
// file lists still reload as each lands. Every file has its own state and
|
|
6
|
+
// progress, and can be cancelled or tried again on its own. No React here,
|
|
7
|
+
// so it is tested in this package; useUploadQueue in @koolbase/react-native
|
|
8
|
+
// and @koolbase/js/react is a thin wrapper. Mirrors the Flutter SDK's
|
|
9
|
+
// KoolbaseUploadQueue.
|
|
10
|
+
//
|
|
11
|
+
// - queued -> uploading -> verifying -> done, or failed / cancelled
|
|
12
|
+
// - a queued file cancelled never starts; one uploading stops (once the
|
|
13
|
+
// server is verifying it, it lands or fails on its own)
|
|
14
|
+
// - a failed or cancelled file can be tried again: it queues once more,
|
|
15
|
+
// from the start, under the same path
|
|
16
|
+
// - a file that fails never stops the others
|
|
17
|
+
// - progress is the files' average, a cancelled one not counted
|
|
18
|
+
import { KoolbaseUploadCancelledError } from './storage-errors.js';
|
|
19
|
+
export const initialUploadQueueState = Object.freeze({
|
|
20
|
+
items: Object.freeze([]),
|
|
21
|
+
progress: 0,
|
|
22
|
+
running: false,
|
|
23
|
+
done: 0,
|
|
24
|
+
failed: 0,
|
|
25
|
+
});
|
|
26
|
+
export class KoolbaseUploadQueue {
|
|
27
|
+
constructor(storage, options = {}) {
|
|
28
|
+
this.entries = [];
|
|
29
|
+
this.state = initialUploadQueueState;
|
|
30
|
+
this.listeners = new Set();
|
|
31
|
+
this.idleWaiters = [];
|
|
32
|
+
this.nextId = 0;
|
|
33
|
+
this.disposed = false;
|
|
34
|
+
this.storage = storage;
|
|
35
|
+
this.concurrency = Math.max(1, Math.floor(options.concurrency ?? 3));
|
|
36
|
+
}
|
|
37
|
+
getState() {
|
|
38
|
+
return this.state;
|
|
39
|
+
}
|
|
40
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
41
|
+
subscribe(listener) {
|
|
42
|
+
this.listeners.add(listener);
|
|
43
|
+
return () => {
|
|
44
|
+
this.listeners.delete(listener);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Adds files to the end of the queue and starts as many as may run. Returns their ids. */
|
|
48
|
+
add(files) {
|
|
49
|
+
if (this.disposed)
|
|
50
|
+
return [];
|
|
51
|
+
const ids = [];
|
|
52
|
+
for (const source of files) {
|
|
53
|
+
const id = `u${++this.nextId}`;
|
|
54
|
+
ids.push(id);
|
|
55
|
+
this.entries.push({
|
|
56
|
+
item: { id, bucket: source.bucket, path: source.path, name: source.name || source.path.split('/').pop() || source.path, status: 'queued', progress: 0, error: null },
|
|
57
|
+
source,
|
|
58
|
+
task: null,
|
|
59
|
+
run: 0,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
this.pump();
|
|
63
|
+
this.publish();
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
66
|
+
/** Stops one file: a queued one never starts; one uploading stops. Returns whether it was stopped. */
|
|
67
|
+
cancel(id) {
|
|
68
|
+
const e = this.entries.find((x) => x.item.id === id);
|
|
69
|
+
if (!e)
|
|
70
|
+
return false;
|
|
71
|
+
if (e.item.status === 'queued') {
|
|
72
|
+
this.patch(e, { status: 'cancelled', progress: 0 });
|
|
73
|
+
this.publish();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (e.item.status === 'uploading')
|
|
77
|
+
return e.task?.cancel() ?? false;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
/** Stops every file that can still be stopped. */
|
|
81
|
+
cancelAll() {
|
|
82
|
+
for (const e of [...this.entries])
|
|
83
|
+
this.cancel(e.item.id);
|
|
84
|
+
}
|
|
85
|
+
/** Tries a failed or cancelled file again, from the start. Returns whether it was queued. */
|
|
86
|
+
retry(id) {
|
|
87
|
+
const e = this.entries.find((x) => x.item.id === id);
|
|
88
|
+
if (!e || (e.item.status !== 'failed' && e.item.status !== 'cancelled') || this.disposed)
|
|
89
|
+
return false;
|
|
90
|
+
e.task = null;
|
|
91
|
+
this.patch(e, { status: 'queued', progress: 0, error: null });
|
|
92
|
+
this.pump();
|
|
93
|
+
this.publish();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** Removes the files that have finished: landed, failed or cancelled. */
|
|
97
|
+
clearFinished() {
|
|
98
|
+
const before = this.entries.length;
|
|
99
|
+
this.entries = this.entries.filter((e) => e.item.status === 'queued' || e.item.status === 'uploading' || e.item.status === 'verifying');
|
|
100
|
+
if (this.entries.length !== before)
|
|
101
|
+
this.publish();
|
|
102
|
+
}
|
|
103
|
+
/** Resolves once nothing is queued, uploading or verifying. */
|
|
104
|
+
whenIdle() {
|
|
105
|
+
if (!this.state.running)
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
return new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
108
|
+
}
|
|
109
|
+
/** Stops reporting. Uploads already running finish on their own. */
|
|
110
|
+
dispose() {
|
|
111
|
+
this.disposed = true;
|
|
112
|
+
this.listeners.clear();
|
|
113
|
+
}
|
|
114
|
+
active() {
|
|
115
|
+
return this.entries.filter((e) => e.item.status === 'uploading' || e.item.status === 'verifying').length;
|
|
116
|
+
}
|
|
117
|
+
pump() {
|
|
118
|
+
if (this.disposed)
|
|
119
|
+
return;
|
|
120
|
+
let free = this.concurrency - this.active();
|
|
121
|
+
for (const e of this.entries) {
|
|
122
|
+
if (free <= 0)
|
|
123
|
+
break;
|
|
124
|
+
if (e.item.status !== 'queued')
|
|
125
|
+
continue;
|
|
126
|
+
free--;
|
|
127
|
+
this.start(e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
start(e) {
|
|
131
|
+
const run = ++e.run;
|
|
132
|
+
this.patch(e, { status: 'uploading', progress: 0, error: null });
|
|
133
|
+
let task;
|
|
134
|
+
try {
|
|
135
|
+
task = this.storage.startUpload({ bucket: e.source.bucket, path: e.source.path, file: e.source.file, overwrite: e.source.overwrite, metadata: e.source.metadata });
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
this.finish(e, run, 'failed', err);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
e.task = task;
|
|
142
|
+
task.onProgress((p) => {
|
|
143
|
+
if (e.run !== run || e.item.status === 'cancelled' || p.isDone)
|
|
144
|
+
return;
|
|
145
|
+
const status = p.state === 'verifying' ? 'verifying' : 'uploading';
|
|
146
|
+
if (status === e.item.status && p.fraction === e.item.progress)
|
|
147
|
+
return;
|
|
148
|
+
this.patch(e, { status, progress: p.fraction });
|
|
149
|
+
this.publish();
|
|
150
|
+
});
|
|
151
|
+
task.result.then(() => this.finish(e, run, 'done', null), (err) => this.finish(e, run, err instanceof KoolbaseUploadCancelledError ? 'cancelled' : 'failed', err));
|
|
152
|
+
}
|
|
153
|
+
finish(e, run, status, err) {
|
|
154
|
+
if (e.run !== run)
|
|
155
|
+
return;
|
|
156
|
+
e.task = null;
|
|
157
|
+
this.patch(e, { status, progress: status === 'done' ? 1 : status === 'cancelled' ? 0 : e.item.progress, error: status === 'failed' ? err : null });
|
|
158
|
+
this.pump();
|
|
159
|
+
this.publish();
|
|
160
|
+
}
|
|
161
|
+
patch(e, change) {
|
|
162
|
+
e.item = Object.freeze({ ...e.item, ...change });
|
|
163
|
+
}
|
|
164
|
+
publish() {
|
|
165
|
+
const items = Object.freeze(this.entries.map((e) => e.item));
|
|
166
|
+
const counted = items.filter((i) => i.status !== 'cancelled');
|
|
167
|
+
const running = items.some((i) => i.status === 'queued' || i.status === 'uploading' || i.status === 'verifying');
|
|
168
|
+
this.state = Object.freeze({
|
|
169
|
+
items,
|
|
170
|
+
progress: counted.length ? counted.reduce((s, i) => s + i.progress, 0) / counted.length : 0,
|
|
171
|
+
running,
|
|
172
|
+
done: items.filter((i) => i.status === 'done').length,
|
|
173
|
+
failed: items.filter((i) => i.status === 'failed').length,
|
|
174
|
+
});
|
|
175
|
+
if (!running && this.idleWaiters.length) {
|
|
176
|
+
const waiters = this.idleWaiters;
|
|
177
|
+
this.idleWaiters = [];
|
|
178
|
+
for (const w of waiters)
|
|
179
|
+
w();
|
|
180
|
+
}
|
|
181
|
+
if (this.disposed)
|
|
182
|
+
return;
|
|
183
|
+
for (const l of [...this.listeners])
|
|
184
|
+
l();
|
|
185
|
+
}
|
|
186
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koolbase/core",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.22.0",
|
|
4
4
|
"description": "Koolbase SDK core \u2014 shared behaviour behind @koolbase/react-native and @koolbase/js. Install one of those, not this.",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"types": "./dist/esm/index.d.ts",
|