77 *
88 * This implementation uses WASM GC arrays with intoCharCodeArray/fromCharCodeArray
99 * for efficient bulk string operations instead of character-by-character processing.
10+ *
11+ * When available (Node.js 24+ with --experimental-wasm-rab-integration), uses
12+ * resizable ArrayBuffer integration for reduced glue code and auto-tracking views.
1013 */
1114
1215import { wasmBinary } from "./utf8-wasm-binary.ts" ;
@@ -46,6 +49,12 @@ interface WasmExports extends WebAssembly.Exports {
4649let wasmInstance : WasmExports | null = null ;
4750let wasmInitError : Error | null = null ;
4851
52+ // Resizable ArrayBuffer integration (RAB)
53+ // When available, provides auto-tracking views that don't detach on memory.grow()
54+ let wasmResizableBuffer : ArrayBuffer | null = null ;
55+ let wasmMemoryView : Uint8Array | null = null ;
56+ let useResizableBuffer = false ;
57+
4958function base64ToBytes ( base64 : string ) : Uint8Array {
5059 // @ts -expect-error - fromBase64 is not yet supported in TypeScript
5160 if ( Uint8Array . fromBase64 ) {
@@ -65,6 +74,24 @@ function base64ToBytes(base64: string): Uint8Array {
6574 }
6675}
6776
77+ function tryInitializeResizableBuffer ( ) : void {
78+ if ( ! wasmInstance ) return ;
79+
80+ try {
81+ // Check if toResizableBuffer() is available (Node.js 24+ with --experimental-wasm-rab-integration)
82+ const memory = wasmInstance . memory as any ;
83+ if ( typeof memory . toResizableBuffer === "function" ) {
84+ wasmResizableBuffer = memory . toResizableBuffer ( ) ;
85+ // Create auto-tracking view (no explicit length = tracks buffer size)
86+ wasmMemoryView = new Uint8Array ( wasmResizableBuffer ) ;
87+ useResizableBuffer = true ;
88+ }
89+ } catch {
90+ // RAB integration not available, will use fallback
91+ useResizableBuffer = false ;
92+ }
93+ }
94+
6895function tryInitializeWasmInstance ( ) : void {
6996 if ( WASM_MODE === "never" ) {
7097 wasmInitError = new Error ( "MSGPACK_WASM=never: wasm disabled" ) ;
@@ -82,6 +109,9 @@ function tryInitializeWasmInstance(): void {
82109 const module : WebAssembly . Module = new ( WebAssembly . Module as any ) ( bytes , { builtins : [ "js-string" ] } ) ;
83110 const instance = new WebAssembly . Instance ( module ) ;
84111 wasmInstance = instance . exports as WasmExports ;
112+
113+ // Try to enable resizable buffer integration for reduced glue code
114+ tryInitializeResizableBuffer ( ) ;
85115 } catch ( e ) {
86116 wasmInitError = e instanceof Error ? e : new Error ( String ( e ) ) ;
87117
@@ -99,6 +129,12 @@ tryInitializeWasmInstance();
99129// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
100130export const WASM_AVAILABLE = wasmInstance !== null ;
101131
132+ /**
133+ * Whether resizable ArrayBuffer integration is available.
134+ * When true, uses auto-tracking views for reduced glue code.
135+ */
136+ export const RAB_AVAILABLE = useResizableBuffer ;
137+
102138export function getWasmError ( ) : Error | null {
103139 return wasmInitError ;
104140}
@@ -114,50 +150,77 @@ export function utf8CountWasm(str: string): number {
114150 return wasmInstance ! . utf8Count ( str ) ;
115151}
116152
153+ // Page size constant
154+ const PAGE_SIZE = 65536 ;
155+
156+ /**
157+ * Ensure wasm memory is large enough.
158+ * With RAB integration, the view auto-tracks size changes.
159+ * Without RAB, we need to recreate the view after grow.
160+ */
161+ function ensureMemorySize ( requiredBytes : number ) : void {
162+ const requiredPages = Math . ceil ( requiredBytes / PAGE_SIZE ) ;
163+ const currentPages = wasmInstance ! . memory . buffer . byteLength / PAGE_SIZE ;
164+
165+ if ( requiredPages > currentPages ) {
166+ wasmInstance ! . memory . grow ( requiredPages - currentPages ) ;
167+ // With RAB, wasmMemoryView auto-tracks the new size
168+ // Without RAB, we don't maintain a persistent view
169+ }
170+ }
171+
172+ /**
173+ * Get a view of wasm memory for reading/writing.
174+ * With RAB integration, returns the auto-tracking view.
175+ * Without RAB, creates a fresh view each time.
176+ */
177+ function getMemoryView ( ) : Uint8Array {
178+ if ( useResizableBuffer ) {
179+ // Auto-tracking view - no need to recreate
180+ return wasmMemoryView ! ;
181+ }
182+ // Fallback: create fresh view (handles detached buffer after grow)
183+ return new Uint8Array ( wasmInstance ! . memory . buffer ) ;
184+ }
185+
117186/**
118187 * Encode string to UTF-8 bytes in the provided buffer.
119188 * Returns the number of bytes written.
189+ *
190+ * With RAB integration: uses auto-tracking view, minimal glue code.
191+ * Without RAB: recreates view after potential memory growth.
120192 */
121193export function utf8EncodeWasm ( str : string , output : Uint8Array , outputOffset : number ) : number {
122- // Estimate max byte length without a full pass over the string.
123- // Each UTF-16 code unit can produce at most 3 UTF-8 bytes (BMP chars).
124- // Surrogate pairs (2 code units) produce 4 bytes, so 3 bytes/code unit is safe.
194+ // Estimate max byte length: each UTF-16 code unit produces at most 3 UTF-8 bytes
125195 const maxByteLength = str . length * 3 ;
126196
127- // Ensure wasm memory is large enough
128- const requiredPages = Math . ceil ( maxByteLength / 65536 ) ;
129- const currentPages = wasmInstance ! . memory . buffer . byteLength / 65536 ;
197+ // Ensure memory is large enough (view auto-tracks with RAB)
198+ ensureMemorySize ( maxByteLength ) ;
130199
131- if ( requiredPages > currentPages ) {
132- wasmInstance ! . memory . grow ( requiredPages - currentPages ) ;
133- }
134-
135- // Encode to wasm memory (uses intoCharCodeArray for bulk char extraction)
200+ // Encode to wasm memory
136201 const bytesWritten = wasmInstance ! . utf8Encode ( str , 0 ) ;
137202
138203 // Copy from wasm memory to output buffer
139- const wasmBytes = new Uint8Array ( wasmInstance ! . memory . buffer , 0 , bytesWritten ) ;
140- output . set ( wasmBytes , outputOffset ) ;
204+ const view = getMemoryView ( ) ;
205+ output . set ( view . subarray ( 0 , bytesWritten ) , outputOffset ) ;
141206
142207 return bytesWritten ;
143208}
144209
145210/**
146211 * Decode UTF-8 bytes to string.
147212 * Uses GC arrays with fromCharCodeArray for efficient string creation.
213+ *
214+ * With RAB integration: uses auto-tracking view, minimal glue code.
215+ * Without RAB: recreates view after potential memory growth.
148216 */
149217export function utf8DecodeWasm ( bytes : Uint8Array , inputOffset : number , byteLength : number ) : string {
150- // Ensure wasm memory is large enough for UTF-8 input
151- const requiredPages = Math . ceil ( byteLength / 65536 ) ;
152- const currentPages = wasmInstance ! . memory . buffer . byteLength / 65536 ;
153-
154- if ( requiredPages > currentPages ) {
155- wasmInstance ! . memory . grow ( requiredPages - currentPages ) ;
156- }
218+ // Ensure memory is large enough (view auto-tracks with RAB)
219+ ensureMemorySize ( byteLength ) ;
157220
158- // Copy UTF-8 bytes to wasm linear memory at offset 0
159- const wasmBytes = new Uint8Array ( wasmInstance ! . memory . buffer , 0 , byteLength ) ;
160- wasmBytes . set ( bytes . subarray ( inputOffset , inputOffset + byteLength ) ) ;
221+ // Copy UTF-8 bytes to wasm linear memory
222+ const view = getMemoryView ( ) ;
223+ view . set ( bytes . subarray ( inputOffset , inputOffset + byteLength ) , 0 ) ;
161224
162225 // Allocate GC array for UTF-16 output (max size = byteLength for ASCII)
163226 const arr = wasmInstance ! . allocArray ( byteLength ) ;
0 commit comments