@tanstack/ai-persistence
Version:
Composable state persistence for TanStack AI messages, runs, interrupts, metadata, and locks.
84 lines (83 loc) • 3.27 kB
JavaScript
//#region src/blob-range.ts
/**
* Resolve a requested {@link BlobRange} against an object's real size, the way
* every byte-storing blob store has to before it slices.
*
* Clamps `length` to the end of the object and treats an absent `length` as
* "to the end", so the result is always the slice actually served — which is
* what `BlobObject.range` reports and what a `206` response's `Content-Range`
* is built from.
*
* Throws on an `offset` outside the object: that is a caller error, not a
* store error. A serve route knows the size (it is on the artifact record) and
* answers `416` from it, so a store only ever sees a satisfiable range unless
* something upstream is wrong — and silently returning an empty body there
* would serve a `206` that claims bytes it does not carry.
*
* @example
* ```ts
* const { offset, length } = resolveBlobRange(bytes.byteLength, range)
* const slice = bytes.subarray(offset, offset + length)
* ```
*/
function resolveBlobRange(size, range) {
const { offset } = range;
if (!Number.isInteger(offset) || offset < 0 || offset >= size) throw new RangeError(`Blob range offset ${offset} is outside the object (size ${size}).`);
const remaining = size - offset;
if (range.length === void 0) return {
offset,
length: remaining
};
if (!Number.isInteger(range.length) || range.length < 0) throw new RangeError(`Blob range length ${range.length} is not valid.`);
return {
offset,
length: Math.min(range.length, remaining)
};
}
/**
* Resolve an HTTP `Range` header against a known object size, for a route that
* serves artifact bytes.
*
* Returns the {@link BlobRange} to pass to `retrieveBlob` / `BlobStore.get`,
* `'unsatisfiable'` when the request names bytes the object does not have — answer
* `416`, whose `content-range` is the literal `bytes` `*` then a slash then the
* size — or `undefined` when there is no range to honour and the whole object
* should be served: an absent header, an invalid byte-range-spec (`bytes=100-50`,
* which RFC 9110 says to ignore rather than reject), and the forms this does not
* implement (multiple ranges, units other than `bytes`), which a server is
* always free to answer in full.
*
* @example
* ```ts
* const range = parseRangeHeader(request.headers.get('range'), record.size)
* if (range === 'unsatisfiable') return new Response(null, { status: 416 })
* const blob = await retrieveBlob(
* persistence,
* record,
* range ? { range } : undefined,
* )
* ```
*/
function parseRangeHeader(header, size) {
const match = /^bytes=(\d*)-(\d*)$/.exec(header?.trim() ?? "");
if (!match) return void 0;
const [, rawStart, rawEnd] = match;
if (rawStart === "" && rawEnd === "") return void 0;
if (rawStart === "") {
const suffix = Number(rawEnd);
if (suffix === 0 || size === 0) return "unsatisfiable";
return { offset: Math.max(0, size - suffix) };
}
const start = Number(rawStart);
const end = rawEnd === "" ? void 0 : Number(rawEnd);
if (end !== void 0 && end < start) return void 0;
if (start >= size) return "unsatisfiable";
if (end === void 0) return { offset: start };
return {
offset: start,
length: Math.min(end, size - 1) - start + 1
};
}
//#endregion
export { parseRangeHeader, resolveBlobRange };
//# sourceMappingURL=blob-range.js.map