import { FC, InputHTMLAttributes, ReactNode } from "react"; //#region src/uploader/errors.d.ts type UploadErrorType = "REJECTED" | "REQUEST" | "PART" | "PART_SIZES" | "COMPLETE" | "ABORTED"; interface UploadErrorOptions { message?: string; cause?: unknown; /** Name of the file the failure concerns. Feeds the default message. */ fileName?: string; /** HTTP status for `PART` failures. `0` means no response at all: network error or stall. */ status?: number; /** Parsed `Retry-After`, in ms. Honored by the retry policy when present. */ retryAfterMs?: number; } /** * The single error type the uploader surfaces. `type` discriminates the failure source; when the * uploader wraps an application-level error (a rejected `requestUpload`, a transport failure), the * original is preserved on the standard `cause` property. * * Consumers may also throw `UploadError` from `requestUpload` / `completeUpload` — e.g. * `throw new UploadError("REQUEST", { message: "Daily quota reached." })` — and it passes through * unwrapped, so the message reaches `file.error` verbatim. */ declare class UploadError extends Error { readonly type: UploadErrorType; readonly fileName?: string; readonly status?: number; readonly retryAfterMs?: number; /** * Whether this is a bug in the integration rather than a runtime condition. Dev-bug errors are * logged unconditionally in addition to being surfaced, and are never retried — retrying a * contract violation only hides it. */ get dev(): boolean; constructor(type: UploadErrorType, options?: UploadErrorOptions); } /** * Wrap anything thrown by a consumer callback. An existing `UploadError` passes through unchanged * so a deliberate `throw new UploadError(...)` keeps its type and message. Otherwise a thrown * `Error`'s own message is preferred over the generic default — consumers throw from * `requestUpload` precisely to say something specific ("Files cannot be larger than 10 MB"), and * replacing that with "Could not start the upload" would throw away the only useful part. */ declare const toUploadError: (error: unknown, type: UploadErrorType, options?: UploadErrorOptions) => UploadError; //#endregion //#region src/uploader/uploader.types.d.ts /** * A part's lifecycle. Separate from `FileStatus`: parts have no signing or completion phase, and * they have a retry-backoff phase (`WAITING`) that files don't. * * `WAITING` exists so backoff costs no concurrency — only `UPLOADING` holds a part slot. It is a * real status rather than `QUEUED` plus a `retryAt` timestamp because the scheduler's eligibility * test has to be reactive, and MobX cannot invalidate a computed that reads the clock. The wait is * therefore expressed as a timer that flips the status, not as a comparison against `Date.now()`. * * There is no `CANCELED`: a canceled part is disposed and its file removed from the collection, so * no observer can ever see one. */ type PartStatus = "QUEUED" | "UPLOADING" | "WAITING" | "COMPLETED" | "FAILED"; /** * A file's lifecycle. One member per phase with a distinct concurrency budget or a distinct set of * legal transitions: * * - `PENDING` — accepted, waiting for a slot. No network activity. * - `REQUESTING` — `requestUpload` is in flight. No parts exist yet. * - `UPLOADING` — parts exist. The only status whose parts are eligible for part slots. * - `COMPLETING` — every part uploaded and `completeUpload` is in flight. Without this phase a * `completeUpload` failure would have to retract an already-emitted value. * - `COMPLETED` — terminal success, and the only status that contributes to `values`. * - `FAILED` — terminal failure; re-enterable via `retry()`. * * There is no `QUEUED`: "parts exist but none has started" is not a phase, it is `UPLOADING` at * `progress === 0`. There is no `CANCELED`: a canceled upload is removed from `uploads`, which is * the single source of truth for whether an upload exists. */ type FileStatus = "PENDING" | "REQUESTING" | "UPLOADING" | "COMPLETING" | "COMPLETED" | "FAILED"; /** One signed part URL and the exact number of bytes the server signed it for. */ interface UploadPart { url: string; /** * Exact byte length of this part. **Not advisory** — the uploader slices the file with a running * offset from these sizes and never derives them itself. * * Backends that sign each part URL with `Content-Length` in `signableHeaders` reject any other * split with `403 SignatureDoesNotMatch`, because `Content-Length` is a forbidden header: the * browser derives it from the blob and a script cannot override it. Sizes must sum exactly to * `file.size`; a mismatch fails the file with `UploadError("PART_SIZES")` and is logged as an * integration bug. */ size: number; } /** What `requestUpload` resolves to. */ interface UploadRequestResult { /** * Server-side identifier, handed back to `completeUpload` / `cancelUpload` and emitted as the * form value. Opaque to the uploader — an S3 key, a UUID, anything. */ id: string; /** * Canonical filename, carried explicitly rather than parsed out of `id`. Wins over the local * `File.name` once known, so server-side normalization or de-duplication shows up in the UI. */ name: string; /** One entry per part, in order. An empty array is legal for a zero-byte file. */ parts: UploadPart[]; } /** * A completed upload, as emitted to `onChange` and accepted by `value`. * * `name` is required rather than optional, and a bare id is deliberately **not** accepted. The name is * not derivable from the id — an id may be a storage key that happens to end in a filename, or an * opaque uuid that doesn't — so a library that guessed would render a uuid as a filename for half its * consumers. Persist the name alongside the id in whatever holds your form value, or fetch it when the * form loads. */ interface UploadValue { id: string; name: string; } /** Everything in `uploads`: files this uploader is uploading, plus rehydrated completed uploads. */ type Upload = FileModel | CompletedUploadModel; /** * The surface every `Upload` shares, so list UIs need no `instanceof` branching. The reference * implementation's two upload classes had no common shape, which is how single-file replacement * ended up cancelling only the in-flight files and leaving a rehydrated upload behind. */ interface UploadLike { /** Stable client-side identity, for React keys. Never the server id. */ readonly key: string; readonly name: string; readonly extension: string; readonly status: FileStatus; /** 0–100. Never `NaN`. */ readonly progress: number; /** The server's identifier, once known. */ readonly uploadId: string | undefined; /** The `{ id, name }` pair emitted to `onChange`, or `undefined` until the upload completes. */ readonly value: UploadValue | undefined; readonly error: UploadError | undefined; remove(): void; activate(): void; dispose(): void; } /** * Transport for a single part attempt. Must reject with `UploadError("ABORTED")` when `signal` * aborts, and report cumulative bytes sent through `onProgress`. */ type UploadPartFn = (signal: AbortSignal, part: PartModel, onProgress: (loaded: number) => void) => Promise; interface UploaderConfig { /** * Ask the server for an upload id and one signed URL per part. The `signal` aborts when the file * is removed or the uploader is disposed — forward it to your fetch/client so a cancel during * signing doesn't leave a request in flight. * * Not auto-retried: it is your API call through your client, which likely has its own retry * policy. Throw `UploadError` to control the message the user sees; any other throw is wrapped as * `UploadError("REQUEST", { cause })` and keeps its own message. Recover with `file.retry()`. */ requestUpload: (signal: AbortSignal, file: FileModel) => Promise; /** * Optional client-driven finalization (e.g. `POST /uploads/:id/complete`) for backends that don't * complete the multipart upload themselves. Called at most once per file, after every part has * uploaded; the file sits in `COMPLETING` until it resolves and only then reaches `COMPLETED`, so * a failure surfaces as `FAILED` instead of retracting an already-emitted value. A rejection * produces `UploadError("COMPLETE", { cause })`; `file.retry()` re-issues just this call. * * **Must be idempotent.** If the uploader is disposed mid-call, `activate()` re-issues it — an * aborted request gives no evidence about whether the server processed it, and re-issuing an * idempotent call is strictly better than silently dropping an upload. */ completeUpload?: (signal: AbortSignal, file: FileModel) => Promise; /** * Best-effort server-side cleanup when an upload that never completed is removed (e.g. * `DELETE /uploads/:id`). Only called for files that obtained an `uploadId` and have not reached * `COMPLETED` — there is nothing to abort otherwise. * * Deliberately gets no `AbortSignal`: it is issued during teardown and must outlive the model. * Rejections are reported through `onError` and never block removal. */ cancelUpload?: (file: FileModel) => void | Promise; /** * Fires when a `COMPLETED` upload is removed, for consumers that want to delete the durable * object. `cancelUpload` covers only abandonment of an upload still in flight. */ onRemove?: (value: UploadValue, uploader: UploaderModel) => void; /** * Override the part transport. Defaults to `xhrPutUpload` (XHR, because `fetch` still cannot * report upload progress). Also the seam that makes the uploader testable without a DOM. */ uploadPart?: UploadPartFn; /** * Reject a file before it is added. Return a message to reject it, or nothing to accept it. * Called once per file in order, *after* earlier files in the same batch have been added — so a * count rule can read `uploader.uploads.length` and see them. * * Rejections surface as `UploadError("REJECTED")` through `onError` and never create an upload, * so a refused file cannot appear as a failed row. */ validate?: (file: File, uploader: UploaderModel) => string | undefined | void; /** * Passed straight through to the hidden `` rendered by ``. * Not re-checked by the model: the file dialog is the filter. If you need a hard guarantee (or * you call `addFiles` yourself), enforce it in `validate`. */ accept?: string; /** * Whether the uploader holds more than one upload. Default `false`. * * Sets the hidden input's `multiple` attribute, and — unlike `accept` — also governs the * collection: in single-file mode `addFiles` *replaces* what is there rather than accumulating, so * the previous upload is removed (and cancelled or reported through `onRemove`) first. */ multiple?: boolean; /** Passed straight through to the hidden input's `capture` attribute. */ capture?: boolean | "user" | "environment"; /** * How many uploads the field holds at once. Default: unlimited when `multiple` is set, `1` when it * isn't. Picks past the cap are refused with `UploadError("REJECTED")` through `onError`, and * `uploader.full` / `uploader.remainingSlots` let your UI gate on the same number. * * This lives here rather than in `validate` because it is the one limit that depends on the * *collection* rather than on the file: only the uploader can see both the files it is uploading and * the already-uploaded ones rehydrated from `value`. A design system's own file cap counts only local * `File`s, so it will happily accept a second file when one already exists server-side — leave that * setting open and let this one do the work. * * Limits that depend only on the file — type, size — belong to whatever does the selecting, since it * can reject before the uploader ever sees them. Use `accept` / `validate`, or your design system's * equivalents. * * The cap governs *picking*, not rehydration: `value` is authoritative, so a controlled value longer * than `maxFiles` is applied in full rather than silently truncated. */ maxFiles?: number; /** * How many part uploads may be in flight at once, across all files. Default `4`. * * This also throttles signing: a file is only requested when the already-signed work can't keep * the pipeline busy without it, which keeps the number of signed-but-uncompleted uploads at * roughly this number and stops presigned URLs expiring while queued behind large files. Parts in * retry backoff hold no slot. */ concurrency?: number; /** * Hard cap on signed-but-uncompleted uploads, for backends that advertise one (and reject the * next request outright). Default: unlimited, since `concurrency` already bounds this to roughly * its own value — set it when the backend's limit must be *guaranteed* rather than approximated. */ maxPendingUploads?: number; /** Total attempts per part, including the first. Default `4`. */ maxPartAttempts?: number; /** First backoff window in ms, doubling per attempt with equal jitter. Default `500`. */ retryBaseMs?: number; /** * Backoff ceiling in ms. Default `8000`. With the defaults the three retry delays sum to at most * 3.5s, and nothing is ever awaited after the decision to fail. */ retryCapMs?: number; /** * Abort and retry a part attempt after this long with no upload-progress event. Default `60000`; * `0` disables. A *stall* budget rather than `xhr.timeout`'s total-request budget, which no * single value can set correctly for both a 1 MB and a 500 MB part. */ stallTimeoutMs?: number; /** * Override which part failures are retried. Default: no response (`status === 0`), 408, 429, and * 5xx other than 501. Everything else is fatal — notably 403, an expired or mismatched presign, * which fails identically no matter how many times it is retried. */ isRetryable?: (error: UploadError) => boolean; /** * The controlled value: uploads that already exist server-side. Reconciled by `id`; uploads still * in flight are never touched. Passing the key at all (even as `undefined`) makes the uploader * controlled, so `undefined` and `[]` both mean "no uploads". */ value?: UploadValue[]; /** * Fires when the set of completed uploads changes. Compared structurally, so a fresh array of * fresh objects with the same contents is not a change. Requires `activate()`. */ onChange?: (values: UploadValue[], uploader: UploaderModel) => void; /** * Every failure, including files refused by `validate` — which have no model to carry an `error`, * so this is the only way they become visible. Also receives failures already reflected on * `file.error`, for toasts and telemetry. */ onError?: (error: UploadError, file: FileModel | undefined, uploader: UploaderModel) => void; } /** Constructor data for a `FileModel`. */ interface FileConfig { file: File; } /** Constructor data for a `PartModel`. */ interface PartConfig { index: number; url: string; blob: Blob; } /** Constructor data for a `CompletedUploadModel`. */ interface CompletedUploadConfig { id: string; name: string; } //#endregion //#region src/uploader/part.model.d.ts /** * One part of a multipart upload: a slice of the file plus the URL it was signed for. * * A part is a **single-attempt unit**. It does not loop over retries itself — a retryable failure * parks it in `WAITING`, releasing its concurrency slot for the duration of the backoff, and a timer * flips it back to `QUEUED` for the uploader's scheduler to pick up. The retry "loop" is therefore * the scheduler, which is what makes backoff free of concurrency cost and every intermediate state * inspectable. (The reference implementation slept inside `upload()` with the status still * `UPLOADING`, so a part waiting 3.3s held one of only four slots.) */ declare class PartModel { readonly file: FileModel; readonly config: PartConfig; status: PartStatus; /** * Bytes confirmed on the wire. Bytes rather than a percentage: with exact server-supplied part * sizes the parts are not equal-sized, so only a byte-weighted roll-up reports the file's real * progress. */ loaded: number; /** 1-based count of attempts started. */ attempt: number; error: UploadError | undefined; private controller; private retryTimer; get index(): number; get url(): string; get blob(): Blob; get size(): number; /** 0–100 for this part alone. Zero-guarded: a zero-byte part is 0% until it completes. */ get progress(): number; constructor(file: FileModel, config: PartConfig); /** * @internal Scheduler entry point: `QUEUED` -> `UPLOADING`. Sets the status synchronously, before * the first await, so the scheduler's slot accounting is correct the moment this returns. */ start(): void; /** @internal `FAILED` -> `QUEUED` with the attempt counter reset, for `file.retry()`. */ requeue(): void; /** * @internal Abandon the current attempt and become eligible again. Used when the file fails or the * uploader is parked. Object-store part PUTs are not range-resumable, so the part restarts from 0. */ abort(): void; /** Release every resource. Leaves the part in a resumable status; pairs with the uploader's `activate`. */ dispose(): void; private attemptUpload; /** `UPLOADING` -> `WAITING`. Releases the part slot; the timer re-queues. */ private wait; /** `WAITING` -> `QUEUED`, from the backoff timer. */ private queue; private settle; private setLoaded; } //#endregion //#region src/uploader/file.model.d.ts /** * One local `File` being uploaded: its server identity, its parts, and its phase. * * `status` is **explicit observable state**, not a derivation from the parts. `PENDING` and * `REQUESTING` both have zero parts, `COMPLETING` and `COMPLETED` both have all parts complete, and * `COMPLETED` has to be sticky because it is what gets emitted into the consumer's form value — a * derivation would un-complete a file the moment any part model was touched. Parts still *drive* the * status, but through the uploader's scheduler rather than through a getter. */ declare class FileModel implements UploadLike { readonly uploader: UploaderModel; readonly config: FileConfig; /** Stable client identity for React keys; distinct from the server's `uploadId`. */ readonly key: string; status: FileStatus; parts: PartModel[]; uploadId: string | undefined; error: UploadError | undefined; /** The server's canonical name, once known. Falls back to the local `File.name`. */ private serverName; private controller; private completeRequested; private objectUrlValue; get file(): File; get size(): number; get type(): string; get isImage(): boolean; get isVideo(): boolean; get previewable(): boolean; /** * A blob URL for previewing the file, minted on first read and revoked by `dispose`. * * Deliberately **not** a `computed`. A computed's body must be pure, and `URL.createObjectURL` * allocates a document-scoped handle; worse, computeds suspend when unobserved and recompute on * the next read, so a computed here mints a fresh blob URL every time a preview unmounts and * remounts and leaks the previous one for the page's lifetime. This is a plain getter over * readonly, non-observable state memoized into a plain field, so it can never invalidate: it mints * at most once per activate/dispose cycle and `dispose` revokes exactly what was minted. * * `undefined` rather than `""` for non-previewable files, so consumers don't render `` * (which requests the current page). */ get objectUrl(): string | undefined; get name(): string; get extension(): string; get value(): UploadValue | undefined; get queuedParts(): PartModel[]; get activeParts(): PartModel[]; get waitingParts(): PartModel[]; get completedParts(): PartModel[]; get failedParts(): PartModel[]; /** Vacuously true for a zero-part file, which is how a zero-byte upload completes. */ get partsComplete(): boolean; /** Bytes confirmed on the wire across every part. */ get loaded(): number; /** * 0–100, weighted by **bytes** rather than by part count — with exact server-supplied sizes the * parts are unequal, so averaging their percentages misreports. Zero-guarded: the reference * divided by `parts.length` and rendered `NaN` for the entire pre-signing phase. */ get progress(): number; /** Whether this file has reached a terminal status. */ get settled(): boolean; constructor(uploader: UploaderModel, config: FileConfig); /** * @internal Scheduler entry point: `PENDING` -> `REQUESTING`. Sets the status synchronously, * before the first await, so the scheduler's slot accounting is correct the moment this returns. */ startRequest(): void; /** @internal Scheduler entry point: `UPLOADING` -> `COMPLETING`. Fires `completeUpload` once. */ startComplete(): void; /** @internal Terminal success. */ complete(): void; /** @internal Terminal failure. */ fail(error: UploadError): void; /** * Try a failed upload again. Re-signs when no id was ever obtained, re-issues just the completion * call when that is what failed, and otherwise re-queues the failed parts. */ retry(): void; /** Remove this upload from the uploader, cancelling it server-side if it started. */ remove(): void; /** Re-arm anything `dispose` released. Transient phases are re-issued from the top. */ activate(): void; /** * Release every resource and park the work: the in-flight request is aborted, parts are aborted, * and the preview URL is revoked. Transient phases are rolled back to resumable ones rather than * failed, so `activate` can pick the upload back up. */ dispose(): void; private runRequest; private runComplete; /** `REQUESTING` -> `UPLOADING`, slicing the file against the server's exact part sizes. */ private applyRequestResult; } //#endregion //#region src/uploader/uploader.model.d.ts /** * Owns the list of uploads and the scheduling of all network work. * * Scheduling is an explicit, synchronous, idempotent `pump()` action rather than a reaction. The * reference implementation used a self-triggering `autorun` that read the part queues and mutated * part status, which is a category error — a reaction's contract is "state to outside world", not * "state to state" — and it made the number of reaction passes an emergent property of the code * (O(N) passes to dispatch N parts, each recomputing every queue), untestable without leaning on * MobX's scheduler, and unable to express two concurrency budgets at once. `pump()` is called from * every transition point, so no free slot ever goes unfilled. */ declare class UploaderModel { readonly config: UploaderConfig; /** Every upload, in display order: files being uploaded plus rehydrated completed uploads. */ uploads: Upload[]; private active; private pumping; private pumpQueued; private changeReactionDisposer; get concurrency(): number; get maxPendingUploads(): number; /** * How many uploads the field holds at once, counting rehydrated ones. Defaults from `multiple`: * unlimited when it is set, `1` when it isn't. */ get maxFiles(): number; get maxPartAttempts(): number; get stallTimeoutMs(): number; /** Only the uploads that have a local `File` behind them. */ get files(): FileModel[]; /** Every upload that has reached `COMPLETED`, in display order. */ get completedUploads(): Upload[]; /** The form value: one `{ id, name }` per completed upload. */ get values(): UploadValue[]; /** Just the ids, for consumers whose field stores bare identifiers. */ get ids(): string[]; get activeParts(): PartModel[]; get queuedParts(): PartModel[]; /** Files whose signing request is in flight; each is a prospective part the pipeline can't see yet. */ get requestingFiles(): FileModel[]; /** * Uploads that exist server-side but are not finished — the count a backend's pending-upload limit * applies to. * * `REQUESTING` files count even though they have no id yet: the request that is in flight is what * *creates* the server-side pending upload, so excluding them would let successive pumps sign past * `maxPendingUploads` while the first requests were still resolving. */ get pendingUploads(): FileModel[]; /** * Whether the field is at `maxFiles`. Gate your browse control on this rather than keeping your own * count — it is the same number the model refuses additions with, and it counts both kinds of upload. */ get full(): boolean; /** How many more uploads will be accepted. `Infinity` when unlimited. */ get remainingSlots(): number; /** Whether any upload is still working. Use this instead of peeking at part counts. */ get uploading(): boolean; /** Whether any upload has failed. */ get failed(): boolean; /** Whether anything is not yet completed — the "block submit" flag. */ get invalid(): boolean; /** Every error currently attached to an upload. */ get errors(): UploadError[]; /** Total bytes across every file being uploaded. */ get size(): number; /** Bytes confirmed on the wire across every file. */ get loaded(): number; /** * 0–100 across every non-failed file, weighted by bytes so a 1 GB file doesn't count the same as a * 1 KB one. Zero-guarded — the reference computed `Math.floor(0 / 0)` and returned `NaN` whenever * there was nothing to upload. */ get progress(): number; constructor(config: UploaderConfig); /** * (Re)arm the `onChange` reaction and resume scheduling. Idempotent. Pairs with `dispose` — * `useUploader` calls both across effect cycles, so a StrictMode dev remount (mount, cleanup, * mount against the same model) resumes rather than leaving the uploader parked. */ activate(): void; /** * Release every resource and park the work: in-flight requests and part uploads are aborted, retry * and stall timers cleared, preview URLs revoked, the `onChange` reaction dropped, and nothing new * is scheduled. * * Uploads are left in the collection with resumable statuses — call `activate` to pick them back * up, or `clear` to abandon them. A destructive dispose would abort every in-flight upload on a * StrictMode dev remount. */ dispose(): void; /** * **Add** files to whatever is already here. Accepts a `FileList` (from an ``'s change event), * an array, or any iterable of `File`. * * Each file goes through `config.validate` in order, after earlier files in the batch have been * added — so a count rule sees them. * * A file already in the list is skipped rather than uploaded twice, matched by {@link isSameFile} * — so `setFiles` and `addFiles` agree about identity, and re-picking the same file can't produce * two uploads of the same bytes, two server-side pending uploads and two entries in the form value. * Skips are reported through `onError` as `UploadError("REJECTED")` rather than dropped silently. * * This is the *delta* API, for a selection layer that reports only what was newly picked — which is * what an `` change event gives you (and what `` uses). If your * selection layer owns the list and hands back the whole thing on every change, use * {@link UploaderModel.setFiles} instead: that also removes files the layer dropped, which this * cannot see. */ addFiles(files: FileList | Iterable): void; /** * **Reconcile** the picked files to exactly this set — additions and removals both. * * This is the API for a selection layer that owns the file list and reports the whole thing on every * change rather than a delta. Chakra UI's and Ark UI's `FileUpload` work this way: `onFileAccept` * fires from the machine's `acceptedFiles` binding, so it receives the complete accepted list — and * it fires on deletions too, not just additions. Passing that to `addFiles` would duplicate every * file already present. * * Files are matched to existing uploads by reference, then by name + size + type (see * {@link isSameFile}) — the same identity `@zag-js/file-utils` uses, so both layers agree about what * "the same file" is. Matched uploads keep their position and their in-flight state, so a * reconciliation never restarts an upload that is already running. Uploads whose file is absent are * removed, cancelling them server-side if they had started. New files are appended through * `config.validate`. * * Rehydrated completed uploads are left alone: they have no `File`, they aren't part of the selection * layer's list, and they are owned by the controlled `value`. So `setFiles([])` clears the picked * files without discarding uploads that already exist server-side. */ setFiles(files: FileList | Iterable): void; /** Add a rehydrated upload that already exists server-side. */ addCompletedUpload(value: UploadValue): CompletedUploadModel; /** * Reconcile the controlled value, matching **by `id` only**. * * Only `COMPLETED` uploads take part in the removal diff. `values` contains nothing else, so an * upload still in flight was never in the parent's value and cannot be something the parent * "dropped" — which is what keeps a `value` echo from cancelling work in progress, even after the * upload has been signed and therefore has an `uploadId`. * * Being a single action means the diff cannot emit a half-applied list — the reference's bare * `addCompletedUpload` loop ended a MobX batch per push, so `onChange` could fire mid-reconciliation. */ applyValue(value: UploadValue[]): void; /** * Remove an upload. A file that started server-side but never completed is cancelled through * `config.cancelUpload`; a completed one is reported to `config.onRemove`. */ removeUpload(upload: Upload): void; /** Remove every upload, of either kind, through the one `removeUpload` path. */ clear(): void; /** Retry every failed upload. */ retryAll(): void; /** @internal Surface an error to `config.onError`. */ reportError(error: UploadError, file?: FileModel): void; /** @internal The default retryable-status policy, overridable via `config.isRetryable`. */ isRetryableError(error: UploadError): boolean; /** * The module's only scheduling primitive: inspect the current state, fill whatever concurrency * slots are free, return. Synchronous, idempotent, and safe to call at any time — nothing else in * the module starts network work. */ pump(): void; private addFile; /** * One scheduling pass. The order is load-bearing: * * 1. `settleFiles` advances files whose parts are done (or one of which failed), releasing slots — * so a freed slot is reused in this same pass rather than the next one. * 2. `startPendingFiles` signs files, creating the parts pass 3 needs. * 3. `startQueuedParts` fills the part slots. */ private pumpOnce; /** `UPLOADING` -> `FAILED` / `COMPLETED` / `COMPLETING`, driven by part aggregates. */ private settleFiles; /** * `PENDING` -> `REQUESTING`. * * A file is signed only when the already-signed work can't keep the part pipeline busy without it. * Counting `requestingFiles` is what makes this self-limiting: without that term, a hundred picked * files would all sign on the first tick (none has produced parts yet), which is exactly what a * backend's pending-upload limit rejects. With it, signing stays one step ahead of the pipeline — * so the signing round trip overlaps the tail of the previous file and there is no bubble — while * the number of signed-but-unfinished uploads stays near `concurrency`. */ private startPendingFiles; /** * `QUEUED` -> `UPLOADING`, in `(file order, part index)` order. * * Files drain one at a time rather than interleaving: an upload is worthless to the consumer until * every part lands, so FIFO over indivisible jobs minimizes mean completion time — ten interleaved * files give you nothing usable until the end, ten drained gives you the first at a tenth of the * time. It also finalizes uploads earlier and wastes no bytes when a queued file is cancelled. */ private startQueuedParts; } //#endregion //#region src/uploader/completed-upload.model.d.ts /** * An upload that already exists server-side, rehydrated from the controlled `value` — no blob, no * parts, nothing in flight. * * Implements `UploadLike` with literal answers (`status` is always `"COMPLETED"`, `progress` always * 100) so list UIs, `values`, `invalid` and `clear` treat it exactly like a `FileModel` and no * `instanceof` branch is needed anywhere. * * `name` is carried explicitly. The reference implementation derived it from the identifier with * `uploadKey.replace(/^.*[\\/]/, "")`, which only worked because that backend's client-facing id was * the bucket key; against an opaque id it renders the id itself and yields no extension. */ declare class CompletedUploadModel implements UploadLike { readonly uploader: UploaderModel; readonly config: CompletedUploadConfig; /** Stable client identity for React keys; distinct from the server's `uploadId`. */ readonly key: string; /** The parent owns this: a `value` update may rename an upload the uploader never uploaded. */ private currentName; get uploadId(): string; get name(): string; get extension(): string; get status(): FileStatus; get progress(): number; get error(): UploadError | undefined; get value(): UploadValue; constructor(uploader: UploaderModel, config: CompletedUploadConfig); setName(name: string): void; remove(): void; /** No-op: there is nothing to arm. Present so the `Upload` union is uniform. */ activate(): void; /** No-op: no blob, no request, no timers. */ dispose(): void; } //#endregion //#region src/uploader/components/uploader-root.d.ts interface UploaderRootProps { uploader: UploaderModel; /** * Escape hatch for the hidden file input (`id`, `name`, `form`, …). Spread *before* the library's * own attributes. `accept`, `multiple` and `capture` come from `uploader.config` so there is one * place to set them, and `type`/`onChange` are the component's own. */ inputProps?: Omit, "type" | "value" | "onChange" | "accept" | "multiple" | "capture">; children?: ReactNode; } /** * Owns the hidden `` — and therefore the only way to open the file dialog, which * it publishes on the context as `openFileDialog` — and provides the model to everything below. * * Renders **no wrapper element**: an uploader has no structural DOM requirement the way a virtualized * table's scroll viewport does, so a div here would only be a box you have to style around. Wrap the * children in your own container. */ declare const UploaderRoot: FC; //#endregion //#region src/uploader/components/uploader-uploads.d.ts interface UploaderUploadsProps { /** Renders one upload — in-flight and already-completed alike. */ children: (upload: Upload) => ReactNode; } /** * Renders every upload through your render prop, in display order, keyed on `upload.key`. * * Emits no DOM element of its own, so it drops straight into whatever list markup you already have. * Reading `uploader.uploads` yourself inside an `observer` is equivalent — this only saves reaching * for the context and remembering which identity is the stable one. */ declare const UploaderUploads: FC; //#endregion //#region src/uploader/components/namespace.d.ts /** * Compound namespace for the uploader skeleton. Consumers compose these into their own closed * component (styles + defaults captured once), e.g. `…`. */ declare const Uploader: { Root: import("react").FC; Uploads: import("react").FC; }; //#endregion //#region src/uploader/uploader.context.d.ts /** * What the parts below `` need: the model, plus the one thing only the root can * provide — a handle on its hidden ``. Opening the file dialog is a DOM * capability, not model state, so it rides the context rather than the model. */ interface UploaderContextValue { uploader: UploaderModel; /** Opens the root's hidden file input. Must be called from within a user gesture. */ openFileDialog: () => void; } declare const uploaderContext: import("react").Context; declare const useUploaderContext: () => UploaderContextValue; declare const UploaderProvider: import("react").Provider; //#endregion //#region src/uploader/uploader.util.d.ts /** Default first-retry window; see `UploaderConfig.retryBaseMs`. */ declare const RETRY_BASE_MS = 500; /** Default backoff ceiling; see `UploaderConfig.retryCapMs`. */ declare const RETRY_CAP_MS = 8000; /** A server's `Retry-After` is honored up to this; beyond it we would stall the whole queue. */ declare const MAX_RETRY_AFTER_MS = 30000; /** Default stall budget; see `UploaderConfig.stallTimeoutMs`. */ declare const STALL_TIMEOUT_MS = 60000; /** * A stable client-side identity for an upload. Used only for React keys — it is never transmitted, * persisted, or compared against anything server-side, so it needs uniqueness within the page and * nothing more. * * A plain counter rather than `crypto.randomUUID()`: that is unavailable outside a secure context * (plain HTTP on a LAN address is enough to make it `undefined`), and a random id would differ * between a server render and hydration, remounting every rehydrated row. A counter is deterministic, * so the same sequence of constructions yields the same keys on both sides. * * `useId` can't serve here — it is a hook returning one id per component, while uploads are * constructed from event handlers, and the models deliberately don't depend on React at all. */ declare const nextUploadKey: () => string; /** * Whether two `File`s should be treated as the same selection: reference identity first, then * name + size + type. * * The structural fallback matters for interop — it is exactly the comparison * `@zag-js/file-utils`' `isFileEqual` uses, so a design system's file list and the uploader's agree * about what "the same file" means. Reference equality alone would churn whenever the selection layer * hands back re-created `File` objects (Zag's `transformFiles` does this). */ declare const isSameFile: (a: File, b: File) => boolean; /** * The filename's extension, lowercased, or `""` when there isn't one. * * Deliberately reports what the name says and normalizes nothing — the reference implementation * rewrote `jpeg` to `jpg` for one backend's content-type table, which is an application concern. */ declare const getFileExtension: (fileName: string) => string; /** * Whether another attempt against the same signed URL could plausibly succeed. * * - `0` — no response at all: network error, or our own stall abort * - `408` — server-side request timeout * - `429` — rate limited (`Retry-After` honored when present) * - `5xx` except `501` — transient server failure * * Everything else is fatal, most importantly `403`: an expired or mismatched presign fails * identically forever, so retrying it only delays the error. */ declare const isRetryableStatus: (status: number) => boolean; /** `Retry-After` as ms; accepts delta-seconds or an HTTP-date. */ declare const parseRetryAfter: (header: string | null) => number | undefined; interface RetryDelayOptions { baseMs?: number; capMs?: number; retryAfterMs?: number; } /** * Equal-jitter exponential backoff: half the window fixed, half random. Full jitter can return ~0ms, * which just re-hammers a server that is already struggling; a fixed delay synchronizes every part * of every file into a thundering herd. * * With the defaults (base 500, factor 2, cap 8000) a part's three retry delays are 250–500ms, * 500–1000ms and 1000–2000ms, so backoff totals at most 3.5s. * * @param attempt 1-based number of the attempt that just failed. */ declare const retryDelayMs: (attempt: number, options?: RetryDelayOptions) => number; /** * The default part transport: a plain `PUT` of the part's blob to its signed URL. * * XHR rather than `fetch` because `fetch` still cannot report upload progress in most browsers. * * Every terminal path settles the promise — `onload`, `onerror`, `ontimeout`, `onabort`, and the * stall timer. The reference implementation registered only `onload` and `onerror`, so an aborted * part's promise never settled: the part stayed `UPLOADING` forever, permanently holding a * concurrency slot, and enough cancellations deadlocked the whole uploader. */ declare const xhrPutUpload: UploadPartFn; //#endregion //#region src/uploader/use-uploader.d.ts declare const useUploader: (config: UploaderConfig) => UploaderModel; //#endregion export { CompletedUploadConfig, CompletedUploadModel, FileConfig, FileModel, FileStatus, MAX_RETRY_AFTER_MS, PartConfig, PartModel, PartStatus, RETRY_BASE_MS, RETRY_CAP_MS, RetryDelayOptions, STALL_TIMEOUT_MS, Upload, UploadError, UploadErrorOptions, UploadErrorType, UploadLike, UploadPart, UploadPartFn, UploadRequestResult, UploadValue, Uploader, UploaderConfig, UploaderContextValue, UploaderModel, UploaderProvider, UploaderRoot, UploaderRootProps, UploaderUploads, UploaderUploadsProps, getFileExtension, isRetryableStatus, isSameFile, nextUploadKey, parseRetryAfter, retryDelayMs, toUploadError, uploaderContext, useUploader, useUploaderContext, xhrPutUpload }; //# sourceMappingURL=uploader.d.mts.map