85 lines
3.2 KiB
JavaScript
85 lines
3.2 KiB
JavaScript
|
import {getStreamContents} from './contents.js';
|
||
|
import {noop, throwObjectStream, getLengthProp} from './utils.js';
|
||
|
|
||
|
export async function getStreamAsArrayBuffer(stream, options) {
|
||
|
return getStreamContents(stream, arrayBufferMethods, options);
|
||
|
}
|
||
|
|
||
|
const initArrayBuffer = () => ({contents: new ArrayBuffer(0)});
|
||
|
|
||
|
const useTextEncoder = chunk => textEncoder.encode(chunk);
|
||
|
const textEncoder = new TextEncoder();
|
||
|
|
||
|
const useUint8Array = chunk => new Uint8Array(chunk);
|
||
|
|
||
|
const useUint8ArrayWithOffset = chunk => new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
||
|
|
||
|
const truncateArrayBufferChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize);
|
||
|
|
||
|
// `contents` is an increasingly growing `Uint8Array`.
|
||
|
const addArrayBufferChunk = (convertedChunk, {contents, length: previousLength}, length) => {
|
||
|
const newContents = hasArrayBufferResize() ? resizeArrayBuffer(contents, length) : resizeArrayBufferSlow(contents, length);
|
||
|
new Uint8Array(newContents).set(convertedChunk, previousLength);
|
||
|
return newContents;
|
||
|
};
|
||
|
|
||
|
// Without `ArrayBuffer.resize()`, `contents` size is always a power of 2.
|
||
|
// This means its last bytes are zeroes (not stream data), which need to be
|
||
|
// trimmed at the end with `ArrayBuffer.slice()`.
|
||
|
const resizeArrayBufferSlow = (contents, length) => {
|
||
|
if (length <= contents.byteLength) {
|
||
|
return contents;
|
||
|
}
|
||
|
|
||
|
const arrayBuffer = new ArrayBuffer(getNewContentsLength(length));
|
||
|
new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0);
|
||
|
return arrayBuffer;
|
||
|
};
|
||
|
|
||
|
// With `ArrayBuffer.resize()`, `contents` size matches exactly the size of
|
||
|
// the stream data. It does not include extraneous zeroes to trim at the end.
|
||
|
// The underlying `ArrayBuffer` does allocate a number of bytes that is a power
|
||
|
// of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`.
|
||
|
const resizeArrayBuffer = (contents, length) => {
|
||
|
if (length <= contents.maxByteLength) {
|
||
|
contents.resize(length);
|
||
|
return contents;
|
||
|
}
|
||
|
|
||
|
const arrayBuffer = new ArrayBuffer(length, {maxByteLength: getNewContentsLength(length)});
|
||
|
new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0);
|
||
|
return arrayBuffer;
|
||
|
};
|
||
|
|
||
|
// Retrieve the closest `length` that is both >= and a power of 2
|
||
|
const getNewContentsLength = length => SCALE_FACTOR ** Math.ceil(Math.log(length) / Math.log(SCALE_FACTOR));
|
||
|
|
||
|
const SCALE_FACTOR = 2;
|
||
|
|
||
|
const finalizeArrayBuffer = ({contents, length}) => hasArrayBufferResize() ? contents : contents.slice(0, length);
|
||
|
|
||
|
// `ArrayBuffer.slice()` is slow. When `ArrayBuffer.resize()` is available
|
||
|
// (Node >=20.0.0, Safari >=16.4 and Chrome), we can use it instead.
|
||
|
// eslint-disable-next-line no-warning-comments
|
||
|
// TODO: remove after dropping support for Node 20.
|
||
|
// eslint-disable-next-line no-warning-comments
|
||
|
// TODO: use `ArrayBuffer.transferToFixedLength()` instead once it is available
|
||
|
const hasArrayBufferResize = () => 'resize' in ArrayBuffer.prototype;
|
||
|
|
||
|
const arrayBufferMethods = {
|
||
|
init: initArrayBuffer,
|
||
|
convertChunk: {
|
||
|
string: useTextEncoder,
|
||
|
buffer: useUint8Array,
|
||
|
arrayBuffer: useUint8Array,
|
||
|
dataView: useUint8ArrayWithOffset,
|
||
|
typedArray: useUint8ArrayWithOffset,
|
||
|
others: throwObjectStream,
|
||
|
},
|
||
|
getSize: getLengthProp,
|
||
|
truncateChunk: truncateArrayBufferChunk,
|
||
|
addChunk: addArrayBufferChunk,
|
||
|
getFinalChunk: noop,
|
||
|
finalize: finalizeArrayBuffer,
|
||
|
};
|