-
-
Notifications
You must be signed in to change notification settings - Fork 144
feat: add TextEncoder/TextDecoder and atob/btoa on a lazy-global tier #2026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2e843f1
feat: add TextEncoder/TextDecoder and atob/btoa on a lazy-global tier
edusperoni 01bc3bd
feat: expose TextEncoder/TextDecoder from ns:util and node:util
edusperoni df40e48
fix: keep the encodeInto fast path off the JS heap
edusperoni 96677ef
test: bump shared tests to pin the utf-16 single-error flush
edusperoni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # TextEncoder / TextDecoder and atob / btoa | ||
|
|
||
| Native, WHATWG-conformant `TextEncoder`, `TextDecoder` | ||
| ([Encoding Standard](https://encoding.spec.whatwg.org)) and `atob` / `btoa` | ||
| ([HTML Standard §8.3](https://html.spec.whatwg.org/multipage/webappapis.html#atob)) | ||
| globals, and the **lazy-global tier** they ride on. | ||
|
|
||
| ## Lazy globals | ||
|
|
||
| These globals are registered on the global template as lazy data properties | ||
| (`LazyGlobals`, `test-app/runtime/src/main/cpp/LazyGlobals.cpp`): the builtin | ||
| behind a name is not compiled, run, or allocated until app code first reads it, | ||
| and V8 then replaces the property with a plain data property so later reads | ||
| cost nothing. Sibling names from one builtin (`TextEncoder` + `TextDecoder`) | ||
| share a single run per isolate. Workers get the same globals — the tier is | ||
| registered in every isolate's template. Assigning over one of these names | ||
| before its first read replaces the global, like any other writable global. | ||
|
|
||
| The tier is the intended home for further web globals (`Blob`, `fetch`, | ||
| `crypto`, `DOMException`, …) with zero cost when unused; see | ||
| `test-app/runtime/src/main/cpp/js/README.md` for the rules a lazy builtin | ||
| lives by. | ||
|
|
||
| The per-isolate exports cache behind the tier (`BuiltinLoader::GetExports`) is | ||
| shared with the `ns:`/`node:` module registry: `require("ns:util").TextDecoder` | ||
| and `require("node:util").TextDecoder` are the very class objects the globals | ||
| hold, whichever entry point is reached first | ||
| (see [ns-builtin-modules](ns-builtin-modules.md)). | ||
|
|
||
| ## TextEncoder / TextDecoder | ||
|
|
||
| Node's split: `js/text-encoding.js` owns the WebIDL surface (brand checks via | ||
| private fields, enumerable prototype members, `Symbol.toStringTag`), | ||
| `TextEncoding.cpp` owns the bytes. | ||
|
|
||
| - **Decoder encodings**: the `TextDecoder` constructor resolves utf-8, | ||
| utf-16le, utf-16be and windows-1252, each with its complete WHATWG label | ||
| set; an unknown label throws `RangeError`. (Precedent: Node without ICU | ||
| ships utf-8/utf-16le; utf-16be and windows-1252 are cheap, and windows-1252 | ||
| covers the `ascii`/`latin1`/`iso-8859-1` aliases web code actually uses.) | ||
| `TextEncoder` is UTF-8-only and takes no label, as the spec defines it. | ||
| - **Streaming**: full `decode(…, { stream: true })` support. Incomplete | ||
| sequences (split BOMs and split utf-16 code units included) carry across | ||
| calls in a 16-byte `Uint8Array` the builtin owns — no per-instance native | ||
| handle, no finalizer. | ||
| - **Replacement semantics**: WHATWG utf-8 state machine with one U+FFFD per | ||
| maximal invalid subpart; `fatal: true` throws `TypeError`; `ignoreBOM` | ||
| honored. | ||
| - `encode()` / `encodeInto()` with correct USV conversion and partial-write | ||
| boundaries (`encodeInto` never splits an encoded code point). | ||
| - **Fast paths**: pure-ASCII utf-8 and C1-free windows-1252 decode straight | ||
| through `String::NewFromOneByte`; results downgrade to one-byte strings when | ||
| possible. `encodeInto` registers a V8 Fast API overload | ||
| (`NATIVESCRIPT_ENABLE_FAST_API`, default on), live once a call site tiers | ||
| up. | ||
|
|
||
| ## atob / btoa | ||
|
|
||
| WHATWG forgiving-base64 (`Base64.cpp`): whitespace stripping, padding rules, | ||
| alphabet validation. With no `DOMException` in the runtime yet, failures throw | ||
| the name-patched `Error` (`InvalidCharacterError`) stand-in the abort-signal | ||
| and performance builtins already use; a follow-up will introduce | ||
| `DOMException` and upgrade these. | ||
|
|
||
| ## Tests | ||
|
|
||
| The shared suite (`test-app/app/src/main/assets/app/shared/TextEncoding`) | ||
| holds the conformance specs, feature-detecting so runtimes without these | ||
| globals report pending rather than failing; it was independently validated | ||
| against Node 24 (full ICU) as a reference. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Submodule shared
updated
4 files
| +222 −0 | TextEncoding/Base64.js | |
| +736 −0 | TextEncoding/Encoding.js | |
| +4 −0 | TextEncoding/index.js | |
| +5 −0 | index.js |
28 changes: 28 additions & 0 deletions
28
test-app/app/src/main/assets/app/tests/nsUtilEncodingOrderWorker.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // A worker is a fresh isolate, which is what makes the access order testable: | ||
| // the parent realm has already materialized TextEncoder/TextDecoder by the | ||
| // time any spec runs. Nothing here may touch either name before the handler, | ||
| // or the requested order is lost. | ||
| onmessage = function (msg) { | ||
| var order = msg.data; | ||
| var results = { order: order }; | ||
|
|
||
| if (order === "global-first") { | ||
| var globalEncoder = globalThis.TextEncoder; | ||
| var globalDecoder = globalThis.TextDecoder; | ||
| var nsUtil = require("ns:util"); | ||
| var nodeUtil = require("node:util"); | ||
| results.encoder = nsUtil.TextEncoder === globalEncoder && nodeUtil.TextEncoder === globalEncoder; | ||
| results.decoder = nsUtil.TextDecoder === globalDecoder && nodeUtil.TextDecoder === globalDecoder; | ||
| results.roundTrip = new nsUtil.TextDecoder().decode(new nodeUtil.TextEncoder().encode("ok")); | ||
| } else { | ||
| var util = require("ns:util"); | ||
| var node = require("node:util"); | ||
| var utilEncoder = util.TextEncoder; | ||
| var utilDecoder = node.TextDecoder; | ||
| results.encoder = globalThis.TextEncoder === utilEncoder && node.TextEncoder === utilEncoder; | ||
| results.decoder = globalThis.TextDecoder === utilDecoder && util.TextDecoder === utilDecoder; | ||
| results.roundTrip = new node.TextDecoder().decode(new util.TextEncoder().encode("ok")); | ||
| } | ||
|
|
||
| postMessage(results); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.