@koolbase/core 12.17.0 → 12.19.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.
Files changed (37) hide show
  1. package/dist/cjs/files-controller.d.ts +62 -0
  2. package/dist/cjs/files-controller.js +160 -0
  3. package/dist/cjs/files-refresh.d.ts +9 -0
  4. package/dist/cjs/files-refresh.js +39 -0
  5. package/dist/cjs/index.d.ts +2 -0
  6. package/dist/cjs/index.js +6 -1
  7. package/dist/cjs/react/index.d.ts +1 -0
  8. package/dist/cjs/react/index.js +3 -1
  9. package/dist/cjs/react/use-files.d.ts +8 -0
  10. package/dist/cjs/react/use-files.js +37 -0
  11. package/dist/cjs/storage-errors.d.ts +14 -0
  12. package/dist/cjs/storage-errors.js +29 -1
  13. package/dist/cjs/storage-multipart.d.ts +29 -0
  14. package/dist/cjs/storage-multipart.js +168 -0
  15. package/dist/cjs/storage-transfer.d.ts +25 -6
  16. package/dist/cjs/storage-transfer.js +78 -37
  17. package/dist/cjs/storage.d.ts +40 -0
  18. package/dist/cjs/storage.js +205 -15
  19. package/dist/esm/files-controller.d.ts +62 -0
  20. package/dist/esm/files-controller.js +155 -0
  21. package/dist/esm/files-refresh.d.ts +9 -0
  22. package/dist/esm/files-refresh.js +34 -0
  23. package/dist/esm/index.d.ts +2 -0
  24. package/dist/esm/index.js +2 -0
  25. package/dist/esm/react/index.d.ts +1 -0
  26. package/dist/esm/react/index.js +1 -0
  27. package/dist/esm/react/use-files.d.ts +8 -0
  28. package/dist/esm/react/use-files.js +34 -0
  29. package/dist/esm/storage-errors.d.ts +14 -0
  30. package/dist/esm/storage-errors.js +26 -0
  31. package/dist/esm/storage-multipart.d.ts +29 -0
  32. package/dist/esm/storage-multipart.js +164 -0
  33. package/dist/esm/storage-transfer.d.ts +25 -6
  34. package/dist/esm/storage-transfer.js +77 -37
  35. package/dist/esm/storage.d.ts +40 -0
  36. package/dist/esm/storage.js +205 -15
  37. package/package.json +1 -1
@@ -33,6 +33,7 @@ export declare class KoolbaseUploadTask {
33
33
  private cancelRequested;
34
34
  private abortTransfer;
35
35
  private total;
36
+ private _uploadId;
36
37
  /**
37
38
  * The uploaded file. Rejects with `KoolbaseUploadCancelledError` if it was
38
39
  * cancelled, or the error that made it fail.
@@ -44,6 +45,15 @@ export declare class KoolbaseUploadTask {
44
45
  constructor();
45
46
  /** Where the upload is now. */
46
47
  get current(): KoolbaseUploadProgress;
48
+ /**
49
+ * A large file's upload goes in parts, and can be resumed for a day: keep
50
+ * this id, and pass it to `KoolbaseStorage.resumeUpload` with the same
51
+ * file after the app restarts or the upload fails. Null for a file sent
52
+ * in one piece, and until the upload has begun.
53
+ */
54
+ get uploadId(): string | null;
55
+ /** @internal */
56
+ setUploadId(id: string): void;
47
57
  /**
48
58
  * Calls `listener` on every change of state and every chunk sent, starting
49
59
  * with where the upload is now. Returns a function that stops it.
@@ -74,15 +84,24 @@ export declare class KoolbaseUploadTask {
74
84
  }
75
85
  /** Runs `body` as the task's upload, starting on the next turn so a caller can listen to every event. */
76
86
  export declare function runUploadTask(body: (task: KoolbaseUploadTask) => Promise<UploadResult>): KoolbaseUploadTask;
77
- interface PutResult {
87
+ export interface PutResult {
78
88
  status: number;
79
89
  etag: string;
80
90
  }
91
+ /** A PUT under way: its outcome, and how to stop it. */
92
+ export interface Transfer {
93
+ done: Promise<PutResult>;
94
+ abort(): void;
95
+ }
96
+ /**
97
+ * PUTs `body` to `url`, reporting bytes as they are sent. XMLHttpRequest
98
+ * where the host has it (browsers and React Native both report upload
99
+ * progress); fetch elsewhere, where progress jumps from 0 to the total.
100
+ * An aborted transfer rejects with `KoolbaseUploadCancelledError`.
101
+ */
102
+ export declare function transferBytes(url: string, body: Blob, contentType: string | null, onSent: (sent: number) => void): Transfer;
81
103
  /**
82
- * Sends `body` to a presigned `url` with PUT, reporting bytes as they are
83
- * sent. XMLHttpRequest where the host has it (browsers and React Native,
84
- * both report upload progress); fetch elsewhere, where progress jumps from
85
- * 0 to the total. A cancel aborts it.
104
+ * Sends `body` to a presigned `url` with PUT as the task's one transfer,
105
+ * reporting bytes as they are sent; a cancel aborts it.
86
106
  */
87
107
  export declare function putWithProgress(task: KoolbaseUploadTask, url: string, body: Blob, contentType: string): Promise<PutResult>;
88
- export {};
@@ -9,6 +9,7 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.KoolbaseUploadTask = void 0;
11
11
  exports.runUploadTask = runUploadTask;
12
+ exports.transferBytes = transferBytes;
12
13
  exports.putWithProgress = putWithProgress;
13
14
  const network_js_1 = require("./network.js");
14
15
  const storage_errors_js_1 = require("./storage-errors.js");
@@ -37,6 +38,7 @@ class KoolbaseUploadTask {
37
38
  this.cancelRequested = false;
38
39
  this.abortTransfer = null;
39
40
  this.total = 0;
41
+ this._uploadId = null;
40
42
  this._current = progressOf('pending', 0, 0);
41
43
  this.result = new Promise((res, rej) => {
42
44
  this.resolve = res;
@@ -50,6 +52,19 @@ class KoolbaseUploadTask {
50
52
  get current() {
51
53
  return this._current;
52
54
  }
55
+ /**
56
+ * A large file's upload goes in parts, and can be resumed for a day: keep
57
+ * this id, and pass it to `KoolbaseStorage.resumeUpload` with the same
58
+ * file after the app restarts or the upload fails. Null for a file sent
59
+ * in one piece, and until the upload has begun.
60
+ */
61
+ get uploadId() {
62
+ return this._uploadId;
63
+ }
64
+ /** @internal */
65
+ setUploadId(id) {
66
+ this._uploadId = id;
67
+ }
53
68
  /**
54
69
  * Calls `listener` on every change of state and every chunk sent, starting
55
70
  * with where the upload is now. Returns a function that stops it.
@@ -145,70 +160,96 @@ function runUploadTask(body) {
145
160
  return task;
146
161
  }
147
162
  /**
148
- * Sends `body` to a presigned `url` with PUT, reporting bytes as they are
149
- * sent. XMLHttpRequest where the host has it (browsers and React Native,
150
- * both report upload progress); fetch elsewhere, where progress jumps from
151
- * 0 to the total. A cancel aborts it.
163
+ * PUTs `body` to `url`, reporting bytes as they are sent. XMLHttpRequest
164
+ * where the host has it (browsers and React Native both report upload
165
+ * progress); fetch elsewhere, where progress jumps from 0 to the total.
166
+ * An aborted transfer rejects with `KoolbaseUploadCancelledError`.
152
167
  */
153
- function putWithProgress(task, url, body, contentType) {
154
- task.checkCancelled();
155
- const total = body.size;
156
- const report = (sent) => {
157
- if (!task.cancelled)
158
- task.emit('uploading', Math.min(sent, total));
159
- };
160
- report(0);
168
+ function transferBytes(url, body, contentType, onSent) {
161
169
  const XHR = globalThis.XMLHttpRequest;
162
170
  if (XHR) {
163
- return new Promise((resolve, reject) => {
164
- const xhr = new XHR();
165
- let settled = false;
166
- const done = (fn) => {
171
+ const xhr = new XHR();
172
+ let settled = false;
173
+ let fail = () => undefined;
174
+ const done = new Promise((resolve, reject) => {
175
+ const finish = (fn) => {
167
176
  if (settled)
168
177
  return;
169
178
  settled = true;
170
- task.setAbort(null);
171
179
  fn();
172
180
  };
181
+ fail = (e) => finish(() => reject(e));
173
182
  xhr.open('PUT', url);
174
- xhr.setRequestHeader('Content-Type', contentType);
183
+ if (contentType)
184
+ xhr.setRequestHeader('Content-Type', contentType);
175
185
  if (xhr.upload) {
176
- xhr.upload.onprogress = (ev) => report(ev.loaded);
186
+ xhr.upload.onprogress = (ev) => onSent(Math.min(ev.loaded, body.size));
177
187
  }
178
- xhr.onload = () => done(() => {
179
- if (task.cancelled)
180
- return reject(new storage_errors_js_1.KoolbaseUploadCancelledError());
181
- resolve({ status: xhr.status, etag: xhr.getResponseHeader('etag') ?? '' });
182
- });
183
- xhr.onerror = () => done(() => reject(task.cancelled ? new storage_errors_js_1.KoolbaseUploadCancelledError() : new network_js_1.KoolbaseNetworkError(url, new Error('the upload did not complete'))));
184
- xhr.onabort = () => done(() => reject(new storage_errors_js_1.KoolbaseUploadCancelledError()));
185
- task.setAbort(() => {
186
- xhr.abort();
187
- // Some hosts do not fire onabort for an aborted send.
188
- done(() => reject(new storage_errors_js_1.KoolbaseUploadCancelledError()));
189
- });
188
+ xhr.onload = () => finish(() => resolve({ status: xhr.status, etag: xhr.getResponseHeader('etag') ?? '' }));
189
+ xhr.onerror = () => fail(new network_js_1.KoolbaseNetworkError(url, new Error('the upload did not complete')));
190
+ xhr.onabort = () => fail(new storage_errors_js_1.KoolbaseUploadCancelledError());
191
+ onSent(0);
190
192
  xhr.send(body);
191
193
  });
194
+ return {
195
+ done,
196
+ abort: () => {
197
+ xhr.abort();
198
+ // Some hosts do not fire onabort for an aborted send.
199
+ fail(new storage_errors_js_1.KoolbaseUploadCancelledError());
200
+ },
201
+ };
192
202
  }
193
203
  const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
194
- task.setAbort(() => controller?.abort());
195
- return fetch(url, {
204
+ let aborted = false;
205
+ onSent(0);
206
+ const done = fetch(url, {
196
207
  method: 'PUT',
197
- headers: { 'Content-Type': contentType },
208
+ headers: contentType ? { 'Content-Type': contentType } : {},
198
209
  body,
199
210
  signal: controller?.signal,
200
211
  })
201
212
  .then((res) => {
202
- task.checkCancelled();
213
+ if (aborted)
214
+ throw new storage_errors_js_1.KoolbaseUploadCancelledError();
215
+ onSent(body.size);
203
216
  return { status: res.status, etag: res.headers.get('etag') ?? '' };
204
217
  })
205
218
  .catch((e) => {
206
- if (task.cancelled || e?.name === 'AbortError') {
219
+ if (aborted || e?.name === 'AbortError')
207
220
  throw new storage_errors_js_1.KoolbaseUploadCancelledError();
208
- }
209
221
  if (e instanceof storage_errors_js_1.KoolbaseStorageError)
210
222
  throw e;
211
223
  throw new network_js_1.KoolbaseNetworkError(url, e);
224
+ });
225
+ return {
226
+ done,
227
+ abort: () => {
228
+ aborted = true;
229
+ controller?.abort();
230
+ },
231
+ };
232
+ }
233
+ /**
234
+ * Sends `body` to a presigned `url` with PUT as the task's one transfer,
235
+ * reporting bytes as they are sent; a cancel aborts it.
236
+ */
237
+ function putWithProgress(task, url, body, contentType) {
238
+ task.checkCancelled();
239
+ const total = body.size;
240
+ const report = (sent) => {
241
+ if (!task.cancelled)
242
+ task.emit('uploading', Math.min(sent, total));
243
+ };
244
+ const t = transferBytes(url, body, contentType, report);
245
+ task.setAbort(() => t.abort());
246
+ return t.done
247
+ .then((r) => {
248
+ task.checkCancelled();
249
+ return r;
250
+ })
251
+ .catch((e) => {
252
+ throw task.cancelled ? new storage_errors_js_1.KoolbaseUploadCancelledError() : e;
212
253
  })
213
254
  .finally(() => task.setAbort(null));
214
255
  }
@@ -72,6 +72,46 @@ export declare class KoolbaseStorage {
72
72
  */
73
73
  startUpload(options: UploadOptions): KoolbaseUploadTask;
74
74
  private runUpload;
75
+ /**
76
+ * Files larger than this go up in parts (S3): each part retried on its
77
+ * own, the upload resumable for a day with `resumeUpload`. 16 MiB.
78
+ */
79
+ static multipartThreshold: number;
80
+ /**
81
+ * Resumes a large file's upload that failed or was interrupted -- the
82
+ * network dropped, the app was closed -- sending only the parts that did
83
+ * not arrive. `uploadId` is the failed task's `uploadId`; `file` must be
84
+ * the same file. Within a day of the upload beginning.
85
+ *
86
+ * @example
87
+ * const task = Koolbase.storage.startUpload({ bucket: 'video', path: 'talk.mp4', file });
88
+ * saveForLater(task.uploadId); // once the upload has begun
89
+ * // ... after a restart:
90
+ * await Koolbase.storage.resumeUpload({ bucket: 'video', uploadId, file }).result;
91
+ */
92
+ resumeUpload(options: {
93
+ bucket: string;
94
+ uploadId: string;
95
+ file: UploadOptions['file'];
96
+ metadata?: Record<string, string>;
97
+ overwrite?: boolean;
98
+ }): KoolbaseUploadTask;
99
+ private runInParts;
100
+ /**
101
+ * Downloads a file's bytes, reporting them as they arrive (S3):
102
+ * `onProgress(received, total)`, total null when the size is not known.
103
+ * Aborting `signal` stops it with `KoolbaseDownloadCancelledError`.
104
+ *
105
+ * @example
106
+ * const blob = await Koolbase.storage.download({ bucket: 'video', path: 'talk.mp4', onProgress: (r, t) => setPct(t ? r / t : 0) });
107
+ */
108
+ download(options: {
109
+ bucket: string;
110
+ path: string;
111
+ versionId?: string;
112
+ onProgress?: (received: number, total: number | null) => void;
113
+ signal?: AbortSignal;
114
+ }): Promise<Blob>;
75
115
  /**
76
116
  * One page of a bucket's files. The bucket's view rule is applied to every
77
117
  * file: the page holds only files the signed-in person may view. Pass the
@@ -6,7 +6,16 @@ const network_js_1 = require("./network.js");
6
6
  const errors_js_1 = require("./errors.js");
7
7
  const storage_errors_js_2 = require("./storage-errors.js");
8
8
  const storage_transfer_js_1 = require("./storage-transfer.js");
9
+ const storage_multipart_js_1 = require("./storage-multipart.js");
10
+ const storage_errors_js_3 = require("./storage-errors.js");
11
+ const files_refresh_js_1 = require("./files-refresh.js");
12
+ const network_js_2 = require("./network.js");
9
13
  const storage_files_js_1 = require("./storage-files.js");
14
+ /** An upload task whose success reloads the bucket's file lists (storage S4). */
15
+ function listsReload(task, bucket) {
16
+ task.result.then(() => { void (0, files_refresh_js_1.refreshBucketFiles)(bucket); }, () => { });
17
+ return task;
18
+ }
10
19
  // --- Cloudflare image-transform URL helpers -------------------------------
11
20
  // Module-private — callers use KoolbaseStorage.publicUrl / publicUrlForObject.
12
21
  function clampInt(v, min, max) {
@@ -129,7 +138,7 @@ class KoolbaseStorage {
129
138
  * const { object } = await task.result;
130
139
  */
131
140
  startUpload(options) {
132
- return (0, storage_transfer_js_1.runUploadTask)((task) => this.runUpload(options, task));
141
+ return listsReload((0, storage_transfer_js_1.runUploadTask)((task) => this.runUpload(options, task)), options.bucket);
133
142
  }
134
143
  async runUpload(options, task) {
135
144
  task.checkCancelled();
@@ -144,6 +153,19 @@ class KoolbaseStorage {
144
153
  : options.file.type;
145
154
  if (isBlob)
146
155
  task.setTotal(options.file.size);
156
+ // A browser hands us the bytes already: a File from an input is a Blob,
157
+ // and fetching its .uri would be fetching undefined. React Native hands
158
+ // us a local URI, and its fetch resolves that into a Blob -- read first,
159
+ // since its size decides how it is sent (S3).
160
+ const fileBlob = isBlob
161
+ ? options.file
162
+ : await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
163
+ task.setTotal(fileBlob.size);
164
+ task.checkCancelled();
165
+ // Large files go in parts: resumable, each part retried (S3).
166
+ if (fileBlob.size > KoolbaseStorage.multipartThreshold) {
167
+ return this.runInParts(options.bucket, options, task, fileBlob, contentType, null);
168
+ }
147
169
  // ─── Step 1: Get presigned upload URL ───
148
170
  const urlRes = await (0, network_js_1.sessionFetch)(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
149
171
  method: 'POST',
@@ -165,16 +187,7 @@ class KoolbaseStorage {
165
187
  task.checkCancelled();
166
188
  // ─── Step 2: Upload directly to R2 ───
167
189
  // R2 presigned URLs expect raw binary, NOT multipart/form-data.
168
- //
169
- // A browser hands us the bytes already: a File from an input is a Blob,
170
- // and fetching its .uri would be fetching undefined. React Native hands
171
- // us a local URI, and its fetch resolves that into a Blob — which is why
172
- // the two hosts differ here and nowhere else in this method.
173
- const fileBlob = isBlob
174
- ? options.file
175
- : await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
176
190
  const fileSize = fileBlob.size;
177
- task.setTotal(fileSize);
178
191
  const uploadRes = await (0, storage_transfer_js_1.putWithProgress)(task, upload_url, fileBlob, contentType);
179
192
  if (uploadRes.status < 200 || uploadRes.status >= 300) {
180
193
  // R2 PUT errors don't follow the Koolbase error shape — surface as a
@@ -222,6 +235,173 @@ class KoolbaseStorage {
222
235
  const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
223
236
  return { object, downloadUrl };
224
237
  }
238
+ /**
239
+ * Resumes a large file's upload that failed or was interrupted -- the
240
+ * network dropped, the app was closed -- sending only the parts that did
241
+ * not arrive. `uploadId` is the failed task's `uploadId`; `file` must be
242
+ * the same file. Within a day of the upload beginning.
243
+ *
244
+ * @example
245
+ * const task = Koolbase.storage.startUpload({ bucket: 'video', path: 'talk.mp4', file });
246
+ * saveForLater(task.uploadId); // once the upload has begun
247
+ * // ... after a restart:
248
+ * await Koolbase.storage.resumeUpload({ bucket: 'video', uploadId, file }).result;
249
+ */
250
+ resumeUpload(options) {
251
+ return listsReload((0, storage_transfer_js_1.runUploadTask)(async (task) => {
252
+ const isBlob = typeof Blob !== 'undefined' && options.file instanceof Blob;
253
+ const blob = isBlob
254
+ ? options.file
255
+ : await (await (0, network_js_1.sessionFetch)(options.file.uri)).blob();
256
+ task.setTotal(blob.size);
257
+ return this.runInParts(options.bucket, options, task, blob, '', options.uploadId);
258
+ }), options.bucket);
259
+ }
260
+ async runInParts(bucket, options, task, file, contentType, resumeId) {
261
+ const base = `${this.config.baseUrl}/v1/sdk/storage/uploads`;
262
+ const json = async () => ({ ...(await this.buildHeaders()), 'Content-Type': 'application/json' });
263
+ const overwrite = options.overwrite ?? false;
264
+ let plan;
265
+ if (resumeId) {
266
+ const res = await (0, network_js_1.sessionFetch)(`${base}?bucket=${encodeURIComponent(bucket)}&upload_id=${encodeURIComponent(resumeId)}`, {
267
+ headers: await this.buildHeaders(),
268
+ });
269
+ if (!res.ok)
270
+ throw await this.error(res, 'Failed to resume the upload');
271
+ plan = (await res.json());
272
+ if (plan.size !== file.size) {
273
+ throw new storage_errors_js_2.KoolbaseStorageError(`This is not the file the upload began with: it has ${file.size} bytes, the upload ${plan.size}`, 'validation_error');
274
+ }
275
+ }
276
+ else {
277
+ const res = await (0, network_js_1.sessionFetch)(base, {
278
+ method: 'POST',
279
+ headers: await json(),
280
+ body: JSON.stringify({ bucket, path: options.path, content_type: contentType, size: file.size, overwrite }),
281
+ });
282
+ if (!res.ok)
283
+ throw await this.error(res, 'Failed to begin the upload');
284
+ plan = (await res.json());
285
+ }
286
+ task.setUploadId(plan.upload_id);
287
+ task.checkCancelled();
288
+ await (0, storage_multipart_js_1.sendParts)(task, plan, file, {
289
+ links: async (parts) => {
290
+ const res = await (0, network_js_1.sessionFetch)(`${base}/parts`, {
291
+ method: 'POST',
292
+ headers: await json(),
293
+ body: JSON.stringify({ bucket, upload_id: plan.upload_id, parts }),
294
+ });
295
+ if (!res.ok)
296
+ throw await this.error(res, 'Failed to get upload links');
297
+ return (await res.json()).links;
298
+ },
299
+ abort: async () => {
300
+ await (0, network_js_1.sessionFetch)(base, {
301
+ method: 'DELETE',
302
+ headers: await json(),
303
+ body: JSON.stringify({ bucket, upload_id: plan.upload_id }),
304
+ });
305
+ },
306
+ });
307
+ // The parts have arrived: the server joins and checks them. No longer
308
+ // cancellable.
309
+ task.verifying();
310
+ const body = { bucket, upload_id: plan.upload_id, overwrite };
311
+ if (options.metadata !== undefined)
312
+ body.metadata = options.metadata;
313
+ const done = await (0, network_js_1.sessionFetch)(`${base}/complete`, { method: 'POST', headers: await json(), body: JSON.stringify(body) });
314
+ if (!done.ok)
315
+ throw await this.error(done, 'Failed to complete the upload');
316
+ const object = mapObjectFromServer(await done.json());
317
+ const downloadUrl = await this.getDownloadUrl(bucket, object.path);
318
+ return { object, downloadUrl };
319
+ }
320
+ /**
321
+ * Downloads a file's bytes, reporting them as they arrive (S3):
322
+ * `onProgress(received, total)`, total null when the size is not known.
323
+ * Aborting `signal` stops it with `KoolbaseDownloadCancelledError`.
324
+ *
325
+ * @example
326
+ * const blob = await Koolbase.storage.download({ bucket: 'video', path: 'talk.mp4', onProgress: (r, t) => setPct(t ? r / t : 0) });
327
+ */
328
+ async download(options) {
329
+ if (options.signal?.aborted)
330
+ throw new storage_errors_js_3.KoolbaseDownloadCancelledError();
331
+ const url = await this.getDownloadUrl(options.bucket, options.path, options.versionId);
332
+ const report = options.onProgress ?? (() => undefined);
333
+ const XHR = globalThis.XMLHttpRequest;
334
+ if (XHR) {
335
+ return new Promise((resolve, reject) => {
336
+ const xhr = new XHR();
337
+ let settled = false;
338
+ const finish = (fn) => {
339
+ if (settled)
340
+ return;
341
+ settled = true;
342
+ options.signal?.removeEventListener('abort', stop);
343
+ fn();
344
+ };
345
+ const stop = () => {
346
+ xhr.abort();
347
+ finish(() => reject(new storage_errors_js_3.KoolbaseDownloadCancelledError()));
348
+ };
349
+ options.signal?.addEventListener('abort', stop);
350
+ xhr.open('GET', url);
351
+ xhr.responseType = 'blob';
352
+ xhr.onprogress = (ev) => report(ev.loaded, ev.lengthComputable ? ev.total : null);
353
+ xhr.onload = () => finish(() => {
354
+ if (xhr.status < 200 || xhr.status >= 300) {
355
+ reject(new storage_errors_js_2.KoolbaseStorageError(`Download failed: ${xhr.status}`));
356
+ return;
357
+ }
358
+ const blob = xhr.response;
359
+ report(blob.size, blob.size);
360
+ resolve(blob);
361
+ });
362
+ xhr.onerror = () => finish(() => reject(new network_js_2.KoolbaseNetworkError(url, new Error('the download did not complete'))));
363
+ xhr.onabort = () => finish(() => reject(new storage_errors_js_3.KoolbaseDownloadCancelledError()));
364
+ xhr.send();
365
+ });
366
+ }
367
+ let res;
368
+ try {
369
+ res = await fetch(url, { signal: options.signal });
370
+ }
371
+ catch (e) {
372
+ if (options.signal?.aborted)
373
+ throw new storage_errors_js_3.KoolbaseDownloadCancelledError();
374
+ throw new network_js_2.KoolbaseNetworkError(url, e);
375
+ }
376
+ if (!res.ok)
377
+ throw new storage_errors_js_2.KoolbaseStorageError(`Download failed: ${res.status}`);
378
+ const total = Number(res.headers.get('content-length')) || null;
379
+ const reader = res.body?.getReader?.();
380
+ if (!reader) {
381
+ const blob = await res.blob();
382
+ report(blob.size, blob.size);
383
+ return blob;
384
+ }
385
+ const chunks = [];
386
+ let received = 0;
387
+ report(0, total);
388
+ try {
389
+ for (;;) {
390
+ const { done, value } = await reader.read();
391
+ if (done)
392
+ break;
393
+ chunks.push(value);
394
+ received += value.length;
395
+ report(received, total);
396
+ }
397
+ }
398
+ catch (e) {
399
+ if (options.signal?.aborted)
400
+ throw new storage_errors_js_3.KoolbaseDownloadCancelledError();
401
+ throw new network_js_2.KoolbaseNetworkError(url, e);
402
+ }
403
+ return new Blob(chunks, { type: res.headers.get('content-type') ?? '' });
404
+ }
225
405
  /**
226
406
  * One page of a bucket's files. The bucket's view rule is applied to every
227
407
  * file: the page holds only files the signed-in person may view. Pass the
@@ -292,7 +472,9 @@ class KoolbaseStorage {
292
472
  if (!res.ok) {
293
473
  throw await this.error(res, op === 'move' ? 'Failed to move the file' : 'Failed to copy the file');
294
474
  }
295
- return mapObjectFromServer(await res.json());
475
+ const moved = mapObjectFromServer(await res.json());
476
+ void (0, files_refresh_js_1.refreshBucketFiles)(o.bucket, ...(o.toBucket && o.toBucket !== o.bucket ? [o.toBucket] : []));
477
+ return moved;
296
478
  }
297
479
  /**
298
480
  * Apply a partial metadata update to an existing object. Returns the
@@ -340,6 +522,7 @@ class KoolbaseStorage {
340
522
  throw await this.error(res, 'Failed to update metadata');
341
523
  }
342
524
  const raw = await res.json();
525
+ void (0, files_refresh_js_1.refreshBucketFiles)(bucket);
343
526
  return mapObjectFromServer(raw);
344
527
  }
345
528
  /**
@@ -471,11 +654,11 @@ class KoolbaseStorage {
471
654
  },
472
655
  body: JSON.stringify({ bucket, path }),
473
656
  });
474
- if (res.status === 204)
657
+ if (res.ok) {
658
+ void (0, files_refresh_js_1.refreshBucketFiles)(bucket);
475
659
  return;
476
- if (!res.ok) {
477
- throw await this.error(res, 'Failed to delete file');
478
660
  }
661
+ throw await this.error(res, 'Failed to delete file');
479
662
  }
480
663
  /**
481
664
  * List all versions of a file path, newest-first. Returns a flat list
@@ -532,7 +715,9 @@ class KoolbaseStorage {
532
715
  if (!res.ok) {
533
716
  throw await this.error(res, 'Failed to restore version');
534
717
  }
535
- return mapObjectFromServer(await res.json());
718
+ const restored = mapObjectFromServer(await res.json());
719
+ void (0, files_refresh_js_1.refreshBucketFiles)(bucket);
720
+ return restored;
536
721
  }
537
722
  /**
538
723
  * Hard-remove a single history version — both the metadata row and
@@ -555,6 +740,11 @@ class KoolbaseStorage {
555
740
  }
556
741
  }
557
742
  exports.KoolbaseStorage = KoolbaseStorage;
743
+ /**
744
+ * Files larger than this go up in parts (S3): each part retried on its
745
+ * own, the upload resumable for a day with `resumeUpload`. 16 MiB.
746
+ */
747
+ KoolbaseStorage.multipartThreshold = 16 * 1024 * 1024;
558
748
  /**
559
749
  * Maps the snake_case server JSON to the camelCase {@link KoolbaseObject}.
560
750
  * Defensive: missing or null `metadata` (older / non-Koolbase responses)
@@ -0,0 +1,62 @@
1
+ import type { KoolbaseFileInfo, KoolbaseFilePage, KoolbaseFileSort, ListFilesOptions } from './storage-files.js';
2
+ export type FilesStatus = 'loading' | 'loaded' | 'error';
3
+ /** Which files, in what order. The bucket's view rule decides the rest. */
4
+ export interface FilesQuery {
5
+ /** Only files whose path starts with this, taken literally. */
6
+ prefix?: string;
7
+ /** By path unless set. */
8
+ sort?: KoolbaseFileSort;
9
+ /** Z to A, newest first, largest first. */
10
+ descending?: boolean;
11
+ /** Files per page: 50 unless set, at most 100. */
12
+ limit?: number;
13
+ }
14
+ export interface FilesState {
15
+ readonly status: FilesStatus;
16
+ readonly files: readonly KoolbaseFileInfo[];
17
+ /** Why the first load failed, while status is 'error'. */
18
+ readonly error: unknown;
19
+ /** True while refresh() runs. */
20
+ readonly refreshing: boolean;
21
+ /** True while loadMore() runs. */
22
+ readonly loadingMore: boolean;
23
+ /** Whether there is a page past the last one loaded. True before the first load. */
24
+ readonly hasMore: boolean;
25
+ }
26
+ export declare const initialFilesState: FilesState;
27
+ /** One string per distinct listing. */
28
+ export declare function filesQueryKey(bucket: string, query?: FilesQuery): string;
29
+ type FilesReader = {
30
+ listFiles(options: ListFilesOptions): Promise<KoolbaseFilePage>;
31
+ };
32
+ export declare class KoolbaseFilesController {
33
+ readonly bucket: string;
34
+ private readonly storage;
35
+ private readonly spec;
36
+ private state;
37
+ private readonly listeners;
38
+ private generation;
39
+ private disposed;
40
+ private pageOne;
41
+ private laterPages;
42
+ private cursor;
43
+ private unregister;
44
+ constructor(storage: FilesReader, bucket: string, query?: FilesQuery);
45
+ getState(): FilesState;
46
+ /** Called on every change. Returns the unsubscribe. */
47
+ subscribe(listener: () => void): () => void;
48
+ /** The first load. Call once; refresh() for later loads. */
49
+ load(): Promise<void>;
50
+ /** Page one again. What is shown stays while it runs, and stays if it fails. */
51
+ refresh(): Promise<void>;
52
+ /** The next page, appended. Does nothing unless loaded with a page to come. */
53
+ loadMore(): Promise<void>;
54
+ /** Stops all updates. Results still in flight are dropped when they land. */
55
+ dispose(): void;
56
+ private firstPage;
57
+ private publish;
58
+ private options;
59
+ private isStale;
60
+ private set;
61
+ }
62
+ export {};