Skip to content

fix(compilation): deep-clone Date/RegExp/TypedArray/Buffer/Error in compiled structuredClone; throw DataCloneError on uncloneable#1257

Merged
nickna merged 1 commit into
mainfrom
worktree-issue-1255
Jul 5, 2026
Merged

fix(compilation): deep-clone Date/RegExp/TypedArray/Buffer/Error in compiled structuredClone; throw DataCloneError on uncloneable#1257
nickna merged 1 commit into
mainfrom
worktree-issue-1255

Conversation

@nickna

@nickna nickna commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #1255: the compiled/emitted structured-clone core ($Runtime.StructuredCloneStructuredCloneCore) was shallow — it only deep-cloned List<object?>/Dictionary<string,object?>/Dictionary<object,object?>/HashSet<object?> and returned everything else by reference, diverging from the interpreter's StructuredClone.Clone in two ways:

  1. Cloneable-but-non-container types were aliased instead of deep-clonedDate, RegExp, TypedArrays, ArrayBuffer, Buffer, Error instances.
  2. Uncloneable values were silently passed through instead of throwing DataCloneError — functions/closures, symbols, class instances, Promise, etc., both at the top level and nested inside objects/arrays.

Fix

  • StructuredCloneCore now deep-clones $TSDate, $RegExp, $TypedArray/$ArrayBuffer (independent copy, or a shared view when backed by a SharedArrayBuffer), $Buffer, $Object (recurses into its Fields dict), and the built-in $Error hierarchy (name-based reconstruction + Stack preservation).
  • The previous alias-on-fallback is flipped to throw a new standalone $DataCloneError : Exception (no SharpTS.dll reference — stays fully standalone), matching the interpreter's exhaustive switch. This now correctly rejects uncloneable values at any nesting depth, not just the top level.
  • A value whose CLR type lives outside the emitted assembly (e.g. an interpreter runtime value crossing in via CompiledMessagePortBridge, which already structured-clones on the interpreter side before handing off) passes through unchanged rather than throwing — only the compiled program's own uncloneable constructs throw.
  • $MessagePort.PostMessage / $BroadcastChannel.PostMessage now catch $DataCloneError around the clone call and convert it to the existing receiver-side 'messageerror' sentinel, replacing the old top-level typeof pre-check (which missed nested uncloneables). $BroadcastChannel gained messageerror support end-to-end (sentinel + Drain handling), matching $MessagePort.
  • WrapException recognizes $DataCloneError and returns its raw message string, matching the interpreter's catch binding for a non-guest-throw exception.

Bugs found and fixed along the way

  • SharpTSBroadcastChannel.EnqueueMessageError's scheduled closure never invoked the property-style onmessageerror handler (only the EventEmitter event fired), and re-checked _closed at fire time — dropping an already-enqueued notification if the receiver closed before the timer ran, inconsistent with DrainPendingMessages' "in-flight deliveries complete after Close()" behavior.

Test plan

  • dotnet test — 15452/15452 passing (22 new tests added)
  • SharpTS.Test262 — 22/0 (4 skipped diagnostics), unchanged
  • SharpTS.TypeScriptConformance — 31/0, unchanged
  • Manual dual-mode verification of every example from the issue (Date/TypedArray/function/nested-function/symbol), plus RegExp/Buffer/Error/class-instance/plain-object, matching interpreter output exactly
  • --compile --verify and --compile --standalone --verify both pass (no SharpTS.dll dependency introduced)
  • New regression tests for $MessagePort/$BroadcastChannel messageerror (including a nested-uncloneable case) and structuredClone deep-clone/throw behavior in SharpTS.Tests/SharedTests/WorkerThreadsTests.cs and BroadcastChannelTests.cs

…ompiled structuredClone and throw DataCloneError on uncloneable values (#1255)

Compiled StructuredCloneCore previously only deep-cloned List/Dictionary/
HashSet and aliased everything else by reference (Date, RegExp, TypedArray,
ArrayBuffer, Buffer, Error) or silently passed through uncloneable values
(functions, symbols, class instances, Promises) instead of throwing, unlike
the interpreter's StructuredClone.Clone. Extends cloneCore to deep-clone the
former and throw a new standalone $DataCloneError for the latter — at any
nesting depth, not just top-level — flipping the previous alias-fallback to
throw by default so unrecognized constructs fail closed like the
interpreter's exhaustive switch. $MessagePort/$BroadcastChannel now catch
$DataCloneError around their clone call and convert it to the existing
receiver-side 'messageerror' sentinel instead of relying on a narrower
top-level typeof pre-check. WrapException recognizes $DataCloneError so a
caught value is the raw message string, matching the interpreter's non-
guest-throw catch binding.

Also fixes two bugs found along the way: a value whose CLR type lives outside
the emitted assembly (interpreter runtime values crossing via
CompiledMessagePortBridge) must pass through unchanged rather than throw, and
SharpTSBroadcastChannel's onmessageerror property handler was never invoked
(only the EventEmitter event fired) and dropped delivery if the receiver
closed before the scheduled notification ran.
@nickna nickna merged commit adc8dc4 into main Jul 5, 2026
2 checks 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

1 participant