feat(mock): Animated as a live node graph with subscribing wrappers#76
Open
danfry1 wants to merge 2 commits into
Open
feat(mock): Animated as a live node graph with subscribing wrappers#76danfry1 wants to merge 2 commits into
danfry1 wants to merge 2 commits into
Conversation
The mock's Animated was a snapshot system: numeric interpolate() returned a
dead value computed at call time (string interpolation was live via closure —
the two paths disagreed), add/multiply/&c coerced node operands to 0 and
snapshotted, setOffset/flattenOffset/extractOffset were no-ops, plain values
lacked __getValue(), and the Animated.* wrappers resolved style once at
render and never re-rendered — so a setValue()/timing().start() after render
silently kept the stale style. This is the class real-app bake-offs had to
monkeypatch around, and the crosscheck corpus explicitly excluded it.
Now:
- AnimatedNode base with live __getValue() and listener fan-out; derived
nodes (AnimatedInterpolation, AnimatedOp, AnimatedDiffClamp) subscribe to
their parents at construction and recompute on every read. Numeric
interpolations chain; string-interpolation chaining still throws like RN.
- Wrappers collect every node in their style and re-render on change (the
mock equivalent of real RN's createAnimatedComponent update path);
createAnimatedComponent gets the same treatment. Subscriptions are
re-established per render and removed on unmount.
- Offsets implement RN semantics on Value and ValueXY (the PanResponder drag
pattern); ValueXY.addListener reports the joint {x,y}.
- useAnimatedValue/XY/Color are useRef-memoized hooks (previously each
render minted a fresh node — resetting animation state — and rebuilt the
whole Animated namespace); they now require a component context, exactly
like on-device. The RN-0.86 export tests were exercising them bare and
are updated to render through a probe component.
Trust wiring: three new crosscheck probes gate the fixed class against real
React Native — post-render setValue reflow, live interpolation, live
transform — returning the OBSERVED style values so the engines are compared
on what a test would read. Corpus 75 → 78, all matching; verified live
against real RN 0.86. 17 new unit tests cover graph semantics, offsets,
listener propagation, wrapper subscription/unmount, and hook stability;
the 139 pre-existing Animated tests pass unchanged.
… parity)
Pre-PR review findings, both rooted in one design gap — user listeners and
graph propagation shared a single listener map with no detach lifecycle:
- Render-constructed derived nodes leaked: style={{opacity: v.interpolate(…)}}
builds a new interpolation each re-render, and each subscribed permanently
to v (O(N²) work across N updates). Derived nodes now attach to their
parents LAZILY — only while they have a consumer of their own — mirroring
RN's __attach/__detach; when the wrapper unsubscribes from a stale
interpolation it detaches from the source and can be collected.
- removeAllListeners() severed view updates and derived-node propagation,
where real RN removes only JS listeners. Graph edges (attached wrappers,
derived nodes, timing-tracking, color channels) now live in a separate
children set that removeAllListeners never touches.
diffClamp stays a PERMANENT child of its parent (it accumulates history and
must observe every move); tracking and color channels are children too, so
they survive the source's removeAllListeners like their RN counterparts.
The offset comment now states the real reason none of the three offset ops
notify; the registry hooks lazily initialize their useRef.
New tests: render-loop leak bound (children ≤ 1 after five re-renders),
post-unmount consumer emptiness, removeAllListeners keeps the mounted
component updating while user listeners stop. 158 Animated tests + 78/78
crosscheck probes green.
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.
Problem
The mock engine's Animated was a snapshot system — the largest fidelity gap the code audit found, and the class real-app bake-offs (react-native-paper) had to monkeypatch around:
interpolate()returned a dead value computed at call time (string interpolation was live via closure — the two paths disagreed with each other).add/subtract/multiply/divide/modulosnapshotted, and coerced node operands (e.g. interpolations) to 0.setOffset/flattenOffset/extractOffsetwere no-ops — the canonical PanResponder drag pattern silently did nothing.__getValue()(RN's own tests call it).Animated.*wrappers resolved style once at render and never re-rendered: asetValue()ortiming().start()after render kept the stale style, sotoHaveStyleassertions silently tested nothing. The crosscheck corpus explicitly excluded this class — the trust mechanism was blind exactly where the mock was weakest.Change
A live node graph mirroring real RN's architecture:
AnimatedNodebase with live__getValue(); derived nodes (AnimatedInterpolation,AnimatedOp,AnimatedDiffClamp) recompute from their parents on every read. Numeric interpolations chain; derived nodes are valid operands; string-interpolation chaining still throws like RN.__attach/__detachmodel):removeAllListeners()removes only JS listeners — mounted components and derived nodes keep updating, exactly like on-device. Derived nodes attach to parents lazily (only while they have consumers), so the common pattern of constructing an interpolation inside a render function cannot accumulate subscriptions on the source across re-renders.Animated.View/Text/… andcreateAnimatedComponentregister as graph children of every node in their style; post-rendersetValue/timing().start()updates the rendered style. Subscriptions are re-established per render and removed on unmount.ValueandValueXY;ValueXY.addListenerreports the joint{x, y}.useAnimatedValue/XY/Colorare real (lazily-initialized,useRef-memoized) hooks — the value survives re-renders instead of silently resetting; like on-device, they now require a component context (the RN-0.86 export tests exercised them bare and are updated).Trust wiring
Three new crosscheck probes gate the fixed class against real React Native: post-render
setValuereflow, live interpolation, and live transform — returning the observed style values so the engines are compared on exactly what a test would read. Corpus 75 → 78, all matching, verified live against RN 0.86. (The probes were also validated the honest way: before the mock fix they diverged — native live, mock stale.)Review
Independently adversarially reviewed pre-PR (no blockers). Both should-fixes — the render-loop listener leak and the
removeAllListenersdivergence, sharing the single-listener-map root cause — are fixed via the children/listeners separation, with regression tests (leak bound across re-renders, post-unmount consumer emptiness, removeAllListeners keeps views updating). The reviewer verified the offset-inclusive notify and tracking semantics against real RN's source, and confirmed no re-entrancy in the wrapper effect.Tests
20 new unit tests (graph liveness, chaining, ops with node operands, offsets/drag pattern, joint listeners, wrapper subscribe/unmount/leak-bound, hook stability, preserved shapes incl. divide-by-zero bug-compat) + 3 crosscheck probes; the 139 pre-existing Animated tests pass unchanged.
Full gate: mock 1311, native suites green, crosscheck 78/78, fidelity artifacts regenerated, lint/format/typecheck clean.