Skip to content

fix(worker_threads): preserve structured clone values - #7091

Merged
proggeramlug merged 5 commits into
mainfrom
fix/6763-broadcast-channel-typed-clone
Jul 31, 2026
Merged

fix(worker_threads): preserve structured clone values#7091
proggeramlug merged 5 commits into
mainfrom
fix/6763-broadcast-channel-typed-clone

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the JSON-only MessagePort/BroadcastChannel queue with typed snapshots for top-level TypedArrays and ArrayBuffers
  • recursively reject uncloneable values (including nested marked objects, functions, symbols, and MessagePorts) while preserving Node's ArrayBuffer marker exception
  • throw branded DataCloneError/InvalidStateError DOMExceptions and validate a closed BroadcastChannel before cloneability
  • resolve typed-array prototype methods when a cloned value reaches the dynamic property path

This is a focused increment on the worker_threads parity umbrella; it does not close the remaining constructor, EventTarget, transfer-list, or nested structured-clone gaps.

Refs #6763

Verification

  • cargo check -p perry-runtime -p perry-stdlib
  • cargo test -p perry --test issue_6763_broadcast_clone -- --nocapture
  • Node/Perry byte parity:
    • worker_threads/broadcast-channel/close-and-clone.ts
    • worker_threads/broadcast-channel/error-precedence.ts
    • worker_threads/transfer-markers/uncloneable-post.ts
  • full BroadcastChannel folder audit: 7/16 currently pass; remaining mismatches are outside this clone-focused increment

Summary by CodeRabbit

  • Bug Fixes
    • Improved same-agent worker_threads structured cloning to preserve ArrayBuffer/typed-array contents and bigint typed-array identity.
    • Message posting now rejects uncloneable values with a DataCloneError.
    • Fixed inherited property resolution for custom typed-array prototypes (including buffer-backed Uint8Array) and improved prototype-cycle handling.
    • Sending to a closed BroadcastChannel now throws InvalidStateError.
  • Tests
    • Expanded structured-clone integration coverage for typed-array observable identity and error names/codes.
    • Added typed-array bigint lane snapshot and GC prototype-resolution root scanning regression tests.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c2bc8f79-bd53-489b-9c47-1fe0983651fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5b7d049 and 17c3566.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/object/prototype_chain.rs

📝 Walkthrough

Walkthrough

Worker message channels now serialize typed arrays and ArrayBuffers as structured snapshots, recursively reject uncloneable message graphs, and emit DOM-style clone/state errors. Typed-array property lookup resolves inherited properties through custom prototypes, with cycle protection, GC root scanning, and exception-safe restoration.

Changes

Structured clone runtime behavior

Layer / File(s) Summary
Prototype-aware typed-array lookup
crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs, crates/perry-runtime/src/object/prototype_chain.rs, crates/perry-runtime/src/exception.rs, crates/perry-runtime/src/gc/...
Typed-array property reads resolve inherited values through custom or builtin prototypes; prototype walks detect cycles, remain GC-rooted during collection, and restore resolution state during exception unwinding.
BigInt typed-array snapshot support
crates/perry-runtime/src/typedarray/access.rs, crates/perry-runtime/src/typedarray/mod.rs, crates/perry-stdlib/src/worker_threads.rs
Exact 64-bit BigInt lanes are captured and restored in structured snapshots with typed-array kind preservation.
Message snapshots and dispatch
crates/perry-stdlib/src/worker_threads.rs, crates/perry-stdlib/src/worker_threads/channel_pump.rs
Worker channel inboxes and dispatch structures use SerializedMessage snapshots for serialization, queueing, and deserialization.
Clone validation and error coverage
crates/perry-stdlib/src/worker_threads.rs, crates/perry/tests/issue_6763_broadcast_clone.rs, changelog.d/7091-worker-structured-clone.md
Message posting validates submitted graphs, reports DataCloneError and InvalidStateError through DOM exceptions, and tests binary cloning, getter receiver identity, and error outputs end to end.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Suggested labels: type:bug

Sequence Diagram(s)

sequenceDiagram
  participant Sender
  participant MessagePort
  participant CloneValidator
  participant SerializedMessage
  participant ChannelPump

  Sender->>MessagePort: post message
  MessagePort->>CloneValidator: validate submitted graph
  CloneValidator-->>MessagePort: cloneability result
  MessagePort->>SerializedMessage: serialize binary or ordinary value
  SerializedMessage->>ChannelPump: enqueue snapshot
  ChannelPump->>SerializedMessage: deserialize snapshot
  SerializedMessage-->>Sender: deliver cloned message
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main worker_threads structured-clone fix.
Description check ✅ Passed The description covers the summary, issue reference, and verification steps; only the template’s explicit Changes/checklist sections are missing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/6763-broadcast-channel-typed-clone

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
crates/perry/tests/issue_6763_broadcast_clone.rs (1)

12-122: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider mirroring this coverage in a unit test.

As per coding guidelines, crates/*/tests/*.rs integration suites are lower-priority than unit tests visible to cargo-test: Prefer acceptance coverage in unit tests visible to cargo-test, because integration suites under crates/*/tests/*.rs do not run on every PR. Based on learnings, a past PR confirmed that changed suites under this path do run via the diff-scoped e2e-scoped CI job for that PR, which reduces (but doesn't eliminate) the risk here.
[recommended_refactor:low_effort_and_low_reward]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry/tests/issue_6763_broadcast_clone.rs` around lines 12 - 122,
Mirror the coverage from
broadcast_and_port_clone_preserve_binary_values_and_errors in a unit test
visible to cargo-test, rather than relying solely on the integration test under
crates/*/tests. Preserve assertions for typed-array cloning, closed-port errors,
markAsUncloneable behavior, and ArrayBuffer cloning, while keeping the existing
integration coverage unchanged unless reuse is straightforward.

Sources: Coding guidelines, Learnings

crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs (1)

544-571: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate prototype-fallback logic between TypedArray and buffer branches.

The prototype-resolution/fallback logic (Lines 553-571 and 616-632) is identical except for the fallback prototype name. Consider extracting a shared helper to avoid future divergence.

♻️ Suggested helper extraction
+fn resolve_inherited_via_prototype(
+    addr: usize,
+    key: *const crate::StringHeader,
+    fallback_name: &str,
+) -> Option<JSValue> {
+    let proto = match super::super::prototype_chain::object_static_prototype(addr) {
+        Some(crate::value::TAG_NULL) => None,
+        Some(bits) => Some(f64::from_bits(bits)),
+        None => Some(crate::object::builtin_prototype_value(fallback_name)),
+    };
+    let proto_value = JSValue::from_bits(proto?.to_bits());
+    if proto_value.is_pointer() {
+        let inherited = super::super::js_object_get_field_by_name(
+            proto_value.as_pointer::<ObjectHeader>(),
+            key,
+        );
+        if !inherited.is_undefined() {
+            return Some(inherited);
+        }
+    }
+    None
+}

Then both call sites reduce to:

-                    let proto = match super::super::prototype_chain::object_static_prototype(addr) {
-                        Some(crate::value::TAG_NULL) => None,
-                        Some(bits) => Some(f64::from_bits(bits)),
-                        None => Some(crate::object::builtin_prototype_value(
-                            crate::typedarray::name_for_kind(kind),
-                        )),
-                    };
-                    if let Some(proto) = proto {
-                        let proto_value = JSValue::from_bits(proto.to_bits());
-                        if proto_value.is_pointer() {
-                            let inherited = super::super::js_object_get_field_by_name(
-                                proto_value.as_pointer::<ObjectHeader>(),
-                                key,
-                            );
-                            if !inherited.is_undefined() {
-                                return inherited;
-                            }
-                        }
-                    }
+                    if let Some(inherited) =
+                        resolve_inherited_via_prototype(addr, key, crate::typedarray::name_for_kind(kind))
+                    {
+                        return inherited;
+                    }

Also applies to: 611-632

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs` around
lines 544 - 571, Extract the duplicated prototype-resolution and
inherited-property lookup logic from the typed-array and buffer branches into a
shared helper near the existing field-get utilities. Parameterize the helper
with the fallback prototype value or name, then replace both blocks around the
typed-array branch and buffer branch with calls to it while preserving custom
prototypes, null handling, and undefined-result behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs`:
- Around line 544-571: Prevent unbounded prototype recursion in the TypedArray
fallback lookup and the custom constructor-prototype hop around
resolve_inherited_field. Reuse the existing max-depth/visited tracking approach
to detect cycles and return undefined when a prototype repeats or the limit is
reached, while preserving inherited property resolution for valid chains.

In `@crates/perry-stdlib/src/worker_threads.rs`:
- Around line 598-651: Exclude generic typed arrays from
message_value_is_uncloneable by checking lookup_typed_array_kind(raw) alongside
is_registered_buffer, while preserving existing cloneability handling. In
crates/perry/tests/issue_6763_broadcast_clone.rs:12-122, extend the fixture to
post a non-Uint8Array typed array such as Int32Array both top-level and nested
in a plain object, asserting both clones round-trip correctly.
- Around line 511-519: Update SerializedMessage and the serialized_message path
to preserve BigInt64Array and BigUint64Array values without converting lanes
through f64. For these typed-array kinds, use a 64-bit-backed snapshot
representation and reconstruct the corresponding typed array with exact integer
values; alternatively, bypass this snapshot path for them. Keep existing
behavior unchanged for other typed-array kinds and JSON values.

---

Nitpick comments:
In `@crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs`:
- Around line 544-571: Extract the duplicated prototype-resolution and
inherited-property lookup logic from the typed-array and buffer branches into a
shared helper near the existing field-get utilities. Parameterize the helper
with the fallback prototype value or name, then replace both blocks around the
typed-array branch and buffer branch with calls to it while preserving custom
prototypes, null handling, and undefined-result behavior.

In `@crates/perry/tests/issue_6763_broadcast_clone.rs`:
- Around line 12-122: Mirror the coverage from
broadcast_and_port_clone_preserve_binary_values_and_errors in a unit test
visible to cargo-test, rather than relying solely on the integration test under
crates/*/tests. Preserve assertions for typed-array cloning, closed-port errors,
markAsUncloneable behavior, and ArrayBuffer cloning, while keeping the existing
integration coverage unchanged unless reuse is straightforward.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc50bade-0204-49a7-afa9-66bcb29090fc

📥 Commits

Reviewing files that changed from the base of the PR and between c86835f and 6281e2d.

📒 Files selected for processing (5)
  • changelog.d/7091-worker-structured-clone.md
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-stdlib/src/worker_threads.rs
  • crates/perry-stdlib/src/worker_threads/channel_pump.rs
  • crates/perry/tests/issue_6763_broadcast_clone.rs

Comment thread crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
Comment thread crates/perry-stdlib/src/worker_threads.rs
Comment thread crates/perry-stdlib/src/worker_threads.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs`:
- Around line 555-579: Update both fallback paths in get_field_by_name.rs at
lines 555-579 and 624-646 to use a receiver-aware inherited-lookup helper,
passing addr as the accessor receiver instead of invoking
js_object_get_field_by_name with the builtin prototype; apply this for both the
per-kind TypedArray prototype and the Uint8Array prototype.

In `@crates/perry-runtime/src/object/prototype_chain.rs`:
- Around line 47-86: Replace the raw usize owner identities used by
PrototypeResolutionGuard and PROTOTYPE_RESOLUTION_STACK with GC-stable,
rooted/updateable object handles or the runtime’s stable object identity
mechanism, so identities remain valid across accessor/proxy-triggered moving GC
and reentrant lookups. Update enter, Drop, resolution_stack_savepoint, and
resolution_stack_restore consistently, and add a regression test covering GC
reentry during js_object_get_field_by_name that verifies cyclic prototype
resolution terminates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d4489994-d535-4c9f-a180-8ae535599246

📥 Commits

Reviewing files that changed from the base of the PR and between 6281e2d and 5715714.

📒 Files selected for processing (7)
  • crates/perry-runtime/src/exception.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-runtime/src/object/prototype_chain.rs
  • crates/perry-runtime/src/typedarray/access.rs
  • crates/perry-runtime/src/typedarray/mod.rs
  • crates/perry-stdlib/src/worker_threads.rs
  • crates/perry/tests/issue_6763_broadcast_clone.rs

Comment thread crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
Comment thread crates/perry-runtime/src/object/prototype_chain.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry-runtime/src/object/prototype_chain.rs (1)

438-442: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Root saved receiver state across reentrant property reads. Both paths restore a raw saved receiver after an operation that can invoke user code and trigger moving GC; that saved value is neither rooted nor rewritten.

  • crates/perry-runtime/src/object/prototype_chain.rs#L438-L442: store previous_this in an updateable runtime handle before js_proxy_get, then reload before restoring it.
  • crates/perry-runtime/src/object/prototype_chain.rs#L458-L466: do the same around recursive lookup, including prev_override when it holds a JSValue.

Based on learnings, raw Rust pointer locals are neither GC roots nor reliable pins across user-code-invoking operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/prototype_chain.rs` around lines 438 - 442,
Update both receiver-saving paths in
crates/perry-runtime/src/object/prototype_chain.rs at lines 438-442 and 458-466:
store previous_this in an updateable runtime handle before js_proxy_get, and
reload it before restoring after the reentrant operation. Apply the same
handle-based treatment around recursive lookup, including prev_override when it
contains a JSValue; do not restore raw pointer locals across user-code-invoking
calls.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/perry-runtime/src/object/prototype_chain.rs`:
- Around line 438-442: Update both receiver-saving paths in
crates/perry-runtime/src/object/prototype_chain.rs at lines 438-442 and 458-466:
store previous_this in an updateable runtime handle before js_proxy_get, and
reload it before restoring after the reentrant operation. Apply the same
handle-based treatment around recursive lookup, including prev_override when it
contains a JSValue; do not restore raw pointer locals across user-code-invoking
calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a574d51-0b30-406f-9130-2feb6ce40766

📥 Commits

Reviewing files that changed from the base of the PR and between 5715714 and 5b7d049.

📒 Files selected for processing (5)
  • crates/perry-runtime/src/gc/mod.rs
  • crates/perry-runtime/src/gc/tests/runtime_roots.rs
  • crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
  • crates/perry-runtime/src/object/prototype_chain.rs
  • crates/perry/tests/issue_6763_broadcast_clone.rs

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Addressed the late CodeRabbit outside-diff finding in 17c3566: both inherited-lookup paths now root and reload the saved implicit receiver state across reentrant Proxy/accessor calls, including the optional accessor override. Validation: cargo fmt/check, both prototype-resolution GC root tests, and the full issue_6763_broadcast_clone integration test (1 passed, 156.55s).

@proggeramlug
proggeramlug merged commit dbb2490 into main Jul 31, 2026
7 checks passed
@proggeramlug
proggeramlug deleted the fix/6763-broadcast-channel-typed-clone branch July 31, 2026 01:09
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