@hostr/upload 0.1.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/README.md +298 -0
- package/dist/api/client.d.ts +71 -0
- package/dist/browser/index.d.ts +50 -0
- package/dist/browser.js +1061 -0
- package/dist/core/engine.d.ts +150 -0
- package/dist/core/events.d.ts +33 -0
- package/dist/core/retry.d.ts +20 -0
- package/dist/core/session.d.ts +28 -0
- package/dist/core/types.d.ts +44 -0
- package/dist/core/upload.d.ts +32 -0
- package/dist/hash/composite.d.ts +2 -0
- package/dist/hash/hasher.d.ts +2 -0
- package/dist/hash/hex.d.ts +2 -0
- package/dist/hash/md5.d.ts +24 -0
- package/dist/hash/sha256.d.ts +25 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1012 -0
- package/dist/node/hasher.d.ts +2 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node.js +1089 -0
- package/dist/react-native/background-plan.d.ts +57 -0
- package/dist/react-native/background.d.ts +126 -0
- package/dist/react-native/fs.d.ts +28 -0
- package/dist/react-native/index.d.ts +69 -0
- package/dist/react-native/native-upload.d.ts +76 -0
- package/dist/react-native.js +896 -0
- package/package.json +48 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Clock, FileSource, HttpTransport, StateStore, NetworkMonitor, Hasher } from './types.js';
|
|
2
|
+
import { type SessionRecord, type SessionStatus } from './session.js';
|
|
3
|
+
import { type Progress } from './events.js';
|
|
4
|
+
import { type HostrApi } from '../api/client.js';
|
|
5
|
+
export type CompleteInfo = {
|
|
6
|
+
assetId: string;
|
|
7
|
+
etag: string;
|
|
8
|
+
sha256?: string;
|
|
9
|
+
bytes: number;
|
|
10
|
+
};
|
|
11
|
+
export type EngineDeps = {
|
|
12
|
+
api: HostrApi;
|
|
13
|
+
file: FileSource;
|
|
14
|
+
transport: HttpTransport;
|
|
15
|
+
store: StateStore;
|
|
16
|
+
hasher: Hasher;
|
|
17
|
+
clock: Clock;
|
|
18
|
+
network?: NetworkMonitor;
|
|
19
|
+
rng?: () => number;
|
|
20
|
+
};
|
|
21
|
+
export type EngineOptions = {
|
|
22
|
+
concurrency: number;
|
|
23
|
+
checksum: 'sha256' | 'none';
|
|
24
|
+
presignBatch: number;
|
|
25
|
+
presignTtlSec: number;
|
|
26
|
+
onTokenExpired?: (assetId: string) => Promise<string>;
|
|
27
|
+
};
|
|
28
|
+
/** Advisory signal that every part PUT is dying at the network layer while
|
|
29
|
+
* zero bytes have EVER been confirmed for the session. To XHR a CORS-blocked
|
|
30
|
+
* PUT is indistinguishable from network loss, so without this the two cases
|
|
31
|
+
* present identically (running↔stalled forever). Zero-ever-confirmed is the
|
|
32
|
+
* discriminator from mid-upload connectivity loss; a genuinely-offline client
|
|
33
|
+
* usually never gets here at all (API calls stall first, or a NetworkMonitor
|
|
34
|
+
* parks dispatch before any PUT is attempted). Never terminal — a long
|
|
35
|
+
* offline stretch from byte zero looks the same and must still recover — the
|
|
36
|
+
* listener decides what to do with it. */
|
|
37
|
+
export type TransportDiagnostic = {
|
|
38
|
+
code: 'systemic-transport-failure';
|
|
39
|
+
consecutiveNetworkFailures: number;
|
|
40
|
+
bytesEverConfirmed: number;
|
|
41
|
+
hint: string;
|
|
42
|
+
};
|
|
43
|
+
/** Consecutive network-class part-PUT failures (with zero bytes ever
|
|
44
|
+
* confirmed) before a 'diagnostic' event fires. */
|
|
45
|
+
export declare const SYSTEMIC_TRANSPORT_FAILURE_THRESHOLD = 3;
|
|
46
|
+
export type EngineEvents = {
|
|
47
|
+
progress: Progress;
|
|
48
|
+
statechange: SessionStatus;
|
|
49
|
+
error: Error;
|
|
50
|
+
complete: CompleteInfo;
|
|
51
|
+
diagnostic: TransportDiagnostic;
|
|
52
|
+
};
|
|
53
|
+
export declare class IntegrityError extends Error {
|
|
54
|
+
constructor(message?: string);
|
|
55
|
+
}
|
|
56
|
+
export declare class UploadEngine {
|
|
57
|
+
readonly done: Promise<CompleteInfo>;
|
|
58
|
+
private resolveDone;
|
|
59
|
+
private rejectDone;
|
|
60
|
+
private emitter;
|
|
61
|
+
private presigned;
|
|
62
|
+
/** Presign fetches since the last confirmed part. Healthy uploads confirm
|
|
63
|
+
* constantly, so this stays at zero and costs nothing; in a sustained
|
|
64
|
+
* failure loop (every PUT dying without landing a part) it grows, and
|
|
65
|
+
* presign() spaces further /parts fetches on the same backoff curve the
|
|
66
|
+
* 'expired' storm uses — the API call rate is what trips rate limits, so
|
|
67
|
+
* it gets its own governor, independent of per-part attempt cadence and
|
|
68
|
+
* worker count. */
|
|
69
|
+
private presignsSinceConfirm;
|
|
70
|
+
/** The in-flight /parts fetch, shared so concurrent cache-missing workers
|
|
71
|
+
* ride one round-trip and re-check the cache instead of each firing an
|
|
72
|
+
* overlapping batch of their own (same memo shape as hashLanePromise). */
|
|
73
|
+
private presignInflight;
|
|
74
|
+
private claimed;
|
|
75
|
+
private inflightSent;
|
|
76
|
+
private stopped;
|
|
77
|
+
private running;
|
|
78
|
+
private restartPending;
|
|
79
|
+
private startedAt;
|
|
80
|
+
private confirmedAtStart;
|
|
81
|
+
private lastEmit;
|
|
82
|
+
private deps;
|
|
83
|
+
private opts;
|
|
84
|
+
private session;
|
|
85
|
+
/** The in-flight sha256 lane, tracked so a pause→resume never starts a
|
|
86
|
+
* second concurrent lane over the same file. Cleared once settled so a
|
|
87
|
+
* later resume can start a fresh one from the persisted midstate. */
|
|
88
|
+
private hashLanePromise;
|
|
89
|
+
/** Engine-wide (not per-part): a CORS-blocked bucket fails every worker's
|
|
90
|
+
* PUT the same way, so the streak accumulates across parts. Deliberately
|
|
91
|
+
* not bumped by apiCall's network stalls — the API answering while every
|
|
92
|
+
* R2 PUT dies is exactly the bucket-CORS signature. */
|
|
93
|
+
private consecutiveNetworkFailures;
|
|
94
|
+
private diagnosticFired;
|
|
95
|
+
/** Resolvers for anyone currently parked in waitOnline(), woken the
|
|
96
|
+
* instant pause()/abort()/fail() halts the engine — without this a
|
|
97
|
+
* stalled engine could only be interrupted by the network itself
|
|
98
|
+
* coming back online. */
|
|
99
|
+
private stopWaiters;
|
|
100
|
+
constructor(deps: EngineDeps, opts: EngineOptions, session: SessionRecord);
|
|
101
|
+
on<K extends keyof EngineEvents>(ev: K, cb: (v: EngineEvents[K]) => void): () => void;
|
|
102
|
+
get state(): SessionStatus;
|
|
103
|
+
start(): void;
|
|
104
|
+
pause(): void;
|
|
105
|
+
resume(): void;
|
|
106
|
+
abort(): Promise<void>;
|
|
107
|
+
/** Stop dispatch everywhere at once: new part claims, waitOnline's
|
|
108
|
+
* parked callers, and apiCall's network-retry loop all observe this on
|
|
109
|
+
* their next check. */
|
|
110
|
+
private halt;
|
|
111
|
+
private setStatus;
|
|
112
|
+
private fail;
|
|
113
|
+
private run;
|
|
114
|
+
private finish;
|
|
115
|
+
private expectedComposite;
|
|
116
|
+
/** Reconcile local bookkeeping against what R2 actually holds before resuming.
|
|
117
|
+
* Local claims R2 can't confirm are stale and forgotten; parts whose stored
|
|
118
|
+
* md5 matches R2's etag are kept as-is; a mismatch means the landed bytes
|
|
119
|
+
* aren't verified as ours, so it's dropped for re-upload. For parts R2 holds
|
|
120
|
+
* but local `done` has no record of (state lost — reinstall, cleared
|
|
121
|
+
* storage), the local byte range is re-read and re-hashed; it's only
|
|
122
|
+
* adopted as done when that hash matches R2's etag, never trusted blind. */
|
|
123
|
+
private reconcile;
|
|
124
|
+
private nextPart;
|
|
125
|
+
private worker;
|
|
126
|
+
private uploadPart;
|
|
127
|
+
private presign;
|
|
128
|
+
/** Fresh = safely usable for a new PUT: at least 60s of validity left, so
|
|
129
|
+
* a part starting now won't have its URL expire mid-flight. */
|
|
130
|
+
private presignFresh;
|
|
131
|
+
/** API calls: infinite patience for network loss, one token refresh on 401, else throw.
|
|
132
|
+
* Throws Stopped (never surfaced) if pause()/abort() interrupts a stuck retry. */
|
|
133
|
+
private apiCall;
|
|
134
|
+
private waitOnline;
|
|
135
|
+
/** Lazily starts the sha256 lane and remembers the in-flight promise so a
|
|
136
|
+
* fast pause→resume can never spawn a second concurrent lane over the
|
|
137
|
+
* same file. The promise carries its own no-op .catch so it can never
|
|
138
|
+
* become an unhandledRejection while nothing is actively awaiting it
|
|
139
|
+
* (e.g. during a paused window); a real failure still surfaces normally
|
|
140
|
+
* to whoever does await it, and from there through run() to fail(). */
|
|
141
|
+
private getHashLane;
|
|
142
|
+
private hashLane;
|
|
143
|
+
/** Feed every part-PUT outcome to the systemic-transport-failure detector.
|
|
144
|
+
* Any outcome that isn't network-class — success, 403, 5xx, even corrupt
|
|
145
|
+
* bytes — proves a request reached the server, so the streak resets. */
|
|
146
|
+
private notePutOutcome;
|
|
147
|
+
private bytesConfirmed;
|
|
148
|
+
private onPartProgress;
|
|
149
|
+
private emitProgress;
|
|
150
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Minimal typed pub/sub. A throwing listener is isolated: it never
|
|
2
|
+
* prevents emit from reaching later listeners, and never propagates to
|
|
3
|
+
* the caller of emit. */
|
|
4
|
+
export declare class Emitter<E extends Record<string, unknown>> {
|
|
5
|
+
private listeners;
|
|
6
|
+
on<K extends keyof E>(ev: K, cb: (v: E[K]) => void): () => void;
|
|
7
|
+
emit<K extends keyof E>(ev: K, v: E[K]): void;
|
|
8
|
+
}
|
|
9
|
+
export type Progress = {
|
|
10
|
+
bytesSent: number;
|
|
11
|
+
bytesConfirmed: number;
|
|
12
|
+
totalBytes: number;
|
|
13
|
+
throughputBps: number | null;
|
|
14
|
+
etaSec: number | null;
|
|
15
|
+
partsDone: number;
|
|
16
|
+
partsTotal: number;
|
|
17
|
+
/** Optional passthrough for a per-part status grid (Task 23's playground).
|
|
18
|
+
* computeProgress does not populate this itself; the engine will. */
|
|
19
|
+
partStates?: Record<number, 'inflight' | 'done'>;
|
|
20
|
+
};
|
|
21
|
+
export declare function computeProgress(i: {
|
|
22
|
+
confirmed: number;
|
|
23
|
+
inflight: number;
|
|
24
|
+
total: number;
|
|
25
|
+
partsDone: number;
|
|
26
|
+
partsTotal: number;
|
|
27
|
+
startedAtMs: number;
|
|
28
|
+
nowMs: number;
|
|
29
|
+
/** Bytes confirmed when this run began (not a timestamp, despite the
|
|
30
|
+
* similar-looking field names elsewhere — this is a byte count). */
|
|
31
|
+
confirmedAtStart: number;
|
|
32
|
+
partStates?: Record<number, 'inflight' | 'done'>;
|
|
33
|
+
}): Progress;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PutResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Design rationale: network-class errors never exhaust a budget —
|
|
4
|
+
* connectivity loss stalls patiently, since it says nothing about whether
|
|
5
|
+
* the server-side operation was valid. Finite retry budgets exist only for
|
|
6
|
+
* unexpected server responses (5xx, 429) where retrying blindly could be
|
|
7
|
+
* futile or harmful. 'expired' and 'fatal' are not retried at all, just
|
|
8
|
+
* surfaced for the caller to recover (re-presign) or abort.
|
|
9
|
+
*/
|
|
10
|
+
export type ErrorClass = 'network' | 'transient' | 'expired' | 'corrupt' | 'fatal';
|
|
11
|
+
export declare const RETRY_BUDGET = 6;
|
|
12
|
+
/** Classify the outcome of a PUT (e.g. an R2/S3 presigned upload). */
|
|
13
|
+
export declare function classifyPut(res: PutResult | null, err: unknown): ErrorClass;
|
|
14
|
+
/** Classify the outcome of an API call (create/complete/token endpoints). */
|
|
15
|
+
export declare function classifyApi(err: unknown): ErrorClass;
|
|
16
|
+
/**
|
|
17
|
+
* Exponential backoff with jitter: min(cap, 500 * 2^(attempt-1) * (1 + 0.5*rng())).
|
|
18
|
+
* `rng` defaults to Math.random; `capMs` defaults to 60000.
|
|
19
|
+
*/
|
|
20
|
+
export declare function backoffMs(attempt: number, rng?: () => number, capMs?: number): number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StateStore } from './types.js';
|
|
2
|
+
export type SessionStatus = 'idle' | 'running' | 'stalled' | 'paused' | 'completing' | 'complete' | 'failed' | 'aborted';
|
|
3
|
+
export type SessionRecord = {
|
|
4
|
+
v: 1;
|
|
5
|
+
assetId: string;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
uploadToken: string;
|
|
8
|
+
key: string;
|
|
9
|
+
partSize: number;
|
|
10
|
+
expectedParts: number;
|
|
11
|
+
declaredSize: number;
|
|
12
|
+
fileId: string;
|
|
13
|
+
done: Record<number, string>;
|
|
14
|
+
hash?: {
|
|
15
|
+
offset: number;
|
|
16
|
+
state: string;
|
|
17
|
+
};
|
|
18
|
+
sha256?: string;
|
|
19
|
+
status: SessionStatus;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
};
|
|
22
|
+
export declare const storageKey: (fileId: string) => string;
|
|
23
|
+
export declare function loadSession(store: StateStore, fileId: string): Promise<SessionRecord | null>;
|
|
24
|
+
export declare function saveSession(store: StateStore, s: SessionRecord): Promise<void>;
|
|
25
|
+
export declare function clearSession(store: StateStore, fileId: string): Promise<void>;
|
|
26
|
+
/** partSize except a short final part. */
|
|
27
|
+
export declare function partSizeOf(s: SessionRecord, n: number): number;
|
|
28
|
+
export declare function memoryStore(): StateStore;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Adapter seams. Everything platform-specific enters the engine through these. */
|
|
2
|
+
export interface FileSource {
|
|
3
|
+
size: number;
|
|
4
|
+
/** Stable identity across relaunches: name:size:mtime (browser) or path:size:mtime. */
|
|
5
|
+
id: string;
|
|
6
|
+
read(offset: number, length: number): Promise<Uint8Array>;
|
|
7
|
+
close?(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export type PutResult = {
|
|
10
|
+
status: number;
|
|
11
|
+
etag: string | null;
|
|
12
|
+
};
|
|
13
|
+
export interface HttpTransport {
|
|
14
|
+
put(url: string, body: Uint8Array, opts?: {
|
|
15
|
+
onProgress?: (sentBytes: number) => void;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}): Promise<PutResult>;
|
|
18
|
+
}
|
|
19
|
+
/** Persisted across kill/relaunch. Values are opaque strings (JSON). */
|
|
20
|
+
export interface StateStore {
|
|
21
|
+
get(key: string): Promise<string | null>;
|
|
22
|
+
set(key: string, value: string): Promise<void>;
|
|
23
|
+
delete(key: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface NetworkMonitor {
|
|
26
|
+
isOnline(): boolean;
|
|
27
|
+
onChange(cb: (online: boolean) => void): () => void;
|
|
28
|
+
}
|
|
29
|
+
export interface IncrementalHash {
|
|
30
|
+
update(data: Uint8Array): void;
|
|
31
|
+
digestHex(): string;
|
|
32
|
+
digestBytes(): Uint8Array;
|
|
33
|
+
exportState?(): string;
|
|
34
|
+
importState?(state: string): void;
|
|
35
|
+
}
|
|
36
|
+
/** Per-part hashing seam. Node swaps in node:crypto MD5; the SHA-256 lane
|
|
37
|
+
* always uses the pure implementation for portable midstate. */
|
|
38
|
+
export interface Hasher {
|
|
39
|
+
md5(): IncrementalHash;
|
|
40
|
+
}
|
|
41
|
+
export interface Clock {
|
|
42
|
+
now(): number;
|
|
43
|
+
sleep(ms: number): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Clock, FileSource, HttpTransport, StateStore, Hasher, NetworkMonitor } from './types.js';
|
|
2
|
+
import { UploadEngine } from './engine.js';
|
|
3
|
+
export type UploadOptions = {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
uploadToken: string;
|
|
6
|
+
assetId: string;
|
|
7
|
+
file: FileSource;
|
|
8
|
+
transport: HttpTransport;
|
|
9
|
+
store: StateStore;
|
|
10
|
+
hasher: Hasher;
|
|
11
|
+
network?: NetworkMonitor;
|
|
12
|
+
clock?: Clock;
|
|
13
|
+
fetchFn?: typeof fetch;
|
|
14
|
+
concurrency?: number;
|
|
15
|
+
checksum?: 'sha256' | 'none';
|
|
16
|
+
presignTtlSec?: number;
|
|
17
|
+
onTokenExpired?: (assetId: string) => Promise<string>;
|
|
18
|
+
};
|
|
19
|
+
export type Upload = Pick<UploadEngine, 'done' | 'state' | 'on' | 'start' | 'pause' | 'resume' | 'abort'>;
|
|
20
|
+
/** Resolves (or creates) the persisted session for `file.id`, wires an
|
|
21
|
+
* `HostrApi` and `UploadEngine` around it, and returns the engine as the
|
|
22
|
+
* public `Upload` surface. A session already saved for this file wins over
|
|
23
|
+
* `opts.assetId` — the caller's backend may have minted a fresh upload
|
|
24
|
+
* without knowing one was already in flight — but `opts.uploadToken` is
|
|
25
|
+
* always adopted, since the saved token may be stale. */
|
|
26
|
+
export declare function buildUpload(opts: UploadOptions): Promise<Upload>;
|
|
27
|
+
/** Lets an app skip minting a fresh upload token when one is already in
|
|
28
|
+
* flight for this file — check first, then only call the backend if this
|
|
29
|
+
* returns null. */
|
|
30
|
+
export declare function findResumable(store: StateStore, fileId: string): Promise<{
|
|
31
|
+
assetId: string;
|
|
32
|
+
} | null>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { IncrementalHash } from '../core/types.js';
|
|
2
|
+
export declare class Md5 implements IncrementalHash {
|
|
3
|
+
private h;
|
|
4
|
+
private buf;
|
|
5
|
+
private bufLen;
|
|
6
|
+
private len;
|
|
7
|
+
update(data: Uint8Array): void;
|
|
8
|
+
private compress;
|
|
9
|
+
/**
|
|
10
|
+
* Finalize on a clone so the hash can keep accepting updates.
|
|
11
|
+
*
|
|
12
|
+
* Padding: capture the message length in bits BEFORE touching the clone,
|
|
13
|
+
* then build the entire 0x80 + zero-padding + 64-bit length tail as one
|
|
14
|
+
* buffer sized so the clone's running byte count lands on a block
|
|
15
|
+
* boundary, and hand it to update() in a single call. update()'s own
|
|
16
|
+
* `len` bookkeeping on the clone is irrelevant after this point (the
|
|
17
|
+
* clone is discarded), so there is no need to keep it meaningful.
|
|
18
|
+
*/
|
|
19
|
+
digestBytes(): Uint8Array;
|
|
20
|
+
digestHex(): string;
|
|
21
|
+
exportState(): string;
|
|
22
|
+
importState(state: string): void;
|
|
23
|
+
private clone;
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { IncrementalHash } from '../core/types.js';
|
|
2
|
+
export declare class Sha256 implements IncrementalHash {
|
|
3
|
+
private h;
|
|
4
|
+
private buf;
|
|
5
|
+
private bufLen;
|
|
6
|
+
private len;
|
|
7
|
+
private w;
|
|
8
|
+
update(data: Uint8Array): void;
|
|
9
|
+
private compress;
|
|
10
|
+
/**
|
|
11
|
+
* Finalize on a clone so the hash can keep accepting updates.
|
|
12
|
+
*
|
|
13
|
+
* Padding: capture the message length in bits BEFORE touching the clone,
|
|
14
|
+
* then build the entire 0x80 + zero-padding + 64-bit length tail as one
|
|
15
|
+
* buffer sized so the clone's running byte count lands on a block
|
|
16
|
+
* boundary, and hand it to update() in a single call. update()'s own
|
|
17
|
+
* `len` bookkeeping on the clone is irrelevant after this point (the
|
|
18
|
+
* clone is discarded), so there is no need to keep it meaningful.
|
|
19
|
+
*/
|
|
20
|
+
digestBytes(): Uint8Array;
|
|
21
|
+
digestHex(): string;
|
|
22
|
+
exportState(): string;
|
|
23
|
+
importState(state: string): void;
|
|
24
|
+
private clone;
|
|
25
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type * from './core/types.js';
|
|
2
|
+
export { ApiError, NetworkError, HostrApi, createUploadSession, mintUploadToken, } from './api/client.js';
|
|
3
|
+
export type { RemotePart, RemoteUpload, CreateResult, CompleteResult } from './api/client.js';
|
|
4
|
+
export { storageKey, loadSession, saveSession, clearSession, partSizeOf, memoryStore, } from './core/session.js';
|
|
5
|
+
export type { SessionStatus, SessionRecord } from './core/session.js';
|
|
6
|
+
export { classifyPut, classifyApi, backoffMs, RETRY_BUDGET } from './core/retry.js';
|
|
7
|
+
export type { ErrorClass } from './core/retry.js';
|
|
8
|
+
export { Emitter, computeProgress } from './core/events.js';
|
|
9
|
+
export type { Progress } from './core/events.js';
|
|
10
|
+
export { UploadEngine, IntegrityError, SYSTEMIC_TRANSPORT_FAILURE_THRESHOLD } from './core/engine.js';
|
|
11
|
+
export type { CompleteInfo, EngineDeps, EngineOptions, EngineEvents, TransportDiagnostic } from './core/engine.js';
|
|
12
|
+
export { buildUpload, findResumable } from './core/upload.js';
|
|
13
|
+
export type { UploadOptions, Upload } from './core/upload.js';
|