@koolbase/core 12.14.0 → 12.16.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.
@@ -1,4 +1,4 @@
1
- import { koolbaseFetch } from './network.js';
1
+ import { sessionFetch } from './network.js';
2
2
  import { functionInvokeError } from './function-errors.js';
3
3
  import { KoolbaseUnauthenticatedError } from './errors.js';
4
4
  import { FunctionRuntime, } from './types.js';
@@ -11,7 +11,7 @@ export class KoolbaseFunctions {
11
11
  // ─── Deploy ────────────────────────────────────────────────────────────────
12
12
  async deploy(options) {
13
13
  const runtime = options.runtime ?? FunctionRuntime.Deno;
14
- const res = await koolbaseFetch(`${this.config.baseUrl}/v1/sdk/functions/deploy`, {
14
+ const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/functions/deploy`, {
15
15
  method: 'POST',
16
16
  headers: {
17
17
  'Content-Type': 'application/json',
@@ -55,7 +55,7 @@ export class KoolbaseFunctions {
55
55
  if (userToken) {
56
56
  headers['Authorization'] = `Bearer ${userToken}`;
57
57
  }
58
- const res = await koolbaseFetch(`${this.config.baseUrl}/v1/sdk/functions/${name}`, {
58
+ const res = await sessionFetch(`${this.config.baseUrl}/v1/sdk/functions/${name}`, {
59
59
  method: 'POST',
60
60
  headers,
61
61
  body: JSON.stringify({ body: body ?? {} }),
@@ -20,6 +20,9 @@ export { KoolbaseRealtime } from './realtime.js';
20
20
  export { KoolbaseConnectivity } from './connectivity.js';
21
21
  export type { ConnectivityState } from './connectivity.js';
22
22
  export { KoolbaseStorage } from './storage.js';
23
+ export { KoolbaseUploadTask } from './storage-transfer.js';
24
+ export type { KoolbaseUploadProgress, KoolbaseUploadState } from './storage-transfer.js';
25
+ export type { KoolbaseFileInfo, KoolbaseFilePage, KoolbaseFileSort, ListFilesOptions } from './storage-files.js';
23
26
  export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
24
27
  export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
25
28
  export type { FiscalIntentResult, FiscalStatus } from './fiscal.js';
package/dist/esm/index.js CHANGED
@@ -22,6 +22,7 @@ export { KoolbaseFunctions } from './functions.js';
22
22
  export { KoolbaseRealtime } from './realtime.js';
23
23
  export { KoolbaseConnectivity } from './connectivity.js';
24
24
  export { KoolbaseStorage } from './storage.js';
25
+ export { KoolbaseUploadTask } from './storage-transfer.js';
25
26
  export { KoolbaseAnalytics, disabledAnalytics } from './analytics.js';
26
27
  export { KoolbaseFiscal, KoolbaseFiscalError, decodeFiscalResponse, isFiscalized, isPending } from './fiscal.js';
27
28
  export { KoolbaseMessaging } from './messaging.js';
@@ -19,3 +19,17 @@ export declare class KoolbaseNetworkError extends TypeError {
19
19
  }
20
20
  /** fetch, with network failures turned into a KoolbaseNetworkError. */
21
21
  export declare function koolbaseFetch(input: string, init?: RequestInit): Promise<Response>;
22
+ type Renewal = (refusedToken: string) => Promise<string | null>;
23
+ /** Installed by Koolbase.initialize: renews the session a request was refused with. */
24
+ export declare function setSessionRenewal(fn: Renewal | null): void;
25
+ /**
26
+ * koolbaseFetch for a data request. A request refused as unauthenticated
27
+ * (401) while carrying the signed-in person's token is the session needing
28
+ * renewal, not ending: the access token expired early, the clock is off, or
29
+ * another tab renewed it. The session is renewed once and the request sent
30
+ * again with the new token, so the caller never sees the first refusal.
31
+ * Only when renewal is refused does the 401 reach the caller, and with it the
32
+ * client's onSessionExpired, which ends the session.
33
+ */
34
+ export declare function sessionFetch(input: string, init?: RequestInit): Promise<Response>;
35
+ export {};
@@ -47,3 +47,61 @@ export async function koolbaseFetch(input, init) {
47
47
  throw new KoolbaseNetworkError(input, e);
48
48
  }
49
49
  }
50
+ let renewal = null;
51
+ /** Installed by Koolbase.initialize: renews the session a request was refused with. */
52
+ export function setSessionRenewal(fn) {
53
+ renewal = fn;
54
+ }
55
+ function headerOf(headers, name) {
56
+ if (!headers)
57
+ return undefined;
58
+ if (typeof headers.get === 'function')
59
+ return headers.get(name) ?? undefined;
60
+ if (Array.isArray(headers))
61
+ return headers.find(([k]) => k.toLowerCase() === name.toLowerCase())?.[1];
62
+ const rec = headers;
63
+ const key = Object.keys(rec).find((k) => k.toLowerCase() === name.toLowerCase());
64
+ return key ? rec[key] : undefined;
65
+ }
66
+ function withHeader(headers, name, value) {
67
+ const out = {};
68
+ if (headers && typeof headers.forEach === 'function' && !Array.isArray(headers)) {
69
+ headers.forEach((v, k) => { out[k] = v; });
70
+ }
71
+ else if (Array.isArray(headers)) {
72
+ for (const [k, v] of headers)
73
+ out[k] = v;
74
+ }
75
+ else if (headers) {
76
+ Object.assign(out, headers);
77
+ }
78
+ for (const k of Object.keys(out))
79
+ if (k.toLowerCase() === name.toLowerCase())
80
+ delete out[k];
81
+ out[name] = value;
82
+ return out;
83
+ }
84
+ /**
85
+ * koolbaseFetch for a data request. A request refused as unauthenticated
86
+ * (401) while carrying the signed-in person's token is the session needing
87
+ * renewal, not ending: the access token expired early, the clock is off, or
88
+ * another tab renewed it. The session is renewed once and the request sent
89
+ * again with the new token, so the caller never sees the first refusal.
90
+ * Only when renewal is refused does the 401 reach the caller, and with it the
91
+ * client's onSessionExpired, which ends the session.
92
+ */
93
+ export async function sessionFetch(input, init) {
94
+ const res = await koolbaseFetch(input, init);
95
+ if (res.status !== 401 || !renewal)
96
+ return res;
97
+ const auth = headerOf(init?.headers, 'Authorization');
98
+ if (!auth || !auth.startsWith('Bearer '))
99
+ return res;
100
+ // Only a body that can be sent twice. Data requests send JSON text.
101
+ if (init?.body !== undefined && init.body !== null && typeof init.body !== 'string')
102
+ return res;
103
+ const fresh = await renewal(auth.slice('Bearer '.length));
104
+ if (!fresh)
105
+ return res;
106
+ return koolbaseFetch(input, { ...init, headers: withHeader(init?.headers, 'Authorization', `Bearer ${fresh}`) });
107
+ }
@@ -187,6 +187,45 @@ export declare class KoolbaseStorageMetadataInvalidError extends KoolbaseStorage
187
187
  detail?: string;
188
188
  constructor(message?: string, detail?: string);
189
189
  }
190
+ /**
191
+ * An upload that was cancelled -- by `KoolbaseUploadTask.cancel()` or the
192
+ * upload's `signal` -- before the server began checking it. Nothing was
193
+ * stored. Code: `upload_cancelled`.
194
+ */
195
+ export declare class KoolbaseUploadCancelledError extends KoolbaseStorageError {
196
+ constructor(message?: string);
197
+ }
198
+ /**
199
+ * The confirm named an upload the server does not have: it expired, was
200
+ * confirmed already, or belongs to someone else. Upload the file again.
201
+ * Code: `upload_not_found`.
202
+ */
203
+ export declare class KoolbaseUploadNotFoundError extends KoolbaseStorageError {
204
+ constructor(message?: string);
205
+ }
206
+ /**
207
+ * The bucket is busy with other changes (a large move, many uploads at
208
+ * once). Nothing was changed; retry shortly. Code: `bucket_busy`.
209
+ */
210
+ export declare class KoolbaseStorageBusyError extends KoolbaseStorageError {
211
+ constructor(message?: string);
212
+ }
213
+ /**
214
+ * A listing's cursor was not one this listing issued -- made up, or from a
215
+ * listing with another sort or order. Start again from the first page.
216
+ * Code: `invalid_cursor`.
217
+ */
218
+ export declare class KoolbaseStorageInvalidCursorError extends KoolbaseStorageError {
219
+ constructor(message?: string);
220
+ }
221
+ /**
222
+ * The bucket's rules allow this only to someone signed in, and nobody is.
223
+ * Not a session that ended: the SDK's session is left alone. Code:
224
+ * `sign_in_required`.
225
+ */
226
+ export declare class KoolbaseStorageSignInRequiredError extends KoolbaseStorageError {
227
+ constructor(message?: string);
228
+ }
190
229
  /**
191
230
  * Maps a non-2xx storage-layer response to a typed
192
231
  * {@link KoolbaseStorageError}, preferring the server's stable `code` and
@@ -234,6 +234,65 @@ export class KoolbaseStorageMetadataInvalidError extends KoolbaseStorageError {
234
234
  Object.setPrototypeOf(this, KoolbaseStorageMetadataInvalidError.prototype);
235
235
  }
236
236
  }
237
+ /**
238
+ * An upload that was cancelled -- by `KoolbaseUploadTask.cancel()` or the
239
+ * upload's `signal` -- before the server began checking it. Nothing was
240
+ * stored. Code: `upload_cancelled`.
241
+ */
242
+ export class KoolbaseUploadCancelledError extends KoolbaseStorageError {
243
+ constructor(message) {
244
+ super(message ?? 'The upload was cancelled', 'upload_cancelled');
245
+ this.name = 'KoolbaseUploadCancelledError';
246
+ Object.setPrototypeOf(this, KoolbaseUploadCancelledError.prototype);
247
+ }
248
+ }
249
+ /**
250
+ * The confirm named an upload the server does not have: it expired, was
251
+ * confirmed already, or belongs to someone else. Upload the file again.
252
+ * Code: `upload_not_found`.
253
+ */
254
+ export class KoolbaseUploadNotFoundError extends KoolbaseStorageError {
255
+ constructor(message) {
256
+ super(message ?? 'This upload was not found -- please upload the file again', 'upload_not_found');
257
+ this.name = 'KoolbaseUploadNotFoundError';
258
+ Object.setPrototypeOf(this, KoolbaseUploadNotFoundError.prototype);
259
+ }
260
+ }
261
+ /**
262
+ * The bucket is busy with other changes (a large move, many uploads at
263
+ * once). Nothing was changed; retry shortly. Code: `bucket_busy`.
264
+ */
265
+ export class KoolbaseStorageBusyError extends KoolbaseStorageError {
266
+ constructor(message) {
267
+ super(message ?? 'The bucket is busy -- try again shortly', 'bucket_busy');
268
+ this.name = 'KoolbaseStorageBusyError';
269
+ Object.setPrototypeOf(this, KoolbaseStorageBusyError.prototype);
270
+ }
271
+ }
272
+ /**
273
+ * A listing's cursor was not one this listing issued -- made up, or from a
274
+ * listing with another sort or order. Start again from the first page.
275
+ * Code: `invalid_cursor`.
276
+ */
277
+ export class KoolbaseStorageInvalidCursorError extends KoolbaseStorageError {
278
+ constructor(message) {
279
+ super(message ?? 'The cursor is not valid for this listing', 'invalid_cursor');
280
+ this.name = 'KoolbaseStorageInvalidCursorError';
281
+ Object.setPrototypeOf(this, KoolbaseStorageInvalidCursorError.prototype);
282
+ }
283
+ }
284
+ /**
285
+ * The bucket's rules allow this only to someone signed in, and nobody is.
286
+ * Not a session that ended: the SDK's session is left alone. Code:
287
+ * `sign_in_required`.
288
+ */
289
+ export class KoolbaseStorageSignInRequiredError extends KoolbaseStorageError {
290
+ constructor(message) {
291
+ super(message ?? 'Sign in to do this', 'sign_in_required');
292
+ this.name = 'KoolbaseStorageSignInRequiredError';
293
+ Object.setPrototypeOf(this, KoolbaseStorageSignInRequiredError.prototype);
294
+ }
295
+ }
237
296
  /**
238
297
  * Maps a non-2xx storage-layer response to a typed
239
298
  * {@link KoolbaseStorageError}, preferring the server's stable `code` and
@@ -271,6 +330,16 @@ export function koolbaseStorageError(status, body, fallbackMessage = 'Storage re
271
330
  return new KoolbaseStorageMetadataInvalidError(message, body?.detail);
272
331
  case 'upload_url_failed':
273
332
  return new KoolbaseUploadURLFailedError(message);
333
+ case 'upload_not_found':
334
+ return new KoolbaseUploadNotFoundError(message);
335
+ case 'bucket_busy':
336
+ return new KoolbaseStorageBusyError(message);
337
+ case 'invalid_cursor':
338
+ return new KoolbaseStorageInvalidCursorError(message);
339
+ case 'sign_in_required':
340
+ return new KoolbaseStorageSignInRequiredError(message);
341
+ case 'not_allowed':
342
+ return new KoolbaseStoragePermissionError(message);
274
343
  }
275
344
  // ─── status fallback (pre-code servers or uncoded paths) ───
276
345
  switch (status) {
@@ -0,0 +1,53 @@
1
+ /** What a listing is ordered by. */
2
+ export type KoolbaseFileSort = 'path' | 'createdAt' | 'updatedAt' | 'size';
3
+ export interface ListFilesOptions {
4
+ bucket: string;
5
+ /** Only files whose path starts with this, taken literally. */
6
+ prefix?: string;
7
+ /** Files per page: 50 unless set, at most 100. */
8
+ limit?: number;
9
+ /** A page's `nextCursor`, for the page after it. */
10
+ cursor?: string;
11
+ /** By path unless set. */
12
+ sort?: KoolbaseFileSort;
13
+ /** Z to A, newest first, largest first. */
14
+ descending?: boolean;
15
+ }
16
+ /** One file. */
17
+ export interface KoolbaseFileInfo {
18
+ /** Its path in the bucket: what a record stores to point at it. */
19
+ path: string;
20
+ /** The last part of its path. */
21
+ name: string;
22
+ size: number;
23
+ contentType: string | null;
24
+ etag: string | null;
25
+ metadata: Record<string, string>;
26
+ versionId: string | null;
27
+ /** Who uploaded it: a project user's id; null when the project did. */
28
+ uploadedBy: string | null;
29
+ /** ISO 8601. */
30
+ createdAt: string;
31
+ /** ISO 8601. */
32
+ updatedAt: string;
33
+ /**
34
+ * Its permanent link, while its bucket is public; null otherwise -- ask
35
+ * for a download link instead.
36
+ */
37
+ publicUrl: string | null;
38
+ }
39
+ /** One page of a listing. */
40
+ export interface KoolbaseFilePage {
41
+ files: KoolbaseFileInfo[];
42
+ /**
43
+ * Pass to the next call for the page after this one; null at the end. A
44
+ * page can hold fewer files than asked for -- the server passes over
45
+ * files you may not view -- and still have a next one.
46
+ */
47
+ nextCursor: string | null;
48
+ hasMore: boolean;
49
+ }
50
+ /** The query string for a listing. */
51
+ export declare function listFilesQuery(o: ListFilesOptions): string;
52
+ export declare function fileInfoFromJson(j: Record<string, unknown>): KoolbaseFileInfo;
53
+ export declare function filePageFromJson(j: Record<string, unknown>): KoolbaseFilePage;
@@ -0,0 +1,56 @@
1
+ // A bucket's files, as an app sees them (S1).
2
+ //
3
+ // The server applies the bucket's view rule to every file: a listing holds
4
+ // only files the signed-in person may view, and says nothing of the rest.
5
+ const sortWire = {
6
+ path: 'path',
7
+ createdAt: 'created_at',
8
+ updatedAt: 'updated_at',
9
+ size: 'size',
10
+ };
11
+ /** The query string for a listing. */
12
+ export function listFilesQuery(o) {
13
+ const q = new URLSearchParams();
14
+ q.set('bucket', o.bucket);
15
+ if (o.prefix)
16
+ q.set('prefix', o.prefix);
17
+ if (o.limit != null)
18
+ q.set('limit', String(o.limit));
19
+ if (o.cursor)
20
+ q.set('cursor', o.cursor);
21
+ q.set('sort', sortWire[o.sort ?? 'path'] ?? 'path');
22
+ q.set('order', o.descending ? 'desc' : 'asc');
23
+ return q.toString();
24
+ }
25
+ const str = (v) => (typeof v === 'string' && v !== '' ? v : null);
26
+ export function fileInfoFromJson(j) {
27
+ const path = String(j.path ?? '');
28
+ const metadata = {};
29
+ if (j.metadata && typeof j.metadata === 'object') {
30
+ for (const [k, v] of Object.entries(j.metadata)) {
31
+ metadata[k] = typeof v === 'string' ? v : String(v);
32
+ }
33
+ }
34
+ return {
35
+ path,
36
+ name: path.split('/').pop() ?? path,
37
+ size: Number(j.size ?? 0),
38
+ contentType: str(j.content_type),
39
+ etag: str(j.etag),
40
+ metadata,
41
+ versionId: str(j.version_id),
42
+ uploadedBy: str(j.uploaded_by),
43
+ createdAt: String(j.created_at ?? ''),
44
+ updatedAt: String(j.updated_at ?? ''),
45
+ publicUrl: str(j.public_url),
46
+ };
47
+ }
48
+ export function filePageFromJson(j) {
49
+ const files = Array.isArray(j.files) ? j.files : [];
50
+ const nextCursor = str(j.next_cursor);
51
+ return {
52
+ files: files.map((f) => fileInfoFromJson(f)),
53
+ nextCursor,
54
+ hasMore: nextCursor !== null,
55
+ };
56
+ }
@@ -0,0 +1,88 @@
1
+ import type { UploadResult } from './types.js';
2
+ /** Where an upload is. */
3
+ export type KoolbaseUploadState =
4
+ /** Not started: asking the server for an upload link. */
5
+ 'pending'
6
+ /** Sending the file's bytes; see `bytesSent`. */
7
+ | 'uploading'
8
+ /** The bytes have arrived; the server is checking them and putting the file in place. It can no longer be cancelled. */
9
+ | 'verifying'
10
+ /** Done: the file is in the bucket. */
11
+ | 'completed'
12
+ /** It failed; `error` says why. */
13
+ | 'failed'
14
+ /** Cancelled before the server began checking it. Nothing was stored. */
15
+ | 'cancelled';
16
+ /** One moment of an upload. */
17
+ export interface KoolbaseUploadProgress {
18
+ state: KoolbaseUploadState;
19
+ /** Bytes sent so far. */
20
+ bytesSent: number;
21
+ bytesTotal: number;
22
+ /** 0 to 1: the share of bytes sent; 1 once verifying. */
23
+ fraction: number;
24
+ /** Whether the upload has ended, one way or another. */
25
+ isDone: boolean;
26
+ /** Why it failed, when `state` is `'failed'`. */
27
+ error?: Error;
28
+ }
29
+ /** A running upload: watch it with `onProgress`, `cancel` it, await `result`. */
30
+ export declare class KoolbaseUploadTask {
31
+ private _current;
32
+ private listeners;
33
+ private cancelRequested;
34
+ private abortTransfer;
35
+ private total;
36
+ /**
37
+ * The uploaded file. Rejects with `KoolbaseUploadCancelledError` if it was
38
+ * cancelled, or the error that made it fail.
39
+ */
40
+ readonly result: Promise<UploadResult>;
41
+ private resolve;
42
+ private reject;
43
+ /** @internal */
44
+ constructor();
45
+ /** Where the upload is now. */
46
+ get current(): KoolbaseUploadProgress;
47
+ /**
48
+ * Calls `listener` on every change of state and every chunk sent, starting
49
+ * with where the upload is now. Returns a function that stops it.
50
+ */
51
+ onProgress(listener: (p: KoolbaseUploadProgress) => void): () => void;
52
+ /**
53
+ * Stops the upload, if the server has not begun checking it yet. Returns
54
+ * whether it was stopped: false once verifying (the file will land, or
55
+ * fail on its own) or done.
56
+ */
57
+ cancel(): boolean;
58
+ /** @internal */
59
+ get cancelled(): boolean;
60
+ /** @internal Throws if the upload was cancelled, between steps. */
61
+ checkCancelled(): void;
62
+ /** @internal */
63
+ setTotal(total: number): void;
64
+ /** @internal */
65
+ setAbort(abort: (() => void) | null): void;
66
+ /** @internal */
67
+ emit(state: KoolbaseUploadState, sent?: number, error?: Error): void;
68
+ /** @internal Marks the bytes sent: from here the upload cannot be cancelled. */
69
+ verifying(): void;
70
+ /** @internal */
71
+ finish(result: UploadResult): void;
72
+ /** @internal */
73
+ fail(error: unknown): void;
74
+ }
75
+ /** Runs `body` as the task's upload, starting on the next turn so a caller can listen to every event. */
76
+ export declare function runUploadTask(body: (task: KoolbaseUploadTask) => Promise<UploadResult>): KoolbaseUploadTask;
77
+ interface PutResult {
78
+ status: number;
79
+ etag: string;
80
+ }
81
+ /**
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.
86
+ */
87
+ export declare function putWithProgress(task: KoolbaseUploadTask, url: string, body: Blob, contentType: string): Promise<PutResult>;
88
+ export {};
@@ -0,0 +1,208 @@
1
+ // An upload's lifecycle, as an app watches it (S1).
2
+ //
3
+ // pending -> uploading (bytes sent of total) -> verifying -> completed, or
4
+ // failed / cancelled. Verifying is its own phase: the bytes have arrived and
5
+ // the server is checking them (size, type, the bucket's rules and cap) and
6
+ // putting them in place. Only completed means the file can be referenced
7
+ // from a record.
8
+ import { KoolbaseNetworkError } from './network.js';
9
+ import { KoolbaseStorageError, KoolbaseUploadCancelledError } from './storage-errors.js';
10
+ function progressOf(state, bytesSent, bytesTotal, error) {
11
+ let fraction;
12
+ if (state === 'verifying' || state === 'completed')
13
+ fraction = 1;
14
+ else if (bytesTotal <= 0)
15
+ fraction = 0;
16
+ else
17
+ fraction = Math.min(1, Math.max(0, bytesSent / bytesTotal));
18
+ return {
19
+ state,
20
+ bytesSent,
21
+ bytesTotal,
22
+ fraction,
23
+ isDone: state === 'completed' || state === 'failed' || state === 'cancelled',
24
+ ...(error ? { error } : {}),
25
+ };
26
+ }
27
+ /** A running upload: watch it with `onProgress`, `cancel` it, await `result`. */
28
+ export class KoolbaseUploadTask {
29
+ /** @internal */
30
+ constructor() {
31
+ this.listeners = new Set();
32
+ this.cancelRequested = false;
33
+ this.abortTransfer = null;
34
+ this.total = 0;
35
+ this._current = progressOf('pending', 0, 0);
36
+ this.result = new Promise((res, rej) => {
37
+ this.resolve = res;
38
+ this.reject = rej;
39
+ });
40
+ // An app that only listens to progress is not left with an unhandled
41
+ // rejection; awaiting `result` still sees the error.
42
+ this.result.catch(() => undefined);
43
+ }
44
+ /** Where the upload is now. */
45
+ get current() {
46
+ return this._current;
47
+ }
48
+ /**
49
+ * Calls `listener` on every change of state and every chunk sent, starting
50
+ * with where the upload is now. Returns a function that stops it.
51
+ */
52
+ onProgress(listener) {
53
+ this.listeners.add(listener);
54
+ listener(this._current);
55
+ return () => {
56
+ this.listeners.delete(listener);
57
+ };
58
+ }
59
+ /**
60
+ * Stops the upload, if the server has not begun checking it yet. Returns
61
+ * whether it was stopped: false once verifying (the file will land, or
62
+ * fail on its own) or done.
63
+ */
64
+ cancel() {
65
+ if (this._current.isDone || this._current.state === 'verifying')
66
+ return false;
67
+ this.cancelRequested = true;
68
+ this.abortTransfer?.();
69
+ return true;
70
+ }
71
+ /** @internal */
72
+ get cancelled() {
73
+ return this.cancelRequested;
74
+ }
75
+ /** @internal Throws if the upload was cancelled, between steps. */
76
+ checkCancelled() {
77
+ if (this.cancelRequested)
78
+ throw new KoolbaseUploadCancelledError();
79
+ }
80
+ /** @internal */
81
+ setTotal(total) {
82
+ this.total = total;
83
+ }
84
+ /** @internal */
85
+ setAbort(abort) {
86
+ this.abortTransfer = abort;
87
+ }
88
+ /** @internal */
89
+ emit(state, sent, error) {
90
+ if (this._current.isDone)
91
+ return;
92
+ this._current = progressOf(state, sent ?? this._current.bytesSent, this.total, error);
93
+ for (const l of [...this.listeners]) {
94
+ try {
95
+ l(this._current);
96
+ }
97
+ catch {
98
+ // A listener's own failure is not the upload's.
99
+ }
100
+ }
101
+ }
102
+ /** @internal Marks the bytes sent: from here the upload cannot be cancelled. */
103
+ verifying() {
104
+ this.checkCancelled();
105
+ this.emit('verifying', this.total);
106
+ }
107
+ /** @internal */
108
+ finish(result) {
109
+ this.emit('completed', this.total);
110
+ this.listeners.clear();
111
+ this.resolve(result);
112
+ }
113
+ /** @internal */
114
+ fail(error) {
115
+ if (this.cancelRequested) {
116
+ const e = new KoolbaseUploadCancelledError();
117
+ this.emit('cancelled');
118
+ this.reject(e);
119
+ }
120
+ else {
121
+ const e = error instanceof Error ? error : new KoolbaseStorageError(String(error));
122
+ this.emit('failed', undefined, e);
123
+ this.reject(e);
124
+ }
125
+ this.listeners.clear();
126
+ }
127
+ }
128
+ /** Runs `body` as the task's upload, starting on the next turn so a caller can listen to every event. */
129
+ export function runUploadTask(body) {
130
+ const task = new KoolbaseUploadTask();
131
+ Promise.resolve().then(async () => {
132
+ try {
133
+ task.finish(await body(task));
134
+ }
135
+ catch (e) {
136
+ task.fail(e);
137
+ }
138
+ });
139
+ return task;
140
+ }
141
+ /**
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.
146
+ */
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);
155
+ const XHR = globalThis.XMLHttpRequest;
156
+ if (XHR) {
157
+ return new Promise((resolve, reject) => {
158
+ const xhr = new XHR();
159
+ let settled = false;
160
+ const done = (fn) => {
161
+ if (settled)
162
+ return;
163
+ settled = true;
164
+ task.setAbort(null);
165
+ fn();
166
+ };
167
+ xhr.open('PUT', url);
168
+ xhr.setRequestHeader('Content-Type', contentType);
169
+ if (xhr.upload) {
170
+ xhr.upload.onprogress = (ev) => report(ev.loaded);
171
+ }
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
+ });
184
+ xhr.send(body);
185
+ });
186
+ }
187
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
188
+ task.setAbort(() => controller?.abort());
189
+ return fetch(url, {
190
+ method: 'PUT',
191
+ headers: { 'Content-Type': contentType },
192
+ body,
193
+ signal: controller?.signal,
194
+ })
195
+ .then((res) => {
196
+ task.checkCancelled();
197
+ return { status: res.status, etag: res.headers.get('etag') ?? '' };
198
+ })
199
+ .catch((e) => {
200
+ if (task.cancelled || e?.name === 'AbortError') {
201
+ throw new KoolbaseUploadCancelledError();
202
+ }
203
+ if (e instanceof KoolbaseStorageError)
204
+ throw e;
205
+ throw new KoolbaseNetworkError(url, e);
206
+ })
207
+ .finally(() => task.setAbort(null));
208
+ }