UNPKG

@tanstack/ai-persistence

Version:

Composable state persistence for TanStack AI messages, runs, interrupts, metadata, and locks.

102 lines (97 loc) 4.35 kB
import type { BlobRange } from './types' /** * 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) * ``` */ export function resolveBlobRange( size: number, range: BlobRange, ): { offset: number; length: number } { 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 === undefined) 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, * ) * ``` */ export function parseRangeHeader( header: string | null | undefined, size: number, ): BlobRange | 'unsatisfiable' | undefined { const match = /^bytes=(\d*)-(\d*)$/.exec(header?.trim() ?? '') if (!match) return undefined const [, rawStart, rawEnd] = match // `bytes=-` names nothing at all. if (rawStart === '' && rawEnd === '') return undefined // `bytes=-500` is the LAST 500 bytes, not "from 0 to 500" — the one form // that is easy to read backwards, and reading it backwards serves the wrong // bytes with a 206 that claims they are the right ones. if (rawStart === '') { const suffix = Number(rawEnd) // A zero-length suffix names no bytes, and NO range is satisfiable against // a zero-byte object — without the size check, `bytes=-1` on an empty // artifact resolves to `{ offset: 0 }` and throws out of the store instead // of answering 416. if (suffix === 0 || size === 0) return 'unsatisfiable' return { offset: Math.max(0, size - suffix) } } const start = Number(rawStart) const end = rawEnd === '' ? undefined : Number(rawEnd) // `bytes=100-50` is an INVALID byte-range-spec, not an unsatisfiable one // (RFC 9110 §14.1.1). An invalid spec is ignored and the whole // representation is served — answering 416 would fail a request that is // supposed to succeed. Checked before satisfiability so the size cannot turn // an ignorable spec into a 416. if (end !== undefined && end < start) return undefined if (start >= size) return 'unsatisfiable' if (end === undefined) return { offset: start } // `end` is inclusive, and past the end of the object it simply clamps. return { offset: start, length: Math.min(end, size - 1) - start + 1 } }