@sparkvault/sdk-mobile 5.3.0 → 6.0.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 +4 -35
- package/dist/errors.d.ts +11 -3
- package/dist/errors.js +7 -3
- package/dist/errors.js.map +1 -1
- package/dist/identity-dialog.d.ts +2 -7
- package/dist/identity-dialog.js +62 -133
- package/dist/identity-dialog.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +6 -0
- package/dist/ingots.js +8 -3
- package/dist/ingots.js.map +1 -1
- package/dist/tus.d.ts +21 -10
- package/dist/tus.js +97 -100
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +17 -6
- package/package.json +1 -1
- package/src/errors.ts +11 -6
- package/src/identity-dialog.tsx +89 -181
- package/src/index.ts +1 -0
- package/src/ingots.ts +9 -4
- package/src/tus.ts +176 -126
- package/src/types.ts +17 -6
package/src/tus.ts
CHANGED
|
@@ -149,17 +149,6 @@ function getErrorMessage(err: unknown): string {
|
|
|
149
149
|
return err instanceof Error ? err.message : String(err);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function inferTusPhase(error: Error): TusUploadError['phase'] {
|
|
153
|
-
const message = error.message;
|
|
154
|
-
if (message.includes('cancelled') || message.includes('aborted')) return 'cancelled';
|
|
155
|
-
if (message.includes('timeout') || message.includes('timed out')) return 'timeout';
|
|
156
|
-
if (message.includes('stall')) return 'stalled';
|
|
157
|
-
if (message.includes('network')) return 'network';
|
|
158
|
-
if (message.includes('create') || message.includes('POST')) return 'create';
|
|
159
|
-
if (message.includes('upload') || message.includes('PATCH') || message.includes('Chunk')) return 'upload';
|
|
160
|
-
return 'unknown';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
152
|
function ensureNotAborted(signal?: AbortSignal): void {
|
|
164
153
|
if (signal?.aborted) {
|
|
165
154
|
throw new TusUploadError('Upload cancelled', { phase: 'cancelled' });
|
|
@@ -194,6 +183,87 @@ function parseIntegerHeader(raw: string | null, minimum: number): number | null
|
|
|
194
183
|
return Number.isSafeInteger(value) && value >= minimum ? value : null;
|
|
195
184
|
}
|
|
196
185
|
|
|
186
|
+
/** File context carried on every `TusUploadError` a request throws. */
|
|
187
|
+
interface TusErrorContext {
|
|
188
|
+
filename?: string;
|
|
189
|
+
fileSize?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Forge's answers that mean the tus session no longer exists: it expired,
|
|
194
|
+
* or a DELETE terminated it. One list, so every request that can meet a
|
|
195
|
+
* dead session (HEAD, the finalization PATCH, the OS engine's PATCHes,
|
|
196
|
+
* DELETE) and the app's own session bookkeeping agree on what "gone" is.
|
|
197
|
+
*/
|
|
198
|
+
const SESSION_GONE_STATUSES: ReadonlySet<number> = new Set([404, 410]);
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* True when a Forge status says the tus session is gone (expired or
|
|
202
|
+
* terminated): the persisted session is worthless, and a caller starts a
|
|
203
|
+
* fresh upload instead of retrying it.
|
|
204
|
+
*/
|
|
205
|
+
export function isSessionGone(status: number): boolean {
|
|
206
|
+
return SESSION_GONE_STATUSES.has(status);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function sessionLostError(
|
|
210
|
+
status: number,
|
|
211
|
+
context: TusErrorContext,
|
|
212
|
+
phase: TusUploadError['phase'],
|
|
213
|
+
cause: Error | null = null
|
|
214
|
+
): TusUploadError {
|
|
215
|
+
return new TusUploadError(`TUS session no longer exists on the server (${status})`, {
|
|
216
|
+
cause,
|
|
217
|
+
httpStatus: status,
|
|
218
|
+
...context,
|
|
219
|
+
phase,
|
|
220
|
+
sessionLost: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface TusRequestInit {
|
|
225
|
+
method: 'POST' | 'HEAD' | 'PATCH' | 'DELETE';
|
|
226
|
+
headers: Record<string, string>;
|
|
227
|
+
signal?: AbortSignal;
|
|
228
|
+
timeoutMs: number;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The one seam every tus request sent through `config.fetch` (the create
|
|
233
|
+
* POST, HEAD, the finalization PATCH, DELETE) passes its rejection through,
|
|
234
|
+
* so a request that never got an HTTP answer is classified once and the
|
|
235
|
+
* same way everywhere. The caller's own abort is `cancelled` and the request
|
|
236
|
+
* timeout is `timeout`: both arrive as an `AbortError` and the signal tells
|
|
237
|
+
* them apart, and an XHR adapter's `TimeoutError` is a timeout too (the
|
|
238
|
+
* same contract http.ts honours). An SDK error passes through untouched.
|
|
239
|
+
* Everything else is `network` (an offline device, a DNS failure, a reset
|
|
240
|
+
* connection), so `isNetworkError` holds and the caller retries instead of
|
|
241
|
+
* charging the failure to the file. Chunk PATCHes ride XHR directly and
|
|
242
|
+
* classify their own events (see uploadChunk).
|
|
243
|
+
*/
|
|
244
|
+
async function tusRequest(
|
|
245
|
+
config: ResolvedMobileConfig,
|
|
246
|
+
url: string,
|
|
247
|
+
init: TusRequestInit,
|
|
248
|
+
label: string,
|
|
249
|
+
context: TusErrorContext = {}
|
|
250
|
+
): Promise<Response> {
|
|
251
|
+
try {
|
|
252
|
+
return await config.fetch(url, init);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (err instanceof SparkVaultMobileError) throw err;
|
|
255
|
+
const cause = err instanceof Error ? err : null;
|
|
256
|
+
const name = getErrorName(err);
|
|
257
|
+
if (name === 'AbortError' && init.signal?.aborted) {
|
|
258
|
+
throw new TusUploadError('Upload cancelled', { cause, ...context, phase: 'cancelled' });
|
|
259
|
+
}
|
|
260
|
+
if (name === 'AbortError' || name === 'TimeoutError') {
|
|
261
|
+
throw new TusUploadError(`${label} timed out`, { cause, ...context, phase: 'timeout' });
|
|
262
|
+
}
|
|
263
|
+
throw new TusUploadError(`${label} failed: ${getErrorMessage(err)}`, { cause, ...context, phase: 'network' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
197
267
|
/**
|
|
198
268
|
* Case-insensitive header lookup for adapter results. Native HTTP stacks
|
|
199
269
|
* hand headers back in whatever case they please (iOS lower-cases them),
|
|
@@ -242,27 +312,23 @@ async function createTusUpload(
|
|
|
242
312
|
ensureNotAborted(signal);
|
|
243
313
|
|
|
244
314
|
const metadata = `filename ${base64EncodeUtf8(filename)},filetype ${base64EncodeUtf8(contentType || 'application/octet-stream')}`;
|
|
245
|
-
const response = await
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
'
|
|
250
|
-
|
|
251
|
-
|
|
315
|
+
const response = await tusRequest(
|
|
316
|
+
config,
|
|
317
|
+
parsed.tusEndpoint,
|
|
318
|
+
{
|
|
319
|
+
method: 'POST',
|
|
320
|
+
headers: {
|
|
321
|
+
'Tus-Resumable': TUS_VERSION,
|
|
322
|
+
'Upload-Length': String(fileSize),
|
|
323
|
+
'Upload-Metadata': metadata,
|
|
324
|
+
'X-ISTK': parsed.istk,
|
|
325
|
+
},
|
|
326
|
+
signal,
|
|
327
|
+
timeoutMs: config.tusPostTimeoutMs,
|
|
252
328
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (getErrorName(err) === 'AbortError') {
|
|
257
|
-
throw new TusUploadError('TUS POST request timed out', {
|
|
258
|
-
cause: err instanceof Error ? err : null,
|
|
259
|
-
filename,
|
|
260
|
-
fileSize,
|
|
261
|
-
phase: 'timeout',
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
throw err;
|
|
265
|
-
});
|
|
329
|
+
'TUS POST request',
|
|
330
|
+
{ filename, fileSize }
|
|
331
|
+
);
|
|
266
332
|
|
|
267
333
|
if (!response.ok) {
|
|
268
334
|
const errorText = await response.text().catch(() => '');
|
|
@@ -315,10 +381,11 @@ async function createTusUpload(
|
|
|
315
381
|
* Ask Forge where a session stands (tus HEAD). This is the only way to learn
|
|
316
382
|
* what a background URLSession delivered after the process was killed, and
|
|
317
383
|
* what a failed attempt left behind — so resume never re-sends bytes the
|
|
318
|
-
* server holds. A
|
|
319
|
-
* error carries `sessionLost` so the caller
|
|
320
|
-
* creates a fresh one rather than retrying
|
|
321
|
-
* (a 5xx, an auth rejection, a timeout)
|
|
384
|
+
* server holds. A status `isSessionGone` recognises means the session
|
|
385
|
+
* expired or was terminated: the error carries `sessionLost` so the caller
|
|
386
|
+
* drops its persisted session and creates a fresh one rather than retrying
|
|
387
|
+
* a dead URL. Any other failure (a 5xx, an auth rejection, a timeout)
|
|
388
|
+
* leaves `sessionLost` false: the
|
|
322
389
|
* session may well be intact, and treating it as dead would re-upload a
|
|
323
390
|
* whole file over a blip.
|
|
324
391
|
*/
|
|
@@ -327,38 +394,28 @@ async function headTusSession(
|
|
|
327
394
|
uploadUrl: string,
|
|
328
395
|
istk: string,
|
|
329
396
|
signal?: AbortSignal,
|
|
330
|
-
context:
|
|
397
|
+
context: TusErrorContext = {}
|
|
331
398
|
): Promise<TusSessionProbe> {
|
|
332
399
|
ensureNotAborted(signal);
|
|
333
400
|
|
|
334
|
-
const response = await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
'
|
|
401
|
+
const response = await tusRequest(
|
|
402
|
+
config,
|
|
403
|
+
uploadUrl,
|
|
404
|
+
{
|
|
405
|
+
method: 'HEAD',
|
|
406
|
+
headers: {
|
|
407
|
+
'Tus-Resumable': TUS_VERSION,
|
|
408
|
+
'X-ISTK': istk,
|
|
409
|
+
},
|
|
410
|
+
signal,
|
|
411
|
+
timeoutMs: config.tusPostTimeoutMs,
|
|
339
412
|
},
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (getErrorName(err) === 'AbortError') {
|
|
344
|
-
// The same AbortError ends a user cancel and the request timeout; the
|
|
345
|
-
// caller's signal tells them apart.
|
|
346
|
-
throw new TusUploadError(signal?.aborted ? 'Upload cancelled' : 'TUS HEAD request timed out', {
|
|
347
|
-
cause: err instanceof Error ? err : null,
|
|
348
|
-
...context,
|
|
349
|
-
phase: signal?.aborted ? 'cancelled' : 'timeout',
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
throw err;
|
|
353
|
-
});
|
|
413
|
+
'TUS HEAD request',
|
|
414
|
+
context
|
|
415
|
+
);
|
|
354
416
|
|
|
355
|
-
if (response.status
|
|
356
|
-
throw
|
|
357
|
-
httpStatus: response.status,
|
|
358
|
-
...context,
|
|
359
|
-
phase: 'create',
|
|
360
|
-
sessionLost: true,
|
|
361
|
-
});
|
|
417
|
+
if (isSessionGone(response.status)) {
|
|
418
|
+
throw sessionLostError(response.status, context, 'create');
|
|
362
419
|
}
|
|
363
420
|
|
|
364
421
|
if (!response.ok) {
|
|
@@ -397,7 +454,8 @@ async function headTusSession(
|
|
|
397
454
|
* to the status poll and could report a never-activated ingot as done.
|
|
398
455
|
*
|
|
399
456
|
* 409 means another finalizer holds the lease right now: retry later, the
|
|
400
|
-
* session is intact.
|
|
457
|
+
* session is intact. A status `isSessionGone` recognises means the session
|
|
458
|
+
* is gone (`sessionLost`).
|
|
401
459
|
*/
|
|
402
460
|
async function confirmTusFinalization(
|
|
403
461
|
config: ResolvedMobileConfig,
|
|
@@ -405,38 +463,30 @@ async function confirmTusFinalization(
|
|
|
405
463
|
istk: string,
|
|
406
464
|
fileSize: number,
|
|
407
465
|
signal?: AbortSignal,
|
|
408
|
-
context:
|
|
466
|
+
context: TusErrorContext = {}
|
|
409
467
|
): Promise<void> {
|
|
410
468
|
ensureNotAborted(signal);
|
|
411
469
|
|
|
412
|
-
const response = await
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
'
|
|
417
|
-
|
|
418
|
-
|
|
470
|
+
const response = await tusRequest(
|
|
471
|
+
config,
|
|
472
|
+
uploadUrl,
|
|
473
|
+
{
|
|
474
|
+
method: 'PATCH',
|
|
475
|
+
headers: {
|
|
476
|
+
'Tus-Resumable': TUS_VERSION,
|
|
477
|
+
'Upload-Offset': String(fileSize),
|
|
478
|
+
'Content-Type': 'application/offset+octet-stream',
|
|
479
|
+
'X-ISTK': istk,
|
|
480
|
+
},
|
|
481
|
+
signal,
|
|
482
|
+
timeoutMs: config.tusChunkTimeoutMs,
|
|
419
483
|
},
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (getErrorName(err) === 'AbortError') {
|
|
424
|
-
throw new TusUploadError(signal?.aborted ? 'Upload cancelled' : 'TUS finalization request timed out', {
|
|
425
|
-
cause: err instanceof Error ? err : null,
|
|
426
|
-
...context,
|
|
427
|
-
phase: signal?.aborted ? 'cancelled' : 'timeout',
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
throw err;
|
|
431
|
-
});
|
|
484
|
+
'TUS finalization request',
|
|
485
|
+
context
|
|
486
|
+
);
|
|
432
487
|
|
|
433
|
-
if (response.status
|
|
434
|
-
throw
|
|
435
|
-
httpStatus: response.status,
|
|
436
|
-
...context,
|
|
437
|
-
phase: 'create',
|
|
438
|
-
sessionLost: true,
|
|
439
|
-
});
|
|
488
|
+
if (isSessionGone(response.status)) {
|
|
489
|
+
throw sessionLostError(response.status, context, 'create');
|
|
440
490
|
}
|
|
441
491
|
if (response.status === 409) {
|
|
442
492
|
throw new TusUploadError('TUS finalization is already in progress on the server; retry shortly', {
|
|
@@ -620,12 +670,12 @@ async function uploadWholeFileInBackground(
|
|
|
620
670
|
|
|
621
671
|
const body = typeof result.body === 'string' ? result.body : '';
|
|
622
672
|
if (result.status >= 200 && result.status < 300) {
|
|
623
|
-
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
return
|
|
673
|
+
// Forge always answers a PATCH with a readable Upload-Offset. A 2xx
|
|
674
|
+
// without one is not Forge (a captive portal's 200 page is the classic
|
|
675
|
+
// case), so report no advance: the stall guard fails the attempt and the
|
|
676
|
+
// next one asks Forge (HEAD) instead of marking bytes uploaded that may
|
|
677
|
+
// never have arrived. Same strict parse as the XHR path (uploadChunk).
|
|
678
|
+
return parseIntegerHeader(getHeaderIgnoreCase(result.headers, 'Upload-Offset'), 0) ?? offset;
|
|
629
679
|
}
|
|
630
680
|
|
|
631
681
|
const gateError = gateErrorFromBody(result.status, parseErrorBody(body));
|
|
@@ -643,9 +693,10 @@ async function uploadWholeFileInBackground(
|
|
|
643
693
|
* taxonomy, so a caller sees the same `TusUploadError` phases and typed gate
|
|
644
694
|
* errors whichever engine carried the bytes. Forge's status and body ride
|
|
645
695
|
* through untouched: a 402 becomes the gate error the XHR path throws, a
|
|
646
|
-
*
|
|
647
|
-
* keeps its number for the caller's own policy
|
|
648
|
-
* retry-later, not a lost session). The
|
|
696
|
+
* status `isSessionGone` recognises marks the session lost exactly as HEAD
|
|
697
|
+
* would, and any other status keeps its number for the caller's own policy
|
|
698
|
+
* (a 409 during finalize is a retry-later, not a lost session). The
|
|
699
|
+
* remaining kinds are transport
|
|
649
700
|
* outcomes: the caller's own abort is `cancelled`; an OS-ended task
|
|
650
701
|
* (`interrupted`) is a network failure, because the right response is the
|
|
651
702
|
* same as for a dropped connection: resume from the server offset. Anything
|
|
@@ -654,7 +705,7 @@ async function uploadWholeFileInBackground(
|
|
|
654
705
|
*/
|
|
655
706
|
export function mapBackgroundTransferError(
|
|
656
707
|
err: unknown,
|
|
657
|
-
context:
|
|
708
|
+
context: TusErrorContext = {}
|
|
658
709
|
): SparkVaultMobileError {
|
|
659
710
|
if (!(err instanceof BackgroundTransferError)) {
|
|
660
711
|
return new TusUploadError(`Background transfer failed: ${getErrorMessage(err)}`, {
|
|
@@ -671,13 +722,11 @@ export function mapBackgroundTransferError(
|
|
|
671
722
|
if (status !== undefined) {
|
|
672
723
|
const gateError = gateErrorFromBody(status, parseErrorBody(body));
|
|
673
724
|
if (gateError) return gateError;
|
|
725
|
+
if (isSessionGone(status)) return sessionLostError(status, context, 'upload', err);
|
|
674
726
|
}
|
|
675
|
-
const sessionLost = status === 404 || status === 410;
|
|
676
727
|
return new TusUploadError(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
: `Background chunk upload failed: ${status ?? 'unknown status'} - ${body.substring(0, 200)}`,
|
|
680
|
-
{ cause: err, httpStatus: status ?? null, ...context, phase: 'upload', sessionLost }
|
|
728
|
+
`Background chunk upload failed: ${status ?? 'unknown status'} - ${body.substring(0, 200)}`,
|
|
729
|
+
{ cause: err, httpStatus: status ?? null, ...context, phase: 'upload' }
|
|
681
730
|
);
|
|
682
731
|
}
|
|
683
732
|
case 'cancelled':
|
|
@@ -919,10 +968,13 @@ export class MobileTusUploader {
|
|
|
919
968
|
// the caller intact so the subscribe / add-on UX fires; never bury them
|
|
920
969
|
// in a generic TusUploadError.
|
|
921
970
|
if (err instanceof SparkVaultMobileError) throw err;
|
|
971
|
+
// Only a fileReader read or an onSessionCreated hook can throw anything
|
|
972
|
+
// else here: every request classifies its own failure (tusRequest,
|
|
973
|
+
// uploadChunk, the adapters). Those are the caller's errors, wrapped
|
|
974
|
+
// with the file context and nothing inferred about them.
|
|
922
975
|
throw TusUploadError.fromError(error, {
|
|
923
976
|
filename: options.filename,
|
|
924
977
|
fileSize: options.fileSize,
|
|
925
|
-
phase: inferTusPhase(error),
|
|
926
978
|
});
|
|
927
979
|
}
|
|
928
980
|
}
|
|
@@ -963,9 +1015,10 @@ export class MobileTusUploader {
|
|
|
963
1015
|
* and will not resume after all: the tus DELETE lets Forge drop the chunk
|
|
964
1016
|
* objects it holds (nothing else cleans them up before expiry) and settle
|
|
965
1017
|
* the partial transfer's billing. Resolves once the session is gone,
|
|
966
|
-
* which includes Forge answering
|
|
967
|
-
* any other failure rejects with a
|
|
968
|
-
*
|
|
1018
|
+
* which includes Forge answering a status `isSessionGone` recognises
|
|
1019
|
+
* (already expired or terminated); any other failure rejects with a
|
|
1020
|
+
* `TusUploadError` (a timeout, a connectivity failure, or the status
|
|
1021
|
+
* Forge sent) so the caller can retry later. Not bound to any
|
|
969
1022
|
* abort signal: a terminate is the last thing a caller does with a
|
|
970
1023
|
* session, never something to cancel.
|
|
971
1024
|
*/
|
|
@@ -978,17 +1031,9 @@ export class MobileTusUploader {
|
|
|
978
1031
|
await this.cancelBackgroundTransfer(backgroundTransfer, session.uploadUrl);
|
|
979
1032
|
}
|
|
980
1033
|
|
|
981
|
-
const response = await this.requestTerminate(session.uploadUrl, session.istk)
|
|
982
|
-
if (getErrorName(err) === 'AbortError') {
|
|
983
|
-
throw new TusUploadError('TUS DELETE request timed out', {
|
|
984
|
-
cause: err instanceof Error ? err : null,
|
|
985
|
-
phase: 'timeout',
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
throw err;
|
|
989
|
-
});
|
|
1034
|
+
const response = await this.requestTerminate(session.uploadUrl, session.istk);
|
|
990
1035
|
|
|
991
|
-
if (response.ok || response.status
|
|
1036
|
+
if (response.ok || isSessionGone(response.status)) return;
|
|
992
1037
|
|
|
993
1038
|
const errorText = await response.text().catch(() => '');
|
|
994
1039
|
throw new TusUploadError(`TUS terminate failed: ${response.status} - ${errorText.substring(0, 200)}`, {
|
|
@@ -1045,14 +1090,19 @@ export class MobileTusUploader {
|
|
|
1045
1090
|
* callers for why neither may carry one).
|
|
1046
1091
|
*/
|
|
1047
1092
|
private requestTerminate(uploadUrl: string, istk: string): Promise<Response> {
|
|
1048
|
-
return
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
'
|
|
1093
|
+
return tusRequest(
|
|
1094
|
+
this.config,
|
|
1095
|
+
uploadUrl,
|
|
1096
|
+
{
|
|
1097
|
+
method: 'DELETE',
|
|
1098
|
+
headers: {
|
|
1099
|
+
'Tus-Resumable': TUS_VERSION,
|
|
1100
|
+
'X-ISTK': istk,
|
|
1101
|
+
},
|
|
1102
|
+
timeoutMs: this.config.tusPostTimeoutMs,
|
|
1053
1103
|
},
|
|
1054
|
-
|
|
1055
|
-
|
|
1104
|
+
'TUS DELETE request'
|
|
1105
|
+
);
|
|
1056
1106
|
}
|
|
1057
1107
|
|
|
1058
1108
|
private async readChunk(
|
package/src/types.ts
CHANGED
|
@@ -603,7 +603,7 @@ export interface UploadResult {
|
|
|
603
603
|
* moments later, but a caller recording "backed up" must not take that on
|
|
604
604
|
* faith (resume the session later; Forge re-confirms finalization).
|
|
605
605
|
*/
|
|
606
|
-
status?:
|
|
606
|
+
status?: Ingot['status'];
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
export interface IngotSharingConfig {
|
|
@@ -769,11 +769,13 @@ export interface BackgroundTransferJob {
|
|
|
769
769
|
}
|
|
770
770
|
|
|
771
771
|
/**
|
|
772
|
-
* Why an OS transfer ended without reaching the final offset
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
772
|
+
* Why an OS transfer ended without reaching the final offset: the engine's
|
|
773
|
+
* own failure kinds plus `cancelled`, which only the app adapter produces
|
|
774
|
+
* for the caller's own abort (the engine never reports a cancel it was asked
|
|
775
|
+
* for). `http` carries Forge's status and body (the SDK maps gates and lost
|
|
776
|
+
* sessions from them); `interrupted` is the OS ending the task without being
|
|
777
|
+
* asked (a relaunch, a system cancel); `stalled` is a 2xx whose offset did
|
|
778
|
+
* not advance; `file` is a source the engine could not read.
|
|
777
779
|
*/
|
|
778
780
|
export type BackgroundTransferFailureKind = 'network' | 'http' | 'file' | 'cancelled' | 'stalled' | 'interrupted';
|
|
779
781
|
|
|
@@ -842,6 +844,15 @@ export interface MobileFileInfo {
|
|
|
842
844
|
sizeBytes?: number;
|
|
843
845
|
}
|
|
844
846
|
|
|
847
|
+
/**
|
|
848
|
+
* App-provided file download adapter. The rejection contract is typed, not
|
|
849
|
+
* textual: a download the caller's `abortSignal` cut short rejects with an
|
|
850
|
+
* Error whose `name` is `'AbortError'` (the SDK reports it as cancelled and
|
|
851
|
+
* never retries it); a download that ran past `timeoutMs` rejects with a
|
|
852
|
+
* `SparkVaultTimeoutError` (retried like any connectivity failure). Any
|
|
853
|
+
* other rejection is a transport failure the SDK retries with a fresh
|
|
854
|
+
* signed URL.
|
|
855
|
+
*/
|
|
845
856
|
export interface MobileFileDownloader {
|
|
846
857
|
download(
|
|
847
858
|
url: string,
|