@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,896 @@
|
|
|
1
|
+
// src/core/events.ts
|
|
2
|
+
var Emitter = class {
|
|
3
|
+
listeners = /* @__PURE__ */ new Map();
|
|
4
|
+
on(ev, cb) {
|
|
5
|
+
let set = this.listeners.get(ev);
|
|
6
|
+
if (!set) {
|
|
7
|
+
set = /* @__PURE__ */ new Set();
|
|
8
|
+
this.listeners.set(ev, set);
|
|
9
|
+
}
|
|
10
|
+
const wrapped = cb;
|
|
11
|
+
set.add(wrapped);
|
|
12
|
+
return () => {
|
|
13
|
+
set.delete(wrapped);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
emit(ev, v) {
|
|
17
|
+
const set = this.listeners.get(ev);
|
|
18
|
+
if (!set) return;
|
|
19
|
+
for (const cb of set) {
|
|
20
|
+
try {
|
|
21
|
+
cb(v);
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function computeProgress(i) {
|
|
28
|
+
const elapsedSec = (i.nowMs - i.startedAtMs) / 1e3;
|
|
29
|
+
const confirmedThisRun = i.confirmed - i.confirmedAtStart;
|
|
30
|
+
const throughputBps = elapsedSec >= 1 && confirmedThisRun > 0 ? confirmedThisRun / elapsedSec : null;
|
|
31
|
+
const bytesSent = Math.min(i.confirmed + i.inflight, i.total);
|
|
32
|
+
const etaSec = throughputBps === null ? null : (i.total - bytesSent) / throughputBps;
|
|
33
|
+
const progress = {
|
|
34
|
+
bytesSent,
|
|
35
|
+
bytesConfirmed: i.confirmed,
|
|
36
|
+
totalBytes: i.total,
|
|
37
|
+
throughputBps,
|
|
38
|
+
etaSec,
|
|
39
|
+
partsDone: i.partsDone,
|
|
40
|
+
partsTotal: i.partsTotal
|
|
41
|
+
};
|
|
42
|
+
if (i.partStates !== void 0) progress.partStates = i.partStates;
|
|
43
|
+
return progress;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/core/session.ts
|
|
47
|
+
var storageKey = (fileId) => `hostr.upload.v1:${fileId}`;
|
|
48
|
+
async function loadSession(store, fileId) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = await store.get(storageKey(fileId));
|
|
51
|
+
if (raw == null) return null;
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (parsed?.v !== 1) return null;
|
|
54
|
+
return parsed;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function saveSession(store, s) {
|
|
60
|
+
await store.set(storageKey(s.fileId), JSON.stringify(s));
|
|
61
|
+
}
|
|
62
|
+
async function clearSession(store, fileId) {
|
|
63
|
+
await store.delete(storageKey(fileId));
|
|
64
|
+
}
|
|
65
|
+
function partSizeOf(s, n) {
|
|
66
|
+
if (n === s.expectedParts) {
|
|
67
|
+
return s.declaredSize - (s.expectedParts - 1) * s.partSize;
|
|
68
|
+
}
|
|
69
|
+
return s.partSize;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/api/client.ts
|
|
73
|
+
var ApiError = class extends Error {
|
|
74
|
+
status;
|
|
75
|
+
code;
|
|
76
|
+
constructor(status, code) {
|
|
77
|
+
super(code ? `hostr api error ${status}: ${code}` : `hostr api error ${status}`);
|
|
78
|
+
this.name = "ApiError";
|
|
79
|
+
this.status = status;
|
|
80
|
+
this.code = code;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var NetworkError = class extends Error {
|
|
84
|
+
constructor(message = "network error") {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = "NetworkError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var trimEndpoint = (endpoint) => endpoint.replace(/\/$/, "");
|
|
90
|
+
async function req(fetchFn, endpoint, bearer, path, init = {}) {
|
|
91
|
+
const url = `${trimEndpoint(endpoint)}${path}`;
|
|
92
|
+
const headers = { authorization: `Bearer ${bearer}` };
|
|
93
|
+
let body;
|
|
94
|
+
if (init.body !== void 0) {
|
|
95
|
+
headers["content-type"] = "application/json";
|
|
96
|
+
body = JSON.stringify(init.body);
|
|
97
|
+
}
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await fetchFn(url, { method: init.method ?? "GET", headers, body });
|
|
101
|
+
} catch {
|
|
102
|
+
throw new NetworkError();
|
|
103
|
+
}
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
const data = text ? JSON.parse(text) : {};
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
const code = data && typeof data === "object" && typeof data.error === "string" ? data.error : void 0;
|
|
108
|
+
throw new ApiError(res.status, code);
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
var HostrApi = class {
|
|
113
|
+
#endpoint;
|
|
114
|
+
#bearer;
|
|
115
|
+
#fetch;
|
|
116
|
+
constructor(endpoint, bearer, fetchFn = fetch) {
|
|
117
|
+
this.#endpoint = endpoint;
|
|
118
|
+
this.#bearer = bearer;
|
|
119
|
+
this.#fetch = fetchFn;
|
|
120
|
+
}
|
|
121
|
+
setBearer(bearer) {
|
|
122
|
+
this.#bearer = bearer;
|
|
123
|
+
}
|
|
124
|
+
getUpload(assetId) {
|
|
125
|
+
return req(this.#fetch, this.#endpoint, this.#bearer, `/v1/uploads/${assetId}`);
|
|
126
|
+
}
|
|
127
|
+
presignParts(assetId, partNumbers, expiresIn) {
|
|
128
|
+
return req(this.#fetch, this.#endpoint, this.#bearer, `/v1/uploads/${assetId}/parts`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: expiresIn === void 0 ? { partNumbers } : { partNumbers, expiresIn }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
complete(assetId, body) {
|
|
134
|
+
return req(this.#fetch, this.#endpoint, this.#bearer, `/v1/uploads/${assetId}/complete`, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
body
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async abort(assetId) {
|
|
140
|
+
await req(this.#fetch, this.#endpoint, this.#bearer, `/v1/uploads/${assetId}`, { method: "DELETE" });
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/hash/hex.ts
|
|
145
|
+
var HEX = "0123456789abcdef";
|
|
146
|
+
function bytesToHex(b) {
|
|
147
|
+
let s = "";
|
|
148
|
+
for (let i = 0; i < b.length; i++) s += HEX[b[i] >> 4] + HEX[b[i] & 15];
|
|
149
|
+
return s;
|
|
150
|
+
}
|
|
151
|
+
function hexToBytes(s) {
|
|
152
|
+
const out = new Uint8Array(s.length >> 1);
|
|
153
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/hash/md5.ts
|
|
158
|
+
var K = new Int32Array(64);
|
|
159
|
+
for (let i = 0; i < 64; i++) K[i] = Math.floor(Math.abs(Math.sin(i + 1)) * 2 ** 32) | 0;
|
|
160
|
+
var S = [7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21];
|
|
161
|
+
var rotl = (x, n) => x << n | x >>> 32 - n;
|
|
162
|
+
var Md5 = class _Md5 {
|
|
163
|
+
h = new Int32Array([1732584193, 4023233417 | 0, 2562383102 | 0, 271733878]);
|
|
164
|
+
buf = new Uint8Array(64);
|
|
165
|
+
bufLen = 0;
|
|
166
|
+
len = 0;
|
|
167
|
+
// total bytes; Number is exact to 2^53
|
|
168
|
+
update(data) {
|
|
169
|
+
this.len += data.length;
|
|
170
|
+
let off = 0;
|
|
171
|
+
if (this.bufLen > 0) {
|
|
172
|
+
const take = Math.min(64 - this.bufLen, data.length);
|
|
173
|
+
this.buf.set(data.subarray(0, take), this.bufLen);
|
|
174
|
+
this.bufLen += take;
|
|
175
|
+
off = take;
|
|
176
|
+
if (this.bufLen === 64) {
|
|
177
|
+
this.compress(this.buf, 0);
|
|
178
|
+
this.bufLen = 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (; off + 64 <= data.length; off += 64) this.compress(data, off);
|
|
182
|
+
if (off < data.length) {
|
|
183
|
+
this.buf.set(data.subarray(off));
|
|
184
|
+
this.bufLen = data.length - off;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
compress(p, off) {
|
|
188
|
+
const m = new Int32Array(16);
|
|
189
|
+
for (let i = 0; i < 16; i++)
|
|
190
|
+
m[i] = p[off + 4 * i] | p[off + 4 * i + 1] << 8 | p[off + 4 * i + 2] << 16 | p[off + 4 * i + 3] << 24;
|
|
191
|
+
let a = this.h[0], b = this.h[1], c = this.h[2], d = this.h[3];
|
|
192
|
+
for (let i = 0; i < 64; i++) {
|
|
193
|
+
let f, g;
|
|
194
|
+
if (i < 16) {
|
|
195
|
+
f = b & c | ~b & d;
|
|
196
|
+
g = i;
|
|
197
|
+
} else if (i < 32) {
|
|
198
|
+
f = d & b | ~d & c;
|
|
199
|
+
g = (5 * i + 1) % 16;
|
|
200
|
+
} else if (i < 48) {
|
|
201
|
+
f = b ^ c ^ d;
|
|
202
|
+
g = (3 * i + 5) % 16;
|
|
203
|
+
} else {
|
|
204
|
+
f = c ^ (b | ~d);
|
|
205
|
+
g = 7 * i % 16;
|
|
206
|
+
}
|
|
207
|
+
const t = d;
|
|
208
|
+
d = c;
|
|
209
|
+
c = b;
|
|
210
|
+
b = b + rotl(a + f + K[i] + m[g] | 0, S[(i >> 4) * 4 + (i & 3)]) | 0;
|
|
211
|
+
a = t;
|
|
212
|
+
}
|
|
213
|
+
this.h[0] = this.h[0] + a | 0;
|
|
214
|
+
this.h[1] = this.h[1] + b | 0;
|
|
215
|
+
this.h[2] = this.h[2] + c | 0;
|
|
216
|
+
this.h[3] = this.h[3] + d | 0;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Finalize on a clone so the hash can keep accepting updates.
|
|
220
|
+
*
|
|
221
|
+
* Padding: capture the message length in bits BEFORE touching the clone,
|
|
222
|
+
* then build the entire 0x80 + zero-padding + 64-bit length tail as one
|
|
223
|
+
* buffer sized so the clone's running byte count lands on a block
|
|
224
|
+
* boundary, and hand it to update() in a single call. update()'s own
|
|
225
|
+
* `len` bookkeeping on the clone is irrelevant after this point (the
|
|
226
|
+
* clone is discarded), so there is no need to keep it meaningful.
|
|
227
|
+
*/
|
|
228
|
+
digestBytes() {
|
|
229
|
+
const c = this.clone();
|
|
230
|
+
const bitLen = c.len * 8;
|
|
231
|
+
const zeroCount = (56 - (c.len + 1) % 64 + 64) % 64;
|
|
232
|
+
const pad = new Uint8Array(1 + zeroCount + 8);
|
|
233
|
+
pad[0] = 128;
|
|
234
|
+
const lo = bitLen >>> 0, hi = Math.floor(bitLen / 2 ** 32);
|
|
235
|
+
const tailOff = 1 + zeroCount;
|
|
236
|
+
for (let i = 0; i < 4; i++) {
|
|
237
|
+
pad[tailOff + i] = lo >>> 8 * i & 255;
|
|
238
|
+
pad[tailOff + 4 + i] = hi >>> 8 * i & 255;
|
|
239
|
+
}
|
|
240
|
+
c.update(pad);
|
|
241
|
+
const out = new Uint8Array(16);
|
|
242
|
+
for (let i = 0; i < 4; i++)
|
|
243
|
+
for (let j = 0; j < 4; j++) out[4 * i + j] = c.h[i] >>> 8 * j & 255;
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
digestHex() {
|
|
247
|
+
return bytesToHex(this.digestBytes());
|
|
248
|
+
}
|
|
249
|
+
exportState() {
|
|
250
|
+
return JSON.stringify({ h: [...this.h], buf: bytesToHex(this.buf.subarray(0, this.bufLen)), len: this.len });
|
|
251
|
+
}
|
|
252
|
+
importState(state) {
|
|
253
|
+
const s = JSON.parse(state);
|
|
254
|
+
this.h = new Int32Array(s.h);
|
|
255
|
+
this.buf.fill(0);
|
|
256
|
+
this.bufLen = s.buf.length / 2;
|
|
257
|
+
this.len = s.len;
|
|
258
|
+
for (let i = 0; i < this.bufLen; i++) this.buf[i] = parseInt(s.buf.slice(i * 2, i * 2 + 2), 16);
|
|
259
|
+
}
|
|
260
|
+
clone() {
|
|
261
|
+
const c = new _Md5();
|
|
262
|
+
c.h = new Int32Array(this.h);
|
|
263
|
+
c.buf = new Uint8Array(this.buf);
|
|
264
|
+
c.bufLen = this.bufLen;
|
|
265
|
+
c.len = this.len;
|
|
266
|
+
return c;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// src/hash/composite.ts
|
|
271
|
+
function compositeEtag(md5Hexes) {
|
|
272
|
+
const h = new Md5();
|
|
273
|
+
for (const hex of md5Hexes) h.update(hexToBytes(hex));
|
|
274
|
+
return `${h.digestHex()}-${md5Hexes.length}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/core/engine.ts
|
|
278
|
+
var IntegrityError = class extends Error {
|
|
279
|
+
constructor(message) {
|
|
280
|
+
super(message);
|
|
281
|
+
this.name = "IntegrityError";
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// src/react-native/background-plan.ts
|
|
286
|
+
function planBackgroundWork(i) {
|
|
287
|
+
const { session, remote, staged, currentEpoch } = i;
|
|
288
|
+
const remoteByPart = new Map(remote.map((p) => [p.partNumber, p]));
|
|
289
|
+
const stagedByPart = new Map(staged.map((s) => [s.partNumber, s]));
|
|
290
|
+
const toStage = [];
|
|
291
|
+
const toCleanup = [];
|
|
292
|
+
const adopt = {};
|
|
293
|
+
for (let n = 1; n <= session.expectedParts; n++) {
|
|
294
|
+
if (session.done[n]) continue;
|
|
295
|
+
const r = remoteByPart.get(n);
|
|
296
|
+
const s = stagedByPart.get(n);
|
|
297
|
+
if (r) {
|
|
298
|
+
if (s && s.md5 === r.etag) {
|
|
299
|
+
adopt[n] = s.md5;
|
|
300
|
+
toCleanup.push(s.fileUri);
|
|
301
|
+
} else {
|
|
302
|
+
toStage.push(n);
|
|
303
|
+
}
|
|
304
|
+
} else if (!s || s.launchEpoch !== currentEpoch) {
|
|
305
|
+
toStage.push(n);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const s of staged)
|
|
309
|
+
if (session.done[s.partNumber]) toCleanup.push(s.fileUri);
|
|
310
|
+
return { toStage, toCleanup, adopt };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/react-native/fs.ts
|
|
314
|
+
async function loadFileSystem() {
|
|
315
|
+
return await import("expo-file-system");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/react-native/background.ts
|
|
319
|
+
var LAUNCH_EPOCH = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
320
|
+
var stripQuotes = (s) => s.replace(/^"|"$/g, "");
|
|
321
|
+
function findHeader(headers, name) {
|
|
322
|
+
if (!headers) return null;
|
|
323
|
+
const lower = name.toLowerCase();
|
|
324
|
+
for (const k of Object.keys(headers)) if (k.toLowerCase() === lower) return headers[k];
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
var fileIdOf = (f) => `${f.uri}:${f.size}:${f.modificationTime ?? 0}`;
|
|
328
|
+
var stagedKey = (assetId) => `hostr.upload.v1.staged:${assetId}`;
|
|
329
|
+
var filePtrKey = (fileId) => `hostr.upload.v1.background.file:${fileId}`;
|
|
330
|
+
var activeKey = "hostr.upload.v1.background:active";
|
|
331
|
+
async function loadJson(store, key, fallback) {
|
|
332
|
+
const raw = await store.get(key);
|
|
333
|
+
if (!raw) return fallback;
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(raw);
|
|
336
|
+
} catch {
|
|
337
|
+
return fallback;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async function loadStaged(store, assetId) {
|
|
341
|
+
return loadJson(store, stagedKey(assetId), []);
|
|
342
|
+
}
|
|
343
|
+
async function saveStaged(store, assetId, parts) {
|
|
344
|
+
if (parts.length === 0) await store.delete(stagedKey(assetId));
|
|
345
|
+
else await store.set(stagedKey(assetId), JSON.stringify(parts));
|
|
346
|
+
}
|
|
347
|
+
async function loadActive(store) {
|
|
348
|
+
return loadJson(store, activeKey, []);
|
|
349
|
+
}
|
|
350
|
+
async function saveFilePtr(store, fileId, file) {
|
|
351
|
+
await store.set(filePtrKey(fileId), JSON.stringify(file));
|
|
352
|
+
}
|
|
353
|
+
async function loadFilePtr(store, fileId) {
|
|
354
|
+
return loadJson(store, filePtrKey(fileId), null);
|
|
355
|
+
}
|
|
356
|
+
async function clearFilePtr(store, fileId) {
|
|
357
|
+
await store.delete(filePtrKey(fileId));
|
|
358
|
+
}
|
|
359
|
+
var chains = /* @__PURE__ */ new Map();
|
|
360
|
+
function serialize(key, fn) {
|
|
361
|
+
const prev = chains.get(key) ?? Promise.resolve();
|
|
362
|
+
const run = prev.catch(() => {
|
|
363
|
+
}).then(fn);
|
|
364
|
+
chains.set(key, run.catch(() => {
|
|
365
|
+
}));
|
|
366
|
+
return run;
|
|
367
|
+
}
|
|
368
|
+
function mutateStaged(store, assetId, mutate) {
|
|
369
|
+
return serialize(stagedKey(assetId), async () => {
|
|
370
|
+
const current = await loadStaged(store, assetId);
|
|
371
|
+
const next = mutate(current);
|
|
372
|
+
await saveStaged(store, assetId, next);
|
|
373
|
+
return next;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
function mutateActive(store, mutate) {
|
|
377
|
+
return serialize(activeKey, async () => {
|
|
378
|
+
const current = await loadActive(store);
|
|
379
|
+
const next = mutate(current);
|
|
380
|
+
if (next.length === 0) await store.delete(activeKey);
|
|
381
|
+
else await store.set(activeKey, JSON.stringify(next));
|
|
382
|
+
return next;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async function markActive(store, fileId) {
|
|
386
|
+
await mutateActive(store, (ids) => ids.includes(fileId) ? ids : [...ids, fileId]);
|
|
387
|
+
}
|
|
388
|
+
async function unmarkActive(store, fileId) {
|
|
389
|
+
await mutateActive(store, (ids) => ids.filter((x) => x !== fileId));
|
|
390
|
+
}
|
|
391
|
+
async function windowed(items, limit, fn) {
|
|
392
|
+
let next = 0;
|
|
393
|
+
const worker = async () => {
|
|
394
|
+
while (next < items.length) {
|
|
395
|
+
const item = items[next++];
|
|
396
|
+
await fn(item);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
400
|
+
}
|
|
401
|
+
var liveTasks = /* @__PURE__ */ new Map();
|
|
402
|
+
function trackTask(assetId, task) {
|
|
403
|
+
let set = liveTasks.get(assetId);
|
|
404
|
+
if (!set) {
|
|
405
|
+
set = /* @__PURE__ */ new Set();
|
|
406
|
+
liveTasks.set(assetId, set);
|
|
407
|
+
}
|
|
408
|
+
set.add(task);
|
|
409
|
+
return () => {
|
|
410
|
+
set.delete(task);
|
|
411
|
+
if (set.size === 0) liveTasks.delete(assetId);
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function cancelBackgroundTasks(assetId) {
|
|
415
|
+
const set = liveTasks.get(assetId);
|
|
416
|
+
if (!set) return 0;
|
|
417
|
+
const n = set.size;
|
|
418
|
+
for (const task of set) {
|
|
419
|
+
try {
|
|
420
|
+
task.cancel();
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return n;
|
|
425
|
+
}
|
|
426
|
+
var stagingDirReady = null;
|
|
427
|
+
async function ensureStagingDir(FS) {
|
|
428
|
+
if (!stagingDirReady) {
|
|
429
|
+
stagingDirReady = (async () => {
|
|
430
|
+
const dir = new FS.Directory(FS.Paths.cache, "hostr");
|
|
431
|
+
dir.create({ intermediates: true, idempotent: true });
|
|
432
|
+
return dir;
|
|
433
|
+
})().catch((e) => {
|
|
434
|
+
stagingDirReady = null;
|
|
435
|
+
throw e;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return stagingDirReady;
|
|
439
|
+
}
|
|
440
|
+
async function stagePart(opts) {
|
|
441
|
+
const FS = await loadFileSystem();
|
|
442
|
+
const handle = new FS.File(opts.file.uri).open();
|
|
443
|
+
let bytes;
|
|
444
|
+
try {
|
|
445
|
+
handle.offset = opts.offset;
|
|
446
|
+
bytes = handle.readBytes(opts.length);
|
|
447
|
+
} finally {
|
|
448
|
+
handle.close();
|
|
449
|
+
}
|
|
450
|
+
if (bytes.length !== opts.length)
|
|
451
|
+
throw new Error(
|
|
452
|
+
`part ${opts.partNumber}: source file changed size mid-upload (expected ${opts.length} bytes, read ${bytes.length}) \u2014 the file was modified or truncated after the upload started`
|
|
453
|
+
);
|
|
454
|
+
const dir = await ensureStagingDir(FS);
|
|
455
|
+
const staged = new FS.File(dir, `${opts.session.assetId}.${opts.partNumber}.part`);
|
|
456
|
+
staged.create({ overwrite: true });
|
|
457
|
+
staged.write(bytes);
|
|
458
|
+
const stagedUri = staged.uri;
|
|
459
|
+
const md5 = staged.md5?.toLowerCase();
|
|
460
|
+
if (!md5)
|
|
461
|
+
throw new Error(`part ${opts.partNumber}: staged file at ${stagedUri} is unreadable (md5 unavailable)`);
|
|
462
|
+
await mutateStaged(opts.store, opts.session.assetId, (current) => {
|
|
463
|
+
const next = current.filter((s) => s.partNumber !== opts.partNumber);
|
|
464
|
+
next.push({ partNumber: opts.partNumber, fileUri: stagedUri, md5, launchEpoch: LAUNCH_EPOCH });
|
|
465
|
+
return next;
|
|
466
|
+
});
|
|
467
|
+
const task = staged.createUploadTask(opts.url, {
|
|
468
|
+
httpMethod: "PUT",
|
|
469
|
+
uploadType: FS.UploadType.BINARY_CONTENT,
|
|
470
|
+
sessionType: "background",
|
|
471
|
+
onProgress: (d) => opts.observer?.onPartProgress?.(opts.partNumber, d.bytesSent)
|
|
472
|
+
});
|
|
473
|
+
const untrack = trackTask(opts.session.assetId, task);
|
|
474
|
+
const forgetStaged = async () => {
|
|
475
|
+
try {
|
|
476
|
+
staged.delete();
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
await mutateStaged(opts.store, opts.session.assetId, (current) => current.filter((s) => s.partNumber !== opts.partNumber));
|
|
480
|
+
};
|
|
481
|
+
task.uploadAsync().then(async (result) => {
|
|
482
|
+
untrack();
|
|
483
|
+
const ok = !!result && result.status >= 200 && result.status < 300;
|
|
484
|
+
const rawEtag = ok ? findHeader(result.headers, "etag") : null;
|
|
485
|
+
const etag = rawEtag ? stripQuotes(rawEtag) : null;
|
|
486
|
+
if (etag === md5) {
|
|
487
|
+
if (opts.session.status !== "running") {
|
|
488
|
+
await forgetStaged();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
opts.session.done[opts.partNumber] = md5;
|
|
492
|
+
await saveSession(opts.store, opts.session);
|
|
493
|
+
await forgetStaged();
|
|
494
|
+
opts.observer?.onPartLanded?.(opts.partNumber);
|
|
495
|
+
if (opts.onAllLanded && Object.keys(opts.session.done).length >= opts.session.expectedParts)
|
|
496
|
+
opts.onAllLanded();
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
await forgetStaged();
|
|
500
|
+
opts.observer?.onPartFailed?.(opts.partNumber);
|
|
501
|
+
}).catch(async () => {
|
|
502
|
+
untrack();
|
|
503
|
+
await forgetStaged();
|
|
504
|
+
opts.observer?.onPartFailed?.(opts.partNumber);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async function safeStagePart(opts) {
|
|
508
|
+
try {
|
|
509
|
+
await stagePart(opts);
|
|
510
|
+
} catch (e) {
|
|
511
|
+
console.warn(
|
|
512
|
+
`hostr background: failed to stage part ${opts.partNumber} for ${opts.session.assetId}: ${e instanceof Error ? e.message : String(e)} \u2014 will retry on the next reconcile`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function apiCall(ctx, fn) {
|
|
517
|
+
try {
|
|
518
|
+
return await fn();
|
|
519
|
+
} catch (e) {
|
|
520
|
+
if (e instanceof ApiError && e.status === 401 && ctx.onTokenExpired) {
|
|
521
|
+
const t = await ctx.onTokenExpired(ctx.session.assetId);
|
|
522
|
+
ctx.session.uploadToken = t;
|
|
523
|
+
ctx.api.setBearer(t);
|
|
524
|
+
await saveSession(ctx.store, ctx.session);
|
|
525
|
+
return await fn();
|
|
526
|
+
}
|
|
527
|
+
throw e;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function loadOrCreateSession(opts) {
|
|
531
|
+
const api = new HostrApi(opts.endpoint, opts.uploadToken);
|
|
532
|
+
const fileId = fileIdOf(opts.file);
|
|
533
|
+
let session = await loadSession(opts.store, fileId);
|
|
534
|
+
if (session) {
|
|
535
|
+
session.uploadToken = opts.uploadToken;
|
|
536
|
+
await saveSession(opts.store, session);
|
|
537
|
+
} else {
|
|
538
|
+
const remote = await api.getUpload(opts.assetId);
|
|
539
|
+
session = {
|
|
540
|
+
v: 1,
|
|
541
|
+
assetId: opts.assetId,
|
|
542
|
+
endpoint: opts.endpoint,
|
|
543
|
+
uploadToken: opts.uploadToken,
|
|
544
|
+
key: remote.key,
|
|
545
|
+
partSize: remote.partSize,
|
|
546
|
+
expectedParts: remote.expectedParts,
|
|
547
|
+
declaredSize: opts.file.size,
|
|
548
|
+
fileId,
|
|
549
|
+
done: {},
|
|
550
|
+
status: "running",
|
|
551
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
552
|
+
};
|
|
553
|
+
await saveSession(opts.store, session);
|
|
554
|
+
}
|
|
555
|
+
return { session, api };
|
|
556
|
+
}
|
|
557
|
+
function runReconcile(opts) {
|
|
558
|
+
return serialize(`reconcile:${opts.session.assetId}`, () => runReconcilePass(opts));
|
|
559
|
+
}
|
|
560
|
+
async function runReconcilePass(opts) {
|
|
561
|
+
const { store, session, api, file } = opts;
|
|
562
|
+
if (session.status === "complete") return "complete";
|
|
563
|
+
if (session.status === "aborted") return "in-progress";
|
|
564
|
+
const ctx = { api, session, store, onTokenExpired: opts.onTokenExpired };
|
|
565
|
+
const remote = (await apiCall(ctx, () => api.getUpload(session.assetId))).parts;
|
|
566
|
+
const staged = await loadStaged(store, session.assetId);
|
|
567
|
+
const plan = planBackgroundWork({ session, remote, staged, currentEpoch: LAUNCH_EPOCH });
|
|
568
|
+
for (const [nStr, md5] of Object.entries(plan.adopt)) session.done[Number(nStr)] = md5;
|
|
569
|
+
if (Object.keys(plan.adopt).length > 0) await saveSession(store, session);
|
|
570
|
+
if (plan.toCleanup.length > 0) {
|
|
571
|
+
const FS = await loadFileSystem();
|
|
572
|
+
for (const uri of plan.toCleanup) {
|
|
573
|
+
try {
|
|
574
|
+
new FS.File(uri).delete();
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
await mutateStaged(store, session.assetId, (current) => current.filter((s) => !plan.toCleanup.includes(s.fileUri)));
|
|
579
|
+
}
|
|
580
|
+
if (plan.toStage.length > 0) {
|
|
581
|
+
if (!file) {
|
|
582
|
+
console.warn(
|
|
583
|
+
`hostr background: cannot stage ${plan.toStage.length} part(s) for ${session.assetId} \u2014 no file pointer available; left for the next reconcile`
|
|
584
|
+
);
|
|
585
|
+
return "in-progress";
|
|
586
|
+
}
|
|
587
|
+
const { urls } = await apiCall(ctx, () => api.presignParts(session.assetId, plan.toStage, 43200));
|
|
588
|
+
const urlByPart = new Map(urls.map((u) => [u.partNumber, u.url]));
|
|
589
|
+
const onAllLanded = () => {
|
|
590
|
+
void runReconcile(opts).then((result) => opts.onAutoReconcile?.(result)).catch(() => {
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
await windowed(plan.toStage, 4, async (n) => {
|
|
594
|
+
const url = urlByPart.get(n);
|
|
595
|
+
if (!url) return;
|
|
596
|
+
const offset = (n - 1) * session.partSize;
|
|
597
|
+
const length = partSizeOf(session, n);
|
|
598
|
+
await safeStagePart({
|
|
599
|
+
store,
|
|
600
|
+
session,
|
|
601
|
+
file,
|
|
602
|
+
partNumber: n,
|
|
603
|
+
offset,
|
|
604
|
+
length,
|
|
605
|
+
url,
|
|
606
|
+
onAllLanded,
|
|
607
|
+
observer: opts.observer
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
return "in-progress";
|
|
611
|
+
}
|
|
612
|
+
if (Object.keys(session.done).length < session.expectedParts) return "in-progress";
|
|
613
|
+
session.status = "completing";
|
|
614
|
+
await saveSession(store, session);
|
|
615
|
+
const res = await apiCall(ctx, () => api.complete(session.assetId, session.sha256 ? { sha256: session.sha256 } : {}));
|
|
616
|
+
const md5s = [];
|
|
617
|
+
for (let n = 1; n <= session.expectedParts; n++) md5s.push(session.done[n]);
|
|
618
|
+
const expected = compositeEtag(md5s);
|
|
619
|
+
if (res.etag && res.etag !== expected)
|
|
620
|
+
throw new IntegrityError(`R2 assembled ${res.etag}; client computed ${expected}`);
|
|
621
|
+
await clearSession(store, session.fileId);
|
|
622
|
+
await unmarkActive(store, session.fileId);
|
|
623
|
+
await clearFilePtr(store, session.fileId);
|
|
624
|
+
session.status = "complete";
|
|
625
|
+
opts.observer?.onComplete?.({
|
|
626
|
+
assetId: session.assetId,
|
|
627
|
+
etag: res.etag ?? expected,
|
|
628
|
+
bytes: session.declaredSize
|
|
629
|
+
});
|
|
630
|
+
return "complete";
|
|
631
|
+
}
|
|
632
|
+
async function startBackgroundUpload(opts) {
|
|
633
|
+
const { session, api } = await loadOrCreateSession(opts);
|
|
634
|
+
await markActive(opts.store, session.fileId);
|
|
635
|
+
await saveFilePtr(opts.store, session.fileId, opts.file);
|
|
636
|
+
const reconcile = () => runReconcile({
|
|
637
|
+
store: opts.store,
|
|
638
|
+
session,
|
|
639
|
+
api,
|
|
640
|
+
file: opts.file,
|
|
641
|
+
onTokenExpired: opts.onTokenExpired,
|
|
642
|
+
onAutoReconcile: opts.onAutoReconcile,
|
|
643
|
+
observer: opts.observer
|
|
644
|
+
});
|
|
645
|
+
await reconcile();
|
|
646
|
+
return {
|
|
647
|
+
reconcile,
|
|
648
|
+
pause: () => cancelBackgroundTasks(session.assetId),
|
|
649
|
+
abort: async () => {
|
|
650
|
+
session.status = "aborted";
|
|
651
|
+
cancelBackgroundTasks(session.assetId);
|
|
652
|
+
await api.abort(session.assetId).catch(() => {
|
|
653
|
+
});
|
|
654
|
+
const staged = await loadStaged(opts.store, session.assetId);
|
|
655
|
+
if (staged.length > 0) {
|
|
656
|
+
const FS = await loadFileSystem();
|
|
657
|
+
for (const s of staged) {
|
|
658
|
+
try {
|
|
659
|
+
new FS.File(s.fileUri).delete();
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
await mutateStaged(opts.store, session.assetId, () => []);
|
|
664
|
+
}
|
|
665
|
+
await clearSession(opts.store, session.fileId);
|
|
666
|
+
await unmarkActive(opts.store, session.fileId);
|
|
667
|
+
await clearFilePtr(opts.store, session.fileId);
|
|
668
|
+
},
|
|
669
|
+
assetId: session.assetId,
|
|
670
|
+
partSize: session.partSize,
|
|
671
|
+
expectedParts: session.expectedParts,
|
|
672
|
+
declaredSize: session.declaredSize,
|
|
673
|
+
donePartNumbers: () => Object.keys(session.done).map(Number)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function reconcileOnLaunch(store, opts) {
|
|
677
|
+
const ids = await loadActive(store);
|
|
678
|
+
for (const fileId of ids) {
|
|
679
|
+
const session = await loadSession(store, fileId);
|
|
680
|
+
if (!session) {
|
|
681
|
+
await unmarkActive(store, fileId);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const file = await loadFilePtr(store, fileId);
|
|
685
|
+
const api = new HostrApi(opts.endpoint, session.uploadToken);
|
|
686
|
+
try {
|
|
687
|
+
await runReconcile({ store, session, api, file });
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/react-native/native-upload.ts
|
|
694
|
+
var RETRY_DELAY_MS = 15e3;
|
|
695
|
+
var PROGRESS_THROTTLE_MS = 250;
|
|
696
|
+
var NativeUpload = class {
|
|
697
|
+
done;
|
|
698
|
+
resolveDone;
|
|
699
|
+
rejectDone;
|
|
700
|
+
emitter = new Emitter();
|
|
701
|
+
handle = null;
|
|
702
|
+
status = "idle";
|
|
703
|
+
doneParts = /* @__PURE__ */ new Set();
|
|
704
|
+
inflight = /* @__PURE__ */ new Map();
|
|
705
|
+
// partNumber -> bytesSent
|
|
706
|
+
confirmedBytes = 0;
|
|
707
|
+
confirmedAtStart = 0;
|
|
708
|
+
startedAtMs = 0;
|
|
709
|
+
lastProgressEmit = 0;
|
|
710
|
+
retryTimer = null;
|
|
711
|
+
// Not a TS parameter property — Node 24's strip-only TS can't run those,
|
|
712
|
+
// and this package's tests execute the source directly.
|
|
713
|
+
opts;
|
|
714
|
+
constructor(opts) {
|
|
715
|
+
this.opts = opts;
|
|
716
|
+
this.done = new Promise((res, rej) => {
|
|
717
|
+
this.resolveDone = res;
|
|
718
|
+
this.rejectDone = rej;
|
|
719
|
+
});
|
|
720
|
+
this.done.catch(() => {
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
get state() {
|
|
724
|
+
return this.status;
|
|
725
|
+
}
|
|
726
|
+
on(ev, cb) {
|
|
727
|
+
return this.emitter.on(ev, cb);
|
|
728
|
+
}
|
|
729
|
+
/** Kicks the upload off. Synchronous by signature (matching the engine);
|
|
730
|
+
* the launch itself is async and reports failures via the 'error' event
|
|
731
|
+
* and the `done` promise. Calling it twice is a no-op. */
|
|
732
|
+
start() {
|
|
733
|
+
if (this.status !== "idle") return;
|
|
734
|
+
this.setStatus("running");
|
|
735
|
+
void this.launch();
|
|
736
|
+
}
|
|
737
|
+
pause() {
|
|
738
|
+
if (this.status !== "running") return;
|
|
739
|
+
this.setStatus("paused");
|
|
740
|
+
this.clearRetry();
|
|
741
|
+
this.handle?.pause();
|
|
742
|
+
this.inflight.clear();
|
|
743
|
+
this.emitProgress(true);
|
|
744
|
+
}
|
|
745
|
+
resume() {
|
|
746
|
+
if (this.status !== "paused") return;
|
|
747
|
+
this.setStatus("running");
|
|
748
|
+
void this.reconcileQuietly();
|
|
749
|
+
}
|
|
750
|
+
abort() {
|
|
751
|
+
if (this.status === "complete" || this.status === "aborted") return;
|
|
752
|
+
this.setStatus("aborted");
|
|
753
|
+
this.clearRetry();
|
|
754
|
+
const h = this.handle;
|
|
755
|
+
this.handle = null;
|
|
756
|
+
void (async () => {
|
|
757
|
+
try {
|
|
758
|
+
await h?.abort();
|
|
759
|
+
} catch {
|
|
760
|
+
}
|
|
761
|
+
this.rejectDone(new Error("upload aborted"));
|
|
762
|
+
})();
|
|
763
|
+
}
|
|
764
|
+
// --- internals ------------------------------------------------------------
|
|
765
|
+
async launch() {
|
|
766
|
+
try {
|
|
767
|
+
const observer = {
|
|
768
|
+
onPartProgress: (n, bytesSent) => {
|
|
769
|
+
if (this.status !== "running") return;
|
|
770
|
+
this.inflight.set(n, bytesSent);
|
|
771
|
+
this.emitProgress(false);
|
|
772
|
+
},
|
|
773
|
+
onPartLanded: (n) => {
|
|
774
|
+
if (this.doneParts.has(n)) return;
|
|
775
|
+
this.doneParts.add(n);
|
|
776
|
+
this.inflight.delete(n);
|
|
777
|
+
this.confirmedBytes += this.partSizeOf(n);
|
|
778
|
+
this.emitProgress(true);
|
|
779
|
+
},
|
|
780
|
+
onPartFailed: (n) => {
|
|
781
|
+
this.inflight.delete(n);
|
|
782
|
+
if (this.status !== "running") return;
|
|
783
|
+
this.scheduleRetry();
|
|
784
|
+
},
|
|
785
|
+
onComplete: (info) => {
|
|
786
|
+
this.clearRetry();
|
|
787
|
+
this.inflight.clear();
|
|
788
|
+
this.emitProgress(true);
|
|
789
|
+
this.setStatus("complete");
|
|
790
|
+
this.resolveDone({ ...info, sha256: void 0 });
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
const handle = await startBackgroundUpload({ ...this.opts, observer });
|
|
794
|
+
if (this.status === "aborted") {
|
|
795
|
+
await handle.abort().catch(() => {
|
|
796
|
+
});
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
this.handle = handle;
|
|
800
|
+
if (this.status === "paused") handle.pause();
|
|
801
|
+
for (const n of handle.donePartNumbers()) {
|
|
802
|
+
if (!this.doneParts.has(n)) {
|
|
803
|
+
this.doneParts.add(n);
|
|
804
|
+
this.confirmedBytes += this.partSizeOf(n);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
this.confirmedAtStart = this.confirmedBytes;
|
|
808
|
+
this.startedAtMs = Date.now();
|
|
809
|
+
this.emitProgress(true);
|
|
810
|
+
if (this.doneParts.size >= handle.expectedParts) void this.reconcileQuietly();
|
|
811
|
+
} catch (e) {
|
|
812
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
813
|
+
this.setStatus("failed");
|
|
814
|
+
this.emitter.emit("error", err);
|
|
815
|
+
this.rejectDone(err);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/** A reconcile whose failure must not kill the upload: log-free retry via
|
|
819
|
+
* the timer, because reconcile failures here are transient by
|
|
820
|
+
* construction (offline, a token mid-refresh) and the store still holds
|
|
821
|
+
* everything needed to finish later. */
|
|
822
|
+
async reconcileQuietly() {
|
|
823
|
+
if (!this.handle || this.status !== "running") return;
|
|
824
|
+
try {
|
|
825
|
+
await this.handle.reconcile();
|
|
826
|
+
} catch {
|
|
827
|
+
this.scheduleRetry();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
scheduleRetry() {
|
|
831
|
+
if (this.retryTimer || this.status !== "running") return;
|
|
832
|
+
this.retryTimer = setTimeout(() => {
|
|
833
|
+
this.retryTimer = null;
|
|
834
|
+
void this.reconcileQuietly();
|
|
835
|
+
}, RETRY_DELAY_MS);
|
|
836
|
+
}
|
|
837
|
+
clearRetry() {
|
|
838
|
+
if (this.retryTimer) {
|
|
839
|
+
clearTimeout(this.retryTimer);
|
|
840
|
+
this.retryTimer = null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
partSizeOf(n) {
|
|
844
|
+
const h = this.handle;
|
|
845
|
+
if (!h) return 0;
|
|
846
|
+
return n < h.expectedParts ? h.partSize : h.declaredSize - (h.expectedParts - 1) * h.partSize;
|
|
847
|
+
}
|
|
848
|
+
setStatus(s) {
|
|
849
|
+
if (this.status === s) return;
|
|
850
|
+
this.status = s;
|
|
851
|
+
this.emitter.emit("statechange", s);
|
|
852
|
+
}
|
|
853
|
+
emitProgress(force) {
|
|
854
|
+
const now = Date.now();
|
|
855
|
+
if (!force && now - this.lastProgressEmit < PROGRESS_THROTTLE_MS) return;
|
|
856
|
+
this.lastProgressEmit = now;
|
|
857
|
+
const h = this.handle;
|
|
858
|
+
const total = h?.declaredSize ?? this.opts.file.size;
|
|
859
|
+
const partsTotal = h?.expectedParts ?? 0;
|
|
860
|
+
let inflightBytes = 0;
|
|
861
|
+
for (const b of this.inflight.values()) inflightBytes += b;
|
|
862
|
+
this.emitter.emit("progress", computeProgress({
|
|
863
|
+
confirmed: this.confirmedBytes,
|
|
864
|
+
inflight: inflightBytes,
|
|
865
|
+
total,
|
|
866
|
+
partsDone: this.doneParts.size,
|
|
867
|
+
partsTotal,
|
|
868
|
+
startedAtMs: this.startedAtMs || now,
|
|
869
|
+
nowMs: now,
|
|
870
|
+
confirmedAtStart: this.confirmedAtStart
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// src/react-native/index.ts
|
|
876
|
+
function asyncStorageStore(as) {
|
|
877
|
+
return {
|
|
878
|
+
get: (key) => as.getItem(key),
|
|
879
|
+
set: (key, value) => as.setItem(key, value),
|
|
880
|
+
delete: (key) => as.removeItem(key)
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
async function createUpload(opts) {
|
|
884
|
+
return new NativeUpload(opts);
|
|
885
|
+
}
|
|
886
|
+
export {
|
|
887
|
+
ApiError,
|
|
888
|
+
IntegrityError,
|
|
889
|
+
NativeUpload,
|
|
890
|
+
NetworkError,
|
|
891
|
+
asyncStorageStore,
|
|
892
|
+
cancelBackgroundTasks,
|
|
893
|
+
createUpload,
|
|
894
|
+
reconcileOnLaunch,
|
|
895
|
+
startBackgroundUpload
|
|
896
|
+
};
|