@koolbase/core 12.19.0 → 12.21.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/file-controller.d.ts +39 -0
- package/dist/cjs/file-controller.js +115 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/react/index.d.ts +2 -0
- package/dist/cjs/react/index.js +5 -1
- package/dist/cjs/react/use-file.d.ts +6 -0
- package/dist/cjs/react/use-file.js +35 -0
- package/dist/cjs/react/use-upload-queue.d.ts +18 -0
- package/dist/cjs/react/use-upload-queue.js +31 -0
- package/dist/cjs/upload-queue.d.ts +81 -0
- package/dist/cjs/upload-queue.js +190 -0
- package/dist/esm/file-controller.d.ts +39 -0
- package/dist/esm/file-controller.js +111 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/react/index.d.ts +2 -0
- package/dist/esm/react/index.js +2 -0
- package/dist/esm/react/use-file.d.ts +6 -0
- package/dist/esm/react/use-file.js +32 -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/upload-queue.d.ts +81 -0
- package/dist/esm/upload-queue.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { KoolbaseFileInfo } from './storage-files.js';
|
|
2
|
+
export type FileStatus = 'loading' | 'loaded' | 'notFound' | 'error';
|
|
3
|
+
export interface FileState {
|
|
4
|
+
readonly status: FileStatus;
|
|
5
|
+
/** The file's details, while status is 'loaded'. */
|
|
6
|
+
readonly file: KoolbaseFileInfo | null;
|
|
7
|
+
/** Why the first load failed, while status is 'error'. */
|
|
8
|
+
readonly error: unknown;
|
|
9
|
+
/** True while refresh() runs. */
|
|
10
|
+
readonly refreshing: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const initialFileState: FileState;
|
|
13
|
+
type FileReader = {
|
|
14
|
+
getFileInfo(bucket: string, path: string): Promise<KoolbaseFileInfo>;
|
|
15
|
+
};
|
|
16
|
+
export declare class KoolbaseFileController {
|
|
17
|
+
readonly bucket: string;
|
|
18
|
+
readonly path: string;
|
|
19
|
+
private readonly storage;
|
|
20
|
+
private state;
|
|
21
|
+
private readonly listeners;
|
|
22
|
+
private generation;
|
|
23
|
+
private disposed;
|
|
24
|
+
private unregister;
|
|
25
|
+
constructor(storage: FileReader, bucket: string, path: string);
|
|
26
|
+
getState(): FileState;
|
|
27
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
28
|
+
subscribe(listener: () => void): () => void;
|
|
29
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
30
|
+
load(): Promise<void>;
|
|
31
|
+
/** Read it again. The file stays shown while it runs, and stays if it fails. */
|
|
32
|
+
refresh(): Promise<void>;
|
|
33
|
+
/** Stops all updates. Results still in flight are dropped when they land. */
|
|
34
|
+
dispose(): void;
|
|
35
|
+
private run;
|
|
36
|
+
private isStale;
|
|
37
|
+
private set;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// One file's details, by path, framework-free (storage S4).
|
|
3
|
+
//
|
|
4
|
+
// The data half of a file detail screen: loading, loaded, not found or
|
|
5
|
+
// error, and refresh. No React here, so it is tested in this package; useFile
|
|
6
|
+
// in @koolbase/react-native and @koolbase/js/react is a thin wrapper.
|
|
7
|
+
// Mirrors the Flutter SDK's KoolbaseFileController, with the record view's
|
|
8
|
+
// rules:
|
|
9
|
+
//
|
|
10
|
+
// - an empty path is not found, with no request
|
|
11
|
+
// - not found is not an error: the API answers a file you may not view as
|
|
12
|
+
// not found too, so a detail screen says "not available"
|
|
13
|
+
// - stale beats blank: a failed refresh keeps the file shown
|
|
14
|
+
// - a change made through this SDK in the bucket (an upload, a delete, a
|
|
15
|
+
// move, a metadata change, a restore) reads it again silently: deleted or
|
|
16
|
+
// moved away, the file is not found; replaced, it shows its new size
|
|
17
|
+
// - a result a later load superseded, or one landing after dispose(), is
|
|
18
|
+
// dropped
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.KoolbaseFileController = exports.initialFileState = void 0;
|
|
21
|
+
const files_refresh_js_1 = require("./files-refresh.js");
|
|
22
|
+
const storage_errors_js_1 = require("./storage-errors.js");
|
|
23
|
+
exports.initialFileState = Object.freeze({
|
|
24
|
+
status: 'loading',
|
|
25
|
+
file: null,
|
|
26
|
+
error: null,
|
|
27
|
+
refreshing: false,
|
|
28
|
+
});
|
|
29
|
+
/** Whether [e] says there is no such file (or none this caller may view). */
|
|
30
|
+
function isNotFound(e) {
|
|
31
|
+
return e instanceof storage_errors_js_1.KoolbaseStorageNotFoundError || e?.code === 'not_found';
|
|
32
|
+
}
|
|
33
|
+
class KoolbaseFileController {
|
|
34
|
+
constructor(storage, bucket, path) {
|
|
35
|
+
this.state = exports.initialFileState;
|
|
36
|
+
this.listeners = new Set();
|
|
37
|
+
this.generation = 0;
|
|
38
|
+
this.disposed = false;
|
|
39
|
+
this.unregister = null;
|
|
40
|
+
this.storage = storage;
|
|
41
|
+
this.bucket = bucket;
|
|
42
|
+
this.path = path;
|
|
43
|
+
}
|
|
44
|
+
getState() {
|
|
45
|
+
return this.state;
|
|
46
|
+
}
|
|
47
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
if (this.disposed)
|
|
50
|
+
return () => { };
|
|
51
|
+
this.listeners.add(listener);
|
|
52
|
+
return () => {
|
|
53
|
+
this.listeners.delete(listener);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
57
|
+
async load() {
|
|
58
|
+
if (this.disposed)
|
|
59
|
+
return;
|
|
60
|
+
if (this.path)
|
|
61
|
+
this.unregister ?? (this.unregister = (0, files_refresh_js_1.registerFilesRefresher)(this.bucket, () => this.run()));
|
|
62
|
+
await this.run();
|
|
63
|
+
}
|
|
64
|
+
/** Read it again. The file stays shown while it runs, and stays if it fails. */
|
|
65
|
+
async refresh() {
|
|
66
|
+
if (this.disposed)
|
|
67
|
+
return;
|
|
68
|
+
this.set({ refreshing: true });
|
|
69
|
+
await this.run();
|
|
70
|
+
}
|
|
71
|
+
/** Stops all updates. Results still in flight are dropped when they land. */
|
|
72
|
+
dispose() {
|
|
73
|
+
this.disposed = true;
|
|
74
|
+
this.unregister?.();
|
|
75
|
+
this.unregister = null;
|
|
76
|
+
this.listeners.clear();
|
|
77
|
+
}
|
|
78
|
+
async run() {
|
|
79
|
+
if (this.disposed)
|
|
80
|
+
return;
|
|
81
|
+
const gen = ++this.generation;
|
|
82
|
+
if (!this.path) {
|
|
83
|
+
this.set({ status: 'notFound', file: null, error: null, refreshing: false });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const file = await this.storage.getFileInfo(this.bucket, this.path);
|
|
88
|
+
if (this.isStale(gen))
|
|
89
|
+
return;
|
|
90
|
+
this.set({ status: 'loaded', file, error: null, refreshing: false });
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
if (this.isStale(gen))
|
|
94
|
+
return;
|
|
95
|
+
if (isNotFound(e))
|
|
96
|
+
this.set({ status: 'notFound', file: null, error: null, refreshing: false });
|
|
97
|
+
// Only a first load with nothing to show is the error state.
|
|
98
|
+
else if (this.state.file === null)
|
|
99
|
+
this.set({ status: 'error', file: null, error: e, refreshing: false });
|
|
100
|
+
else
|
|
101
|
+
this.set({ refreshing: false });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
isStale(gen) {
|
|
105
|
+
return this.disposed || gen !== this.generation;
|
|
106
|
+
}
|
|
107
|
+
set(change) {
|
|
108
|
+
if (this.disposed)
|
|
109
|
+
return;
|
|
110
|
+
this.state = Object.freeze({ ...this.state, ...change });
|
|
111
|
+
for (const l of [...this.listeners])
|
|
112
|
+
l();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.KoolbaseFileController = KoolbaseFileController;
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export type { KoolbaseUploadProgress, KoolbaseUploadState } from './storage-tran
|
|
|
25
25
|
export type { KoolbaseFileInfo, KoolbaseFilePage, KoolbaseFileSort, ListFilesOptions } from './storage-files.js';
|
|
26
26
|
export { KoolbaseFilesController, initialFilesState, filesQueryKey } from './files-controller.js';
|
|
27
27
|
export type { FilesQuery, FilesState, FilesStatus } from './files-controller.js';
|
|
28
|
+
export { KoolbaseFileController, initialFileState } from './file-controller.js';
|
|
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';
|
|
28
32
|
export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
|
|
29
33
|
export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
|
|
30
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.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);
|
|
@@ -58,6 +58,14 @@ var files_controller_js_1 = require("./files-controller.js");
|
|
|
58
58
|
Object.defineProperty(exports, "KoolbaseFilesController", { enumerable: true, get: function () { return files_controller_js_1.KoolbaseFilesController; } });
|
|
59
59
|
Object.defineProperty(exports, "initialFilesState", { enumerable: true, get: function () { return files_controller_js_1.initialFilesState; } });
|
|
60
60
|
Object.defineProperty(exports, "filesQueryKey", { enumerable: true, get: function () { return files_controller_js_1.filesQueryKey; } });
|
|
61
|
+
// One file's details as state (storage S4).
|
|
62
|
+
var file_controller_js_1 = require("./file-controller.js");
|
|
63
|
+
Object.defineProperty(exports, "KoolbaseFileController", { enumerable: true, get: function () { return file_controller_js_1.KoolbaseFileController; } });
|
|
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; } });
|
|
61
69
|
var analytics_js_1 = require("./analytics.js");
|
|
62
70
|
Object.defineProperty(exports, "KoolbaseAnalytics", { enumerable: true, get: function () { return analytics_js_1.KoolbaseAnalytics; } });
|
|
63
71
|
Object.defineProperty(exports, "disabledAnalytics", { enumerable: true, get: function () { return analytics_js_1.disabledAnalytics; } });
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createUseCollection, type UseCollectionResult } from './use-collection.js';
|
|
2
2
|
export { createUseRecord, type UseRecordResult } from './use-record.js';
|
|
3
3
|
export { createUseFiles, type UseFilesResult } from './use-files.js';
|
|
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.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
|
|
@@ -11,3 +11,7 @@ var use_record_js_1 = require("./use-record.js");
|
|
|
11
11
|
Object.defineProperty(exports, "createUseRecord", { enumerable: true, get: function () { return use_record_js_1.createUseRecord; } });
|
|
12
12
|
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
|
+
var use_file_js_1 = require("./use-file.js");
|
|
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,6 @@
|
|
|
1
|
+
import { type FileState, type KoolbaseStorage } from '../index.js';
|
|
2
|
+
export interface UseFileResult extends FileState {
|
|
3
|
+
/** Read it again. The file stays shown while it runs, and stays if it fails. */
|
|
4
|
+
refresh: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createUseFile(getStorage: () => Pick<KoolbaseStorage, 'getFileInfo'>): (bucket: string, path: string | null | undefined) => UseFileResult;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// useFile: one file's details as React state (storage S4).
|
|
3
|
+
//
|
|
4
|
+
// A thin wrapper. Everything about getting the file right lives in
|
|
5
|
+
// @koolbase/core's KoolbaseFileController, which is tested there; this file
|
|
6
|
+
// only ties one controller's lifetime to a component's.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createUseFile = createUseFile;
|
|
9
|
+
const react_1 = require("react");
|
|
10
|
+
const index_js_1 = require("../index.js");
|
|
11
|
+
const noop = () => { };
|
|
12
|
+
const done = () => Promise.resolve();
|
|
13
|
+
function createUseFile(getStorage) {
|
|
14
|
+
return function useFile(bucket, path) {
|
|
15
|
+
const p = path ?? '';
|
|
16
|
+
const key = JSON.stringify([bucket, p]);
|
|
17
|
+
const [current, setCurrent] = (0, react_1.useState)(null);
|
|
18
|
+
(0, react_1.useEffect)(() => {
|
|
19
|
+
// Created here, not during render, so React strict mode's second run
|
|
20
|
+
// never keeps a disposed controller.
|
|
21
|
+
const controller = new index_js_1.KoolbaseFileController(getStorage(), bucket, p);
|
|
22
|
+
setCurrent({ key, controller });
|
|
23
|
+
void controller.load();
|
|
24
|
+
return () => controller.dispose();
|
|
25
|
+
}, [key]);
|
|
26
|
+
// A controller from the previous file reads as a fresh load, never as
|
|
27
|
+
// the previous file.
|
|
28
|
+
const controller = current?.key === key ? current.controller : null;
|
|
29
|
+
const subscribe = (0, react_1.useCallback)((onChange) => (controller ? controller.subscribe(onChange) : noop), [controller]);
|
|
30
|
+
const getSnapshot = (0, react_1.useCallback)(() => (controller ? controller.getState() : index_js_1.initialFileState), [controller]);
|
|
31
|
+
const state = (0, react_1.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
32
|
+
const refresh = (0, react_1.useCallback)(() => (controller ? controller.refresh() : done()), [controller]);
|
|
33
|
+
return { ...state, refresh };
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { KoolbaseFileInfo } from './storage-files.js';
|
|
2
|
+
export type FileStatus = 'loading' | 'loaded' | 'notFound' | 'error';
|
|
3
|
+
export interface FileState {
|
|
4
|
+
readonly status: FileStatus;
|
|
5
|
+
/** The file's details, while status is 'loaded'. */
|
|
6
|
+
readonly file: KoolbaseFileInfo | null;
|
|
7
|
+
/** Why the first load failed, while status is 'error'. */
|
|
8
|
+
readonly error: unknown;
|
|
9
|
+
/** True while refresh() runs. */
|
|
10
|
+
readonly refreshing: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const initialFileState: FileState;
|
|
13
|
+
type FileReader = {
|
|
14
|
+
getFileInfo(bucket: string, path: string): Promise<KoolbaseFileInfo>;
|
|
15
|
+
};
|
|
16
|
+
export declare class KoolbaseFileController {
|
|
17
|
+
readonly bucket: string;
|
|
18
|
+
readonly path: string;
|
|
19
|
+
private readonly storage;
|
|
20
|
+
private state;
|
|
21
|
+
private readonly listeners;
|
|
22
|
+
private generation;
|
|
23
|
+
private disposed;
|
|
24
|
+
private unregister;
|
|
25
|
+
constructor(storage: FileReader, bucket: string, path: string);
|
|
26
|
+
getState(): FileState;
|
|
27
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
28
|
+
subscribe(listener: () => void): () => void;
|
|
29
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
30
|
+
load(): Promise<void>;
|
|
31
|
+
/** Read it again. The file stays shown while it runs, and stays if it fails. */
|
|
32
|
+
refresh(): Promise<void>;
|
|
33
|
+
/** Stops all updates. Results still in flight are dropped when they land. */
|
|
34
|
+
dispose(): void;
|
|
35
|
+
private run;
|
|
36
|
+
private isStale;
|
|
37
|
+
private set;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// One file's details, by path, framework-free (storage S4).
|
|
2
|
+
//
|
|
3
|
+
// The data half of a file detail screen: loading, loaded, not found or
|
|
4
|
+
// error, and refresh. No React here, so it is tested in this package; useFile
|
|
5
|
+
// in @koolbase/react-native and @koolbase/js/react is a thin wrapper.
|
|
6
|
+
// Mirrors the Flutter SDK's KoolbaseFileController, with the record view's
|
|
7
|
+
// rules:
|
|
8
|
+
//
|
|
9
|
+
// - an empty path is not found, with no request
|
|
10
|
+
// - not found is not an error: the API answers a file you may not view as
|
|
11
|
+
// not found too, so a detail screen says "not available"
|
|
12
|
+
// - stale beats blank: a failed refresh keeps the file shown
|
|
13
|
+
// - a change made through this SDK in the bucket (an upload, a delete, a
|
|
14
|
+
// move, a metadata change, a restore) reads it again silently: deleted or
|
|
15
|
+
// moved away, the file is not found; replaced, it shows its new size
|
|
16
|
+
// - a result a later load superseded, or one landing after dispose(), is
|
|
17
|
+
// dropped
|
|
18
|
+
import { registerFilesRefresher } from './files-refresh.js';
|
|
19
|
+
import { KoolbaseStorageNotFoundError } from './storage-errors.js';
|
|
20
|
+
export const initialFileState = Object.freeze({
|
|
21
|
+
status: 'loading',
|
|
22
|
+
file: null,
|
|
23
|
+
error: null,
|
|
24
|
+
refreshing: false,
|
|
25
|
+
});
|
|
26
|
+
/** Whether [e] says there is no such file (or none this caller may view). */
|
|
27
|
+
function isNotFound(e) {
|
|
28
|
+
return e instanceof KoolbaseStorageNotFoundError || e?.code === 'not_found';
|
|
29
|
+
}
|
|
30
|
+
export class KoolbaseFileController {
|
|
31
|
+
constructor(storage, bucket, path) {
|
|
32
|
+
this.state = initialFileState;
|
|
33
|
+
this.listeners = new Set();
|
|
34
|
+
this.generation = 0;
|
|
35
|
+
this.disposed = false;
|
|
36
|
+
this.unregister = null;
|
|
37
|
+
this.storage = storage;
|
|
38
|
+
this.bucket = bucket;
|
|
39
|
+
this.path = path;
|
|
40
|
+
}
|
|
41
|
+
getState() {
|
|
42
|
+
return this.state;
|
|
43
|
+
}
|
|
44
|
+
/** Called on every change. Returns the unsubscribe. */
|
|
45
|
+
subscribe(listener) {
|
|
46
|
+
if (this.disposed)
|
|
47
|
+
return () => { };
|
|
48
|
+
this.listeners.add(listener);
|
|
49
|
+
return () => {
|
|
50
|
+
this.listeners.delete(listener);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The first load. Call once; refresh() for later loads. */
|
|
54
|
+
async load() {
|
|
55
|
+
if (this.disposed)
|
|
56
|
+
return;
|
|
57
|
+
if (this.path)
|
|
58
|
+
this.unregister ?? (this.unregister = registerFilesRefresher(this.bucket, () => this.run()));
|
|
59
|
+
await this.run();
|
|
60
|
+
}
|
|
61
|
+
/** Read it again. The file stays shown while it runs, and stays if it fails. */
|
|
62
|
+
async refresh() {
|
|
63
|
+
if (this.disposed)
|
|
64
|
+
return;
|
|
65
|
+
this.set({ refreshing: true });
|
|
66
|
+
await this.run();
|
|
67
|
+
}
|
|
68
|
+
/** Stops all updates. Results still in flight are dropped when they land. */
|
|
69
|
+
dispose() {
|
|
70
|
+
this.disposed = true;
|
|
71
|
+
this.unregister?.();
|
|
72
|
+
this.unregister = null;
|
|
73
|
+
this.listeners.clear();
|
|
74
|
+
}
|
|
75
|
+
async run() {
|
|
76
|
+
if (this.disposed)
|
|
77
|
+
return;
|
|
78
|
+
const gen = ++this.generation;
|
|
79
|
+
if (!this.path) {
|
|
80
|
+
this.set({ status: 'notFound', file: null, error: null, refreshing: false });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const file = await this.storage.getFileInfo(this.bucket, this.path);
|
|
85
|
+
if (this.isStale(gen))
|
|
86
|
+
return;
|
|
87
|
+
this.set({ status: 'loaded', file, error: null, refreshing: false });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
if (this.isStale(gen))
|
|
91
|
+
return;
|
|
92
|
+
if (isNotFound(e))
|
|
93
|
+
this.set({ status: 'notFound', file: null, error: null, refreshing: false });
|
|
94
|
+
// Only a first load with nothing to show is the error state.
|
|
95
|
+
else if (this.state.file === null)
|
|
96
|
+
this.set({ status: 'error', file: null, error: e, refreshing: false });
|
|
97
|
+
else
|
|
98
|
+
this.set({ refreshing: false });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
isStale(gen) {
|
|
102
|
+
return this.disposed || gen !== this.generation;
|
|
103
|
+
}
|
|
104
|
+
set(change) {
|
|
105
|
+
if (this.disposed)
|
|
106
|
+
return;
|
|
107
|
+
this.state = Object.freeze({ ...this.state, ...change });
|
|
108
|
+
for (const l of [...this.listeners])
|
|
109
|
+
l();
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export type { KoolbaseUploadProgress, KoolbaseUploadState } from './storage-tran
|
|
|
25
25
|
export type { KoolbaseFileInfo, KoolbaseFilePage, KoolbaseFileSort, ListFilesOptions } from './storage-files.js';
|
|
26
26
|
export { KoolbaseFilesController, initialFilesState, filesQueryKey } from './files-controller.js';
|
|
27
27
|
export type { FilesQuery, FilesState, FilesStatus } from './files-controller.js';
|
|
28
|
+
export { KoolbaseFileController, initialFileState } from './file-controller.js';
|
|
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';
|
|
28
32
|
export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
|
|
29
33
|
export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
|
|
30
34
|
export type { FiscalIntentResult, FiscalStatus } from './fiscal.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -25,6 +25,10 @@ export { KoolbaseStorage } from './storage.js';
|
|
|
25
25
|
export { KoolbaseUploadTask } from './storage-transfer.js';
|
|
26
26
|
// A bucket's files as list state (storage S4).
|
|
27
27
|
export { KoolbaseFilesController, initialFilesState, filesQueryKey } from './files-controller.js';
|
|
28
|
+
// One file's details as state (storage S4).
|
|
29
|
+
export { KoolbaseFileController, initialFileState } from './file-controller.js';
|
|
30
|
+
// Several uploads in a queue (storage S4).
|
|
31
|
+
export { KoolbaseUploadQueue, initialUploadQueueState } from './upload-queue.js';
|
|
28
32
|
export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
|
|
29
33
|
export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
|
|
30
34
|
export { KoolbaseMessaging } from './messaging.js';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createUseCollection, type UseCollectionResult } from './use-collection.js';
|
|
2
2
|
export { createUseRecord, type UseRecordResult } from './use-record.js';
|
|
3
3
|
export { createUseFiles, type UseFilesResult } from './use-files.js';
|
|
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
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
export { createUseCollection } from './use-collection.js';
|
|
6
6
|
export { createUseRecord } from './use-record.js';
|
|
7
7
|
export { createUseFiles } from './use-files.js';
|
|
8
|
+
export { createUseFile } from './use-file.js';
|
|
9
|
+
export { createUseUploadQueue } from './use-upload-queue.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type FileState, type KoolbaseStorage } from '../index.js';
|
|
2
|
+
export interface UseFileResult extends FileState {
|
|
3
|
+
/** Read it again. The file stays shown while it runs, and stays if it fails. */
|
|
4
|
+
refresh: () => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createUseFile(getStorage: () => Pick<KoolbaseStorage, 'getFileInfo'>): (bucket: string, path: string | null | undefined) => UseFileResult;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// useFile: one file's details as React state (storage S4).
|
|
2
|
+
//
|
|
3
|
+
// A thin wrapper. Everything about getting the file right lives in
|
|
4
|
+
// @koolbase/core's KoolbaseFileController, which is tested there; this file
|
|
5
|
+
// only ties one controller's lifetime to a component's.
|
|
6
|
+
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
|
|
7
|
+
import { KoolbaseFileController, initialFileState } from '../index.js';
|
|
8
|
+
const noop = () => { };
|
|
9
|
+
const done = () => Promise.resolve();
|
|
10
|
+
export function createUseFile(getStorage) {
|
|
11
|
+
return function useFile(bucket, path) {
|
|
12
|
+
const p = path ?? '';
|
|
13
|
+
const key = JSON.stringify([bucket, p]);
|
|
14
|
+
const [current, setCurrent] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// Created here, not during render, so React strict mode's second run
|
|
17
|
+
// never keeps a disposed controller.
|
|
18
|
+
const controller = new KoolbaseFileController(getStorage(), bucket, p);
|
|
19
|
+
setCurrent({ key, controller });
|
|
20
|
+
void controller.load();
|
|
21
|
+
return () => controller.dispose();
|
|
22
|
+
}, [key]);
|
|
23
|
+
// A controller from the previous file reads as a fresh load, never as
|
|
24
|
+
// the previous file.
|
|
25
|
+
const controller = current?.key === key ? current.controller : null;
|
|
26
|
+
const subscribe = useCallback((onChange) => (controller ? controller.subscribe(onChange) : noop), [controller]);
|
|
27
|
+
const getSnapshot = useCallback(() => (controller ? controller.getState() : initialFileState), [controller]);
|
|
28
|
+
const state = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
29
|
+
const refresh = useCallback(() => (controller ? controller.refresh() : done()), [controller]);
|
|
30
|
+
return { ...state, refresh };
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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.21.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",
|