@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
@@ -32,6 +32,7 @@ export class KoolbaseUploadTask {
32
32
  this.cancelRequested = false;
33
33
  this.abortTransfer = null;
34
34
  this.total = 0;
35
+ this._uploadId = null;
35
36
  this._current = progressOf('pending', 0, 0);
36
37
  this.result = new Promise((res, rej) => {
37
38
  this.resolve = res;
@@ -45,6 +46,19 @@ export class KoolbaseUploadTask {
45
46
  get current() {
46
47
  return this._current;
47
48
  }
49
+ /**
50
+ * A large file's upload goes in parts, and can be resumed for a day: keep
51
+ * this id, and pass it to `KoolbaseStorage.resumeUpload` with the same
52
+ * file after the app restarts or the upload fails. Null for a file sent
53
+ * in one piece, and until the upload has begun.
54
+ */
55
+ get uploadId() {
56
+ return this._uploadId;
57
+ }
58
+ /** @internal */
59
+ setUploadId(id) {
60
+ this._uploadId = id;
61
+ }
48
62
  /**
49
63
  * Calls `listener` on every change of state and every chunk sent, starting
50
64
  * with where the upload is now. Returns a function that stops it.
@@ -139,70 +153,96 @@ export function runUploadTask(body) {
139
153
  return task;
140
154
  }
141
155
  /**
142
- * Sends `body` to a presigned `url` with PUT, reporting bytes as they are
143
- * sent. XMLHttpRequest where the host has it (browsers and React Native,
144
- * both report upload progress); fetch elsewhere, where progress jumps from
145
- * 0 to the total. A cancel aborts it.
156
+ * PUTs `body` to `url`, reporting bytes as they are sent. XMLHttpRequest
157
+ * where the host has it (browsers and React Native both report upload
158
+ * progress); fetch elsewhere, where progress jumps from 0 to the total.
159
+ * An aborted transfer rejects with `KoolbaseUploadCancelledError`.
146
160
  */
147
- export function putWithProgress(task, url, body, contentType) {
148
- task.checkCancelled();
149
- const total = body.size;
150
- const report = (sent) => {
151
- if (!task.cancelled)
152
- task.emit('uploading', Math.min(sent, total));
153
- };
154
- report(0);
161
+ export function transferBytes(url, body, contentType, onSent) {
155
162
  const XHR = globalThis.XMLHttpRequest;
156
163
  if (XHR) {
157
- return new Promise((resolve, reject) => {
158
- const xhr = new XHR();
159
- let settled = false;
160
- const done = (fn) => {
164
+ const xhr = new XHR();
165
+ let settled = false;
166
+ let fail = () => undefined;
167
+ const done = new Promise((resolve, reject) => {
168
+ const finish = (fn) => {
161
169
  if (settled)
162
170
  return;
163
171
  settled = true;
164
- task.setAbort(null);
165
172
  fn();
166
173
  };
174
+ fail = (e) => finish(() => reject(e));
167
175
  xhr.open('PUT', url);
168
- xhr.setRequestHeader('Content-Type', contentType);
176
+ if (contentType)
177
+ xhr.setRequestHeader('Content-Type', contentType);
169
178
  if (xhr.upload) {
170
- xhr.upload.onprogress = (ev) => report(ev.loaded);
179
+ xhr.upload.onprogress = (ev) => onSent(Math.min(ev.loaded, body.size));
171
180
  }
172
- xhr.onload = () => done(() => {
173
- if (task.cancelled)
174
- return reject(new KoolbaseUploadCancelledError());
175
- resolve({ status: xhr.status, etag: xhr.getResponseHeader('etag') ?? '' });
176
- });
177
- xhr.onerror = () => done(() => reject(task.cancelled ? new KoolbaseUploadCancelledError() : new KoolbaseNetworkError(url, new Error('the upload did not complete'))));
178
- xhr.onabort = () => done(() => reject(new KoolbaseUploadCancelledError()));
179
- task.setAbort(() => {
180
- xhr.abort();
181
- // Some hosts do not fire onabort for an aborted send.
182
- done(() => reject(new KoolbaseUploadCancelledError()));
183
- });
181
+ xhr.onload = () => finish(() => resolve({ status: xhr.status, etag: xhr.getResponseHeader('etag') ?? '' }));
182
+ xhr.onerror = () => fail(new KoolbaseNetworkError(url, new Error('the upload did not complete')));
183
+ xhr.onabort = () => fail(new KoolbaseUploadCancelledError());
184
+ onSent(0);
184
185
  xhr.send(body);
185
186
  });
187
+ return {
188
+ done,
189
+ abort: () => {
190
+ xhr.abort();
191
+ // Some hosts do not fire onabort for an aborted send.
192
+ fail(new KoolbaseUploadCancelledError());
193
+ },
194
+ };
186
195
  }
187
196
  const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
188
- task.setAbort(() => controller?.abort());
189
- return fetch(url, {
197
+ let aborted = false;
198
+ onSent(0);
199
+ const done = fetch(url, {
190
200
  method: 'PUT',
191
- headers: { 'Content-Type': contentType },
201
+ headers: contentType ? { 'Content-Type': contentType } : {},
192
202
  body,
193
203
  signal: controller?.signal,
194
204
  })
195
205
  .then((res) => {
196
- task.checkCancelled();
206
+ if (aborted)
207
+ throw new KoolbaseUploadCancelledError();
208
+ onSent(body.size);
197
209
  return { status: res.status, etag: res.headers.get('etag') ?? '' };
198
210
  })
199
211
  .catch((e) => {
200
- if (task.cancelled || e?.name === 'AbortError') {
212
+ if (aborted || e?.name === 'AbortError')
201
213
  throw new KoolbaseUploadCancelledError();
202
- }
203
214
  if (e instanceof KoolbaseStorageError)
204
215
  throw e;
205
216
  throw new KoolbaseNetworkError(url, e);
217
+ });
218
+ return {
219
+ done,
220
+ abort: () => {
221
+ aborted = true;
222
+ controller?.abort();
223
+ },
224
+ };
225
+ }
226
+ /**
227
+ * Sends `body` to a presigned `url` with PUT as the task's one transfer,
228
+ * reporting bytes as they are sent; a cancel aborts it.
229
+ */
230
+ export function putWithProgress(task, url, body, contentType) {
231
+ task.checkCancelled();
232
+ const total = body.size;
233
+ const report = (sent) => {
234
+ if (!task.cancelled)
235
+ task.emit('uploading', Math.min(sent, total));
236
+ };
237
+ const t = transferBytes(url, body, contentType, report);
238
+ task.setAbort(() => t.abort());
239
+ return t.done
240
+ .then((r) => {
241
+ task.checkCancelled();
242
+ return r;
243
+ })
244
+ .catch((e) => {
245
+ throw task.cancelled ? new KoolbaseUploadCancelledError() : e;
206
246
  })
207
247
  .finally(() => task.setAbort(null));
208
248
  }
@@ -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
@@ -3,7 +3,16 @@ import { sessionFetch } from './network.js';
3
3
  import { KoolbaseUnauthenticatedError } from './errors.js';
4
4
  import { KoolbaseStorageError, koolbaseStorageErrorFromResponse, } from './storage-errors.js';
5
5
  import { putWithProgress, runUploadTask } from './storage-transfer.js';
6
+ import { sendParts } from './storage-multipart.js';
7
+ import { KoolbaseDownloadCancelledError } from './storage-errors.js';
8
+ import { refreshBucketFiles } from './files-refresh.js';
9
+ import { KoolbaseNetworkError } from './network.js';
6
10
  import { fileInfoFromJson, filePageFromJson, listFilesQuery, } from './storage-files.js';
11
+ /** An upload task whose success reloads the bucket's file lists (storage S4). */
12
+ function listsReload(task, bucket) {
13
+ task.result.then(() => { void refreshBucketFiles(bucket); }, () => { });
14
+ return task;
15
+ }
7
16
  // --- Cloudflare image-transform URL helpers -------------------------------
8
17
  // Module-private — callers use KoolbaseStorage.publicUrl / publicUrlForObject.
9
18
  function clampInt(v, min, max) {
@@ -126,7 +135,7 @@ export class KoolbaseStorage {
126
135
  * const { object } = await task.result;
127
136
  */
128
137
  startUpload(options) {
129
- return runUploadTask((task) => this.runUpload(options, task));
138
+ return listsReload(runUploadTask((task) => this.runUpload(options, task)), options.bucket);
130
139
  }
131
140
  async runUpload(options, task) {
132
141
  task.checkCancelled();
@@ -141,6 +150,19 @@ export class KoolbaseStorage {
141
150
  : options.file.type;
142
151
  if (isBlob)
143
152
  task.setTotal(options.file.size);
153
+ // A browser hands us the bytes already: a File from an input is a Blob,
154
+ // and fetching its .uri would be fetching undefined. React Native hands
155
+ // us a local URI, and its fetch resolves that into a Blob -- read first,
156
+ // since its size decides how it is sent (S3).
157
+ const fileBlob = isBlob
158
+ ? options.file
159
+ : await (await sessionFetch(options.file.uri)).blob();
160
+ task.setTotal(fileBlob.size);
161
+ task.checkCancelled();
162
+ // Large files go in parts: resumable, each part retried (S3).
163
+ if (fileBlob.size > KoolbaseStorage.multipartThreshold) {
164
+ return this.runInParts(options.bucket, options, task, fileBlob, contentType, null);
165
+ }
144
166
  // ─── Step 1: Get presigned upload URL ───
145
167
  const urlRes = await sessionFetch(`${this.config.baseUrl}/v1/sdk/storage/upload-url`, {
146
168
  method: 'POST',
@@ -162,16 +184,7 @@ export class KoolbaseStorage {
162
184
  task.checkCancelled();
163
185
  // ─── Step 2: Upload directly to R2 ───
164
186
  // R2 presigned URLs expect raw binary, NOT multipart/form-data.
165
- //
166
- // A browser hands us the bytes already: a File from an input is a Blob,
167
- // and fetching its .uri would be fetching undefined. React Native hands
168
- // us a local URI, and its fetch resolves that into a Blob — which is why
169
- // the two hosts differ here and nowhere else in this method.
170
- const fileBlob = isBlob
171
- ? options.file
172
- : await (await sessionFetch(options.file.uri)).blob();
173
187
  const fileSize = fileBlob.size;
174
- task.setTotal(fileSize);
175
188
  const uploadRes = await putWithProgress(task, upload_url, fileBlob, contentType);
176
189
  if (uploadRes.status < 200 || uploadRes.status >= 300) {
177
190
  // R2 PUT errors don't follow the Koolbase error shape — surface as a
@@ -219,6 +232,173 @@ export class KoolbaseStorage {
219
232
  const downloadUrl = await this.getDownloadUrl(options.bucket, options.path);
220
233
  return { object, downloadUrl };
221
234
  }
235
+ /**
236
+ * Resumes a large file's upload that failed or was interrupted -- the
237
+ * network dropped, the app was closed -- sending only the parts that did
238
+ * not arrive. `uploadId` is the failed task's `uploadId`; `file` must be
239
+ * the same file. Within a day of the upload beginning.
240
+ *
241
+ * @example
242
+ * const task = Koolbase.storage.startUpload({ bucket: 'video', path: 'talk.mp4', file });
243
+ * saveForLater(task.uploadId); // once the upload has begun
244
+ * // ... after a restart:
245
+ * await Koolbase.storage.resumeUpload({ bucket: 'video', uploadId, file }).result;
246
+ */
247
+ resumeUpload(options) {
248
+ return listsReload(runUploadTask(async (task) => {
249
+ const isBlob = typeof Blob !== 'undefined' && options.file instanceof Blob;
250
+ const blob = isBlob
251
+ ? options.file
252
+ : await (await sessionFetch(options.file.uri)).blob();
253
+ task.setTotal(blob.size);
254
+ return this.runInParts(options.bucket, options, task, blob, '', options.uploadId);
255
+ }), options.bucket);
256
+ }
257
+ async runInParts(bucket, options, task, file, contentType, resumeId) {
258
+ const base = `${this.config.baseUrl}/v1/sdk/storage/uploads`;
259
+ const json = async () => ({ ...(await this.buildHeaders()), 'Content-Type': 'application/json' });
260
+ const overwrite = options.overwrite ?? false;
261
+ let plan;
262
+ if (resumeId) {
263
+ const res = await sessionFetch(`${base}?bucket=${encodeURIComponent(bucket)}&upload_id=${encodeURIComponent(resumeId)}`, {
264
+ headers: await this.buildHeaders(),
265
+ });
266
+ if (!res.ok)
267
+ throw await this.error(res, 'Failed to resume the upload');
268
+ plan = (await res.json());
269
+ if (plan.size !== file.size) {
270
+ throw new KoolbaseStorageError(`This is not the file the upload began with: it has ${file.size} bytes, the upload ${plan.size}`, 'validation_error');
271
+ }
272
+ }
273
+ else {
274
+ const res = await sessionFetch(base, {
275
+ method: 'POST',
276
+ headers: await json(),
277
+ body: JSON.stringify({ bucket, path: options.path, content_type: contentType, size: file.size, overwrite }),
278
+ });
279
+ if (!res.ok)
280
+ throw await this.error(res, 'Failed to begin the upload');
281
+ plan = (await res.json());
282
+ }
283
+ task.setUploadId(plan.upload_id);
284
+ task.checkCancelled();
285
+ await sendParts(task, plan, file, {
286
+ links: async (parts) => {
287
+ const res = await sessionFetch(`${base}/parts`, {
288
+ method: 'POST',
289
+ headers: await json(),
290
+ body: JSON.stringify({ bucket, upload_id: plan.upload_id, parts }),
291
+ });
292
+ if (!res.ok)
293
+ throw await this.error(res, 'Failed to get upload links');
294
+ return (await res.json()).links;
295
+ },
296
+ abort: async () => {
297
+ await sessionFetch(base, {
298
+ method: 'DELETE',
299
+ headers: await json(),
300
+ body: JSON.stringify({ bucket, upload_id: plan.upload_id }),
301
+ });
302
+ },
303
+ });
304
+ // The parts have arrived: the server joins and checks them. No longer
305
+ // cancellable.
306
+ task.verifying();
307
+ const body = { bucket, upload_id: plan.upload_id, overwrite };
308
+ if (options.metadata !== undefined)
309
+ body.metadata = options.metadata;
310
+ const done = await sessionFetch(`${base}/complete`, { method: 'POST', headers: await json(), body: JSON.stringify(body) });
311
+ if (!done.ok)
312
+ throw await this.error(done, 'Failed to complete the upload');
313
+ const object = mapObjectFromServer(await done.json());
314
+ const downloadUrl = await this.getDownloadUrl(bucket, object.path);
315
+ return { object, downloadUrl };
316
+ }
317
+ /**
318
+ * Downloads a file's bytes, reporting them as they arrive (S3):
319
+ * `onProgress(received, total)`, total null when the size is not known.
320
+ * Aborting `signal` stops it with `KoolbaseDownloadCancelledError`.
321
+ *
322
+ * @example
323
+ * const blob = await Koolbase.storage.download({ bucket: 'video', path: 'talk.mp4', onProgress: (r, t) => setPct(t ? r / t : 0) });
324
+ */
325
+ async download(options) {
326
+ if (options.signal?.aborted)
327
+ throw new KoolbaseDownloadCancelledError();
328
+ const url = await this.getDownloadUrl(options.bucket, options.path, options.versionId);
329
+ const report = options.onProgress ?? (() => undefined);
330
+ const XHR = globalThis.XMLHttpRequest;
331
+ if (XHR) {
332
+ return new Promise((resolve, reject) => {
333
+ const xhr = new XHR();
334
+ let settled = false;
335
+ const finish = (fn) => {
336
+ if (settled)
337
+ return;
338
+ settled = true;
339
+ options.signal?.removeEventListener('abort', stop);
340
+ fn();
341
+ };
342
+ const stop = () => {
343
+ xhr.abort();
344
+ finish(() => reject(new KoolbaseDownloadCancelledError()));
345
+ };
346
+ options.signal?.addEventListener('abort', stop);
347
+ xhr.open('GET', url);
348
+ xhr.responseType = 'blob';
349
+ xhr.onprogress = (ev) => report(ev.loaded, ev.lengthComputable ? ev.total : null);
350
+ xhr.onload = () => finish(() => {
351
+ if (xhr.status < 200 || xhr.status >= 300) {
352
+ reject(new KoolbaseStorageError(`Download failed: ${xhr.status}`));
353
+ return;
354
+ }
355
+ const blob = xhr.response;
356
+ report(blob.size, blob.size);
357
+ resolve(blob);
358
+ });
359
+ xhr.onerror = () => finish(() => reject(new KoolbaseNetworkError(url, new Error('the download did not complete'))));
360
+ xhr.onabort = () => finish(() => reject(new KoolbaseDownloadCancelledError()));
361
+ xhr.send();
362
+ });
363
+ }
364
+ let res;
365
+ try {
366
+ res = await fetch(url, { signal: options.signal });
367
+ }
368
+ catch (e) {
369
+ if (options.signal?.aborted)
370
+ throw new KoolbaseDownloadCancelledError();
371
+ throw new KoolbaseNetworkError(url, e);
372
+ }
373
+ if (!res.ok)
374
+ throw new KoolbaseStorageError(`Download failed: ${res.status}`);
375
+ const total = Number(res.headers.get('content-length')) || null;
376
+ const reader = res.body?.getReader?.();
377
+ if (!reader) {
378
+ const blob = await res.blob();
379
+ report(blob.size, blob.size);
380
+ return blob;
381
+ }
382
+ const chunks = [];
383
+ let received = 0;
384
+ report(0, total);
385
+ try {
386
+ for (;;) {
387
+ const { done, value } = await reader.read();
388
+ if (done)
389
+ break;
390
+ chunks.push(value);
391
+ received += value.length;
392
+ report(received, total);
393
+ }
394
+ }
395
+ catch (e) {
396
+ if (options.signal?.aborted)
397
+ throw new KoolbaseDownloadCancelledError();
398
+ throw new KoolbaseNetworkError(url, e);
399
+ }
400
+ return new Blob(chunks, { type: res.headers.get('content-type') ?? '' });
401
+ }
222
402
  /**
223
403
  * One page of a bucket's files. The bucket's view rule is applied to every
224
404
  * file: the page holds only files the signed-in person may view. Pass the
@@ -289,7 +469,9 @@ export class KoolbaseStorage {
289
469
  if (!res.ok) {
290
470
  throw await this.error(res, op === 'move' ? 'Failed to move the file' : 'Failed to copy the file');
291
471
  }
292
- return mapObjectFromServer(await res.json());
472
+ const moved = mapObjectFromServer(await res.json());
473
+ void refreshBucketFiles(o.bucket, ...(o.toBucket && o.toBucket !== o.bucket ? [o.toBucket] : []));
474
+ return moved;
293
475
  }
294
476
  /**
295
477
  * Apply a partial metadata update to an existing object. Returns the
@@ -337,6 +519,7 @@ export class KoolbaseStorage {
337
519
  throw await this.error(res, 'Failed to update metadata');
338
520
  }
339
521
  const raw = await res.json();
522
+ void refreshBucketFiles(bucket);
340
523
  return mapObjectFromServer(raw);
341
524
  }
342
525
  /**
@@ -468,11 +651,11 @@ export class KoolbaseStorage {
468
651
  },
469
652
  body: JSON.stringify({ bucket, path }),
470
653
  });
471
- if (res.status === 204)
654
+ if (res.ok) {
655
+ void refreshBucketFiles(bucket);
472
656
  return;
473
- if (!res.ok) {
474
- throw await this.error(res, 'Failed to delete file');
475
657
  }
658
+ throw await this.error(res, 'Failed to delete file');
476
659
  }
477
660
  /**
478
661
  * List all versions of a file path, newest-first. Returns a flat list
@@ -529,7 +712,9 @@ export class KoolbaseStorage {
529
712
  if (!res.ok) {
530
713
  throw await this.error(res, 'Failed to restore version');
531
714
  }
532
- return mapObjectFromServer(await res.json());
715
+ const restored = mapObjectFromServer(await res.json());
716
+ void refreshBucketFiles(bucket);
717
+ return restored;
533
718
  }
534
719
  /**
535
720
  * Hard-remove a single history version — both the metadata row and
@@ -551,6 +736,11 @@ export class KoolbaseStorage {
551
736
  }
552
737
  }
553
738
  }
739
+ /**
740
+ * Files larger than this go up in parts (S3): each part retried on its
741
+ * own, the upload resumable for a day with `resumeUpload`. 16 MiB.
742
+ */
743
+ KoolbaseStorage.multipartThreshold = 16 * 1024 * 1024;
554
744
  /**
555
745
  * Maps the snake_case server JSON to the camelCase {@link KoolbaseObject}.
556
746
  * Defensive: missing or null `metadata` (older / non-Koolbase responses)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koolbase/core",
3
- "version": "12.17.0",
3
+ "version": "12.19.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",