Skip to content

Streaming BlobAdapter methods: the byte-array contract forces full buffering of every attachment (deferred until triggers hit) #72

Description

@cuibonobo

Deferred design note from the server design review (haverstack/server#46 is the bounded-buffer symptom fix; this is the contract-level cause). Filed to be on record with explicit wake-up triggers — not proposed for the current implementation phase.

Problem

The blob contract speaks whole byte arrays:

putAttachment(data: Uint8Array): Promise<FileId>;
getAttachment(fileId: FileId): Promise<Uint8Array>;

The wire is not the problem — POST /attachments takes a raw binary body and downloads return one; both are streaming-shaped at the HTTP level. It's this contract in the middle that forces every layer above it to materialize the full payload: the server buffers uploads to call putAttachment and buffers downloads because it receives a complete Uint8Array (whose byteLength is also where Content-Length and the metadata record's size come from). Same shape propagates through Stack/ScopedStack.putAttachment and adapter-api.

Consequences at scale: a served stack's memory floor is roughly concurrent transfers × payload size × ~2. Tolerable at the 50 MB default ceiling; not tolerable the moment the ceiling rises for real media — and self-hosting on small boxes is this project's deployment story. Whole-buffer download also forecloses HTTP Range (Accept-Ranges/206), which is what seekable video/audio playback needs.

Sketch (additive, capability-flagged)

putAttachmentStream(data: ReadableStream<Uint8Array>): Promise<{ fileId: FileId; size: number }>;
getAttachmentStream(fileId: FileId, opts?: { offset?: number; length?: number }): Promise<{ stream: ReadableStream<Uint8Array>; size: number }>;

Why deferred, and what wakes it

Unlike the wire-contract issues, waiting here bakes in no breakage — the change stays additive whenever wanted, and the wire is already the right shape. Wake-up triggers, any one sufficient:

  1. Raising the attachment ceiling past ~100 MB (or a real media use case: video playback wanting Range)
  2. Memory-constrained self-hosting reports
  3. A browser/OPFS blob adapter getting scheduled (forces the incremental-hash decision anyway)

One cheap thing worth doing now: haverstack/server#45's upload-gating fix will likely add a scoped bytes-entry point to core — give that new signature room to accept Uint8Array | ReadableStream<Uint8Array> later without another round of churn.

Work items (when woken)

Refs #45 (atomic-rename idiom), #46 (adapter packaging), #64 (capability-flagged adapter addition precedent), #56 (maxAttachmentBytes discovery — enforceable mid-stream once this lands), #57 (additive-in-place policy), haverstack/server#46 (symptom fix), haverstack/server#45 (signature coordination).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions