Harden LiveView array wrappers and interop wrapper caches for 4.14#2735
Merged
Conversation
Fixes a set of issues found in a pre-release review of the new LiveView default and the wrapper identity caches: - An array crossing under a non-array declared type now honors that contract like other collections do: a member typed IReadOnlyList<T> produces a read-only view instead of a writable view keyed off the runtime type (which silently mutated host state through a read-only API). Copy mode keeps its snapshot behavior for such members. - The static type-mapper memoization no longer registers the array lane under non-array declared types; a later non-array value of the same declared type (e.g. IEnumerable<int> first carrying int[], then List<int>) crashed with InvalidCastException and poisoned every engine in the process. - ConvertArray's identity-map inserts now replace an entry for a different exposed view of the same array (last view wins) instead of throwing ArgumentException on ConditionalWeakTable.Add. - Engine.Dispose releases the recent-wrapper ring (on by default since 4.14); it previously early-returned when the opt-in identity map was never created, leaving up to 8 host objects strongly rooted. - Array-like wrappers enumerate like JS arrays: Object.keys / for-in / spread yield index keys instead of reflected CLR member names (members like Length stay accessible, they just don't enumerate). Dictionary-shaped targets such as Newtonsoft's JObject keep key enumeration. - The in operator matches JS array semantics on array-like views: "-1 in view" and out-of-range indices are false (the reflected indexer reported presence for any parseable index). - Out-of-range and negative element reads yield undefined (JS array hole semantics) instead of null. - Array.prototype methods (map/join/filter/forEach) route element reads through the boxing-free GetJsValueAt lane instead of boxing through GetAt/FromObject. - The per-element host-boundary constraint probe is skipped for plain memory-backed targets (T[], List<T>) where element access cannot run user CLR code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0115tQFNyyQqc1HQGPLUgZND
This was referenced Jul 22, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Pre-release review hardening for the 4.14 default flips (
Interop.ArrayConversion = LiveView,Interop.CacheRecentObjectWrappers = true). All issues below were verified with concrete repros before fixing.Correctness fixes
TryConvertrouted any runtime array through the LiveView lane keyed offvalue.GetType(), dropping the declared member type — soIReadOnlyList<int>backed byint[]came out as a writableArrayWrapper<int>andh.Data[0] = 42silently mutated host state through a read-only API. Arrays crossing under a non-array declared type now flow through the regular wrapper lane and honor the declared contract (read-only view forIReadOnlyList<T>), the same way other collections already do. Copy mode keeps its snapshot behavior for such members._typeMappersmemoization registeredConvertArrayunder the declared type; withIEnumerable<int>first carryingint[]and laterList<int>, the memoized mapper cast the list toArray→InvalidCastException— and since the map is static, one engine's conversion poisoned every engine in the process. Memoization now only happens when the declared type itself is an array type.ArgumentExceptionon exposed-type change.ConvertArray's CWT inserts lacked the Remove-before-Add the general lane got with the type-guarded probe; a type-guard miss followed byAddthrew. Last view wins now, matching the general lane.Engine.Disposeleaked the recent-wrapper ring. It early-returned when the opt-in identity map was never created — the common configuration since the cache default flip — leaving up to 8 host objects strongly rooted by a disposed engine.Object.keys/for-in/ spread on host arrays yielded reflected CLR member names (Length,LongLength,Rank,...) instead of the0,1,2the 4.13 Copy default produced. Array-like wrappers now enumerate index keys like JS arrays; members stay accessible, they just don't enumerate. Dictionary-shaped hybrids (NewtonsoftJObjectis bothIDictionary<string,_>andIList<_>) keep dictionary key enumeration. Note this also applies to wrapped lists, which previously enumeratedCapacity,Count.-1 in hostArraywastrue. The fall-through consulted the reflected indexer, which reports presence for any parseable index. Numeric membership of an array-like view is now exactly[0, length), including string-form and non-canonical keys ('-1','08').nullinstead ofundefined, breakingwhile (a[i] !== undefined)idioms; they now read like JS array holes (matchingArrayLikeOperations.Get, which already did this).Perf
Array.prototypemethods (map/join/filter/forEach/indexOf) on array-like wrappers routed element reads through the boxing path (GetAt+FromObject), missing the Convert primitive array-like wrapper elements without boxing #2731 boxing-free lane entirely;ArrayLikeOperations.ReadValuenow usesGetJsValueAt.Getlane is skipped for plain memory-backed targets (T[],List<T>) where element access cannot execute user CLR code; customIList<T>/IReadOnlyList<T>implementations keep the post-access probe.Docs
ArrayConversionMode.LiveViewandTrackObjectWrapperIdentityXML docs updated: declared-type behavior, index-key enumeration,undefinedfor out-of-range reads,instanceof ArrayvsArray.isArray, and the typed-array-like partial-write behavior ofshift/spliceon fixed-size views.Full suite green on net10.0 + net472 (Jint.Tests, PublicInterface, CommonScripts) and Test262.
🤖 Generated with Claude Code
https://claude.ai/code/session_0115tQFNyyQqc1HQGPLUgZND