Skip to content

[M] Accept any BufferSource in TextDecoder.decode - #3

Merged
evanrichards merged 1 commit into
mainfrom
evan/textdecoder-buffersource
Sep 12, 2026
Merged

evanrichards merged 1 commit into
mainfrom
evan/textdecoder-buffersource

Conversation

@evanrichards

Copy link
Copy Markdown
Collaborator

The problem

TextDecoder.decode returned an empty string for an ArrayBuffer and for a
DataView. The polyfill read input.length. Neither type has a length
property, so the loop never ran. The method did not throw, so a caller lost
the data without a signal.

The backend runs customer-supplied and agent-authored TypeScript in this
sandbox. A script that calls new TextDecoder().decode(someArrayBuffer) gets
an empty string today, and the failure appears far away from the cause.

const bytes = new TextEncoder().encode('hello world')

new TextDecoder().decode(bytes.buffer)            // '' before, 'hello world' now
new TextDecoder().decode(new DataView(bytes.buffer)) // '' before, 'hello world' now
new TextDecoder().decode(bytes)                   // 'hello world' before and now

The change

The WHATWG encoding standard defines the input as a BufferSource, which is
an ArrayBuffer or a view on an ArrayBuffer. The new function
toUint8Array converts the input first. The conversion keeps the byte offset
and the byte length of a view, so new Uint8Array(buffer, 6) still decodes
the correct bytes. An input that is not a BufferSource now throws a
TypeError instead of a silent empty string.

The method also builds the intermediate string in chunks of 8192 bytes with
String.fromCharCode.apply. The previous code called String.fromCharCode
one time for each byte. The chunked loop keeps the same escape and
decodeURIComponent pair, so the output and the error behaviour do not
change.

The measurement

A benchmark in the sandbox decoded 600 KB, three runs, in milliseconds:

decoder ASCII unicode
one call for each byte (before) 131-135 146-207
chunked calls (this change) 19-26 20-22
a hand-written UTF-8 decoder 75-101 77-80

A hand-written UTF-8 decoder removes the Annex B function escape, but it is
slower and it changes the behaviour for invalid UTF-8. This change keeps the
escape pair.

The tests

src/test/sync/util.test.ts and src/test/async/util.test.ts each get 12
cases: an ArrayBuffer, a DataView, a Uint8Array, a view with a byte
offset, a view with a shorter byte length, a signed Int8Array view,
multibyte characters, an empty input, a payload larger than one chunk, and
the two TypeError cases. Five of them failed before the change.

The version

package.json moves to 3.2.1. The fork needs a v3.2.1 GitHub Release after
this merges, so that @loop-payments/ts-sandbox can take the fix.

🤖 Generated with Claude Code

The polyfill for `TextDecoder` read `input.length`. An `ArrayBuffer` and a
`DataView` have no `length` property. The loop never ran, and the method
returned an empty string. The method did not throw, so the caller lost the
data without a signal.

The WHATWG encoding standard defines the input as a `BufferSource`. A
`BufferSource` is an `ArrayBuffer` or a view on an `ArrayBuffer`. This change
converts the input to a `Uint8Array` first. The conversion keeps the byte
offset and the byte length of a view, so a subarray still decodes the correct
bytes. An input that is not a `BufferSource` now throws a `TypeError`.

The method also builds the intermediate string in chunks of 8192 bytes with
`String.fromCharCode.apply`. The previous code called `String.fromCharCode`
one time for each byte. A benchmark in the sandbox decoded 600 KB of JSON. The
time was 131 ms before this change and 20 ms after this change.

This change also increments the package version to 3.2.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@evanrichards

Copy link
Copy Markdown
Collaborator Author

This change is part of the following stack:

Change managed by git-spice.

@evanrichards
evanrichards merged commit 3d0408f into main Sep 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant