array-buffer.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. import {getStreamContents} from './contents.js';
  2. import {noop, throwObjectStream, getLengthProperty} from './utils.js';
  3. export async function getStreamAsArrayBuffer(stream, options) {
  4. return getStreamContents(stream, arrayBufferMethods, options);
  5. }
  6. const initArrayBuffer = () => ({contents: new ArrayBuffer(0)});
  7. const useTextEncoder = chunk => textEncoder.encode(chunk);
  8. const textEncoder = new TextEncoder();
  9. const useUint8Array = chunk => new Uint8Array(chunk);
  10. const useUint8ArrayWithOffset = chunk => new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
  11. const truncateArrayBufferChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize);
  12. // `contents` is an increasingly growing `Uint8Array`.
  13. const addArrayBufferChunk = (convertedChunk, {contents, length: previousLength}, length) => {
  14. const newContents = hasArrayBufferResize() ? resizeArrayBuffer(contents, length) : resizeArrayBufferSlow(contents, length);
  15. new Uint8Array(newContents).set(convertedChunk, previousLength);
  16. return newContents;
  17. };
  18. // Without `ArrayBuffer.resize()`, `contents` size is always a power of 2.
  19. // This means its last bytes are zeroes (not stream data), which need to be
  20. // trimmed at the end with `ArrayBuffer.slice()`.
  21. const resizeArrayBufferSlow = (contents, length) => {
  22. if (length <= contents.byteLength) {
  23. return contents;
  24. }
  25. const arrayBuffer = new ArrayBuffer(getNewContentsLength(length));
  26. new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0);
  27. return arrayBuffer;
  28. };
  29. // With `ArrayBuffer.resize()`, `contents` size matches exactly the size of
  30. // the stream data. It does not include extraneous zeroes to trim at the end.
  31. // The underlying `ArrayBuffer` does allocate a number of bytes that is a power
  32. // of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`.
  33. const resizeArrayBuffer = (contents, length) => {
  34. if (length <= contents.maxByteLength) {
  35. contents.resize(length);
  36. return contents;
  37. }
  38. const arrayBuffer = new ArrayBuffer(length, {maxByteLength: getNewContentsLength(length)});
  39. new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0);
  40. return arrayBuffer;
  41. };
  42. // Retrieve the closest `length` that is both >= and a power of 2
  43. const getNewContentsLength = length => SCALE_FACTOR ** Math.ceil(Math.log(length) / Math.log(SCALE_FACTOR));
  44. const SCALE_FACTOR = 2;
  45. const finalizeArrayBuffer = ({contents, length}) => hasArrayBufferResize() ? contents : contents.slice(0, length);
  46. // `ArrayBuffer.slice()` is slow. When `ArrayBuffer.resize()` is available
  47. // (Node >=20.0.0, Safari >=16.4 and Chrome), we can use it instead.
  48. // eslint-disable-next-line no-warning-comments
  49. // TODO: remove after dropping support for Node 20.
  50. // eslint-disable-next-line no-warning-comments
  51. // TODO: use `ArrayBuffer.transferToFixedLength()` instead once it is available
  52. const hasArrayBufferResize = () => 'resize' in ArrayBuffer.prototype;
  53. const arrayBufferMethods = {
  54. init: initArrayBuffer,
  55. convertChunk: {
  56. string: useTextEncoder,
  57. buffer: useUint8Array,
  58. arrayBuffer: useUint8Array,
  59. dataView: useUint8ArrayWithOffset,
  60. typedArray: useUint8ArrayWithOffset,
  61. others: throwObjectStream,
  62. },
  63. getSize: getLengthProperty,
  64. truncateChunk: truncateArrayBufferChunk,
  65. addChunk: addArrayBufferChunk,
  66. getFinalChunk: noop,
  67. finalize: finalizeArrayBuffer,
  68. };