Skip to main content

Storage and Uploads

@vef-framework-react/core provides a chunked, resumable file-upload client built against the framework's sys/storage backend resource. It handles multipart splitting, bounded-concurrency part uploads, retry-with-backoff, and — opt-in — resuming an interrupted upload across page reloads.

For the React-hook adapter, see useUpload in @vef-framework-react/hooks; it wraps everything on this page for the common single-in-flight-upload case. Reach for Uploader directly for parallel/batch uploads.

Uploader

Drives one file through the four-step protocol (init_upload → upload_part* → complete_upload) exposed by the backend's sys/storage resource. One-shot: a completed or failed run cannot be restarted — create a fresh Uploader per file.

import { HTTP_CLIENT, Uploader, useApiClient } from "@vef-framework-react/core";

const apiClient = useApiClient();
const http = apiClient[HTTP_CLIENT];

const uploader = new Uploader(http, file, {
onProgress: progress => console.log(progress.percent),
onStatusChange: status => console.log(status)
});

const result = await uploader.start();
// result: { bucket, key, eTag, size, contentType, lastModified, originalFilename, metadata? }

Constructor

new Uploader(http: Readonly<HttpClient>, file: Blob, options?: UploaderOptions)

http is the HttpClient obtained from apiClient[HTTP_CLIENT] (see HTTP and API Client). file.name is required when file is a plain Blob rather than a File — set it via options.init.filename.

UploaderOptions

OptionTypeDefaultDescription
apiPathstring"/api"RPC entrypoint URL
resourcestring"sys/storage"RPC resource name
versionstring"v1"RPC version
initUploadInitInit-time overrides for the backend session
partConcurrencynumber3Maximum parts uploaded in parallel
maxPartRetriesnumber3Retry budget per part (the initial attempt counts as 1)
retryBaseDelaynumber500Base delay (ms) for exponential per-part retry backoff
retryMaxDelaynumber8000Maximum delay (ms) any single retry will wait
signalAbortSignalExternal abort signal, combined with the uploader's internal controller
onProgress(progress: UploadProgress) => voidInvoked after every part progress tick
onStatusChange(status: UploadStatus) => voidInvoked on every UploadStatus transition
onSessionOpened(session: UploadSessionSnapshot) => voidInvoked once the backend has confirmed an upload session (fresh or resumed)

UploadInit

FieldTypeDescription
filenamestringOverrides the original filename; defaults to file.name
contentTypestringOverrides the declared MIME type; defaults to file.type
publicbooleanLands the object under pub/ (anonymous-readable) when true, priv/ (authenticated reads) otherwise

UploadProgress

FieldTypeDescription
loadednumberBytes acknowledged so far (never exceeds total)
totalnumberTotal file size in bytes
partsCompletednumberNumber of parts fully accepted by the backend
partsTotalnumberTotal number of parts in the upload plan
percentnumberloaded / total, rounded to an integer percentage

UploadStatus

type UploadStatus =
| "idle"
| "initializing"
| "uploading"
| "completing"
| "aborting"
| "succeeded"
| "failed"
| "aborted";

Transitions are linear up to a terminal state (succeeded, failed, aborted).

UploadResult

FieldTypeDescription
bucketstringBackend bucket name
keystringObject key (prefixed pub/ or priv/)
eTagstringBackend-assigned ETag
sizenumberFinal object size in bytes
contentTypestringStored content type
lastModifiedstringISO-8601 timestamp
originalFilenamestringThe filename tracked through the upload
metadataRecord<string, string> | undefinedBackend-attached metadata

Instance API

MemberTypeDescription
statusUploadStatus (getter)Current lifecycle status
progressUploadProgress (getter)Latest aggregated progress snapshot
start(plan?)(plan?: ResumePlan) => Promise<UploadResult>Starts the upload; repeat calls return the original promise. Pass a { kind: "resume", ... } plan (see below) to skip init_upload and resume a prior session
abort()() => Promise<void>Cancels an in-flight upload and best-effort aborts the backend session; safe to call from any state

uploadFile

Convenience wrapper that constructs an Uploader and immediately starts it:

import { uploadFile } from "@vef-framework-react/core";

const result = await uploadFile(http, file, { onProgress: p => console.log(p.percent) });

Use the Uploader class directly when the UI needs to call abort() or observe status transitions independently of the returned promise.

Resuming an Interrupted Upload

Resume support is opt-in and splits into two concerns: persisting a ResumeRecord locally, and asking the backend which parts already landed before deciding whether to resume.

resolveResumePlan

Computes a ResumePlan for a given file fingerprint: looks up the persisted record, validates it against the wall clock and the backend's live list_parts, and hands the result to a caller-supplied decision handler.

import { LocalStoragePersistence, PrefixFingerprinter, resolveResumePlan } from "@vef-framework-react/core";

const persistence = new LocalStoragePersistence();
const fingerprinter = new PrefixFingerprinter();
const fingerprint = await fingerprinter.fingerprint(file);

const plan = await resolveResumePlan({
fingerprint,
persistence,
ctx: { http, apiPath: "/api", resource: "sys/storage", version: "v1" },
onResumeDetected: async candidate => {
const resume = window.confirm(`Resume uploading ${candidate.record.key}?`);
return resume ? { kind: "resume" } : { kind: "discard" };
}
});

const result = await uploader.start(plan);

Without an onResumeDetected handler, the default decision is { kind: "discard" } — a resume that picks the wrong file is worse than a redundant fresh upload.

ResolveResumeInputs

FieldTypeDescription
fingerprintstringStable identifier for the file, computed by a FileFingerprinter
persistenceResumablePersistenceWhere the resume record is read from / written to
ctxProtocolContext{ http, apiPath, resource, version } — same shape Uploader builds internally
onResumeDetectedResumeDecisionHandlerOptional; defaults to always discarding
signalGenericAbortSignalOptional cancellation signal for the in-flight list_parts / abort calls

ResumePlan

type ResumePlan =
| { kind: "fresh" }
| {
kind: "resume";
claimId: string;
key: string;
partSize: number;
partCount: number;
expiresAt: string;
completedParts: readonly ListedPart[];
};

ResumeCandidate and ResumeDecision

TypeShapeDescription
ResumeCandidate{ record: ResumeRecord; completedParts: ListedPart[] }Passed to onResumeDetected; completedParts is the live list_parts result, not the persisted record
ResumeDecision{ kind: "resume" } | { kind: "discard" }The caller's choice
ResumeDecisionHandler(candidate: ResumeCandidate) => Promise<ResumeDecision>The handler type

Persistence

ResumablePersistence

Pluggable storage for ResumeRecord values.

interface ResumablePersistence {
load: (fingerprint: string) => Promise<ResumeRecord | null>;
save: (record: ResumeRecord) => Promise<void>;
remove: (fingerprint: string) => Promise<void>;
}

LocalStoragePersistence

The default implementation, backed by window.localStorage.

import { LocalStoragePersistence } from "@vef-framework-react/core";

const persistence = new LocalStoragePersistence(); // keys namespaced under "__VEF_UPLOAD_RESUME__"
MemberTypeDescription
constructor(keyPrefix?: string)Namespace prefix for stored keys (default "__VEF_UPLOAD_RESUME__")
clearAll()() => Promise<void>Drops every record this instance owns (matched by its key prefix); call on logout / account switch

ResumeRecord

FieldTypeDescription
fingerprintstringLookup key, computed by a FileFingerprinter
claimIdstringServer-assigned claim ID
keystringObject key the backend assigned
partSizenumberBackend-authoritative part size
partCountnumberTotal number of parts planned
expiresAtstringISO-8601 expiry
savedAtnumberDate.now() when the record was last written

Fingerprinting

FileFingerprinter

interface FileFingerprinter {
fingerprint: (file: File) => Promise<string>;
}
ImplementationBasisNotes
WeakFingerprintername:size:lastModifiedSynchronous, always available; collides if content changes without touching name/size/mtime
PrefixFingerprinterWeakFingerprinter's fields plus the SHA-256 of the file's first 4 MiB (window size overridable via the constructor's prefixBytes param)Requires crypto.subtle; throws synchronously if unavailable — fall back to WeakFingerprinter in that case

Constants and Errors

ExportValueDescription
STORAGE_API_PATH"/api"Default apiPath
STORAGE_RESOURCE"sys/storage"Default resource
STORAGE_VERSION"v1"Default version
PUBLIC_PREFIX"pub/"Object-key prefix for anonymous-readable objects
PRIVATE_PREFIX"priv/"Object-key prefix for authenticated-read objects
ErrorExtendsThrown when
UploadErrorErrorBase class for every storage error; instanceof UploadError discriminates uploader failures
UploadAbortedErrorUploadErrorThe caller (or an external signal) cancels an in-flight upload
UploadProtocolErrorUploadErrorA protocol RPC (init_upload / complete_upload / abort_upload / list_parts) fails unrecoverably; carries action
UploadPartErrorUploadErrorA single part fails past maxPartRetries; carries partNumber and attempts

Type Exports

TypeDescription
UploaderOptionsConstructor options for Uploader
UploadInitInit-time filename/contentType/public overrides
UploadProgressAggregated progress snapshot
UploadStatusLifecycle status union
UploadResultSuccessful-upload result
UploadSessionSnapshot{ claimId, key, partSize, partCount, expiresAt } passed to onSessionOpened
ResumablePersistencePersistence interface
ResumeRecordOne persisted resume record
FileFingerprinterFingerprinting interface
ResolveResumeInputsInputs to resolveResumePlan
ResumeCandidate{ record, completedParts } passed to onResumeDetected
ResumeDecision{ kind: "resume" } | { kind: "discard" }
ResumeDecisionHandler(candidate: ResumeCandidate) => Promise<ResumeDecision>
ResumePlan{ kind: "fresh" } or the full resume-session shape

Also exported: the lower-level protocol functions (initUpload, uploadPart, listParts, completeUpload, abortUpload) and their request/response types (InitUploadParams, InitUploadResponse, UploadPartResponse, ListedPart, ListPartsResponse, CompleteUploadResponse, ObjectInfo, ProtocolContext) for callers building a custom upload flow on the same backend protocol.