/**
* The `Blob`/`File` adapter.
*
* Layer 5. The DOM `Blob` is never named: `BlobLike` is the structural shim from `types.ts`, so
* a real `File` from an `` remains assignable while `lib: ["DOM"]` stays out
* of the published `.d.ts`.
*
* A `Blob` read is the one place where the platform can legitimately hand back fewer bytes than
* asked (a `File` whose backing file changed on disk since the picker ran), so the exact-length
* contract is verified rather than assumed.
*/
import { EdfSourceError } from '../errors.js';
import { describeValue } from '../text/describe.js';
import type { BlobLike, ByteSource, ReadOptions } from '../types.js';
import { assertExactRead, assertReadRange, throwIfAborted } from './source.js';
/**
* A `ByteSource` over a `Blob` or `File` — the browser entry point, and what an
* `` hands you. Reads are ranged, so opening a file the user picked costs the
* header rather than the recording.
*/
export function blobSource(blob: BlobLike): ByteSource {
/*
* Checked structurally, and at construction, for both of the reasons `byteSource` gives: a
* `BlobLike` is an interface a caller may implement, and a source built over something that is
* not a blob surfaces later as `[SOURCE_TOO_SMALL] the header is 0 bytes` — blaming the FILE for
* a mistake in the argument. Until now it did not even get that far: `blob.size` on an omitted
* argument was V8's `Cannot read properties of undefined (reading 'size')` (fixed in 0.6.102).
*/
const given = blob as BlobLike | null | undefined;
if (typeof given?.size !== 'number' || typeof given.slice !== 'function') {
throw new EdfSourceError(
'blobSource() needs a Blob or a File — an object with a size and a slice() — and received ' +
`${describeValue(blob)}. Next: pass the File an or a drop event hands ` +
'you, or byteSource(bytes) if you already have the bytes in memory.',
{ offset: 0, requestedLength: 0 },
);
}
/*
* The SIZE, which the check above only established is a number.
*
* `fileHandleSource` refuses a `byteLength` that is not a byte count (0.6.85) and `fileSource`
* refuses the one it reads off the handle; this is the third adapter that takes a size and the
* one that took whatever arrived. `BlobLike` is a structural shim precisely so a caller can
* IMPLEMENT it — a stream-backed file, a mocked blob in a test, a wrapper around a native file
* picker — and an implementation is where a computed `size` comes from.
*
* A `NaN` did not fail; it disabled the range guard. `assertReadRange` compares every read
* against `byteLength`, every comparison against `NaN` is false, so the check silently stopped
* happening and the source advertised `byteLength: NaN` to everything downstream. A negative or
* fractional size passed the same way. `options.ts` names this shape exactly: "a guard written
* as `if (value < 1)` simply does not fire".
*/
if (!Number.isSafeInteger(blob.size) || blob.size < 0) {
throw new EdfSourceError(
`blobSource() was given ${describeValue(blob.size)} as the blob's size, which is not a ` +
'byte count edfcore can address, so no read could be bounded by it. Next: pass a real ' +
'Blob or File, whose size the platform sets, or byteSource(bytes) if you built the ' +
'bytes yourself.',
{ offset: 0, requestedLength: 0 },
);
}
const byteLength = blob.size;
return {
byteLength,
async read(offset: number, length: number, options?: ReadOptions): Promise {
throwIfAborted(options);
assertReadRange(offset, length, byteLength);
if (length === 0) return new Uint8Array(0);
// `Blob.slice` takes an EXCLUSIVE end, unlike an HTTP byte range.
const buffer = await blob.slice(offset, offset + length).arrayBuffer();
throwIfAborted(options);
/*
* Diagnosed HERE rather than left to `assertExactRead`, which is the argument `node.ts` makes
* for its own short read — and the case it names there is this one: "a picked `File`'s backing
* file shrank". It was the only adapter of the three without the diagnosis.
*
* `assertExactRead` exists for a `ByteSource` the CALLER wrote. Its message says a source
* "must resolve with exactly the requested number of bytes or reject", which accuses the
* browser of breaking a contract it kept: the platform answered correctly for a file that is
* now shorter than the one the picker measured. This module's own docblock calls that the one
* legitimate short read there is.
*
* Same shape as the HTTP buffered-body path (0.3.75) and the file handle (0.3.93).
*/
if (buffer.byteLength < length) {
throw new EdfSourceError(
`Reading bytes ${offset}..${offset + length - 1}: the blob ended after ` +
`${buffer.byteLength} of them. This source was built for ${byteLength} bytes, so the ` +
'range asked for is past the end of the blob as it is now. Next: a picked File is a ' +
'handle on a file that can still change — the backing file was truncated or replaced ' +
'since it was chosen, so ask for it again.',
{ offset, requestedLength: length, receivedLength: buffer.byteLength },
);
}
return assertExactRead(new Uint8Array(buffer), offset, length);
},
};
}