@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.
@@ -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';
@@ -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;
@@ -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';
@@ -6,3 +6,4 @@ export { createUseCollection } from './use-collection.js';
6
6
  export { createUseRecord } from './use-record.js';
7
7
  export { createUseFiles } from './use-files.js';
8
8
  export { createUseFile } from './use-file.js';
9
+ export { createUseUploadQueue } from './use-upload-queue.js';
@@ -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.20.0",
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",