TL;DR;
Uncaught (in promise) TypeError: Cannot perform Construct on a detached ArrayBuffer
みたいな問題に遭遇したときへの対処です。
また原因についてそれっぽいことを書いておきます
例
Chrome バージョン: 124.0.6367.119(Official Build) (arm64)
const audioCtx = new AudioContext();
const arrayBuffer = await fetch('./test.aac')
.then((response) => response.arrayBuffer());
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
const binary = new Uint8Array(arrayBuffer);
コードそのものはaacのメタデータとかほしかったのでdecodeAudioDataを実行し、その後にUint8ArrayをもとのArrayBufferをつかって構築しようとしますが、最後の行で
main.ts:34 Uncaught (in promise) TypeError: Cannot perform Construct on a detached ArrayBuffer
at new Uint8Array (<anonymous>)
となります
解決
元のArrayBufferをcopyして使うことで回避できます
const audioCtx = new AudioContext();
const arrayBuffer = await fetch('./test.aac')
.then((response) => response.arrayBuffer());
const copied = new ArrayBuffer(arrayBuffer.byteLength);
new Uint8Array(copied).set(new Uint8Array(arrayBuffer))
const audioBuffer = await audioCtx.decodeAudioData(copied);
const binary = new Uint8Array(arrayBuffer);
元のファイルのArrayBufferのコピーを一つつくって、それぞれUint8Arrayを構築するのとAudioContext#decodeAudioDataに使用することで正しく処理されます
ただしメモリーの使用量は倍になります
ArrayBufferのcopyは思った以上にエレガントでしたが、これについてはstackoverflowに記述があります
https://stackoverflow.com/questions/10100798/whats-the-most-straightforward-way-to-copy-an-arraybuffer-object
憶測
ArrayBufferはJavaScriptの実行環境内で確保された一定のメモリー領域に関するhandlerであり情報です
それ自体はただのメモリーなので、メモリーに直接的に書きこむ操作とかはできるというわけではありませんが、たとえばUint8Arrayなどを介して読み書きができます
そして共通のArrayBufferをつかって構築された変数では副作用によって他の変数の中身についても破壊的な変更の影響をうけます
const arrayBuffer = await fetch('./test.aac')
.then((response) => response.arrayBuffer());
const binary = new Uint8Array(arrayBuffer);
console.log('original', binary)
const binary16 = new Uint16Array(arrayBuffer)
console.log('original 16', binary16)
binary.set([0,0], 0) // 副作用があるよ
console.log('after 0', binary) // 値がかわってる
console.log('after 1(16)', binary16) // こっちもかわる
const binary16_ = new Uint16Array(arrayBuffer)
console.log('after 2', binary16_) // これも
こういった意図しない変更を避けるため、ArrayBuffer#transferという権限移譲のためのメソッドが用意されています
これを行うことで
- 他の変数にメモリーの領域についての権限が移譲される
- 元の変数はdetachされ、それを用いて構築されたUint8Arrayなどの変数からもアクセスできないようになる
といったことがあります。(Rustっぽい)
権限移譲された(detachされた)ArrayBufferを使用してUint8Arrayを構築できなくなります
(それが最初のエラーです)
また、構築されたあとでArrayBufferが権限移譲されてしまったUint8Arrayの変数からも、元のArrayBufferにアクセスできなくなるので内容が空になります
const arrayBuffer = await fetch('./test.aac')
.then((response) => response.arrayBuffer());
const binary = new Uint8Array(arrayBuffer);
console.log('before', binary)
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
console.log('detached', arrayBuffer.detached)
console.log('after', binary)
console
before Uint8Array(241596) [255, 241, 80, 128, 11, 191, 252, 222, 2, 0, 76, 97, 118, 99, 53, 56, 46, 51, 52, 46, 49, 48, 48, 0, 66, 53, 144, 0, 32, 0, 0, 30, 228, 14, 16, 130, 117, 226, 67, 199, 188, 231, 43, 40, 154, 205, 111, 246, 0, 25, 128, 0, 51, 26, 102, 37, 218, 153, 220, 128, 130, 16, 72, 132, 8, 33, 2, 107, 196, 129, 229, 189, 243, 204, 86, 36, 235, 57, 253, 128, 177, 80, 0, 5, 175, 242, 12, 230, 125, 167, 229, 24, 240, 255, 241, 80, 128, 16, 63, 252, …]
detached true
after Uint8Array [buffer: ArrayBuffer(0), byteLength: 0, byteOffset: 0, length: 0, Symbol(Symbol.toStringTag): 'Uint8Array']
AudioContext#decodeAudioDataを使用したあとのArrayBufferではおそらく内部でtransferされてdetached trueとなります
ArrayBufferがtransferされる前にUint8Arrayを構築することはできますが、detachされたあとにアクセスする場合、中身が空になります
まとめ
- ArrayBufferは処理系の内部にあるメモリーへのアクセスを有する変数であり、破壊的である
- 同じArrayBufferを用いて構築したクラスのいくつかは破壊的操作の影響をうける
- 影響を排除するためにArrayBuffer#transferを使用して、別の変数にBufferの使用権限を移譲できる
- 移譲された元の変数はdetachされ、元のメモリーへのアクセスができなくなる
- detachされたArrayBufferを使用していて新規にインスタンスを構築できないクラスがある
- 使用しているArrayBufferが他からdetachされた場合、自身のArrayBufferも(コピーでない限り)メモリーへのアクセスができなくなり、空となる
感想
ちゃんと理解していないとめんどくさい