Custom Adapter
Build a custom adapter when you’re uploading to your own backend or a third party service. You implement one interface (UploadAdapter) to handle the upload, and optionally a second (StatusChecker) if your backend needs time to process after upload completes. Everything else is handled for you: queueing, concurrency, retries, and UI state.
Implementing an adapter
Section titled “Implementing an adapter”An adapter is a class that implements UploadAdapter<TOptions>. The generic type parameter lets you define whatever options your backend needs.
interface UploadAdapter<TOptions = Record<string, unknown>> { upload( file: FileRef, options: TOptions, callbacks: { onProgress: (pct: number) => void }, signal: AbortSignal, ): Promise<UploadResult>;}
type UploadResult = { videoId: string; playbackUrl?: string; metadata?: Record<string, unknown>;};Here’s a complete example that requests a pre-signed URL from your backend, then uploads directly to storage:
import type { FileRef, UploadAdapter, UploadResult, WebFileRef,} from "@hyperserve/upload";
type MyUploadOptions = { folderId?: string;};
class MyAdapter implements UploadAdapter<MyUploadOptions> { constructor(private uploadEndpoint: string) {}
async upload( file: FileRef, options: MyUploadOptions, callbacks: { onProgress: (pct: number) => void }, signal: AbortSignal, ): Promise<UploadResult> { const { uploadUrl, videoId } = await fetch(this.uploadEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: file.name, size: file.size, folderId: options.folderId, }), signal, }).then((r) => r.json());
await uploadWithProgress( uploadUrl, (file as WebFileRef).raw, callbacks.onProgress, signal, );
return { videoId }; }}
function uploadWithProgress( url: string, file: File, onProgress: (pct: number) => void, signal: AbortSignal,): Promise<void> { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { onProgress(Math.round((e.loaded / e.total) * 100)); } }; xhr.onload = () => xhr.status < 400 ? resolve() : reject(new Error(`Upload failed: ${xhr.status}`)); xhr.onerror = () => reject(new Error("Upload failed")); signal.addEventListener("abort", () => xhr.abort()); xhr.open("PUT", url); xhr.send(file); });}If your backend returns a playbackUrl in UploadResult, the file transitions directly to "ready" with no status checker needed.
return { videoId, playbackUrl };Adding a status checker
Section titled “Adding a status checker”If your backend needs time to process after upload (transcoding, thumbnail generation, etc.), implement StatusChecker. The library calls it after each successful upload.
interface StatusChecker { checkStatus(options: { uploadResult: UploadResult; onStatusChange: ( status: "processing" | "ready" | "failed", playbackUrl?: string, statusDetail?: string, ) => void; signal: AbortSignal; }): void;}import type { StatusChecker } from "@hyperserve/upload";
class MyStatusChecker implements StatusChecker { constructor(private statusEndpoint: string) {}
checkStatus({ uploadResult, onStatusChange, signal }) { const poll = async () => { while (!signal.aborted) { const res = await fetch( `${this.statusEndpoint}/${uploadResult.videoId}`, { signal }, ).then((r) => r.json());
if (res.status === "ready") { onStatusChange("ready", res.playbackUrl); return; } if (res.status === "failed") { onStatusChange("failed"); return; }
onStatusChange("processing", undefined, res.statusDetail); await new Promise((r) => setTimeout(r, 3000)); } }; poll(); }}statusDetail is optional. Surface it when your backend provides it (for example, "Transcoding 1080p"). The library exposes it as file.statusDetail during processing.
Wiring it together
Section titled “Wiring it together”import type { UploadConfig } from "@hyperserve/upload";
const config: UploadConfig<MyUploadOptions> = { adapter: new MyAdapter("/api/upload-url"), uploadOptions: { folderId: "my-folder" }, statusChecker: new MyStatusChecker("/api/video-status"), maxConcurrentUploads: 2, onFileReady: (file) => { console.log("Ready:", file.videoId, file.playbackUrl); },};The generic on UploadConfig<MyUploadOptions> ensures uploadOptions is type-checked against your adapter.
Driving status from a webhook or SSE
Section titled “Driving status from a webhook or SSE”If you prefer to push status updates rather than poll, skip the StatusChecker entirely and call updateFileStatus from useUpload() when your server sends an event.
const { updateFileStatus } = useUpload();
socket.onmessage = (event) => { const { videoId, status, playbackUrl } = JSON.parse(event.data); updateFileStatus(videoId, status, playbackUrl);};Without a StatusChecker, files stay in "processing" after upload until updateFileStatus is called.