import type { Result } from "../../result.ts"; import type { FileByteSource } from "../browser-file-storage/file.ts"; import type { BrowserDataFailure, BrowserDataResult, TransferProgress, } from "../browser-file-storage/shared.ts"; export const RESUMABLE_UPLOAD_PROTOCOL = "PRESIGNED_MULTIPART_V1" as const; export type ResumableUploadProtocol = typeof RESUMABLE_UPLOAD_PROTOCOL; /** * Multipart uploads use a bounded part manifest instead of a whole-file * ArrayBuffer. The digest is SHA-256 over the canonical ordered part metadata * and SHA-256 part digests. */ export type UploadFileFingerprint = Readonly<{ algorithm: "SHA-256-PARTS-V1"; digestHex: string; byteLength: number; partSizeBytes: number; partCount: number; }>; export type UploadPartDescriptor = Readonly<{ partNumber: number; offset: number; byteLength: number; checksumSha256: string; }>; /** * A non-authorizing server acknowledgement. It must be opaque, contain no PII, * URL or credential, and be accepted only with the exact descriptor binding. */ export type UploadPartReceipt = UploadPartDescriptor & Readonly<{ receiptToken: string; }>; export type PartitionDeleteOutcome = | Readonly<{ state: "DELETED"; effect: "APPLIED" }> | Readonly<{ state: "PENDING"; effect: "UNKNOWN"; reason: "BLOCKED_DEADLINE"; }>; export interface UploadRangeReader { readonly byteLength: number; readRange(input: Readonly<{ offset: number; length: number; signal: AbortSignal; }>): Promise>; } /** * FILE_BYTE_SOURCE supports existing FileByteSource implementations. It must * be replayable for the fingerprint pass and transfer pass. RANGE_READER is * preferred for concurrent uploads and OPFS/file-vault range adapters. */ export type ResumableUploadSource = | Readonly<{ kind: "FILE_BYTE_SOURCE"; bytes: FileByteSource; }> | Readonly<{ kind: "RANGE_READER"; reader: UploadRangeReader; }>; export type ResumableUploadRequest = Readonly<{ /** Opaque, caller-stable operation key. It must not contain a file name. */ uploadKey: string; /** Registry-approved backend purpose identifier, not user-provided text. */ purpose: string; mediaType: string; source: ResumableUploadSource; signal: AbortSignal; onProgress?: (progress: TransferProgress) => void; }>; export type QuarantinedUpload = Readonly<{ state: "QUARANTINED"; resourceId: string; byteLength: number; replayed: boolean; }>; export type UploadAbortOutcome = Readonly<{ state: "ABORTED" | "ORPHANED" | "ALREADY_COMPLETED" | "NOT_FOUND"; }>; export interface ResumableUploadPort { upload( request: ResumableUploadRequest, ): Promise>; abort(request: Readonly<{ uploadKey: string; signal: AbortSignal; }>): Promise>; } /** * Retry-After is transport metadata used only by the runtime. It is bounded * before sleeping and is removed from the application-facing failure. */ export type UploadProviderFailure = BrowserDataFailure & Readonly<{ retryAfterMs?: number; }>; export type UploadProviderResult = Result< Value, UploadProviderFailure >; export type UploadSession = Readonly<{ protocol: ResumableUploadProtocol; sessionId: string; requestBindingSha256: string; fingerprint: UploadFileFingerprint; partSizeBytes: number; partCount: number; maxConcurrency: number; expiresAtEpochMs: number; }>; export type ActiveUploadStatus = Readonly<{ state: "ACTIVE"; session: UploadSession; acceptedParts: readonly UploadPartReceipt[]; }>; export type UploadSessionStatus = | ActiveUploadStatus | Readonly<{ state: "QUARANTINED"; session: UploadSession; resourceId: string; }> | Readonly<{ state: "ABORTED" | "EXPIRED" | "NOT_FOUND"; protocol: ResumableUploadProtocol; sessionId: string; requestBindingSha256: string; }>; /** * The capability value is intentionally generic. The presigned-transfer * adapter owns its URL/method/header contract; this port neither duplicates * that type nor permits it to enter a durable checkpoint. */ export type UploadPartCapability = Readonly<{ capability: Capability; uploadBindingSha256: string; expiresAtEpochMs: number; }>; export interface ResumableUploadControlPlane { createSession(input: Readonly<{ protocol: ResumableUploadProtocol; uploadKey: string; purpose: string; mediaType: string; requestBindingSha256: string; fingerprint: UploadFileFingerprint; requestedPartSizeBytes: number; requestedMaxConcurrency: number; idempotencyKey: string; signal: AbortSignal; }>): Promise>; getStatus(input: Readonly<{ protocol: ResumableUploadProtocol; sessionId: string; requestBindingSha256: string; fingerprint: UploadFileFingerprint; signal: AbortSignal; }>): Promise>; issuePartCapability(input: Readonly<{ protocol: ResumableUploadProtocol; sessionId: string; requestBindingSha256: string; uploadBindingSha256: string; fingerprint: UploadFileFingerprint; mediaType: string; part: UploadPartDescriptor; idempotencyKey: string; signal: AbortSignal; }>): Promise>>; complete(input: Readonly<{ protocol: ResumableUploadProtocol; sessionId: string; requestBindingSha256: string; fingerprint: UploadFileFingerprint; orderedParts: readonly UploadPartReceipt[]; idempotencyKey: string; signal: AbortSignal; }>): Promise>>; abort(input: Readonly<{ protocol: ResumableUploadProtocol; sessionId: string; requestBindingSha256: string; idempotencyKey: string; signal: AbortSignal; }>): Promise>>; } export interface UploadPartExecutor { uploadPart(input: Readonly<{ protocol: ResumableUploadProtocol; capability: Capability; sessionId: string; requestBindingSha256: string; uploadBindingSha256: string; fingerprint: UploadFileFingerprint; mediaType: string; part: UploadPartDescriptor; bytes: Uint8Array; idempotencyKey: string; signal: AbortSignal; }>): Promise>; } /** * Durable, non-secret recovery state. Implementations must reject any unknown * property so a signed URL, authorization header or user metadata cannot be * smuggled into persistence. */ export type ResumableUploadCheckpoint = Readonly<{ schemaVersion: 1; protocol: ResumableUploadProtocol; revision: number; state: "ACTIVE" | "ABORT_PENDING"; uploadKey: string; requestBindingSha256: string; fingerprint: UploadFileFingerprint; sessionId: string; sessionExpiresAtEpochMs: number; sessionMaxConcurrency: number; acceptedParts: readonly UploadPartReceipt[]; updatedAtEpochMs: number; }>; export interface ResumableUploadCheckpointStore { read( uploadKey: string, signal?: AbortSignal, ): Promise>; compareAndSwap(input: Readonly<{ expectedRevision: number | null; checkpoint: ResumableUploadCheckpoint; signal?: AbortSignal; }>): Promise>; remove(input: Readonly<{ uploadKey: string; expectedRevision: number; signal?: AbortSignal; }>): Promise>; close(): void; } export interface ResumableUploadCheckpointAdmin { /** * Account/logout lifecycle operation for this already-bound opaque partition. * The adapter closes its connection before deletion and bounds blocked waits. */ /** * BT-UP-03. An IndexedDB `deleteDatabase()` request cannot be cancelled once * dispatched, so a blocked deadline is not evidence that nothing happened. * `PENDING` reports the effect honestly as `UNKNOWN`; only pre-dispatch * problems are ordinary failures. */ deletePartition( signal?: AbortSignal, ): Promise>; }