{"version":3,"file":"resumable-upload.cjs","names":[],"sources":["../../src/http/resumable-upload.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — a resumable upload is one long-lived\n * state machine: chunk the file, negotiate the offset the server already has, upload\n * with retry and backoff, honour pause, resume and abort, and report progress\n * throughout. Every stage reads the same cursor and the same abort signal, and\n * createResumableUpload is the closure that owns them.\n */\nimport { bytesToBase64 } from \"@/utils/base64\";\nimport { isTrustedCredentialTarget, reportSuppressedCredential } from \"./credential-scope\";\nimport { buildApiError, isApiError, isRetriableStatus, TempestApiError } from \"./errors\";\nimport { generateIdempotencyKey } from \"./idempotency\";\nimport { retry, type RetryOptions } from \"./retry\";\n\n/** The tus protocol version this client speaks. */\nexport const TUS_VERSION = \"1.0.0\";\n\n/** Default chunk size: 5 MiB, the size most tus servers are tuned for. */\nexport const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;\n\n/**\n * Where a resumable upload is.\n *\n * `\"paused\"` and `\"aborted\"` are both \"not running\", but only `\"paused\"` keeps the\n * persisted offset — `abort({ discard: true })` throws it away.\n */\nexport type ResumableUploadState =\n    \"idle\" | \"creating\" | \"uploading\" | \"paused\" | \"done\" | \"error\" | \"aborted\";\n\n/** Byte-level progress for a resumable upload. */\nexport interface ResumableUploadProgress {\n    /** Bytes the server holds, including anything a resume skipped. */\n    loaded: number;\n    /** Total size of the file. */\n    total: number;\n    /** `loaded / total`, between 0 and 1. */\n    fraction: number;\n    /** Bytes already on the server when this run started. `0` on a fresh upload. */\n    resumedFrom: number;\n}\n\n/** What has to survive a page reload for a resume to be possible. */\nexport interface ResumableUploadRecord {\n    /** Upload URL the creation POST returned, absolute. */\n    url: string;\n    /** Last offset the server confirmed. */\n    offset: number;\n    /** File size, so a different file under the same key is not resumed into. */\n    size: number;\n    /** Idempotency key of the creation request, reused if creation is retried. */\n    idempotencyKey: string;\n    /** Epoch ms of the last write, so an app can sweep stale records. */\n    updatedAt: number;\n}\n\n/**\n * Persistence for resume state. Sync or async — both are awaited.\n *\n * Implement it over anything: the default is `localStorage`, and\n * `createOfflineStore` from `@/offline` slots in when you already have a Dexie\n * database open.\n */\nexport interface ResumableUploadStorage {\n    /** Read the record for `key`, or `null`. */\n    get(key: string): Promise<ResumableUploadRecord | null> | ResumableUploadRecord | null;\n    /** Write the record for `key`. */\n    set(key: string, record: ResumableUploadRecord): Promise<void> | void;\n    /** Forget the record for `key`. */\n    delete(key: string): Promise<void> | void;\n}\n\n/** Options for {@link createResumableUpload}. */\nexport interface ResumableUploadOptions {\n    /** tus creation endpoint, e.g. `\"/api/uploads\"`. */\n    endpoint: string;\n    /** The bytes to upload. A `File` also supplies the default resume key. */\n    file: Blob | File;\n    /** Bytes per `PATCH`. Default {@link DEFAULT_CHUNK_SIZE}. */\n    chunkSize?: number;\n    /** Sent as `Upload-Metadata` (base64-encoded values), e.g. `{ filename }`. */\n    metadata?: Record<string, string>;\n    /** Extra headers on every request. */\n    headers?: Record<string, string>;\n    /** Returns the current bearer token, read before each request. */\n    getToken?: () => string | null | undefined;\n    /**\n     * Origins besides `endpoint`'s that may receive the `getToken` credential.\n     *\n     * Since 0.66.0 the bearer token is scoped to the origin of `endpoint`, and\n     * the creation response's `Location` no longer decides where it goes. A\n     * server that hands the upload to object storage on another host still\n     * works — the `PATCH` requests simply carry no `Authorization`, which is\n     * what a presigned storage URL expects anyway.\n     *\n     * List the storage origin here when it does need the API's token.\n     */\n    trustedOrigins?: readonly string[];\n    /** Send cookies. Default `false`. */\n    withCredentials?: boolean;\n    /**\n     * Resume key. Defaults to a fingerprint of endpoint + file name/size/mtime, so\n     * picking the same file after a reload resumes instead of restarting.\n     */\n    key?: string;\n    /**\n     * Where to persist resume state. Defaults to `localStorage`. Pass `null` to\n     * disable persistence — resume then only survives a network blip, not a reload.\n     */\n    storage?: ResumableUploadStorage | null;\n    /** Backoff for a failed chunk. Forwarded to `retry`. Default 5 attempts. */\n    retry?: RetryOptions;\n    /** Called on every upload-progress tick and after every confirmed chunk. */\n    onProgress?: (progress: ResumableUploadProgress) => void;\n    /** Called whenever {@link ResumableUpload.state} changes. */\n    onStateChange?: (state: ResumableUploadState) => void;\n}\n\n/** What a finished upload resolves with. */\nexport interface ResumableUploadResult {\n    /** The tus upload URL — hand this to your API to link the stored file. */\n    url: string;\n    /** Total bytes uploaded. */\n    size: number;\n}\n\n/** A resumable upload in progress. Build one with {@link createResumableUpload}. */\nexport interface ResumableUpload {\n    /**\n     * Create (or re-attach to) the upload and push chunks until it is complete.\n     *\n     * Resolves `null` when the run stopped because of `pause()` or `abort()` —\n     * neither is a failure. Rejects with a `TempestApiError` when the server\n     * refused and the retries ran out.\n     */\n    start(): Promise<ResumableUploadResult | null>;\n    /** Stop after the in-flight chunk is dropped, keeping the resume point. */\n    pause(): void;\n    /** Continue from the server's offset. Same resolution contract as `start`. */\n    resume(): Promise<ResumableUploadResult | null>;\n    /**\n     * Stop for good.\n     *\n     * @param options - `discard: true` also sends `DELETE` (tus termination) and\n     *   forgets the persisted record, so the next `start()` uploads from zero.\n     */\n    abort(options?: { discard?: boolean }): Promise<void>;\n    /** Current state. */\n    readonly state: ResumableUploadState;\n    /** Bytes the server has confirmed. */\n    readonly offset: number;\n    /** The upload URL, once creation succeeded. */\n    readonly url: string | null;\n    /** The resume key in use. */\n    readonly key: string;\n}\n\ninterface RawResponse {\n    status: number;\n    text: string;\n    header(name: string): string | null;\n}\n\n/**\n * Encode a string as standard base64 (padded), UTF-8 first.\n *\n * `Upload-Metadata` carries base64 values precisely so a filename with accents\n * survives an HTTP header, so the UTF-8 step is not optional: `btoa` alone throws\n * on any code point above U+00FF. Only that step is specific here — the\n * bytes-to-text half is {@link bytesToBase64}.\n *\n * @param value - Text to encode.\n * @returns Padded base64.\n */\nfunction base64Utf8(value: string): string {\n    return bytesToBase64(new TextEncoder().encode(value));\n}\n\n/**\n * Build the `Upload-Metadata` header value: comma-separated `key base64(value)`.\n *\n * @param metadata - Plain string map.\n * @returns The header value, or `null` when there is nothing to send.\n */\nfunction encodeMetadata(metadata: Record<string, string> | undefined): string | null {\n    if (!metadata) return null;\n    const parts = Object.entries(metadata).map(([name, value]) => `${name} ${base64Utf8(value)}`);\n    return parts.length > 0 ? parts.join(\",\") : null;\n}\n\n/**\n * A stable-enough identity for a file, used as the default resume key.\n *\n * Name + size + last-modified is what the tus reference clients fingerprint on:\n * it is cheap (hashing the bytes of a 400 MB recording is not) and it changes\n * whenever the file does, which is the property that matters — resuming into the\n * wrong file would corrupt it silently.\n *\n * @param endpoint - Creation endpoint, so the same file to two servers is two uploads.\n * @param file - The blob or file being uploaded.\n * @returns A key safe to use in `localStorage`.\n */\nexport function uploadFingerprint(endpoint: string, file: Blob | File): string {\n    const named = file as File;\n    const name = typeof named.name === \"string\" ? named.name : \"blob\";\n    const modified = typeof named.lastModified === \"number\" ? named.lastModified : 0;\n    return `${endpoint}|${name}|${file.size}|${file.type}|${modified}`;\n}\n\n/**\n * `localStorage`-backed resume storage — the default.\n *\n * `localStorage` and not IndexedDB on purpose. The record is four fields and a\n * URL; the requirement is only that it survives a reload, and pulling Dexie in for\n * that would put an IndexedDB dependency in the bundle of every app that uploads a\n * file. Apps that already have `createOfflineStore` open can pass their own\n * {@link ResumableUploadStorage} instead.\n *\n * @param prefix - Key prefix. Default `\"tempest-upload:\"`.\n * @returns A storage that no-ops when `localStorage` is unavailable.\n */\nexport function createLocalUploadStorage(prefix = \"tempest-upload:\"): ResumableUploadStorage {\n    function backend(): Storage | null {\n        try {\n            return typeof localStorage === \"undefined\" ? null : localStorage;\n        } catch {\n            return null;\n        }\n    }\n\n    return {\n        get(key) {\n            const raw = backend()?.getItem(prefix + key);\n            if (!raw) return null;\n            try {\n                return JSON.parse(raw) as ResumableUploadRecord;\n            } catch {\n                return null;\n            }\n        },\n        set(key, record) {\n            backend()?.setItem(prefix + key, JSON.stringify(record));\n        },\n        delete(key) {\n            backend()?.removeItem(prefix + key);\n        },\n    };\n}\n\n/**\n * Send one request over `XMLHttpRequest`.\n *\n * `XMLHttpRequest` rather than `fetch` for the same reason `uploadWithProgress`\n * uses it — `fetch` still cannot report upload progress in any browser — plus one\n * more: tus answers every write with the new `Upload-Offset` in a **response\n * header**, and `uploadWithProgress` only hands back a parsed body, so it could\n * not be reused here.\n *\n * @param init - Method, URL, headers, optional body and progress callback.\n * @returns Status, raw text and a header reader.\n */\nfunction sendRequest(init: {\n    method: \"POST\" | \"HEAD\" | \"PATCH\" | \"DELETE\";\n    url: string;\n    headers: Record<string, string>;\n    body?: Blob;\n    withCredentials: boolean;\n    onProgress?: (loaded: number) => void;\n    register: (xhr: XMLHttpRequest) => void;\n}): Promise<RawResponse> {\n    return new Promise<RawResponse>((resolve, reject) => {\n        const xhr = new XMLHttpRequest();\n        xhr.open(init.method, init.url);\n        xhr.withCredentials = init.withCredentials;\n        for (const [name, value] of Object.entries(init.headers)) {\n            xhr.setRequestHeader(name, value);\n        }\n        if (init.onProgress) {\n            const report = init.onProgress;\n            xhr.upload.onprogress = (event: ProgressEvent) => report(event.loaded);\n        }\n        xhr.onload = () =>\n            resolve({\n                status: xhr.status,\n                text: xhr.responseText,\n                header: (name) => xhr.getResponseHeader(name),\n            });\n        xhr.onerror = () =>\n            reject(\n                new TempestApiError({\n                    status: 0,\n                    detail: \"Falha de rede no upload resumível.\",\n                }),\n            );\n        xhr.onabort = () => reject(new DOMException(\"Aborted\", \"AbortError\"));\n        init.register(xhr);\n        xhr.send(init.body);\n    });\n}\n\nfunction parseOffset(response: RawResponse): number | null {\n    const raw = response.header(\"Upload-Offset\");\n    if (raw === null) return null;\n    const value = Number(raw);\n    return Number.isFinite(value) && value >= 0 ? value : null;\n}\n\n/**\n * Read an error body without assuming it is JSON.\n *\n * A tus proxy that rejects a chunk often answers with plain text or an HTML error\n * page, and `JSON.parse` throwing there would replace a useful status with a parse\n * error.\n *\n * @param text - Raw response text.\n * @returns The parsed object, the raw text, or `null` when the body was empty.\n */\nfunction parseErrorBody(text: string): unknown {\n    if (!text) return null;\n    try {\n        return JSON.parse(text);\n    } catch {\n        return text;\n    }\n}\n\n/**\n * Turn a refused tus response into a `TempestApiError`.\n *\n * The fallback `detail` is used unless the server sent a real error envelope,\n * because `buildApiError`'s own fallback (`\"Erro 409\"`) says nothing about which\n * step of the protocol broke — and that is the whole diagnostic value here.\n *\n * @param response - The raw response that was not acceptable.\n * @param detail - Message to use when the body carries none.\n * @returns The error to throw.\n */\n/**\n * The two statuses a chunk retry fixes that the shared policy cannot know about.\n *\n * `409` and `412` are the offset-divergence answers, and they are the entire\n * reason `resync` exists: the next attempt re-reads the server's offset with\n * `HEAD` and writes from there. They are 4xx refusals a replay genuinely fixes,\n * which is the one thing {@link isRetriableStatus} has no way to tell — from\n * outside this protocol they look like any other deliberate rejection.\n */\nconst RESYNCABLE_STATUSES: ReadonlySet<number> = new Set([409, 412]);\n\n/**\n * Whether a chunk failure is worth another attempt.\n *\n * The default used to be `true` for everything, which cost five round trips\n * before surfacing an answer the first one already gave. Two groups matter here\n * and both are specific to the resume protocol:\n *\n * - **`409`/`412` retry**, even though the shared policy rejects 4xx: they mean\n *   \"your offset is wrong\", and `resync` is how the next attempt fixes it.\n * - **`404`/`410` do not**, even though a lost resource can look transient.\n *   `probe()` turns them into \"O upload expirou no servidor. Comece de novo.\" and\n *   recreating the upload only happens in `ensureUpload`, at attach time — never\n *   inside the chunk loop. So a retry here re-runs `HEAD` against a resource that\n *   is gone, five times, and then reports the same thing with the backoff added\n *   on top.\n *\n * Anything with no API shape still retries: a transport failure has no status to\n * judge, and losing a large upload to one dropped connection is the outcome this\n * whole module exists to avoid.\n *\n * @param error - Whatever the attempt threw.\n * @returns Whether the chunk loop should try again.\n */\nfunction isRetriableChunkFailure(error: unknown): boolean {\n    if (!isApiError(error)) return true;\n    if (RESYNCABLE_STATUSES.has(error.status)) return true;\n    return isRetriableStatus(error.status);\n}\n\nfunction failed(response: RawResponse, detail: string): TempestApiError {\n    const body = parseErrorBody(response.text);\n    const envelope = buildApiError(response.status, body, { get: response.header });\n    const hasDetail =\n        typeof body === \"object\" && body !== null && (\"detail\" in body || \"message\" in body);\n    return new TempestApiError({ ...envelope, detail: hasDetail ? envelope.detail : detail });\n}\n\n/**\n * Resolve a `Location` header against the page, so a relative upload URL works.\n *\n * tus servers are free to answer creation with either an absolute URL or a\n * path, and the spec does not prefer one — a client that only handles absolute\n * URLs breaks against half the implementations.\n *\n * @param value - The raw `Location` header.\n * @returns An absolute URL, or the input when there is no base to resolve against.\n */\nfunction resolveUploadUrl(value: string): string {\n    const base = typeof window === \"undefined\" ? undefined : window.location.href;\n    try {\n        return new URL(value, base).href;\n    } catch {\n        return value;\n    }\n}\n\n/**\n * Chunked, resumable upload speaking the **tus 1.0.0** protocol (core plus the\n * *creation* and *termination* extensions).\n *\n * ## Why tus and not a bespoke scheme\n *\n * A resumable client whose wire format is undocumented cannot be integrated, and\n * inventing one means the backend is ours forever. tus is a published spec with\n * off-the-shelf servers (`tusd`, `tuspy`, `tus-node-server`), so a caller can point\n * this at something they did not write.\n *\n * ## What the backend must implement\n *\n * Every request carries `Tus-Resumable: 1.0.0`.\n *\n * | Step | Request | Expected response |\n * | --- | --- | --- |\n * | Create | `POST {endpoint}` + `Upload-Length`, `Upload-Metadata`, `Idempotency-Key` | `201` + `Location` (the upload URL, absolute or endpoint-relative) |\n * | Probe | `HEAD {uploadUrl}` | `200`/`204` + `Upload-Offset` |\n * | Write | `PATCH {uploadUrl}` + `Upload-Offset`, `Content-Type: application/offset+octet-stream`, chunk body | `204` + the new `Upload-Offset`; `409` when the offset does not match |\n * | Discard | `DELETE {uploadUrl}` | `204` |\n *\n * ## The failure that actually happens\n *\n * A chunk that the server stored but whose response never arrived. The client\n * cannot tell that from a chunk that was lost, and re-sending it blindly would\n * duplicate bytes. Two things prevent that:\n *\n * - **Writes are addressed, not appended.** Every `PATCH` states the offset it\n *   writes at, so a retry after a lost response is asked to write bytes the server\n *   already has and answers `409`. On any retry the client re-reads the truth with\n *   `HEAD` first and continues from there.\n * - **Creation carries an `Idempotency-Key`** (from `generateIdempotencyKey`),\n *   persisted before the first attempt and reused on retry. tus has no idempotent\n *   creation of its own, so without this a lost `201` leaves an orphan upload on\n *   the server. A backend that honours the header returns the same `Location`; one\n *   that ignores it still works, it just keeps the orphan.\n *\n * @param options - Endpoint, file, and the knobs above.\n * @returns A handle with `start`/`pause`/`resume`/`abort` and live `state`/`offset`.\n *\n * @example\n * const upload = createResumableUpload({\n *     endpoint: \"/api/uploads\",\n *     file: recording,\n *     metadata: { filename: \"nota.webm\", ticket: ticketId },\n *     getToken: () => auth.getToken(),\n *     onProgress: ({ fraction }) => setPercent(Math.round(fraction * 100)),\n * });\n *\n * const done = await upload.start();\n * if (done) await api.post(\"/api/tickets/1/audio\", { body: { url: done.url } });\n */\nexport function createResumableUpload(options: ResumableUploadOptions): ResumableUpload {\n    const {\n        endpoint,\n        file,\n        chunkSize = DEFAULT_CHUNK_SIZE,\n        metadata,\n        headers = {},\n        getToken,\n        trustedOrigins,\n        withCredentials = false,\n        key = uploadFingerprint(endpoint, file),\n        storage = createLocalUploadStorage(),\n        retry: retryOptions,\n        onProgress,\n        onStateChange,\n    } = options;\n\n    let state: ResumableUploadState = \"idle\";\n    let offset = 0;\n    let url: string | null = null;\n    let idempotencyKey: string | null = null;\n    let stopping: \"pause\" | \"abort\" | null = null;\n    let inFlight: XMLHttpRequest | null = null;\n    let resumedFrom = 0;\n\n    function setState(next: ResumableUploadState): void {\n        if (state === next) return;\n        state = next;\n        onStateChange?.(next);\n    }\n\n    function report(loaded: number): void {\n        onProgress?.({\n            loaded,\n            total: file.size,\n            fraction: file.size === 0 ? 1 : loaded / file.size,\n            resumedFrom,\n        });\n    }\n\n    /**\n     * Headers every tus request carries, with the credential scoped to the\n     * endpoint's origin.\n     *\n     * The target is a parameter because after creation it is not ours: tus\n     * answers `POST {endpoint}` with a `Location` the server chooses, and the\n     * spec allows an absolute URL on another host — handing the upload to\n     * object storage is the ordinary deployment, not an attack. Before 0.66.0\n     * the bearer token followed that header wherever it pointed, along with the\n     * file bytes.\n     *\n     * @param target - The URL this particular request goes to.\n     * @returns The headers to send.\n     */\n    function baseHeaders(target: string): Record<string, string> {\n        const result: Record<string, string> = { ...headers, \"Tus-Resumable\": TUS_VERSION };\n        const token = getToken?.();\n        if (token && !(\"Authorization\" in result)) {\n            if (isTrustedCredentialTarget(target, endpoint, trustedOrigins)) {\n                result.Authorization = `Bearer ${token}`;\n            } else {\n                reportSuppressedCredential(target, endpoint);\n            }\n        }\n        return result;\n    }\n\n    function register(xhr: XMLHttpRequest): void {\n        inFlight = xhr;\n    }\n\n    async function persist(): Promise<void> {\n        if (!storage || !url || !idempotencyKey) return;\n        await storage.set(key, {\n            url,\n            offset,\n            size: file.size,\n            idempotencyKey,\n            updatedAt: Date.now(),\n        });\n    }\n\n    /**\n     * Ask the server how much it holds. The only source of truth after any failure.\n     */\n    async function probe(target: string): Promise<number> {\n        const response = await sendRequest({\n            method: \"HEAD\",\n            url: target,\n            headers: baseHeaders(target),\n            withCredentials,\n            register,\n        });\n        if (response.status === 404 || response.status === 410) {\n            throw new TempestApiError({\n                status: response.status,\n                detail: \"O upload expirou no servidor. Comece de novo.\",\n            });\n        }\n        const confirmed = parseOffset(response);\n        if (confirmed === null) throw failed(response, \"HEAD sem Upload-Offset.\");\n        return confirmed;\n    }\n\n    /**\n     * Re-attach to a persisted upload, or create a new one.\n     *\n     * The persisted record is only trusted when the file size still matches, and the\n     * offset it holds is re-checked with `HEAD` — the client's copy can be ahead of\n     * the server's whenever the last response was lost.\n     */\n    async function ensureUpload(): Promise<string> {\n        const stored = storage ? await storage.get(key) : null;\n        if (stored && stored.size === file.size) {\n            idempotencyKey = stored.idempotencyKey;\n            if (stored.url) {\n                try {\n                    offset = await probe(stored.url);\n                    url = stored.url;\n                    return stored.url;\n                } catch {\n                    offset = 0;\n                }\n            }\n        }\n\n        setState(\"creating\");\n        idempotencyKey ??= generateIdempotencyKey();\n        url = null;\n        offset = 0;\n        if (storage) {\n            await storage.set(key, {\n                url: \"\",\n                offset: 0,\n                size: file.size,\n                idempotencyKey,\n                updatedAt: Date.now(),\n            });\n        }\n\n        const creationHeaders: Record<string, string> = {\n            ...baseHeaders(endpoint),\n            \"Upload-Length\": String(file.size),\n            \"Idempotency-Key\": idempotencyKey,\n        };\n        const encoded = encodeMetadata(metadata);\n        if (encoded) creationHeaders[\"Upload-Metadata\"] = encoded;\n\n        const response = await sendRequest({\n            method: \"POST\",\n            url: endpoint,\n            headers: creationHeaders,\n            withCredentials,\n            register,\n        });\n        if (response.status !== 201) throw failed(response, \"Criação do upload recusada.\");\n        const locationHeader = response.header(\"Location\");\n        if (!locationHeader) throw failed(response, \"Criação do upload sem cabeçalho Location.\");\n\n        url = resolveUploadUrl(locationHeader);\n        await persist();\n        return url;\n    }\n\n    /** Push one chunk, resyncing the offset first when a previous attempt failed. */\n    async function writeChunk(target: string, resync: { needed: boolean }): Promise<void> {\n        if (resync.needed) {\n            offset = await probe(target);\n            resync.needed = false;\n            report(offset);\n            await persist();\n            if (offset >= file.size) return;\n        }\n\n        const end = Math.min(offset + chunkSize, file.size);\n        const from = offset;\n        const response = await sendRequest({\n            method: \"PATCH\",\n            url: target,\n            headers: {\n                ...baseHeaders(target),\n                \"Content-Type\": \"application/offset+octet-stream\",\n                \"Upload-Offset\": String(from),\n            },\n            body: file.slice(from, end),\n            withCredentials,\n            onProgress: (loaded) => report(Math.min(from + loaded, file.size)),\n            register,\n        });\n\n        if (response.status === 409 || response.status === 412) {\n            resync.needed = true;\n            throw failed(response, \"Offset divergente — o servidor já tinha esses bytes.\");\n        }\n        if (response.status !== 204 && response.status !== 200) {\n            throw failed(response, \"Chunk recusado pelo servidor.\");\n        }\n\n        offset = parseOffset(response) ?? end;\n        report(offset);\n        await persist();\n    }\n\n    /**\n     * Drive the whole upload: attach or create, then chunk until complete.\n     *\n     * The `shouldRetry` predicate does double duty — besides deciding, it arms\n     * `resync` so the next attempt re-reads the server's offset with `HEAD` before\n     * writing. That is deliberate: it is the one place that sees *every* chunk\n     * failure, whatever the cause, and after any failure the client's idea of the\n     * offset is exactly what cannot be trusted.\n     *\n     * `resync` is armed only when the attempt is actually going to happen. A\n     * caller's own `shouldRetry` still wins the decision, and still arms the\n     * resync when it says yes — the flag describes what the *next* attempt must\n     * do, so setting it for an attempt that never comes describes nothing.\n     * {@link isRetriableChunkFailure} is the default, and it is where `409`/`412`\n     * earn a retry the shared policy would refuse and `404`/`410` lose one it\n     * would have granted.\n     *\n     * @returns The result, or `null` when `pause`/`abort` stopped the run.\n     */\n    async function run(): Promise<ResumableUploadResult | null> {\n        stopping = null;\n        const target = await ensureUpload();\n        resumedFrom = offset;\n        setState(\"uploading\");\n        report(offset);\n\n        const resync = { needed: false };\n        while (offset < file.size) {\n            if (stopping) break;\n            await retry(() => writeChunk(target, resync), {\n                retries: 5,\n                ...retryOptions,\n                shouldRetry: (error, attempt) => {\n                    if (stopping) return false;\n                    if (error instanceof DOMException && error.name === \"AbortError\") return false;\n                    const again =\n                        retryOptions?.shouldRetry?.(error, attempt) ??\n                        isRetriableChunkFailure(error);\n                    if (again) resync.needed = true;\n                    return again;\n                },\n            });\n        }\n\n        if (stopping === \"pause\") {\n            setState(\"paused\");\n            return null;\n        }\n        if (stopping === \"abort\") {\n            setState(\"aborted\");\n            return null;\n        }\n\n        setState(\"done\");\n        if (storage) await storage.delete(key);\n        return { url: target, size: file.size };\n    }\n\n    async function guarded(): Promise<ResumableUploadResult | null> {\n        try {\n            return await run();\n        } catch (error) {\n            if (\n                stopping !== null ||\n                (error instanceof DOMException && error.name === \"AbortError\")\n            ) {\n                setState(stopping === \"abort\" ? \"aborted\" : \"paused\");\n                return null;\n            }\n            setState(\"error\");\n            throw error;\n        } finally {\n            inFlight = null;\n        }\n    }\n\n    function stop(reason: \"pause\" | \"abort\"): void {\n        stopping = reason;\n        inFlight?.abort();\n        inFlight = null;\n    }\n\n    return {\n        start: guarded,\n        resume: guarded,\n        pause: () => stop(\"pause\"),\n        abort: async ({ discard = false } = {}) => {\n            stop(\"abort\");\n            setState(\"aborted\");\n            if (!discard) return;\n            if (url) {\n                await sendRequest({\n                    method: \"DELETE\",\n                    url,\n                    headers: baseHeaders(url),\n                    withCredentials,\n                    register: () => undefined,\n                }).catch(() => undefined);\n            }\n            if (storage) await storage.delete(key);\n        },\n        get state() {\n            return state;\n        },\n        get offset() {\n            return offset;\n        },\n        get url() {\n            return url;\n        },\n        key,\n    };\n}\n"],"mappings":"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"}