Skip to content

Latest commit

 

History

History
794 lines (673 loc) · 33.6 KB

EXAMPLES.md

File metadata and controls

794 lines (673 loc) · 33.6 KB

Examples

Recipes beyond the README's Quick Start. Every serving handler manages the full HTTP protocol (200, 206, 304, 412, 416, HEAD) automatically; the upload handlers manage the full resumable-upload protocol (creation, offset probes, appends, termination, expiry) the same way.

Hono

import { Hono } from "hono";
import { S3Client } from "@aws-sdk/client-s3";
import { serveObject } from "partial-content/hono";
import { s3Store } from "partial-content/s3";

const client = new S3Client({ region: "eu-central-1" });
const store = s3Store({ client, bucket: "media" });
const app = new Hono();

app.get("/media/:key", serveObject(store, {
  key: (c) => c.req.param("key"),
  cacheControl: "public, max-age=31536000, immutable",
}));

Cloudflare Workers (R2 native, no AWS SDK)

import { Hono } from "hono";
import { serveObject } from "partial-content/hono";
import { r2Store } from "partial-content/r2";

const app = new Hono<{ Bindings: { MY_BUCKET: R2Bucket } }>();

app.get("/files/:key", (c) => {
  const store = r2Store({ bucket: c.env.MY_BUCKET });
  return serveObject(store, { key: (c) => c.req.param("key") })(c);
});

Raw http.createServer (no framework)

The Node adapter takes (IncomingMessage, ServerResponse), which is the raw http server's own callback signature, so there is nothing to bridge:

import { createServer } from "node:http";
import { serveObject } from "partial-content/node";
import { fsStore } from "partial-content/fs";
import { lookupMime } from "partial-content/mime";

const store = fsStore({ root: "/var/data/uploads" });

// Everything below /files/ is one storage key. Percent-decoding is yours to
// do (the URL carries `%20`, the storage key does not); the store rejects
// traversal, NUL bytes, and Windows device names on top of whatever you pass.
const keyOf = (req: { url?: string | undefined }) =>
  decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname.slice("/files/".length));

const files = serveObject(store, {
  key: keyOf,
  // Without a mime the response is application/octet-stream, which no browser
  // renders inline: a PDF or video downloads instead of playing.
  mime: (req) => lookupMime(keyOf(req)),
  disposition: "inline",
  // 206 range serving, 304/412 conditionals, and HEAD are all handled here;
  // anything that is not GET/HEAD/OPTIONS is answered 405.
});

createServer((req, res) => {
  if (req.url?.startsWith("/files/")) {
    void files(req, res);
    return;
  }
  res.writeHead(404).end();
}).listen(3000);

serveObject never throws at the caller: storage failures are mapped to the truthful status (404, 502, 503 with Retry-After) and reported to onError, so the returned promise settling is the only signal the request is done.

Kernel only: the evaluation flow

Request                        evaluateConditionalRequest()
  │                                       │
  │  If-Match / If-Unmodified-Since       ├──► 412 Precondition Failed
  │  If-None-Match / If-Modified-Since    ├──► 304 Not Modified
  │  If-Range + Range                     ├──► 416 Range Not Satisfiable
  │                                       ├──► 206 Partial Content
  │                                       └──► 200 OK
  │
  ▼
Fetch bytes from storage (you control this)
  │
  ▼
new Response(body, { status, headers })

Node.js / Express (kernel only)

// doc-check: fragment
import { fromNodeHeaders, evaluateConditionalRequest } from "partial-content";

app.get("/files/:key", (req, res) => {
  const headers = fromNodeHeaders(req.headers);
  const { status, headers: resHeaders, range } = evaluateConditionalRequest(
    headers,
    { totalSize: fileSize, etag, lastModified, contentType },
  );
  res.writeHead(status, resHeaders);
  // ...
});

Content-Disposition

import { buildContentDisposition } from "partial-content";

buildContentDisposition("report.pdf");
// => 'attachment; filename=report.pdf'

buildContentDisposition("Årlig_Rapport.pdf");
// => 'attachment; filename="?rlig_Rapport.pdf"; filename*=UTF-8''%C3%85rlig_Rapport.pdf'

buildContentDisposition("slides.pdf", { type: "inline" });
// => 'inline; filename=slides.pdf'

// Handles untrusted input safely
buildContentDisposition("../../etc/passwd");        // Path traversal stripped
buildContentDisposition("evil\r\nX-Injected: yes"); // CRLF injection stripped
buildContentDisposition(null, { fallback: "export.csv" }); // Graceful fallback

RFC 9530 Repr-Digest (end-to-end integrity)

Pass a SHA-256 digest from your storage backend for automatic Repr-Digest headers:

import { evaluateConditionalRequest } from "partial-content";

declare const request: Request;
declare const fileSize: number;

const { status, headers, range } = evaluateConditionalRequest(
  request.headers,
  {
    totalSize: fileSize,
    etag: '"abc123"',
    // S3: x-amz-checksum-sha256; R2: checksums.sha256. GCS has no native
    // SHA-256 (x-goog-hash is crc32c/md5 only): store your own hash as
    // custom metadata and use gcsStore's digestMetadataKey.
    digest: "d2VsY29tZQ==",  // raw base64 SHA-256
  },
);
// Response headers include: Repr-Digest: sha-256=:d2VsY29tZQ==:
// Same digest on both 200 (full) and 206 (partial) -- covers the full representation

Advanced: manual primitives

For full control over the evaluation chain:

import {
  parseRangeHeader,
  buildRangeResponseHeaders,
  isConditionalFresh,
  isPreconditionFailure,
  isRangeFresh,
  build304Headers,
  build412Headers,
  build416Headers,
  type ParsedRange,
} from "partial-content";

// The request's headers, the metadata you already hold for the object, and
// your own read, which `range` tells how many bytes to return.
declare const reqHeaders: Headers;
declare const fileSize: number;
declare const etag: string;
declare const lastModified: string;
declare const contentType: string;
declare const checksum: string;   // raw base64 SHA-256
declare const readBytes: (range: ParsedRange | null) => Promise<ReadableStream<Uint8Array<ArrayBuffer>>>;

export async function respond(): Promise<Response> {
  // Step 1: Preconditions (If-Match / If-Unmodified-Since)
  if (isPreconditionFailure(reqHeaders, etag, lastModified)) {
    return new Response(null, build412Headers());
  }

  // Step 2: Freshness (If-None-Match / If-Modified-Since)
  if (isConditionalFresh(reqHeaders, etag, lastModified)) {
    return new Response(null, build304Headers(etag, lastModified));
  }

  // Step 3: Range (If-Range + Range header)
  const range = isRangeFresh(reqHeaders, etag, lastModified)
    ? parseRangeHeader(reqHeaders.get("range"), fileSize)
    : null;

  if (range === "unsatisfiable") {
    return new Response(null, build416Headers(fileSize));
  }

  const { status, headers } = buildRangeResponseHeaders({
    totalSize: fileSize, range, contentType, etag, lastModified,
    digest: checksum,          // RFC 9530 Repr-Digest
    cacheControl: "private, no-cache",
  });
  return new Response(await readBytes(range), { status, headers });
}

Write-side OCC (evaluateConditionalWrite)

Optimistic concurrency for PUT/PATCH/DELETE: the client echoes the ETag it last saw via If-Match, and a mismatch means someone else wrote first.

import { evaluateConditionalWrite } from "partial-content";

// Your data layer, and the document id this route resolved.
declare const db: {
  getDocumentMeta(id: string): Promise<{ etag: string; updatedAt: string } | null>;
  saveDocument(id: string, body: unknown): Promise<void>;
};
declare const id: string;

export async function PUT(req: Request) {
  const current = await db.getDocumentMeta(id); // { etag, updatedAt } or null

  const verdict = evaluateConditionalWrite(req.headers, {
    etag: current?.etag,               // strong validator required for If-Match
    lastModified: current?.updatedAt,
    exists: current !== null,          // enables If-None-Match: * (create-only)
  });
  if (!verdict.proceed) {
    // 412; headers include the CURRENT ETag so the client can resync
    return new Response(null, { status: verdict.status, headers: verdict.headers });
  }

  await db.saveDocument(id, await req.json());
  return new Response(null, { status: 204 });
}

If-None-Match: * makes the write create-only (fails 412 if the resource exists); exists must be passed explicitly for that pattern, or the kernel throws rather than guess and overwrite.

Multi-range (multipart/byteranges)

Nothing to wire: serveObject serves Range: bytes=0-99,5000-5099 as a multipart/byteranges 206 with an exact precomputed Content-Length, parts in request order, gap/overlap coalescing, and the maxRanges amplification cap. Up to maxBackendReads parts each cost one ranged getObject, so tune both to your backend:

import { serveObject } from "partial-content/web";
import type { ObjectStore } from "partial-content";

declare const store: ObjectStore;

const handler = serveObject(store, {
  maxRanges: 5,         // more than 5 coalesced ranges serve the full 200 (default 8)
  maxBackendReads: 2,   // parts past 2 are cut from one buffered span read when
                        // the span is under 256 KiB (default 8)
});
// maxRanges: 1 disables multipart entirely: multi-range requests degrade to
// a full 200 and every request costs at most one backend read.

Kernel-only consumers: parseRanges() + buildMultipartHeaders() + buildMultipartPartHeader() / multipartEpilogue() are the framing primitives (see Advanced: manual primitives).

Digest negotiation (Want-Repr-Digest / Want-Content-Digest)

A client that wants integrity metadata asks for it; one that cannot use it declines. Both fields negotiate independently:

GET /file HTTP/1.1
Want-Repr-Digest: sha-256=5        -> Repr-Digest: sha-256=:<base64>:
Want-Repr-Digest: sha-256=0        -> no digest headers at all
Want-Content-Digest: sha-256=0     -> Repr-Digest only, no Content-Digest
(no Want-* header)                 -> both emitted on a full 200 GET

The negotiation is exported for kernel-only consumers:

import { clientWantsDigest, clientWantsContentDigest } from "partial-content";

// The request, plus the metadata your storage backend reported.
declare const req: Request;
declare const totalSize: number;
declare const etag: string;
declare const lastModified: string;
declare const storedSha256: string;   // raw base64 SHA-256

const meta = {
  totalSize, etag, lastModified,
  digest: clientWantsDigest(req.headers) ? storedSha256 : undefined,
  contentDigest: clientWantsContentDigest(req.headers),
};

Precompressed variants (brotli/zstd/gzip siblings)

Upload encoded siblings at build or ingest time, then let negotiation pick:

# alongside dist/app.js, produce dist/app.js.br and dist/app.js.gz
brotli -k dist/app.js && gzip -k dist/app.js
import { serveObject } from "partial-content/web";
import { s3Store, type S3StoreOptions } from "partial-content/s3";

// Your configured S3 client (`new S3Client({ region: "eu-central-1" })`).
declare const client: S3StoreOptions["client"];

const handler = serveObject(s3Store({ client, bucket: "assets" }), {
  precompressed: true, // probes <key>.br, <key>.zst, <key>.gz
  cacheControl: "public, max-age=31536000, immutable, no-transform",
});
// A browser sending `Accept-Encoding: gzip, br` gets app.js.br with
// Content-Encoding: br, Vary: Accept-Encoding, the .br object's ETag,
// and Range support over the encoded bytes.

Split proxy/redirect on one route (preferSignedUrl)

Proxy the requests where the protocol layer earns its keep (ranges, revalidations), offload bulk full-file egress to the bucket:

import { serveObject } from "partial-content/web";
import type { ObjectStore } from "partial-content";

// A store whose backend can mint signed URLs (S3, R2, GCS, Azure).
declare const store: ObjectStore;

const handler = serveObject(store, {
  preferSignedUrl: ({ isRange, isConditional }) => !isRange && !isConditional,
  signedUrlExpiresSeconds: 120,
  cacheControl: "private, no-cache", // rides the signed response too
});

Resumable uploads: a tus 1.0 endpoint

Upload and serving are two sides of the same storage: point an upload store and a read store at the same root/bucket/map and a completed upload is immediately servable with ranges, conditionals, and Repr-Digest.

That shared location also puts the upload store's own bookkeeping inside the read store's namespace: the in-flight bytes of an upload nobody has published yet, and the sidecar naming the key it is destined for, which commonly embeds a filename. An upload token is the only handle meant to reach any of that, and a read handler holds no token. So fsStore, s3Store, r2Store, gcsStore and azureStore refuse keys at or under a reservedPrefix with the same ObjectNotFoundError a missing object gets, on headObject, getObject and createSignedUrl. It defaults to the sibling upload store's own prefix (.uploads/ for fs and s3, .partial-content-uploads/ for r2, gcs and azure), so the safe posture is what you get without asking for it. Set reservedPrefix: "" to serve the whole namespace when no upload store shares the location, or name your own when you changed uploadPrefix. memoryStore needs none of this: its in-flight uploads live in a separate map, never in objects.

import { createTusHandler } from "partial-content/tus";
import { serveObject } from "partial-content/web";
import { fsStore, fsUploadStore } from "partial-content/fs";

// Wherever you log.
declare const logger: { info(fields: object, msg: string): void; error(fields: object, msg: string): void };

const root = "/var/data/files";
const uploads = createTusHandler(fsUploadStore({ root }), {
  // The server decides the key; the client filename stays metadata.
  key: () => crypto.randomUUID(),
  location: (token) => `/files/upload/${token}`,
  resolveToken: (req) => new URL(req.url).pathname.split("/").pop() || undefined,
  policy: { maxSize: 1024 * 1024 * 1024, maxAgeSeconds: 24 * 60 * 60 },
  onUploadEvent: ({ uploadToken, event }) => logger.info({ uploadToken, ...event }, "upload"),
  onError: (err, ctx) => logger.error({ err, ...ctx }, "upload.error"),
});
const downloads = serveObject(fsStore({ root }), { disposition: "attachment" });

// Mount: POST /files/upload creates; HEAD/PATCH/DELETE /files/upload/<token>
// probe, append, and terminate; OPTIONS discovers capabilities. Authorize
// BEFORE calling the handler, exactly like the serving side.

On a raw Node server the dialect handlers are still Fetch handlers, so the route converts with toWebRequest and writes back with sendResponse, and everything between the two (routing, the session check) stays yours:

import { createServer } from "node:http";
import { toWebRequest, sendResponse } from "partial-content/node";
import { createTusHandler } from "partial-content/tus";
import { fsUploadStore } from "partial-content/fs";

// Your session check.
declare function authorized(req: Request): Promise<boolean>;

const uploads = createTusHandler(fsUploadStore({ root: "/var/data/files" }), {
  key: () => crypto.randomUUID(),
  location: (token) => `/files/upload/${token}`,
  resolveToken: (req) => new URL(req.url).pathname.split("/")[3] || undefined,
  policy: { maxSize: 1024 * 1024 * 1024 },
});

createServer(async (req, res) => {
  if (!req.url?.startsWith("/files/upload")) {
    res.writeHead(404).end();
    return;
  }
  // The configured origin, not the client's Host header, is what the
  // `Location` on a 201 is built from.
  const request = toWebRequest(req, { origin: "https://files.example.com" });
  const response = (await authorized(request)) ? await uploads(request) : new Response(null, { status: 401 });
  await sendResponse(res, response);
}).listen(3000);

A chunked PATCH with no Content-Length (the ordinary tus shape for a stream of unknown length) arrives as a Request with no length rather than a fabricated one. A sender that opens a body and then goes quiet is torn down after readStallTimeoutMs (60 s), because a Request body is read outside the server's own requestTimeout. The bridge never answers Expect: 100-continue itself; see onExpectContinue in docs/API.md.

Browser clients: any tus 1.0 client works unmodified. Give it the creation URL as its endpoint; the Location header on the 201 is the upload resource every follow-up request (offset probe, append, termination) targets, and the client resumes an interrupted transfer by re-probing that URL with HEAD. The handler advertises creation, creation-with-upload, creation-defer-length, termination, and expiration, so clients that use those extensions (unknown-length streams, upload cancellation, expiry display) negotiate them automatically. Cross-origin uploaders additionally need the tus headers exposed via extraHeaders (e.g. Access-Control-Expose-Headers: Location, Upload-Offset, Upload-Length, Upload-Expires, Tus-Resumable) plus your CORS layer's Access-Control-Allow-* response.

Housekeeping: expired resources refuse probes and appends on their own (maxAgeSeconds), but a client that never comes back to DELETE them leaves their storage to be reclaimed by the store's sweep hook. Run it on a schedule with the same cutoff:

import { fsUploadStore } from "partial-content/fs";

const root = "/var/data/files";
const store = fsUploadStore({ root });

// e.g. hourly. The two cutoffs are different clocks and at least one is
// required, because a sweep with neither reclaims nothing while looking like
// a retention control.
const MAX_AGE_SECONDS = 24 * 60 * 60;
const now = Date.now();
await store.sweepExpired({
  // Abandoned transfers: anchored on the last append, falling back to
  // creation. Keep it comfortably longer than your longest single append,
  // since the sweep holds no lock and an append in flight has not moved
  // lastAppendAt yet, so a short cutoff can reap a resource being written to.
  idleSinceMs: now - 4 * 60 * 60 * 1000,
  // The lifetime every request already refuses the resource for. Derived from
  // the same policy value, so a resource becomes reclaimable at the moment it
  // stops being usable rather than lingering until it also goes idle.
  createdBeforeMs: now - MAX_AGE_SECONDS * 1000,
});

Resumable uploads: a custom ResumableWriteStore

Any backend that can append durably and publish atomically can host resumable uploads. Implement the write contract; the object you pass to createTusHandler/createUploadHandler is checked structurally, and the error classes are matched by name, so a custom store can throw equivalently-named errors without importing them (or import them from a built-in store subpath such as partial-content/memory). Every store subpath also re-exports the contract types (ResumableWriteStore, SweepingWriteStore, StoredUploadState, CreateUploadOptions, AppendChunkOptions, CompleteUploadOptions, CompletedUpload), so the classes and the types you need come from one import.

import {
  UploadNotFoundError,
  UploadOffsetConflictError,
  UploadAppendBoundError,
  UploadOverrunError,
  type ResumableWriteStore,
} from "partial-content/memory";

// Your own storage layer, whatever it is. Spelling it out is what lets the
// compiler check the store below against the contract instead of trusting it.
interface UploadBackend {
  allocate(opts: {
    key: string;
    length?: number;
    metadata?: Record<string, string>;
    createdAt: number;
  }): Promise<string>;
  find(uploadToken: string): Promise<{
    key: string;
    length?: number;
    isComplete: boolean;
    isInvalidated: boolean;
    createdAt: number;
    lastAppendAt?: number;
    metadata?: Record<string, string>;
  } | null>;
  sumDurableBytes(uploadToken: string): Promise<number>;
  recordLength(uploadToken: string, length: number): Promise<void>;
  append(
    uploadToken: string,
    offset: number,
    body: ReadableStream<Uint8Array> | Uint8Array,
    opts: { maxBytes?: number; now: number; signal?: AbortSignal },
  ): Promise<{ bytesWritten: number; crossedBound?: number; interrupted: boolean }>;
  markInvalidated(uploadToken: string): Promise<void>;
  publishAtomically(uploadToken: string): Promise<{ etag?: string; digest?: string }>;
  discard(uploadToken: string): Promise<void>;
  reap(cutoff: {
    idleSinceMs?: number;
    createdBeforeMs?: number;
  }): Promise<{ removed: number; failed: number }>;
}
declare const backend: UploadBackend;

// Annotate the object: the handler checks it structurally at the call site,
// but the annotation makes the compiler check it HERE, against the contract,
// so a missing method or a wrong signature is reported where you can see it.
const myUploadStore: ResumableWriteStore = {
  // ── Capability flags: HONEST, per backend, never aspirational ──
  atomicCompletion: true,      // completeUpload must be all-or-nothing
  enforcesOffsetCheck: true,   // appendChunk refuses a claim that disagrees
                               // with durable state (see below)
  // Both are typed `true`: the orchestrator refuses a store that reports
  // false, so there is no value to write here other than the honest one.
  digestOnComplete: false as const, // e.g. ["sha-256"] ONLY if you verify before publishing
  // Disclosures, not switches. Nothing branches on them; they tell whoever
  // picks this store what they are picking:
  exactOffsetRecovery: true,   // only if the derived offset is crash-durable
  // appendGranularity / uniformPartSize / maxAppendSize: set when the
  // backend forces them; omit for byte-exact appends.

  async createUpload({ key, length, metadata, now }) {
    // Allocate the resource; fold EVERYTHING resumption needs into the
    // token (the built-ins encode key + backend upload id). Nothing
    // upstream ever parses it.
    //
    // The token MUST come from a CSPRNG, with at least the ~122 bits a v4
    // UUID carries. Possession of it IS the authorization: the upload is
    // reachable by URL for minutes or days after the request that created
    // it, and nothing in either protocol re-authenticates the follow-ups, so
    // whoever holds the token can append to, probe and cancel that upload.
    // Building one from the key, a counter or a timestamp turns "authorized
    // the creation" into "authorized anyone who can guess a neighbour".
    const uploadToken = await backend.allocate({ key, length, metadata, createdAt: now });
    return { uploadToken };
  },

  async getUploadState(uploadToken) {
    const record = await backend.find(uploadToken);
    if (!record) throw new UploadNotFoundError(uploadToken);
    return {
      // The key this upload publishes to, as given at creation. You already
      // persist it in order to publish at all, and reporting it is what lets
      // a caller pair an upload with the object it became: the key is chosen
      // before a token exists, so nothing else can make that pairing without
      // racing. It is the key `onUploadComplete` carries.
      key: record.key,
      // THE rule: derive the offset from the backend's own durable
      // bookkeeping (part listing, block list, fsynced size). Never
      // persist an offset counter next to the data: the two cannot be
      // written atomically, and their post-crash drift is the corruption
      // resumable uploads exist to prevent.
      offset: await backend.sumDurableBytes(uploadToken),
      length: record.length,            // immutable once known
      // One lifecycle field; "invalidated" is terminal and recorded durably.
      status: record.isInvalidated ? "invalidated" : record.isComplete ? "complete" : "incomplete",
      createdAt: record.createdAt,
      lastAppendAt: record.lastAppendAt,
      metadata: record.metadata,
    };
  },

  async appendChunk(uploadToken, offset, body, { maxBytes, maxBytesFatal, length, now, signal }) {
    // The preflight the `enforcesOffsetCheck: true` above promises. Required,
    // not defensive: the lock serializes writers but cannot prove the writer
    // still holds it, and an expiry-based distributed lock can be stolen from
    // a holder that stalls past its TTL and resumes. Re-deriving durable state
    // here is what makes that holder's next append loud instead of silently
    // interleaved. A dead or already-published resource refuses every append.
    const record = await backend.find(uploadToken);
    if (!record) throw new UploadNotFoundError(uploadToken);
    const durableOffset = await backend.sumDurableBytes(uploadToken);
    if (record.isComplete || record.isInvalidated || offset !== durableOffset) {
      throw new UploadOffsetConflictError(uploadToken, durableOffset);
    }
    // Deferred-length flow: the first append to carry a length persists it, so
    // the next getUploadState reports it and it turns immutable. It only ever
    // arrives once, and only while the resource has no length.
    if (length !== undefined && record.length === undefined) {
      await backend.recordLength(uploadToken, length);
    }

    // Only you see the stream, so only you can hold it to `maxBytes`. Honor
    // `signal` at every chunk boundary inside the write, flushing what you
    // have: the orchestrator's grace window is already built into it, and the
    // preflight above is not a transaction, so the signal is what stops a
    // holder that stalled MID-call and came back. `now` is recorded as
    // lastAppendAt. Report the bytes made DURABLE (the next getUploadState
    // must agree) plus the bound the body tried to cross, if any.
    const { bytesWritten, crossedBound, interrupted } = await backend.append(
      uploadToken, offset, body, { maxBytes, now, signal },
    );

    // `maxBytesFatal` decides what crossing the bound means, and the two cases
    // are opposites. Past a REPRESENTATION bound (room toward a known length,
    // or the policy's total maxSize) the upload can never be completed
    // coherently, so the death is recorded durably before it is reported:
    // every later probe and append must refuse.
    if (crossedBound !== undefined && maxBytesFatal) {
      await backend.markInvalidated(uploadToken);
      throw new UploadOverrunError(uploadToken, crossedBound);
    }
    // This REQUEST's ceiling instead. The prefix just written stays durable
    // and the resource stays healthy, so the caller answers 413 and the client
    // resumes from the truthful offset in smaller chunks.
    if (crossedBound !== undefined) {
      throw new UploadAppendBoundError(uploadToken, offset + bytesWritten);
    }
    // Report whether the write stopped before the body ran out. Only the code
    // draining the body knows, so it is a fact the store returns rather than
    // one the caller infers: an abort signal says something asked the write to
    // yield, never what happened to the bytes.
    return { bytesWritten, interrupted };
  },

  async completeUpload(uploadToken, { expectedDigests, now }) {
    // Publish atomically: after success the object is readable under its
    // key; after ANY failure nothing new is visible to readers. If you
    // listed algorithms in digestOnComplete, verify expectedDigests against
    // the assembled bytes BEFORE publishing and throw an
    // UploadDigestMismatchError-named error on mismatch.
    const { etag, digest } = await backend.publishAtomically(uploadToken);
    return { etag, digest };
  },

  async abortUpload(uploadToken) {
    // Idempotent (already-gone is fine), and it must discard only the upload
    // resource: an object this upload already published is live data, so a
    // replayed DELETE must not be able to destroy it.
    await backend.discard(uploadToken);
  },

  async sweepExpired({ idleSinceMs, createdBeforeMs }) {
    // Optional, and it takes two independent clocks because a resource can be
    // past its lifetime while still looking busy: honor whichever the caller
    // gave, reaping a resource that matches either.
    // Reclaim the BYTES, not just the record: do what abortUpload does to
    // each resource. A metadata-only sweep strands the data with nothing
    // left pointing at it, which is a storage-limitation gap, not a cleanup.
    // Runs without the upload's lock (a store has no locker), so the caller's
    // idle cutoff must exceed the longest single append the deployment permits.
    // Isolate each resource and report what could not be reclaimed as
    // `failed`: one resource your backend refuses to delete must not end the
    // scan, or a single busy upload stops a scheduled sweep from reclaiming
    // anything and it reports success by not throwing. What stays is retried
    // by the next pass; `failed` is what the caller alerts on.
    return backend.reap({ idleSinceMs, createdBeforeMs });
  },
};

The dialect handlers do not call these methods concurrently for one upload resource: the orchestrator serializes every interaction (probes included) under a cooperative-preemption lock and re-derives fresh state before each decision. Your store's job is durability and honesty, not coordination -- just remember the default lock is in-process, so multi-instance deployments supply a shared locker: redisUploadLocker from partial-content/redis-locker for any Redis-protocol server, or your own (see API.md, Locking).

Proving a custom write store meets the contract

Two of those capability flags are checked at construction, and both are CLAIMS. createUploadOrchestrator can see that a store declares enforcesOffsetCheck: true; it cannot see whether appendChunk actually refuses a stale offset, and a store that declares the check and skips it presents as working right up to the moment a lock is stolen from a holder that stalled past its TTL.

partial-content/conformance runs the obligations as assertions instead. It is not a test file and pulls in no test framework, so the same call works as a CI step, a startup self-test against a staging bucket, or a one-off script:

import { checkResumableWriteStore, formatConformanceReport } from "partial-content/conformance";
import { memoryUploadStore } from "partial-content/memory";

// Your store's factory. Two obligations are only observable across
// instances (an offset held in process memory looks identical to a
// backend-derived one until a second instance reads the same upload), so
// the harness takes a factory and calls it more than once.
const objects = {};
const report = await checkResumableWriteStore(
  () => memoryUploadStore({ objects }),
  {
    // Disposable keys: the harness publishes to some and aborts others.
    key: (n) => `conformance/${n}`,
    // memoryUploadStore holds its in-flight uploads per instance, so a
    // fresh one is genuinely a different store. Saying so SKIPS the
    // cross-instance checks; it does not pass them, and the report says
    // "SKIP" rather than counting an unverified guarantee as green.
    // A store backed by a filesystem or an object store leaves this alone.
    restartable: false,
  },
);

console.log(formatConformanceReport(report));
if (!report.ok) {
  throw new Error(`store failed ${report.failed} of ${report.checks.length} conformance checks`);
}

formatConformanceReport returns the text and prints nothing, so the sink is yours. Each failure names the obligation and explains the consequence rather than the assertion, because the author reading it believes the store is correct: a skipped offset check reports "declared but not implemented, so a stalled writer interleaves silently", not "expected throw". Check names are stable, so a store with a known gap can allow-list one by name in its own CI instead of ignoring the whole report.

Proving a custom ObjectStore meets the read contract

The serving path asks a read store for two things a happy-path smoke test never looks at. A partial read must report the whole representation's size, because that number becomes the Content-Range total the client sizes its remaining requests against, and an absent key must fail in a way the adapter can tell from a backend outage, or a missing object is answered as a 502 that a caching layer retries forever. A store can serve every byte correctly and still get both wrong.

checkObjectStore asserts them against an object the store already serves. It only reads: it never creates and never deletes, since an ObjectStore has no vocabulary for either and a read store may front something immutable, so the same call is safe against a staging bucket:

import { checkObjectStore, formatConformanceReport } from "partial-content/conformance";
import { memoryStore } from "partial-content/memory";

// A key the store already serves, and that object's exact bytes. Every
// assertion is measured against them, so they are the object's content and
// not a fixture the harness uploads first.
const content = new TextEncoder().encode("conformance-read-side-sample-bytes");
const store = memoryStore({ objects: { "docs/sample.bin": { body: content } } });

const report = await checkObjectStore(store, {
  key: "docs/sample.bin",
  content,
  // Optional: a key the store must NOT serve. The default is the served key
  // plus a suffix, so override it only where that could collide with a real
  // object.
  missingKey: "docs/sample.bin.absent",
});

console.log(formatConformanceReport(report));
if (!report.ok) {
  throw new Error(`store failed ${report.failed} of ${report.checks.length} read checks`);
}

A store instance is passed rather than a factory, because nothing in the read contract is observed across instances. What the write side expresses through restartable, the read side expresses through the store's own declaration: a store with supportsRange: false has the two range checks reported "SKIP" rather than "PASS", since the serving path never asks it for a window, and so does an object too short to slice a meaningful window from. An unverified obligation reads as unverified either way.