first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
# range
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Encodes HTTP `Range` semantics for `HttpStorage.getPartial`. Builds the request header from `(offset, length)` and interprets the response in the three documented shapes: `206 Partial Content` (happy path — body returned verbatim), `200 OK` (origin ignored the Range — body sliced client-side and the fallback is flagged so the caller can emit a counter), and `416 Range Not Satisfiable` (request beyond EOF — mapped to `STORAGE_HTTP.NOT_FOUND`).
Without this module, every adapter consumer would have to re-implement the same disambiguation. Keeping it isolated also lets tests pin range behaviour without spinning up the full `HttpStorage`.
## Features
- `buildRangeHeader(offset, length)` — validates integer non-negativity / positivity, returns `bytes=<o>-<o+l-1>`.
- `consumeRangeResponse(response, offset, length, url)` — distinguishes 206 / 200 / 416 / other, returns `{ bytes, fellBack }`.
- Programmer-error inputs throw `STORAGE_HTTP.INVALID_OPTIONS` so they surface in development, not as cryptic upstream failures.
## Usage: Basic
```ts
import { buildRangeHeader, consumeRangeResponse } from "@suaveplan/storage-http";
const header = buildRangeHeader(0, 1024); // "bytes=0-1023"
const response = await fetch(url, { headers: { Range: header } });
const { bytes, fellBack } = await consumeRangeResponse(response, 0, 1024, url);
```
## Usage: Advanced
```ts
import { buildRangeHeader, consumeRangeResponse } from "@suaveplan/storage-http";
async function readSlice(url: string, offset: number, length: number) {
const response = await fetch(url, { headers: { Range: buildRangeHeader(offset, length) } });
const { bytes, fellBack } = await consumeRangeResponse(response, offset, length, url);
if (fellBack) operatorLog.warn({ url }, "origin ignored Range — full body returned");
return bytes;
}
```
## API Reference
### `buildRangeHeader(offset: number, length: number): string`
Returns `"bytes=<offset>-<offset+length-1>"`. Throws `StorageHttpError(INVALID_OPTIONS)` when `offset` is not a non-negative integer or `length` is not a positive integer.
### `interface RangeOutcome`
`{ bytes: Uint8Array; fellBack: boolean }` — `fellBack` is `true` only when the origin returned `200 OK` to a `Range` request and the slice was performed client-side.
### `consumeRangeResponse(response, offset, length, url): Promise<RangeOutcome>`
- `206` → returns the body verbatim with `fellBack = false`.
- `200` → consumes the full body, slices `[offset, offset + length)`, returns with `fellBack = true`.
- `416` → throws `StorageHttpError(NOT_FOUND)` with `httpStatus: 416`.
- other → throws `StorageHttpError(UPSTREAM_ERROR)` with the observed status.
## Implementation Notes
- The `Content-Range` response header is intentionally NOT parsed here — it carries the total object size, which is useful for cache metadata but not required to decode the body. `HttpStorage` parses it in the metadata path when needed.
- Client-side slicing on `200` uses `Math.min` clamps so out-of-bounds requests never throw `RangeError` on `slice`; the resulting array is simply empty if `offset >= body.byteLength`. The caller decides whether to treat that as "object shorter than expected" or pass it through.
- `416` mapping to `NOT_FOUND` is a deliberate simplification: distinguishing "object missing" from "range past EOF on an existing object" requires a `HEAD` round-trip the caller already has cheaper alternatives for (e.g. `exists()` or attaching `If-Range` to disambiguate cache vs miss).