@sparkvault/sdk-mobile 7.0.0 → 7.0.1
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/dist/app-sdk/transfer-retry.d.ts +298 -0
- package/dist/app-sdk/transfer-retry.js +535 -0
- package/dist/app-sdk/transfer-retry.js.map +1 -0
- package/dist/app-sdk/upload-publication.d.ts +81 -0
- package/dist/app-sdk/upload-publication.js +93 -0
- package/dist/app-sdk/upload-publication.js.map +1 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +38 -4
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +30 -18
- package/dist/ingots.js +25 -19
- package/dist/ingots.js.map +1 -1
- package/dist/tus.d.ts +179 -14
- package/dist/tus.js +738 -300
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +16 -8
- package/package.json +1 -1
- package/src/app-sdk/transfer-retry.js +563 -0
- package/src/app-sdk/upload-publication.js +102 -0
- package/src/config.ts +40 -0
- package/src/errors.ts +42 -5
- package/src/index.ts +2 -0
- package/src/ingots.ts +57 -26
- package/src/tus.ts +971 -378
- package/src/types.ts +16 -8
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {'ok'|'retry'|'resync'|'deferred'|'gone'|'refused'} TransferVerdict
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The verdicts a transfer recovers from by trying again that count as
|
|
6
|
+
* failures: the ones a budget's `failed` records.
|
|
7
|
+
* @typedef {'retry'|'resync'} RetryableTransferVerdict
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} TransferOutcome
|
|
11
|
+
* @property {TransferVerdict} verdict - What the client does next
|
|
12
|
+
* @property {number|null} retryAfterMs - The answer's `Retry-After`, in ms from
|
|
13
|
+
* the answer (an HTTP-date already past reads as 0); null when absent or unreadable
|
|
14
|
+
* @property {number|null} offset - The answer's `Upload-Offset`; null when absent or unreadable
|
|
15
|
+
* @property {string|null} coreCode - The code Core ended the upload with, as a
|
|
16
|
+
* terminal answer named it (a 404, 410 or 412: its body's details or, those
|
|
17
|
+
* naming none, its `X-Upload-Refusal`): REFUSED for a final refusal
|
|
18
|
+
* (`UPLOAD_POOL_FULL`, `UPLOAD_SUPERSEDED`), GONE for any other code
|
|
19
|
+
* (`NOT_FOUND`, `UPLOAD_PROTOCOL_RETIRED`, a 412's, one this policy does not
|
|
20
|
+
* know); null when the answer names none
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Read the answer to a transfer request the way the policy does.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} response
|
|
26
|
+
* @param {number} response.status - The HTTP status; 0 when the request got no answer
|
|
27
|
+
* @param {((name: string) => string|null)|null} [response.headers] - Reads a
|
|
28
|
+
* response header by name: a fetch `Headers`' `get` or an XHR's
|
|
29
|
+
* `getResponseHeader`, bound to its owner (`(name) => xhr.getResponseHeader(name)`)
|
|
30
|
+
* @param {unknown} [response.details] - The `details` of the error envelope the
|
|
31
|
+
* answer's body carries (`{ error: { code, message, details } }`), as the
|
|
32
|
+
* runtime's one reader of a failed answer parsed it from the whole body;
|
|
33
|
+
* where Forge names the refusal Core decided (`coreCode`), ahead of the
|
|
34
|
+
* answer's `X-Upload-Refusal`. Anything that is not an object names none.
|
|
35
|
+
* @param {boolean} [response.receipt] - The request was the receipt: the PATCH
|
|
36
|
+
* at the final offset, whose 503 carrying `Retry-After` is a deferral
|
|
37
|
+
* @param {number} [response.now] - When the answer arrived (epoch ms), which an
|
|
38
|
+
* HTTP-date `Retry-After` counts from
|
|
39
|
+
* @returns {TransferOutcome}
|
|
40
|
+
* @throws {TypeError} When the status is not a whole number, headers is not a
|
|
41
|
+
* function, or receipt is not a boolean
|
|
42
|
+
*/
|
|
43
|
+
export function classifyTransferResponse({ status, headers, details, receipt, now }: {
|
|
44
|
+
status: number;
|
|
45
|
+
headers?: ((name: string) => string | null) | null | undefined;
|
|
46
|
+
details?: unknown;
|
|
47
|
+
receipt?: boolean | undefined;
|
|
48
|
+
now?: number | undefined;
|
|
49
|
+
}): TransferOutcome;
|
|
50
|
+
/**
|
|
51
|
+
* When a session lapses, read from its `Upload-Expires` (an HTTP-date).
|
|
52
|
+
*
|
|
53
|
+
* @param {unknown} headerValue - The header as received (null when absent)
|
|
54
|
+
* @param {number} [now] - epoch ms, which places an obsolete two-digit year
|
|
55
|
+
* @returns {number|null} epoch ms, or null when absent or not an HTTP-date
|
|
56
|
+
*/
|
|
57
|
+
export function parseUploadExpires(headerValue: unknown, now?: number): number | null;
|
|
58
|
+
/**
|
|
59
|
+
* 32-bit FNV-1a over the text's UTF-8 bytes: the jitter's hash. A retry's
|
|
60
|
+
* delay only has to spread, so it hashes what identifies the retry and draws
|
|
61
|
+
* nothing (docs/CONVENTIONS.md → Randomness).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} text
|
|
64
|
+
* @returns {number} An unsigned 32-bit integer
|
|
65
|
+
* @throws {TypeError} When the text is not a string
|
|
66
|
+
*/
|
|
67
|
+
export function fnv1a32(text: string): number;
|
|
68
|
+
/**
|
|
69
|
+
* What a transfer's retries spread by when no session URL keys them yet (a
|
|
70
|
+
* session create) or ever (a download, whose link carries its token): the
|
|
71
|
+
* scope (the create endpoint, or the kind of transfer), when the transfer
|
|
72
|
+
* started, and this runtime's count of keyed transfers, which tell
|
|
73
|
+
* concurrent transfers apart. A transfer's token is a credential and never
|
|
74
|
+
* feeds anything observable such as a delay, so no part of the key derives
|
|
75
|
+
* from it (docs/CONVENTIONS.md → Randomness).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} scope - The endpoint or the kind of transfer
|
|
78
|
+
* @returns {string} The key, for every retry of this one transfer
|
|
79
|
+
* @throws {TypeError} For a scope that is not a non-empty string
|
|
80
|
+
*/
|
|
81
|
+
export function transferJitterKey(scope: string): string;
|
|
82
|
+
/**
|
|
83
|
+
* How long to wait before retry `attempt` of a transfer: `baseMs` doubling per
|
|
84
|
+
* attempt, capped at `capMs`, plus a jitter in [0, 1000) ms hashed from the
|
|
85
|
+
* transfer and the attempt, and never less than the server's `Retry-After`.
|
|
86
|
+
* The same transfer and attempt always wait the same; different transfers
|
|
87
|
+
* failing together spread out.
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} options
|
|
90
|
+
* @param {number} options.attempt - Which retry this is, from 1
|
|
91
|
+
* @param {number|null} [options.retryAfterMs] - The answer's `Retry-After`: a floor
|
|
92
|
+
* @param {number} [options.baseMs] - The first retry's backoff
|
|
93
|
+
* @param {number} [options.capMs] - The longest backoff, before jitter
|
|
94
|
+
* @param {string} options.jitterKey - What identifies the transfer (its session
|
|
95
|
+
* URL, or a `transferJitterKey`)
|
|
96
|
+
* @returns {number} ms
|
|
97
|
+
* @throws {TypeError} For an attempt that is not a whole number from 1, a
|
|
98
|
+
* missing jitter key, or a delay bound that is not a positive number of ms
|
|
99
|
+
*/
|
|
100
|
+
export function retryDelayMs({ attempt, retryAfterMs, baseMs, capMs, jitterKey }: {
|
|
101
|
+
attempt: number;
|
|
102
|
+
retryAfterMs?: number | null | undefined;
|
|
103
|
+
baseMs?: number | undefined;
|
|
104
|
+
capMs?: number | undefined;
|
|
105
|
+
jitterKey: string;
|
|
106
|
+
}): number;
|
|
107
|
+
/**
|
|
108
|
+
* @typedef {Object} TransferBudget
|
|
109
|
+
* @property {() => void} progressed - The server confirmed progress (bytes
|
|
110
|
+
* landed, or the offset it names moved forward): the failures in a row and
|
|
111
|
+
* the idle clock start over
|
|
112
|
+
* @property {() => void} alive - A sign of life the server has not confirmed
|
|
113
|
+
* (bytes are leaving, or the device is back online): the idle clock starts
|
|
114
|
+
* over and the failures in a row stand, so attempts that die before the
|
|
115
|
+
* server confirms anything still end the transfer
|
|
116
|
+
* @property {(verdict: RetryableTransferVerdict, retryAfterMs?: number|null) => boolean} failed -
|
|
117
|
+
* Records a retryable answer (RETRY or RESYNC) and says whether another
|
|
118
|
+
* attempt is allowed: the failures in a row stay under the max, and the
|
|
119
|
+
* attempt `retryAfterMs` allows would start inside the idle limit, so a
|
|
120
|
+
* transfer never sleeps on a Retry-After longer than its budget. The limit
|
|
121
|
+
* bounds when a retry is decided; a retry decided inside it still waits its
|
|
122
|
+
* own backoff, so a transfer ends within one backoff of the limit.
|
|
123
|
+
* @property {(retryAfterMs?: number|null) => boolean} deferred - Records a
|
|
124
|
+
* DEFERRED answer and says whether to ask again: only the idle limit bounds
|
|
125
|
+
* it, as `failed` applies it, because a deferral is the server at work,
|
|
126
|
+
* never a failure, so the failures in a row stand
|
|
127
|
+
* @property {number} consecutiveFailures - Retryable failures since the last progress
|
|
128
|
+
* @property {number} attempt - Failures and deferrals since the last progress:
|
|
129
|
+
* the attempt the next wait's delay is computed for, so each deferral backs
|
|
130
|
+
* the next ask off as a failure does, sparing a Core that is struggling to
|
|
131
|
+
* publish
|
|
132
|
+
* @property {number} msSinceProgress - ms since the last progress or sign of life
|
|
133
|
+
*/
|
|
134
|
+
/**
|
|
135
|
+
* The retry budget of one transfer: retries are bounded by failures in a row
|
|
136
|
+
* and by time without progress, deferrals by time alone, and both bounds
|
|
137
|
+
* start over whenever the server confirms progress, so a slow transfer that
|
|
138
|
+
* keeps landing bytes is never cut off while a dead one ends in bounded time.
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} [options]
|
|
141
|
+
* @param {number} [options.idleLimitMs] - How long the transfer may go without
|
|
142
|
+
* progress or a sign of life
|
|
143
|
+
* @param {number} [options.maxConsecutiveFailures] - Failures in a row that end the transfer
|
|
144
|
+
* @param {() => number} [options.now] - The clock, epoch ms
|
|
145
|
+
* @returns {TransferBudget}
|
|
146
|
+
* @throws {TypeError} For a limit that is not a positive number, or a clock that is not a function
|
|
147
|
+
*/
|
|
148
|
+
export function createTransferBudget({ idleLimitMs, maxConsecutiveFailures, now }?: {
|
|
149
|
+
idleLimitMs?: number | undefined;
|
|
150
|
+
maxConsecutiveFailures?: number | undefined;
|
|
151
|
+
now?: (() => number) | undefined;
|
|
152
|
+
}): TransferBudget;
|
|
153
|
+
/**
|
|
154
|
+
* Why a session offset the server named (a HEAD's, or a 409's `Upload-Offset`)
|
|
155
|
+
* cannot be resumed from, or null when it can. Forge commits whole chunks, so
|
|
156
|
+
* a session stands at a multiple of its chunk size or at its total.
|
|
157
|
+
*
|
|
158
|
+
* @param {number|null} offset
|
|
159
|
+
* @param {Object} geometry
|
|
160
|
+
* @param {number} geometry.totalSize - The upload's length (a non-negative whole number)
|
|
161
|
+
* @param {number} geometry.chunkSize - The session's chunk size (a positive whole number)
|
|
162
|
+
* @returns {'invalid'|'beyond_total'|'misaligned'|null}
|
|
163
|
+
* @throws {TypeError} When the length is not a non-negative whole number or
|
|
164
|
+
* the chunk size not a positive one
|
|
165
|
+
*/
|
|
166
|
+
export function resumeOffsetProblem(offset: number | null, { totalSize, chunkSize }: {
|
|
167
|
+
totalSize: number;
|
|
168
|
+
chunkSize: number;
|
|
169
|
+
}): "invalid" | "beyond_total" | "misaligned" | null;
|
|
170
|
+
/** What a client does after a transfer request, read from its answer. */
|
|
171
|
+
export const TRANSFER_VERDICT: Readonly<{
|
|
172
|
+
/** 2xx: the request did what it asked. */
|
|
173
|
+
OK: "ok";
|
|
174
|
+
/**
|
|
175
|
+
* No answer (a reset, a timeout), 408, 425, 429, a 5xx, or a 409 without
|
|
176
|
+
* `Upload-Offset` (another request holds the session's lease): back off,
|
|
177
|
+
* never sooner than `Retry-After`, then ask the server's offset before
|
|
178
|
+
* sending a chunk again.
|
|
179
|
+
*/
|
|
180
|
+
RETRY: "retry";
|
|
181
|
+
/** 409 with `Upload-Offset`: the session stands elsewhere; continue from that offset at once. */
|
|
182
|
+
RESYNC: "resync";
|
|
183
|
+
/**
|
|
184
|
+
* 503 with `Retry-After` to the receipt: every byte is received and the
|
|
185
|
+
* publication is still running. Ask again after a backoff that grows with
|
|
186
|
+
* each deferral, never sooner than `Retry-After`; only the idle limit ends
|
|
187
|
+
* the asking, never the failures in a row.
|
|
188
|
+
*/
|
|
189
|
+
DEFERRED: "deferred";
|
|
190
|
+
/**
|
|
191
|
+
* 404 or 410 naming no code, or a 404, 410 or 412 naming a code of Core's
|
|
192
|
+
* that is not a final refusal (`coreCode` carries it): the session no
|
|
193
|
+
* longer exists; only a new upload from byte 0 can succeed.
|
|
194
|
+
*/
|
|
195
|
+
GONE: "gone";
|
|
196
|
+
/**
|
|
197
|
+
* Any other status, or a 404, 410 or 412 naming a final refusal of Core's
|
|
198
|
+
* (`coreCode`: UPLOAD_POOL_FULL, UPLOAD_SUPERSEDED): the server's verdict on
|
|
199
|
+
* this request, which repeating it, or a new upload of the same file,
|
|
200
|
+
* earns again.
|
|
201
|
+
*/
|
|
202
|
+
REFUSED: "refused";
|
|
203
|
+
}>;
|
|
204
|
+
/** The tus `expiration` extension's header: when the session lapses, as an HTTP-date. */
|
|
205
|
+
export const UPLOAD_EXPIRES_HEADER: "Upload-Expires";
|
|
206
|
+
/**
|
|
207
|
+
* How long a client waits for the answer to an offset HEAD or a receipt
|
|
208
|
+
* PATCH, whatever its other requests get: on a received upload, Forge answers
|
|
209
|
+
* either only after its own call to Core (up to 30 s) has published the
|
|
210
|
+
* upload or deferred it, so each gets twice that.
|
|
211
|
+
*/
|
|
212
|
+
export const HEAD_AND_RECEIPT_TIMEOUT_MS: 60000;
|
|
213
|
+
/** Why an offset the server names cannot be resumed from. */
|
|
214
|
+
export const RESUME_OFFSET_PROBLEM: Readonly<{
|
|
215
|
+
/** Not a non-negative whole number of bytes. */
|
|
216
|
+
INVALID: "invalid";
|
|
217
|
+
/** Past the upload's length: resuming from it would report a partial upload complete. */
|
|
218
|
+
BEYOND_TOTAL: "beyond_total";
|
|
219
|
+
/** Inside a chunk: a session commits whole chunks only, so the answer is corrupt. */
|
|
220
|
+
MISALIGNED: "misaligned";
|
|
221
|
+
}>;
|
|
222
|
+
export type TransferVerdict = "ok" | "retry" | "resync" | "deferred" | "gone" | "refused";
|
|
223
|
+
/**
|
|
224
|
+
* The verdicts a transfer recovers from by trying again that count as
|
|
225
|
+
* failures: the ones a budget's `failed` records.
|
|
226
|
+
*/
|
|
227
|
+
export type RetryableTransferVerdict = "retry" | "resync";
|
|
228
|
+
export type TransferOutcome = {
|
|
229
|
+
/**
|
|
230
|
+
* - What the client does next
|
|
231
|
+
*/
|
|
232
|
+
verdict: TransferVerdict;
|
|
233
|
+
/**
|
|
234
|
+
* - The answer's `Retry-After`, in ms from
|
|
235
|
+
* the answer (an HTTP-date already past reads as 0); null when absent or unreadable
|
|
236
|
+
*/
|
|
237
|
+
retryAfterMs: number | null;
|
|
238
|
+
/**
|
|
239
|
+
* - The answer's `Upload-Offset`; null when absent or unreadable
|
|
240
|
+
*/
|
|
241
|
+
offset: number | null;
|
|
242
|
+
/**
|
|
243
|
+
* - The code Core ended the upload with, as a
|
|
244
|
+
* terminal answer named it (a 404, 410 or 412: its body's details or, those
|
|
245
|
+
* naming none, its `X-Upload-Refusal`): REFUSED for a final refusal
|
|
246
|
+
* (`UPLOAD_POOL_FULL`, `UPLOAD_SUPERSEDED`), GONE for any other code
|
|
247
|
+
* (`NOT_FOUND`, `UPLOAD_PROTOCOL_RETIRED`, a 412's, one this policy does not
|
|
248
|
+
* know); null when the answer names none
|
|
249
|
+
*/
|
|
250
|
+
coreCode: string | null;
|
|
251
|
+
};
|
|
252
|
+
export type TransferBudget = {
|
|
253
|
+
/**
|
|
254
|
+
* - The server confirmed progress (bytes
|
|
255
|
+
* landed, or the offset it names moved forward): the failures in a row and
|
|
256
|
+
* the idle clock start over
|
|
257
|
+
*/
|
|
258
|
+
progressed: () => void;
|
|
259
|
+
/**
|
|
260
|
+
* - A sign of life the server has not confirmed
|
|
261
|
+
* (bytes are leaving, or the device is back online): the idle clock starts
|
|
262
|
+
* over and the failures in a row stand, so attempts that die before the
|
|
263
|
+
* server confirms anything still end the transfer
|
|
264
|
+
*/
|
|
265
|
+
alive: () => void;
|
|
266
|
+
/**
|
|
267
|
+
* -
|
|
268
|
+
* Records a retryable answer (RETRY or RESYNC) and says whether another
|
|
269
|
+
* attempt is allowed: the failures in a row stay under the max, and the
|
|
270
|
+
* attempt `retryAfterMs` allows would start inside the idle limit, so a
|
|
271
|
+
* transfer never sleeps on a Retry-After longer than its budget. The limit
|
|
272
|
+
* bounds when a retry is decided; a retry decided inside it still waits its
|
|
273
|
+
* own backoff, so a transfer ends within one backoff of the limit.
|
|
274
|
+
*/
|
|
275
|
+
failed: (verdict: RetryableTransferVerdict, retryAfterMs?: number | null) => boolean;
|
|
276
|
+
/**
|
|
277
|
+
* - Records a
|
|
278
|
+
* DEFERRED answer and says whether to ask again: only the idle limit bounds
|
|
279
|
+
* it, as `failed` applies it, because a deferral is the server at work,
|
|
280
|
+
* never a failure, so the failures in a row stand
|
|
281
|
+
*/
|
|
282
|
+
deferred: (retryAfterMs?: number | null) => boolean;
|
|
283
|
+
/**
|
|
284
|
+
* - Retryable failures since the last progress
|
|
285
|
+
*/
|
|
286
|
+
consecutiveFailures: number;
|
|
287
|
+
/**
|
|
288
|
+
* - Failures and deferrals since the last progress:
|
|
289
|
+
* the attempt the next wait's delay is computed for, so each deferral backs
|
|
290
|
+
* the next ask off as a failure does, sparing a Core that is struggling to
|
|
291
|
+
* publish
|
|
292
|
+
*/
|
|
293
|
+
attempt: number;
|
|
294
|
+
/**
|
|
295
|
+
* - ms since the last progress or sign of life
|
|
296
|
+
*/
|
|
297
|
+
msSinceProgress: number;
|
|
298
|
+
};
|