All checks were successful
check / check (push) Successful in 20s
No retry on 4xx, backoff on 5xx and transport failures, and a deadline on every request. Before this, one transient 503 or TCP reset failed a file for good, and a CDN connection that went quiet after accepting the request blocked `quak backup` forever, because there was no timeout anywhere. src/retry.ts holds the policy: a classifier that decides whether another attempt could produce a different answer, and a loop that acts on it with exponential backoff and full jitter. Retried: 5xx, 408, 429, transport failures (the errno is read out of the cause chain, which is where Node's fetch puts it), deadline aborts, and truncated transfers. Not retried: every other 4xx, and anything unrecognised — a wrongly retried permanent failure delays every remaining file, while a wrongly abandoned transient one costs a single file the next run picks up. Attempt count, delays, sleep and jitter source are all configurable through ApiClientOptions; sleep being injectable is what lets the suite exercise the policy without waiting. Truncation needed a type before it could be classified. streamDecrypt threw plain Errors whose messages began "download: stream truncated", and classifying on message text would mean the next reword silently turned every truncated download into a permanent failure. It now throws TruncatedStreamError, which lives in src/errors.ts alongside ApiError so the classifier can recognise both without importing the modules that import it; api/client.ts re-exports ApiError, so it stays one class and every existing import path still resolves. Downloads retry the request, the stream consumption and the decryption together. Only the first of those happens inside ApiClient: a socket reset after the headers arrived throws in streamDecrypt, and retrying the request alone would never see it. The client's own retry is switched off for those two calls so the budgets do not multiply into sixteen requests per file, and the atomic write stays outside the loop so a download that took three attempts still performs one write and one rename. Non-idempotent requests are not blindly replayed. postJSON and putJSON reach create-session, two-factor/verify — which burns one of a few second-factor attempts — and files/thumbnail, so they retry only when the connection was never established and the server provably never saw the request. putFile is exempt and retries fully: a presigned PUT stores one whole object at one key, with no partial state to damage. It now throws ApiError with the status, as do the two null-body paths, which previously threw bare Errors that nothing could classify. Timeouts come from AbortSignal.timeout(), renewed per attempt: 30s for JSON and upload calls, 10 minutes for file bodies, since a value short enough to keep a hung API call from stalling a backup would cancel a legitimate multi-gigabyte download. The download deadline is enforced over the body rather than only the headers, by racing each read against the signal, so the guarantee does not depend on the fetch implementation tearing down a stream it already handed over. listMissingThumbnails now separates a genuine 404 from an exhausted retry. Its bare catch reported both as missing, which after this change would have let a few minutes of 500s talk fix-missing-thumbnails into regenerating and re-uploading thumbnails that were fine. runBackup and runMetadataBackup are untouched: the retry sits below them and their per-file resilience is unchanged.
51 lines
1.2 KiB
TypeScript
51 lines
1.2 KiB
TypeScript
export const VERSION = "0.0.0";
|
|
|
|
export { Client, type LoginOptions, type ClientSnapshot } from "./client.js";
|
|
export {
|
|
ApiClient,
|
|
ApiError,
|
|
DEFAULT_DOWNLOAD_TIMEOUT_MS,
|
|
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
type ApiClientOptions,
|
|
type StreamOptions,
|
|
} from "./api/client.js";
|
|
export { TruncatedStreamError } from "./errors.js";
|
|
export {
|
|
DEFAULT_RETRY_OPTIONS,
|
|
isRetryable,
|
|
isSafeToReplay,
|
|
resolveRetryOptions,
|
|
withRetry,
|
|
type ResolvedRetryOptions,
|
|
type RetryOptions,
|
|
type WithRetryOptions,
|
|
} from "./retry.js";
|
|
export { unwrapAuth, type UnwrapResult } from "./auth/unwrap.js";
|
|
export {
|
|
beginLogin,
|
|
submitTOTP,
|
|
requestEmailOTP,
|
|
submitEmailOTP,
|
|
} from "./auth/login.js";
|
|
export { decryptCollection, decryptFile } from "./model/index.js";
|
|
export { downloadFile, downloadThumbnail } from "./download/index.js";
|
|
export type {
|
|
AuthorizationResponse,
|
|
KeyAttributes,
|
|
LoginChallenge,
|
|
SRPAttributes,
|
|
} from "./auth/types.js";
|
|
export type {
|
|
Collection,
|
|
CollectionType,
|
|
EnteFile,
|
|
FileBlob,
|
|
FileMetadata,
|
|
FileType,
|
|
KeyMaterial,
|
|
Microseconds,
|
|
RawCollection,
|
|
RawEnteFile,
|
|
} from "./model/types.js";
|
|
export type { DownloadResult } from "./download/index.js";
|