Skip to content

fix(fetch): #234 real Blob with arrayBuffer/text/bytes/slice instance methods#238

Closed
TheHypnoo wants to merge 2 commits into
PerryTS:mainfrom
TheHypnoo:fix/234-blob-instance-methods
Closed

fix(fetch): #234 real Blob with arrayBuffer/text/bytes/slice instance methods#238
TheHypnoo wants to merge 2 commits into
PerryTS:mainfrom
TheHypnoo:fix/234-blob-instance-methods

Conversation

@TheHypnoo
Copy link
Copy Markdown
Contributor

Summary

Closes #234.

Pre-fix await response.blob() returned a metadata-only stub {size, type} and silently dropped resp.body. Calls to blob.arrayBuffer() / .text() / .bytes() / .slice() got nothing because no codegen dispatch arms existed.

Approach

Three-part fix mirroring the precedent from #232 (real BufferHeader for response.arrayBuffer) and #227 (registered-buffer is_registered_buffer plumbing):

1. Runtime — crates/perry-stdlib/src/fetch.rs

  • New BlobData { body, content_type } + BLOB_REGISTRY: Mutex<HashMap<usize, BlobData>> + NEXT_BLOB_ID + alloc_blob helper, mirroring the HEADERS_REGISTRY / alloc_headers pattern in the same file.
  • js_response_blob rewritten: clones body bytes + content-type into a fresh BlobData, registers it, resolves the promise with the numeric handle as f64. Matches the Response-handle ABI documented at lower_call.rs:2788.
  • 6 new FFIs:
    • js_blob_size(handle) -> f64
    • js_blob_type(handle) -> *mut StringHeader (codegen NaN-boxes with STRING_TAG)
    • js_blob_array_buffer(handle) -> *mut Promise — allocates a real BufferHeader via perry_runtime::buffer::buffer_alloc(len), memcpys body, NaN-boxes as POINTER_TAG. Same shape as js_response_array_buffer from fix(fetch): copy body bytes into response.arrayBuffer() result #232 so new Uint8Array(buf) and Buffer.from(buf) see real bytes via the is_registered_buffer path from response.arrayBuffer() returns metadata-only object — body bytes never reach TS #227.
    • js_blob_bytes(handle) -> *mut Promise — alias to js_blob_array_buffer (the BufferHeader is already byte-array-shaped).
    • js_blob_text(handle) -> *mut Promise — UTF-8 lossy decode to StringHeader, matches WHATWG Blob.text() replacement-character semantics.
    • js_blob_slice(handle, start, end, type_ptr) -> f64 — canonical f64::NAN sentinel for missing numeric args, null type_ptr to inherit the original content-type. Negative indices count from the end and clamp to [0, len] per WHATWG Blob spec; allocates a fresh blob with sliced bytes.

2. HIR — crates/perry-hir/src/destructuring.rs

Two new arms in lower_var_decl next to the existing Response.clone() propagation:

  • const blob = await <recv>.blob() where <recv> is fetch::Responseregister_native_instance(name, "blob", "Blob").
  • const b2 = <recv>.slice(...) where <recv> is blob::Blob → propagate the type so chained slicing keeps Blob-ness.

The existing native-instance method-call lowering at expr_call.rs:723-740 and property access at expr_member.rs:312-326 then route subsequent .text() / .size / etc. to Expr::NativeMethodCall { module: "blob", method: ..., args: [] } automatically — no other HIR changes needed.

3. Codegen — crates/perry-codegen/src/lower_call.rs + runtime_decls.rs

New if module == "blob" arm right after the existing if module == "fetch" block:

  • sizejs_blob_size (DOUBLE return)
  • typejs_blob_type + nanbox_string_inline
  • arrayBuffer / bytes / text → respective Promise-returning FFIs + nanbox_pointer_inline
  • slice → 0-3 args with double_literal(f64::NAN) sentinels for missing numeric args and "0" for missing type pointer; returns the new blob handle as f64 directly.

6 new module.declare_function(...) rows in runtime_decls.rs with the matching ABIs.

Out of scope

Test plan

  • New test-files/test_issue_234_blob_methods.ts covers 11 cases: size, text, arrayBuffer + Uint8Array roundtrip, bytes alias, multi-byte UTF-8 + Buffer.from roundtrip, slice(start, end), slice() (no args, full clone), slice(start) (defaults end to length), slice(0, 3, "text/plain") (typed slice), empty body, negative slice indices. All match node --experimental-strip-types byte-for-byte.
  • test-files/test_issue_227_array_buffer_bytes.ts (the response.arrayBuffer() returns metadata-only object — body bytes never reach TS #227 regression) continues to match Node byte-for-byte.
  • test-files/test_gap_fetch_response.ts continues to match Node modulo the pre-existing experimental-strip-types preamble.
  • cargo build --release -p perry-runtime -p perry-stdlib -p perry clean.

Refs

…nstance methods

Pre-fix `await response.blob()` returned a metadata-only stub `{size, type}`
and silently dropped `resp.body`. Calls to `blob.arrayBuffer()` / `.text()` /
`.bytes()` / `.slice()` got nothing because no codegen dispatch arms existed.

Three-part fix:

1. Runtime (perry-stdlib/src/fetch.rs): new BlobData + BLOB_REGISTRY +
   alloc_blob mirroring HEADERS_REGISTRY/alloc_headers. js_response_blob
   rewritten to clone body bytes + content-type into a fresh BlobData,
   register it, resolve the promise with the numeric handle as f64. 6 new
   FFIs: js_blob_size, js_blob_type, js_blob_array_buffer (allocates real
   BufferHeader same shape as PerryTS#232's response.arrayBuffer fix),
   js_blob_bytes (alias), js_blob_text (UTF-8 lossy decode matching WHATWG
   replacement-character semantics), js_blob_slice with NaN sentinels for
   missing numeric args, null type_ptr to inherit, negative-index
   normalisation per WHATWG Blob spec.

2. HIR (perry-hir/src/destructuring.rs): two new lower_var_decl arms next
   to the existing Response.clone() propagation. `const blob = await
   <recv>.blob()` where <recv> is fetch::Response → register blob::Blob.
   `const b2 = <recv>.slice(...)` where <recv> is blob::Blob → propagate
   so chained slicing keeps Blob-ness. Existing native-instance dispatch
   in expr_call.rs / expr_member.rs then routes .text() / .size / etc. to
   NativeMethodCall { module: "blob" } automatically.

3. Codegen (perry-codegen/src/lower_call.rs + runtime_decls.rs): new
   `if module == "blob"` arm after the existing fetch arm, dispatching
   size → DOUBLE return, type → STRING_TAG NaN-box, arrayBuffer/bytes/text
   → Promise pointer + nanbox_pointer_inline, slice → 0-3 args with
   double_literal(f64::NAN) sentinels. 6 new declare_function rows.

Test: test-files/test_issue_234_blob_methods.ts covers 11 cases (size,
text, arrayBuffer + Uint8Array roundtrip, bytes alias, multi-byte UTF-8 +
Buffer.from roundtrip, slice with various arg shapes, empty body, negative
indices) — all match `node --experimental-strip-types` byte-for-byte.

`blob.stream()` deferred to PerryTS#237 (Web Streams API + ReadableStream needed
project-wide before scoping a Blob-only stub).

Closes PerryTS#234
Refs PerryTS#237 (blob.stream() follow-up)
The new regression test from issue PerryTS#234 includes a Buffer.from(blob.arrayBuffer())
round-trip in the multi-byte-UTF-8 case. Same macOS-14 SDK/linker pattern as
test_gap_buffer_ops / test_buffer_small_alloc / test_issue_227_array_buffer_bytes /
test_gap_fetch_response — compiles and runs byte-for-byte against Node on
local macOS 15.x+, only the macOS-14 GitHub runner is affected.

Add to SKIP_TESTS in compile-smoke (.github/workflows/test.yml) and to
known_failures.json as ci-env, mirroring the v0.5.354 fold-in pattern for PerryTS#232.
proggeramlug added a commit that referenced this pull request Apr 28, 2026
… cleanup

Folds PR #238 (TheHypnoo, closes #234 — real Blob with arrayBuffer/text/
bytes/slice instance methods) on top of v0.5.358. Two correctness/cleanup
edits applied during merge:

(a) crates/perry-stdlib/src/fetch.rs::js_blob_slice — when type_ptr is
    null, default the new blob's content_type to String::new() instead of
    inheriting the original blob's type. Per WHATWG Blob spec / MDN: when
    contentType is absent, the new blob's type is the empty string. Caught
    by edge audit (Node returns "" for `b.slice(0, 1).type` when called
    without a type arg, original PR returned the inherited type). Same fix
    to the inner string_from_header(...).unwrap_or(...) decode-failure
    fallback.

(b) crates/perry-codegen/src/lower_call.rs slice arm had a duplicate
    comment block restating the NaN-sentinel rationale twice. Collapsed
    into one block.

Bumped version 0.5.358 → 0.5.359 (above origin's parallel-track v0.5.357
and v0.5.358 which landed since the PR's 0.5.356 base). Refs #237
(blob.stream() Web Streams follow-up).
@proggeramlug
Copy link
Copy Markdown
Contributor

Folded in via main as commits 1264fb1 (your fix) + 9e72f26 (your CI skip) + d1aa185 (maintainer fold-in: v0.5.359 bump + slice() type-default spec fix per WHATWG + duplicate-comment cleanup). Thanks for the fix — closing as merged. Refs #237 for blob.stream() follow-up.

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.

response.blob() drops body bytes — same shape as #227, needs Blob instance methods

2 participants