diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0a5aa548..6d62dbebf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,5 +53,6 @@ jobs: IOS_TEST_TIMEOUT_MS: "600000" IOS_TEST_INACTIVITY_TIMEOUT_MS: "180000" IOS_LOG_JUNIT: "1" + IOS_TEST_VERBOSE_SPECS: "1" IOS_SIMCTL_QUERY_TIMEOUT_MS: "10000" run: npm run test:ios diff --git a/.oracle-context/ns-rns-first-paint/demo-react-navigation.tsx b/.oracle-context/ns-rns-first-paint/demo-react-navigation.tsx new file mode 100644 index 000000000..fa5fd5671 --- /dev/null +++ b/.oracle-context/ns-rns-first-paint/demo-react-navigation.tsx @@ -0,0 +1,388 @@ + const [transition, setTransition] = useState("idle"); + + useEffect(() => { + const start = navigation.addListener("transitionStart", (event) => { + logReactNavTiming( + `home-transition-start-${event.data.closing ? "closing" : "opening"}`, + ); + setTransition(event.data.closing ? "closing" : "opening"); + }); + const end = navigation.addListener("transitionEnd", (event) => { + logReactNavTiming( + `home-transition-end-${event.data.closing ? "closing" : "opening"}`, + ); + dumpNativeScriptRNSGeometry( + `home-transition-end-${event.data.closing ? "closing" : "opening"}`, + ); + setTransition(event.data.closing ? "closed" : "open"); + }); + + return () => { + start(); + end(); + }; + }, [navigation]); + + const pushDetail = useCallback(() => { + logReactNavTiming("push-onPress"); + navigation.push("Detail", { serial: Date.now() % 10000 }); + scheduleTraceExport("push", 1200); + }, [navigation]); + + const presentModal = useCallback(() => { + logReactNavTiming("modal-present-onPress"); + navigation.navigate("Modal"); + scheduleTraceExport("modal-present", 1200); + }, [navigation]); + + return ( + + + + RNS native-stack parity + + + {buildDescription} + + + + + + NativeScript port path + + + {rows.map(([label, value]) => ( + + + {label} + + + {value} + + + ))} + + + + + Push Detail + + + + Present Modal Route + + + + + + {transition} + + + transition event + + + + + {headerPing.count} + + + header pings + + + + + 26 + + + iOS target + + + + + {headerPing.menuCount} + + + menu actions + + + + + + + Scroll coverage + + + This lower card is here to verify the route uses the full tab height. + The scroll view should move under the native navigation header and tab + bar instead of stopping halfway down the screen. + + + + ); +} + +function DetailScreen({ navigation, route }: DetailProps) { + const headerPing = useContext(HeaderPingContext); + + useEffect(() => { + const start = navigation.addListener("transitionStart", (event) => { + logReactNavTiming( + `detail-transition-start-${event.data.closing ? "closing" : "opening"}`, + ); + }); + const end = navigation.addListener("transitionEnd", (event) => { + logReactNavTiming( + `detail-transition-end-${event.data.closing ? "closing" : "opening"}`, + ); + dumpNativeScriptRNSGeometry( + `detail-transition-end-${event.data.closing ? "closing" : "opening"}`, + ); + }); + + return () => { + start(); + end(); + }; + }, [navigation]); + + return ( + + + + Detail route + + + Pushed with navigation.push. The visible back button is the system + UIKit button in minimal display mode, and the edge swipe should pop + this route. + + + + + + Route params + + + serial = {route.params.serial} + + + + + + + + + + + custom header taps = {headerPing.customCount} + + + This count is driven by a React Navigation headerRight React element + hosted in a UIKit UIBarButtonItem customView. + + + + { + logReactNavTiming("pop-onPress"); + navigation.goBack(); + scheduleTraceExport("pop", 1200); + }} + style={styles.primaryButton} + > + Pop Detail + + + + + Repeated tap guard + + + UIKit owns the transition. React Navigation receives the completed + native stack after the push or pop animation settles. + + + + ); +} + +function DetailHeaderAction() { + const headerPing = useContext(HeaderPingContext); + + return ( + { + logReactNavTiming("custom-header-onPress"); + headerPing.incrementCustomHeader(); + }} + style={styles.customHeaderButton} + > + + {headerPing.customCount > 0 ? headerPing.customCount : "Tap"} + + + ); +} + +function DetailHeaderTitle() { + return ( + + Native Detail + + ); +} + +function ModalScreen({ navigation }: ModalProps) { + useEffect(() => { + const start = navigation.addListener("transitionStart", (event) => { + logReactNavTiming( + `modal-transition-start-${event.data.closing ? "closing" : "opening"}`, + ); + }); + const end = navigation.addListener("transitionEnd", (event) => { + logReactNavTiming( + `modal-transition-end-${event.data.closing ? "closing" : "opening"}`, + ); + }); + + return () => { + start(); + end(); + }; + }, [navigation]); + + const dismissModal = useCallback(() => { + logReactNavTiming("modal-dismiss-onPress"); + navigation.goBack(); + scheduleTraceExport("modal-dismiss", 1200); + }, [navigation]); + + return ( + + + + Modal route + + + This route uses React Navigation presentation options. It is included + to keep checking modal stack semantics in the NativeScript RNS port. + + + + + UIKit presentation + + + This screen is presented by UIKit with React Navigation state still + driving the route lifecycle. Dismissal should animate downward instead + of popping sideways. + + + + Dismiss Modal + + + ); +} + +export default function ReactNavigationTab() { + const [headerPingCount, setHeaderPingCount] = useState(0); + const [customHeaderCount, setCustomHeaderCount] = useState(0); + const [headerMenuCount, setHeaderMenuCount] = useState(0); + + const incrementHeaderPing = useCallback(() => { + logReactNavTiming("header-ping-onPress"); + setHeaderPingCount((count) => count + 1); + }, []); + + const incrementCustomHeader = useCallback(() => { + setCustomHeaderCount((count) => count + 1); + }, []); + + const incrementHeaderMenu = useCallback(() => { + logReactNavTiming("header-menu-onPress"); + setHeaderMenuCount((count) => count + 1); + }, []); + + const headerPingContext = useMemo( + () => ({ + count: headerPingCount, + customCount: customHeaderCount, + menuCount: headerMenuCount, + incrementCustomHeader, + }), + [customHeaderCount, headerMenuCount, headerPingCount, incrementCustomHeader], + ); + + return ( + + + + + [ + { + type: "button", + label: diff --git a/.oracle-context/ns-rns-first-paint/host-ready-layout-transition.ts b/.oracle-context/ns-rns-first-paint/host-ready-layout-transition.ts new file mode 100644 index 000000000..95b3d05ec --- /dev/null +++ b/.oracle-context/ns-rns-first-paint/host-ready-layout-transition.ts @@ -0,0 +1,1509 @@ + : []; + const activeStackKey = stackId ? idsKey(activeStackIds) : undefined; + const hasPendingNativeStackModelBeforeReady = + !!stackId && + (registry.stackPendingReconcileKeys[stackId] != null || + registry.stackNativeKeys[stackId] !== activeStackKey); + const ctx = stackId ? registry.stackContexts?.[stackId] : undefined; + const props = registry.screenProps?.[screenId]; + const isModalScreen = isModalPresentation(props?.stackPresentation); + if ( + previousHandle === contentWrapperViewHandle && + previousReadyKey === readyKey && + wasContentReady && + !hasPendingNativeStackModelBeforeReady + ) { + return false; + } + let contentWrapperView = screenContentWrapperViewFromHostReadyHandle( + screenId, + contentWrapperViewHandle, + registry, + ); + if (!contentWrapperView) { + const registeredContentWrapperView = + registry.screenContentWrapperViews?.[screenId]; + const screenView = screenControllerView(registry.screens?.[screenId]); + if ( + registeredContentWrapperView && + (!screenView || + contentWrapperIsMountedInScreenView( + registeredContentWrapperView, + screenView, + ) || + screenContentWrapperCanMountInScreenView( + screenId, + registry, + screenView, + registeredContentWrapperView, + )) + ) { + contentWrapperView = registeredContentWrapperView; + } + } + traceWorkletEvent( + 'content-wrapper-host-ready-resolved', + `screen=${screenId} handle=${contentWrapperViewHandle} resolved=${ + nativeObjectStableHandle(contentWrapperView) ?? '' + } registered=${ + nativeObjectStableHandle( + registry.screenContentWrapperViews?.[screenId], + ) ?? '' + } window=${windowAttached === true ? 1 : 0} descendants=${ + visibleDescendantCount ?? 0 + }`, + ); + if ( + contentWrapperView && + shouldIgnoreStaleOffWindowContentWrapperHostReady( + screenId, + registry, + contentWrapperView, + windowAttached, + ) + ) { + traceWorkletEvent( + 'content-wrapper-host-ready-ignore-offwindow-stale', + `screen=${screenId} stack=${stackId ?? ''} descendants=${ + visibleDescendantCount ?? 0 + }`, + ); + return false; + } + if (contentWrapperView) { + // NATIVESCRIPT_PORT_DEVIATION: upstream receives the + // RNSScreenContentWrapper component view directly from Fabric mount + // callbacks. NativeScript host-ready crosses the React boundary as a + // generic native handle that can also represent the RNSScreen controller + // host, so only the dedicated ScreenContentWrapper UIView may certify + // content-ready before a push. + const contentWrapperViews = + registry.screenContentWrapperViews ?? + (registry.screenContentWrapperViews = {}); + const hostHandles = + registry.screenContentWrapperHostHandles ?? + (registry.screenContentWrapperHostHandles = {}); + const hostReadyKeys = + registry.screenContentWrapperHostReadyKeys ?? + (registry.screenContentWrapperHostReadyKeys = {}); + const visibleDescendantCounts = + registry.screenContentWrapperVisibleDescendantCounts ?? + (registry.screenContentWrapperVisibleDescendantCounts = {}); + contentWrapperViews[screenId] = contentWrapperView; + hostHandles[screenId] = + nativeObjectStableHandle(contentWrapperView) ?? contentWrapperViewHandle; + hostReadyKeys[screenId] = readyKey; + visibleDescendantCounts[screenId] = visibleDescendantCount ?? 0; + const controllerView = screenControllerView(registry.screens?.[screenId]); + if ( + (visibleDescendantCount ?? 0) > 0 && + screenContentWrapperMatchesMountedFabricChild( + screenId, + registry, + contentWrapperView, + ) + ) { + rememberCommittedScreenContentWrapper( + screenId, + registry, + contentWrapperView, + ); + } + if (controllerView) { + rememberScreenHostHandleForView(screenId, registry, controllerView); + } + } + traceWorkletEvent( + 'content-wrapper-host-ready', + `screen=${screenId} stack=${stackId ?? ''} modal=${ + isModalScreen ? 1 : 0 + } ready=${contentWrapperView ? 1 : 0} previousReady=${ + wasContentReady ? 1 : 0 + } descendants=${visibleDescendantCount ?? 0} window=${ + windowAttached === true ? 1 : 0 + }`, + ); + const isContentReady = screenCanCertifyContentReady(screenId, registry); + const stableCommittedContentRemainsReady = !!( + !isContentReady && + wasContentReady && + previousHandle === contentWrapperViewHandle && + contentWrapperView && + screenHasCurrentStableReadyExposure( + screenId, + registry, + screenControllerView(registry.screens?.[screenId]), + ) + ); + if (isContentReady || stableCommittedContentRemainsReady) { + registry.screenContentReady[screenId] = true; + if (windowAttached === true) { + registry.screenContentWrapperEverWindowAttached[screenId] = true; + } + } else { + registry.screenContentReady[screenId] = undefined; + } + const isActiveNativeStackTopScreen = + !!stackId && + !isModalScreen && + activeStackIds[activeStackIds.length - 1] === screenId; + const canStartNativeStackUpdateFromWrapper = + isActiveNativeStackTopScreen && + stackTopScreenCanStartNativeUpdate(activeStackIds, registry, stackId); + const canUseScreenContent = + isContentReady || stableCommittedContentRemainsReady; + if ( + canUseScreenContent && + stackId && + !isModalScreen && + registry.stackNativeKeys[stackId] === activeStackKey && + registry.stackPendingReconcileKeys[stackId] === activeStackKey + ) { + registry.stackPendingReconcileKeys[stackId] = undefined; + traceWorkletEvent( + 'content-wrapper-host-ready-clear-satisfied-pending', + `screen=${screenId} stack=${stackId} active=${activeStackKey ?? ''}`, + ); + } + const hasPendingNativeStackModel = + !!stackId && + (registry.stackPendingReconcileKeys[stackId] != null || + registry.stackNativeKeys[stackId] !== activeStackKey); + const hasPendingNativeStackTopModel = + hasPendingNativeStackModel && !isModalScreen; + const shouldResumePendingNativeStackFromWindowedWrapper = + windowAttached === true && + canUseScreenContent && + isActiveNativeStackTopScreen && + hasPendingNativeStackTopModel && + !!ctx && + !!stackId && + registry.stackTransitioning[stackId] !== true; + const isFirstWindowAttachAfterOffWindowReady = + windowAttached === true && + typeof previousReadyKey === 'string' && + previousReadyKey.endsWith('|offwindow'); + const currentContentWrapperRefreshKey = contentWrapperView + ? screenContentWrapperRefreshKey( + screenId, + registry, + contentWrapperView, + false, + ) + : undefined; + const previousReadyContentWrapperRefreshKey = + contentWrapperView && typeof previousReadyKey === 'string' + ? screenContentWrapperRefreshKeyWithHostReadyKey( + screenId, + registry, + contentWrapperView, + previousReadyKey, + ) + : undefined; + const previousContentWrapperRefreshKey = + registry.screenContentWrapperRefreshKeys?.[screenId]; + const windowedCommittedWrapperHasStaleRefreshKey = + windowAttached === true && + wasContentReady && + previousReadyKey != null && + previousReadyKey !== readyKey && + currentContentWrapperRefreshKey != null && + previousContentWrapperRefreshKey != null && + previousContentWrapperRefreshKey !== currentContentWrapperRefreshKey && + previousContentWrapperRefreshKey !== previousReadyContentWrapperRefreshKey; + const shouldRefreshWindowedCommittedWrapper = + windowAttached === true && + wasContentReady && + previousReadyKey != null && + previousReadyKey !== readyKey && + typeof previousReadyKey === 'string' && + (!previousReadyKey.endsWith('|window') || + windowedCommittedWrapperHasStaleRefreshKey); + const shouldSkipWindowedCommittedWrapperRefresh = + canUseScreenContent && + wasContentReady && + previousHandle === contentWrapperViewHandle && + previousReadyKey !== readyKey && + windowAttached === true && + !shouldRefreshWindowedCommittedWrapper && + isActiveNativeStackTopScreen && + !isModalScreen && + !!stackId && + registry.stackTransitioning[stackId] !== true; + const shouldSkipWindowedCommittedWrapperRefreshDuringTransition = + canUseScreenContent && + wasContentReady && + previousHandle === contentWrapperViewHandle && + previousReadyKey !== readyKey && + windowAttached === true && + !shouldRefreshWindowedCommittedWrapper && + !isModalScreen && + !!stackId && + registry.stackTransitioning[stackId] === true && + registry.stackNativeKeys[stackId] === activeStackKey && + registry.stackPendingReconcileKeys[stackId] == null; + const shouldSkipReadySameWrapperRefresh = + canUseScreenContent && + wasContentReady && + previousHandle === contentWrapperViewHandle && + previousReadyKey !== readyKey && + windowAttached !== true && + isActiveNativeStackTopScreen && + !isModalScreen && + !!stackId && + registry.stackTransitioning[stackId] !== true; + const parentPresentedModalId = !isModalScreen + ? parentPresentedModalIdForContentScreen(screenId, registry) + : undefined; + const resolvedContentWrapperViewHandle = + contentWrapperView != null + ? nativeObjectStableHandle(contentWrapperView) ?? contentWrapperViewHandle + : contentWrapperViewHandle; + const shouldSkipNoOpNativeStackContainerMutation = + contentWrapperHostReadyIsNoOpNativeStackContainerMutation( + screenId, + stackId, + registry, + previousHandle, + resolvedContentWrapperViewHandle, + previousReadyKey, + readyKey, + windowAttached, + ); + if (shouldSkipNoOpNativeStackContainerMutation) { + traceWorkletEvent( + 'content-wrapper-host-ready-skip-noop-container-mutation', + `screen=${screenId} stack=${stackId ?? ''}`, + ); + return false; + } + if ( + shouldSkipReadySameWrapperRefresh || + shouldSkipWindowedCommittedWrapperRefresh || + shouldSkipWindowedCommittedWrapperRefreshDuringTransition + ) { + if (contentWrapperView) { + const refreshKeys = + registry.screenContentWrapperRefreshKeys ?? + (registry.screenContentWrapperRefreshKeys = {}); + refreshKeys[screenId] = screenContentWrapperRefreshKey( + screenId, + registry, + contentWrapperView, + false, + ); + } + traceWorkletEvent( + 'content-wrapper-host-ready-skip-committed', + `screen=${screenId} stack=${stackId ?? ''} resume=${ + shouldResumePendingNativeStackFromWindowedWrapper ? 1 : 0 + } pending=${hasPendingNativeStackModel ? 1 : 0} ctx=${ + ctx ? 1 : 0 + } activeTop=${isActiveNativeStackTopScreen ? 1 : 0} canUse=${ + canUseScreenContent ? 1 : 0 + } native=${ + stackId ? registry.stackNativeKeys[stackId] ?? '' : '' + } active=${activeStackKey ?? ''} pendingKey=${ + stackId ? registry.stackPendingReconcileKeys[stackId] ?? '' : '' + } windowed=${ + shouldSkipWindowedCommittedWrapperRefresh ? 1 : 0 + } transitioningWindowed=${ + shouldSkipWindowedCommittedWrapperRefreshDuringTransition ? 1 : 0 + } transitioning=${ + stackId && registry.stackTransitioning[stackId] === true ? 1 : 0 + }`, + ); + const didReconcileContainingTab = + canUseScreenContent && stackId + ? reconcileContainingTabControllerFromStackContent( + stackId, + registry, + contentWrapperView, + ) + : false; + if (shouldResumePendingNativeStackFromWindowedWrapper) { + traceWorkletEvent( + 'content-wrapper-host-ready-reconcile-pending', + `screen=${screenId} stack=${stackId}`, + ); + refreshStackContainmentFromHostReady( + stackId, + registry, + ctx, + registry.stacks[stackId], + false, + ); + scheduledReconcileStack( + stackId, + registry, + ctx, + registry.stackNativeKeys[stackId] != null, + ); + return true; + } + return didReconcileContainingTab; + } + const shouldDeferHostRefresh = + contentWrapperView && + canUseScreenContent && + !isFirstWindowAttachAfterOffWindowReady && + shouldDeferReadyContentWrapperRefreshDuringTransition(screenId, registry); + const canPrepareOffWindowHostedContentForTabStack = + windowAttached !== true && + !!stackId && + !!containingTabBarControllerForController(registry.stacks[stackId]); + const shouldSkipHostedRefreshForNativeStackStart = + canStartNativeStackUpdateFromWrapper && + !canPrepareOffWindowHostedContentForTabStack && + !shouldRefreshWindowedCommittedWrapper && + !isModalScreen && + !!stackId && + registry.stackTransitioning[stackId] !== true; + const modalHeaderSuffix = ':modal-header'; + const isModalHeaderScreen = + screenId.indexOf(modalHeaderSuffix) === + screenId.length - modalHeaderSuffix.length; + const shouldSkipOffWindowCommittedWrapperRefresh = + canUseScreenContent && + wasContentReady && + previousHandle === contentWrapperViewHandle && + windowAttached !== true && + (!isModalScreen || isModalHeaderScreen) && + !!stackId && + registry.stackTransitioning[stackId] !== true && + !canStartNativeStackUpdateFromWrapper; + traceWorkletEvent( + 'content-wrapper-host-ready-state', + `screen=${screenId} stack=${stackId ?? ''} contentReady=${ + isContentReady ? 1 : 0 + } wrapperCanStart=${canStartNativeStackUpdateFromWrapper ? 1 : 0} canUse=${ + canUseScreenContent ? 1 : 0 + } ctx=${ctx ? 1 : 0} defer=${shouldDeferHostRefresh ? 1 : 0} skipHosted=${ + shouldSkipHostedRefreshForNativeStackStart ? 1 : 0 + } prepareTab=${ + canPrepareOffWindowHostedContentForTabStack ? 1 : 0 + } transitioning=${ + stackId && registry.stackTransitioning[stackId] === true ? 1 : 0 + }`, + ); + const didCompleteDetachedHeaderDismissal = + completePresentedModalDismissalForDetachedHeaderIfNeeded( + screenId, + stackId, + registry, + wasContentReady, + windowAttached, + ); + if (shouldSkipOffWindowCommittedWrapperRefresh) { + if (contentWrapperView) { + const refreshKeys = + registry.screenContentWrapperRefreshKeys ?? + (registry.screenContentWrapperRefreshKeys = {}); + refreshKeys[screenId] = screenContentWrapperRefreshKey( + screenId, + registry, + contentWrapperView, + false, + ); + } + traceWorkletEvent( + 'content-wrapper-host-ready-skip-offwindow-committed', + `screen=${screenId} stack=${stackId} modalHeader=${ + isModalHeaderScreen ? 1 : 0 + } completedDismiss=${didCompleteDetachedHeaderDismissal ? 1 : 0}`, + ); + return didCompleteDetachedHeaderDismissal; + } + let didReconcileModalContentStack = false; + if ( + canUseScreenContent && + !wasContentReady && + parentPresentedModalId != null && + windowAttached !== true && + !!stackId && + !!ctx && + registry.stackTransitioning[stackId] !== true && + registry.stackNativeKeys[stackId] !== activeStackKey + ) { + registry.stackPendingReconcileKeys[stackId] = activeStackKey; + traceWorkletEvent( + 'content-wrapper-host-ready-modal-content-stack-reconcile', + `screen=${screenId} stack=${stackId} modal=${parentPresentedModalId}`, + ); + scheduledReconcileStack(stackId, registry, ctx, true); + didReconcileModalContentStack = true; + } + let didScheduleParentModalStack = false; + if (canUseScreenContent && !wasContentReady && stackId) { + didScheduleParentModalStack = + scheduleParentModalStackReconcileForContentScreen( + screenId, + registry, + windowAttached, + ); + if (didScheduleParentModalStack) { + traceWorkletEvent( + 'content-wrapper-host-ready-parent-reconcile', + ); + const contentWrapperView = liveScreenContentWrapperViewFromRegistry( + screenId, + registry, + ); + let didMutate = didNormalizeWrapper; + if (controllerView) { + controllerView.hidden = false; + controllerView.alpha = 1; + controllerView.userInteractionEnabled = true; + didMutate = + normalizeScreenControllerViewFrame(screenId, registry, controllerView) || + didMutate; + didMutate = restoreLiveViewInteractionChain(controllerView) || didMutate; + didMutate = + restoreHostedReactSubviewInteractivity(controllerView) || didMutate; + didMutate = + disableStaleEmptyScreenHitTargetDescendants(controllerView) || didMutate; + } + if (contentWrapperView) { + contentWrapperView.userInteractionEnabled = true; + didMutate = + normalizeScreenContentWrapperChildHost(contentWrapperView) || didMutate; + didMutate = + restoreLiveViewInteractionChain( + contentWrapperView, + controllerView ?? undefined, + ) || didMutate; + didMutate = + restoreHostedReactSubviewInteractivity(contentWrapperView) || didMutate; + didMutate = + disableStaleEmptyScreenHitTargetDescendants(contentWrapperView) || + didMutate; + } + didMutate = + refreshScreenContentWrapperTouchHandler( + screenId, + registry, + forceTouchRefresh, + ) || didMutate; + if (controllerView) { + didMutate = + refreshScreenSurfaceTouchHandlerIfNeeded( + screenId, + registry, + controllerView, + contentWrapperView, + forceTouchRefresh, + ) || didMutate; + } + traceWorkletEvent( + 'prepare-visible-screen-interaction', + `screen=${screenId} did=${didMutate ? 1 : 0} force=${ + forceTouchRefresh ? 1 : 0 + }`, + ); + return didMutate; +} +export function screenHostedViewNeedsRefresh( + controller: any, + screenId: string | undefined, + registry: NativeScriptStackRegistry, + rootView: any, + forceRefresh = false, +) { + 'worklet'; + + if (!controller || !rootView) { + return false; + } + if (!screenContentIsReady(screenId, registry)) { + return true; + } + if ( + screenId && + !screenHasVisibleHostedContent(screenId, registry, rootView) + ) { + return true; + } + const refreshKey = screenHostedViewRefreshKey( + controller, + screenId, + registry, + rootView, + ); + const previousRefreshKey = screenId + ? registry.screenHostRefreshKeys[screenId] + : controller.__nativeScriptHostedViewRefreshKey; + if (!forceRefresh && previousRefreshKey === refreshKey) { + return false; + } + rememberScreenHostedViewRefreshKey(controller, screenId, registry, rootView); + return true; +} +export function shouldSkipHostedSubviewRepairDuringNativeTransaction( + screenId: string | undefined, + registry: NativeScriptStackRegistry | undefined, + rootView: any, +) { + 'worklet'; + + if (!screenId || !registry || !rootView) { + return false; + } + const stackId = registry.screenParents?.[screenId]; + const props = registry.screenProps?.[screenId]; + if (!stackId || isModalPresentation(props?.stackPresentation)) { + return false; + } + const hasNativeTransaction = + registry.stackUpdatingModals?.[stackId] === true || + registry.stackTransitioning?.[stackId] === true; + if (!hasNativeTransaction) { + return false; + } + const contentWrapperView = liveScreenContentWrapperViewFromRegistry( + screenId, + registry, + ); + const hasCommittedWrapperContent = + contentWrapperView && + screenContentWrapperHasLiveMountedHostContent( + screenId, + registry, + contentWrapperView, + rootView, + ); + const wrapperMountedInRoot = + !contentWrapperView || + contentWrapperIsMountedInScreenView(contentWrapperView, rootView); + return !!( + screenContentIsReady(screenId, registry) && + wrapperMountedInRoot && + (hasCommittedWrapperContent || + screenHasVisibleHostedContent(screenId, registry, rootView)) + ); +} +export function layoutHostedReactSubviews( + controller: any, + rootViewOverride?: any, + refreshHostedView = true, + repairHostedSubviews = refreshHostedView, + registry?: NativeScriptStackRegistry, + screenId?: string, + forceTouchRefresh = false, + forceRootHostWrapperLayout = false, +) { + 'worklet'; + + const startedAt = traceTimestamp(); + const rootView = rootViewOverride ?? screenControllerView(controller); + if (!rootView) { + return false; + } + let stepStartedAt = traceTimestamp(); + if (registry && screenId) { + refreshScreenSurfaceTouchHandlerIfNeeded( + screenId, + registry, + rootView, + registry.screenContentWrapperViews[screenId], + forceTouchRefresh, + ); + } else { + refreshScreenViewSurfaceTouchHandler(rootView); + } + traceSlowWorklet('layoutHosted.refreshTouch', stepStartedAt); + rootView.userInteractionEnabled = true; + if ( + shouldSkipHostedSubviewRepairDuringNativeTransaction( + screenId, + registry, + rootView, + ) + ) { + traceSlowWorklet( + 'layoutHostedReactSubviews.skipNativeTransaction', + startedAt, + `screen=${screenId ?? ''}`, + ); + return false; + } + stepStartedAt = traceTimestamp(); + const shouldRefreshHostedView = + refreshHostedView && + (!registry || + screenHostedViewNeedsRefresh( + controller, + screenId, + registry, + rootView, + forceTouchRefresh, + )); + const didRefreshHostedViews = + shouldRefreshHostedView && refreshKnownUIKitHostView(rootView) === true; + if (refreshHostedView) { + traceSlowWorklet( + 'layoutHosted.refreshHost', + stepStartedAt, + `attempted=${shouldRefreshHostedView ? 1 : 0}`, + ); + } + const subviews = rootView.subviews; + const count = arrayCount(subviews); + const rootHeight = + rootView.bounds?.size?.height ?? rootView.frame?.size?.height ?? 0; + disableStaleEmptyScreenHitTargetSiblings(rootView); + for (let index = 0; index < count; index += 1) { + const subview = arrayItem(subviews, index); + if (!subview) { + continue; + } + let didNormalizeSubview = false; + const shouldForceRootHostWrapperLayout = + forceRootHostWrapperLayout && viewHasVisibleHostedReactContent(subview); + if ( + hostedSubviewMayNeedFill( + rootView, + subview, + shouldForceRootHostWrapperLayout, + ) && + shouldFillHostedSubview( + rootView, + subview, + 0, + undefined, + shouldForceRootHostWrapperLayout, + ) + ) { + const targetFrame = rectWithZeroOrigin(rootView.bounds); + didNormalizeSubview = + setViewFrameIfNeeded(subview, targetFrame) || didNormalizeSubview; + didNormalizeSubview = + setViewBoundsIfNeeded( + subview, + boundsRectForFilledHostedSubview(subview, targetFrame), + ) || didNormalizeSubview; + subview.autoresizingMask = flexibleSizeMask(); + } + if (repairHostedSubviews || didNormalizeSubview) { + layoutHostedSubviewChain( + subview, + 0, + rootHeight, + [], + undefined, + 0, + didNormalizeSubview || + didRefreshHostedViews || + forceRootHostWrapperLayout, + forceRootHostWrapperLayout, + ); + } + } + traceSlowWorklet( + 'layoutHostedReactSubviews', + startedAt, + `children=${count} refreshed=${didRefreshHostedViews ? 1 : 0}`, + ); + return didRefreshHostedViews && count > 0; +} +export const __nativeScriptLayoutHostedReactSubviewsForTests = + layoutHostedReactSubviews; +export function layoutScreenHostedReactSubviews( + controller: any, + rootViewOverride: any, + registry: NativeScriptStackRegistry, + screenId: string | undefined, + forceRefresh = false, + repairHostedSubviews = true, +) { + 'worklet'; + + const rootView = rootViewOverride ?? screenControllerView(controller); + const hasStableMountedContent = + !forceRefresh && + screenHasStableReadyMountedContent(screenId, registry, rootView); + if (hasStableMountedContent) { + if (rootView) { + rootView.userInteractionEnabled = true; + restoreHostedReactSubviewInteractivity(rootView); + } + rememberScreenHostedViewRefreshKey( + controller, + screenId, + registry, + rootView, + ); + refreshScreenContentWrapperHost( + screenId, + registry, + false, + false, + false, + true, + 'layout-screen-hosted-stable-skip', + ); + refreshScreenContentWrapperTouchHandler(screenId, registry, true); + traceWorkletEvent( + 'layout-screen-hosted-skip-stable', + `screen=${screenId ?? ''}`, + ); + return; + } + const shouldRefreshHost = screenHostedViewNeedsRefresh( + controller, + screenId, + registry, + rootView, + forceRefresh, + ); + if (!shouldRefreshHost && !repairHostedSubviews) { + if (rootView) { + rootView.userInteractionEnabled = true; + } + return; + } + const didRefreshHostedContent = layoutHostedReactSubviews( + controller, + rootView, + shouldRefreshHost, + repairHostedSubviews || shouldRefreshHost, + registry, + screenId, + forceRefresh, + ); + if ( + shouldRefreshHost && + didRefreshHostedContent && + screenId && + screenCanCertifyContentReady(screenId, registry) + ) { + registry.screenContentReady[screenId] = true; + rememberScreenHostedViewRefreshKey( + controller, + screenId, + registry, + rootView, + ); + } + if (screenId) { + rememberScreenStableReadyExposure(screenId, registry, rootView); + } +} +export const __nativeScriptScreenHostedViewNeedsRefreshForTests = + screenHostedViewNeedsRefresh; +export const __nativeScriptLayoutScreenHostedReactSubviewsForTests = + layoutScreenHostedReactSubviews; +export function refreshScreenContentReady( + screenId: string | undefined, + controller: any, + registry: NativeScriptStackRegistry, + forceRefresh = false, +) { + 'worklet'; + + const startedAt = traceTimestamp(); + if (!screenId) { + return false; + } + if (!forceRefresh && screenContentIsReady(screenId, registry)) { + return true; + } + const contentAlreadyReady = screenContentIsReady(screenId, registry); + const controllerView = screenControllerView(controller); + const contentWrapperView = registry.screenContentWrapperViews[screenId]; + const hasContentWrapper = !!contentWrapperView; + const shouldRefreshControllerView = + !!controllerView && + (forceRefresh || + !contentAlreadyReady || + screenHostedViewNeedsRefresh( + controller, + screenId, + registry, + controllerView, + forceRefresh, + )); + const didRefreshControllerView = + shouldRefreshControllerView && refreshKnownUIKitHostView(controllerView); + if (controllerView && didRefreshControllerView) { + layoutHostedReactSubviews( + controller, + controllerView, + false, + true, + registry, + screenId, + forceRefresh, + ); + const didRefreshWrapper = refreshScreenContentWrapperHost( + screenId, + registry, + true, + forceRefresh, + false, + false, + 'screen-content-ready-controller', + ); + if (screenCanCertifyContentReady(screenId, registry)) { + registry.screenContentReady[screenId] = true; + rememberScreenHostedViewRefreshKey( + controller, + screenId, + registry, + controllerView, + ); + } + traceSlowWorklet( + 'refreshScreenContentReady-controller', + startedAt, + `screen=${screenId} force=${forceRefresh}`, + ); + return didRefreshWrapper || didRefreshControllerView; + } + if (hasContentWrapper) { + const didRefreshWrapper = refreshScreenContentWrapperHost( + screenId, + registry, + true, + forceRefresh, + false, + false, + 'screen-content-ready-wrapper', + ); + if (screenCanCertifyContentReady(screenId, registry)) { + registry.screenContentReady[screenId] = true; + if (controllerView) { + rememberScreenHostedViewRefreshKey( + controller, + screenId, + registry, + controllerView, + ); + } + } + traceSlowWorklet( + 'refreshScreenContentReady-wrapper', + startedAt, + `screen=${screenId} force=${forceRefresh}`, + ); + return didRefreshWrapper || screenContentIsReady(screenId, registry); + } + if (controllerView && screenCanCertifyContentReady(screenId, registry)) { + registry.screenContentReady[screenId] = true; + rememberScreenHostedViewRefreshKey( + controller, + screenId, + registry, + controllerView, + ); + traceSlowWorklet( + 'refreshScreenContentReady-controller-visible', + startedAt, + `screen=${screenId} force=${forceRefresh}`, + ); + return true; + } + traceSlowWorklet( + 'refreshScreenContentReady-empty', + startedAt, + `screen=${screenId} force=${forceRefresh}`, + ); + return false; +} +export const __nativeScriptRefreshScreenContentReadyForTests = + refreshScreenContentReady; +export function prepareScreenHostedContentForNativeTransition( + controller: any, + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + const startedAt = traceTimestamp(); + const controllerView = screenControllerView(controller); + if (!screenId || !controllerView) { + return false; + } + const contentWrapperView = registry.screenContentWrapperViews[screenId]; + const wrapperAlreadyMounted = contentWrapperIsMountedInScreenView( + contentWrapperView, + controllerView, + ); + const contentIsReady = screenContentIsReady(screenId, registry); + const hasCommittedNativeStackContent = + screenHasCommittedContentForNativeStack(screenId, registry, controllerView); + const alreadyPreparedCommittedContent = + contentIsReady && + (!contentWrapperView || wrapperAlreadyMounted) && + hasCommittedNativeStackContent; + if (alreadyPreparedCommittedContent) { + restoreHostedReactSubviewInteractivity(controllerView); + refreshScreenSurfaceTouchHandlerIfNeeded( + screenId, + registry, + controllerView, + contentWrapperView, + true, + false, + true, + ); + traceSlowWorklet( + 'prepareScreenHostedContentForNativeTransition.fast', + startedAt, + `screen=${screenId}`, + ); + return true; + } + traceWorkletEvent( + 'prepareScreenHostedContentForNativeTransition-miss', + `screen=${screenId} ready=${contentIsReady ? 1 : 0} wrapper=${ + contentWrapperView ? 1 : 0 + } mounted=${wrapperAlreadyMounted ? 1 : 0} committed=${ + hasCommittedNativeStackContent ? 1 : 0 + }`, + ); + refreshScreenContentReady(screenId, controller, registry, true); + const didNormalizeWrapper = ensureScreenContentWrapperMounted( + screenId, + registry, + ); + layoutHostedReactSubviews( + controller, + controllerView, + false, + true, + registry, + screenId, + didNormalizeWrapper, + ); + const availableHeight = + controllerView.bounds?.size?.height ?? + controllerView.frame?.size?.height ?? + contentWrapperView?.bounds?.size?.height ?? + contentWrapperView?.frame?.size?.height ?? + 0; + if (contentWrapperView) { + layoutHostedSubviewChain( + contentWrapperView, + 0, + availableHeight, + [], + undefined, + 0, + true, + ); + contentWrapperView.setNeedsLayout?.(); + contentWrapperView.layoutIfNeeded?.(); + } + controllerView.setNeedsLayout?.(); + controllerView.layoutIfNeeded?.(); + restoreHostedReactSubviewInteractivity(controllerView); + refreshScreenSurfaceTouchHandlerIfNeeded( + screenId, + registry, + controllerView, + registry.screenContentWrapperViews[screenId], + true, + false, + true, + ); + const hasVisibleHostedContent = screenHasVisibleHostedContent( + screenId, + registry, + controllerView, + ); + traceSlowWorklet( + 'prepareScreenHostedContentForNativeTransition', + startedAt, + `screen=${screenId} visible=${hasVisibleHostedContent ? 1 : 0}`, + ); + return hasVisibleHostedContent; +} +export const __nativeScriptPrepareScreenHostedContentForNativeTransitionForTests = + prepareScreenHostedContentForNativeTransition; +export function prepareRevealedScreenForNativePop( + screenId: string | undefined, + controller: any, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + const startedAt = traceTimestamp(); + if (!screenId || !controller) { + return false; + } + const didRestoreHostView = restoreScreenControllerViewFromHostHandle( + controller, + screenId, + registry, + ); + const controllerView = screenControllerView(controller); + if (controllerView) { + controllerView.hidden = false; + controllerView.alpha = 1; + controllerView.userInteractionEnabled = true; + controllerView.autoresizingMask = flexibleSizeMask(); + normalizeScreenControllerViewFrame(screenId, registry, controllerView); + } + + // Upstream RNS enters pop with the revealed RNSScreen.view already hosting + // its React content. NativeScript can keep that view behind a stable handle + // while UIKit temporarily owns a wrapper/snapshot, so reassert the same + // invariant before handing the controller array to UINavigationController. + const didPrepareContent = prepareScreenHostedContentForNativeTransition( + controller, + screenId, + registry, + ); + const restoredControllerView = screenControllerView(controller); + const hasVisibleHostedContent = screenHasVisibleHostedContent( + screenId, + registry, + restoredControllerView, + ); + const didCertifyContentReady = + hasVisibleHostedContent && screenCanCertifyContentReady(screenId, registry); + if (didCertifyContentReady) { + registry.screenContentReady[screenId] = true; + rememberScreenStableReadyExposure( + screenId, + registry, + restoredControllerView, + ); + } + const scrollView = findScrollViewInFirstDescendantChainFrom(controllerView); + if (!scrollView) { + return; + } + const registry = getRegistry(globalThis as Record); + const screenId = directScreenIdForController(controller); + const injectedTop = screenId + ? registry.screenScrollInsetTops[screenId] ?? 0 + : controller.__rnsNativeScriptNavigationBarContentInsetTop ?? 0; + const existingContentInset = scrollView.contentInset; + const baseTop = Math.max( + 0, + edgeInsetsValue(existingContentInset, 'top') - injectedTop, + ); + const controllerSafeAreaTop = edgeInsetsValue( + controllerView?.safeAreaInsets, + 'top', + ); + if ( + targetTop < 0.5 && + injectedTop < 0.5 && + controllerSafeAreaTop >= 0.5 && + Math.abs( + edgeInsetsValue(existingContentInset, 'top') - controllerSafeAreaTop, + ) < 0.5 + ) { + // NATIVESCRIPT_PORT_DEVIATION: upstream never writes a manual + // UIScrollView.contentInset before the screen is parented. The TS port can + // briefly lay out a stack before UIKit containment is repaired; once UIKit + // supplies the safe area, clear that pre-parent manual top inset so + // contentInsetAdjustmentBehavior does not double it. + const existingOffset = scrollView.contentOffset; + if (screenId) { + registry.screenScrollInsetTops[screenId] = 0; + } else { + controller.__rnsNativeScriptNavigationBarContentInsetTop = 0; + } + scrollView.contentInset = edgeInsetsReplacingTop(existingContentInset, 0); + if (scrollView.scrollIndicatorInsets) { + scrollView.scrollIndicatorInsets = edgeInsetsReplacingTop( + scrollView.scrollIndicatorInsets, + 0, + ); + } + scrollView.contentOffset = pointReplacingY( + existingOffset, + (existingOffset?.y ?? 0) + controllerSafeAreaTop, + ); + return; + } + const nextTop = baseTop + targetTop; + if (Math.abs(nextTop - edgeInsetsValue(existingContentInset, 'top')) < 0.5) { + return; + } + + // NATIVESCRIPT_PORT_DEVIATION: upstream does not need a cleanup path here + // because RNSScreen is parented before UIKit computes ScrollView safe-area + // insets. The TS port may briefly have a pre-parent manual inset from an + // earlier host refresh, so keep this helper generic but pass targetTop=0 for + // header safe-area propagation; UIKit's adjustedContentInset remains the + // source of truth once containment is repaired. + const shouldCompensateContentOffset = + Math.abs(targetTop - injectedTop) >= 0.5; + const offsetDelta = shouldCompensateContentOffset + ? nextTop - edgeInsetsValue(existingContentInset, 'top') + : 0; + const existingOffset = scrollView.contentOffset; + if (screenId) { + registry.screenScrollInsetTops[screenId] = targetTop; + } else { + controller.__rnsNativeScriptNavigationBarContentInsetTop = targetTop; + } + scrollView.contentInset = edgeInsetsReplacingTop( + existingContentInset, + nextTop, + ); + if (scrollView.scrollIndicatorInsets) { + scrollView.scrollIndicatorInsets = edgeInsetsReplacingTop( + scrollView.scrollIndicatorInsets, + nextTop, + ); + } + scrollView.contentOffset = pointReplacingY( + existingOffset, + (existingOffset?.y ?? 0) - offsetDelta, + ); +} +export function navigationControllerIsPresentedModally( + navigationController: any, +) { + 'worklet'; + + return ( + navigationController?.presentingViewController != null || + navigationController?.presentationController != null + ); +} +export function syncNavigationBarSafeAreaInset( + navigationController: any, + controller: any, +) { + 'worklet'; + + if (!navigationController || !controller) { + return; + } + const navigationBar = navigationController.navigationBar; + const controllerView = screenControllerView(controller); + const existingInsets = controller.additionalSafeAreaInsets; + const registry = getRegistry(globalThis as Record); + const screenId = directScreenIdForController(controller); + const injectedTop = screenId + ? registry.screenSafeAreaInsetTops[screenId] ?? + controller.__rnsNativeScriptNavigationBarSafeAreaTop ?? + 0 + : controller.__rnsNativeScriptNavigationBarSafeAreaTop ?? 0; + const baseTop = Math.max( + 0, + edgeInsetsValue(existingInsets, 'top') - injectedTop, + ); + const props = screenId ? registry.screenProps[screenId] : undefined; + if (props?.headerConfig?.translucent === true) { + syncNavigationBarScrollViewInset(controller, 0); + if (injectedTop >= 0.5) { + if (screenId) { + registry.screenSafeAreaInsetTops[screenId] = 0; + } + controller.__rnsNativeScriptNavigationBarSafeAreaTop = 0; + controller.additionalSafeAreaInsets = edgeInsetsWith( + baseTop, + edgeInsetsValue(existingInsets, 'left'), + edgeInsetsValue(existingInsets, 'bottom'), + edgeInsetsValue(existingInsets, 'right'), + ); + controllerView?.setNeedsLayout?.(); + controllerView?.dispatchSafeAreaDidChangeNotification?.(); + } + return; + } + const hostSafeAreaTop = navigationControllerIsPresentedModally( + navigationController, + ) + ? 0 + : navigationControllerHostSafeAreaTop(navigationController); + const targetTop = + navigationBar && navigationBar.hidden !== true + ? Math.max( + 0, + rectMaxY(navigationBar.frame), + hostSafeAreaTop + (navigationBar.frame?.size?.height ?? 0), + ) + : 0; + const baseSafeAreaTop = Math.max( + 0, + edgeInsetsValue(controllerView?.safeAreaInsets, 'top') - injectedTop, + ); + const scrollInjectedTop = screenId + ? registry.screenScrollInsetTops[screenId] ?? 0 + : controller.__rnsNativeScriptNavigationBarContentInsetTop ?? 0; + const missingTop = Math.max(0, targetTop - baseSafeAreaTop); + + // NATIVESCRIPT_PORT_DEVIATION: upstream's RNSScreen view stays in the + // UINavigationController hierarchy while UIKit dismisses modals, so its + // ScrollView safe area is refreshed by UIKit before the next React layout. + // A NativeScript hostView(controller) can observe a parented navigation + // controller while the child safe area is still status-only after dismissal. + // Clear stale injected safe area only once UIKit already propagated the full + // header height; otherwise keep the generic missing-top injection below. + if ( + navigationController.parentViewController != null && + (targetTop < 0.5 || missingTop < 0.5) + ) { + syncNavigationBarScrollViewInset(controller, 0); + if (injectedTop >= 0.5) { + if (screenId) { + registry.screenSafeAreaInsetTops[screenId] = 0; + } + controller.__rnsNativeScriptNavigationBarSafeAreaTop = 0; + controller.additionalSafeAreaInsets = edgeInsetsWith( + baseTop, + edgeInsetsValue(existingInsets, 'left'), + edgeInsetsValue(existingInsets, 'bottom'), + edgeInsetsValue(existingInsets, 'right'), + ); + controllerView?.setNeedsLayout?.(); + controllerView?.dispatchSafeAreaDidChangeNotification?.(); + } + return; + } + if (missingTop < 0.5) { + if (scrollInjectedTop >= 0.5) { + syncNavigationBarScrollViewInset(controller, 0); + } + if (injectedTop >= 0.5) { + syncNavigationBarScrollViewInset(controller, 0); + controller.__rnsNativeScriptNavigationBarSafeAreaTop = 0; + controller.additionalSafeAreaInsets = edgeInsetsWith( + baseTop, + edgeInsetsValue(existingInsets, 'left'), + edgeInsetsValue(existingInsets, 'bottom'), + edgeInsetsValue(existingInsets, 'right'), + ); + controllerView?.setNeedsLayout?.(); + controllerView?.dispatchSafeAreaDidChangeNotification?.(); + } + return; + } + if (Math.abs(missingTop - injectedTop) < 0.5) { + syncNavigationBarScrollViewInset(controller, 0); + return; + } + + // NATIVESCRIPT_PORT_DEVIATION: upstream UINavigationController propagates + // the nav bar's adjusted top safe area to its RNSScreen child naturally. A + // NativeScript hostView(controller) can mount the same UIKit view through a + // generic container before UIKit refreshes the child safe area; copy only the + // missing top inset so ScrollView automatic inset adjustment matches the + // native ObjC implementation without moving the route view frame. + if (screenId) { + registry.screenSafeAreaInsetTops[screenId] = missingTop; + } + controller.__rnsNativeScriptNavigationBarSafeAreaTop = missingTop; + controller.additionalSafeAreaInsets = edgeInsetsWith( + baseTop + missingTop, + edgeInsetsValue(existingInsets, 'left'), + edgeInsetsValue(existingInsets, 'bottom'), + edgeInsetsValue(existingInsets, 'right'), + ); + controllerView?.setNeedsLayout?.(); + controllerView?.dispatchSafeAreaDidChangeNotification?.(); + syncNavigationBarScrollViewInset(controller, 0); +} +export const __nativeScriptSyncNavigationBarSafeAreaInsetForTests = + syncNavigationBarSafeAreaInset; +export function layoutNavigationBarWithinSafeArea(navigationController: any) { + 'worklet'; + 'stack-stable-content-miss', + `stack=${stackId} reason=registry-transitioning updating=${ + registry.stackUpdatingModals[stackId] === true ? 1 : 0 + } pending=${registry.stackPendingReconcileKeys[stackId] ? 1 : 0}`, + ); + return false; + } + const viewControllers = navigationController?.viewControllers; + const count = arrayCount(viewControllers); + if (count <= 0) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${stackId ?? ''} reason=empty`, + ); + return false; + } + const stableControllers = stableNativeViewControllersFromArray( + viewControllers, + registry, + navigationController?.view?.window != null, + ); + if (!stableControllers) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${stackId ?? ''} reason=controller-array count=${count}`, + ); + return false; + } + const { controllers, usedTaggedFallback } = stableControllers; + if (controllers.length !== count) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${ + stackId ?? '' + } reason=controller-count count=${count} controllers=${ + controllers.length + }`, + ); + return false; + } + if ( + !navigationControllerHostsControllerViews(navigationController, controllers) + ) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${stackId ?? ''} reason=controller-hosting count=${count}`, + ); + return false; + } + for (let index = 0; index < count; index += 1) { + const isTopController = index === count - 1; + const controller = arrayItem(viewControllers, index); + const screenId = controllerScreenId(controller); + const controllerView = screenControllerView(controller); + const contentWrapperView = screenId + ? liveScreenContentWrapperViewFromRegistry(screenId, registry) + : undefined; + const topContentHasVisibleHostedContent = + isTopController && + screenHasVisibleHostedContent(screenId, registry, controllerView); + const topContentHasCommittedHostedContent = + isTopController && + screenContentWrapperHasLiveMountedHostContent( + screenId, + registry, + contentWrapperView, + controllerView, + ); + const topContentHasCertifiedStableExposure = + isTopController && + screenHasStableReadyMountedContent(screenId, registry, controllerView); + const topContentIsStableEnough = + topContentHasCertifiedStableExposure && + (topContentHasVisibleHostedContent || + (!usedTaggedFallback && topContentHasCommittedHostedContent)); + if (!screenId) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${stackId ?? ''} reason=missing-screen index=${index}`, + ); + return false; + } + if (!controllerView) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${ + stackId ?? '' + } screen=${screenId} reason=missing-controller-view`, + ); + return false; + } + if (isTopController && !screenContentIsReady(screenId, registry)) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${stackId ?? ''} screen=${screenId} reason=top-not-ready`, + ); + return false; + } + if ( + isTopController && + (controllerView.hidden === true || + (controllerView.alpha ?? 1) <= 0.01 || + controllerViewHasHiddenNavigationAncestor( + controllerView, + navigationController.view, + )) + ) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${stackId ?? ''} screen=${screenId} reason=top-hidden`, + ); + return false; + } + if ( + isTopController && + (!contentWrapperView || + !contentWrapperIsMountedInScreenView( + contentWrapperView, + controllerView, + ) || + hiddenAncestorBetweenViewAndAncestor( + contentWrapperView, + controllerView, + ) != null || + (controllerView.window != null && contentWrapperView.window == null) || + !topContentIsStableEnough) + ) { + traceWorkletEvent( + 'stack-stable-content-miss', + `stack=${stackId ?? ''} screen=${screenId} reason=top-wrapper wrapper=${ + contentWrapperView ? 1 : 0 + } mounted=${ + contentWrapperView && + contentWrapperIsMountedInScreenView( + contentWrapperView, + controllerView, + ) + ? 1 + : 0 + } wrapperWindow=${contentWrapperView?.window ? 1 : 0} visible=${ + topContentHasVisibleHostedContent ? 1 : 0 + } committed=${topContentHasCommittedHostedContent ? 1 : 0} certified=${ + topContentHasCertifiedStableExposure ? 1 : 0 + }`, + ); + return false; + } + } + return true; +} +export function navigationStackTopScreenHasReadyContent( + navigationController: any, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + const viewControllers = navigationController?.viewControllers; + const count = arrayCount(viewControllers); + if (count <= 0) { + return false; + } + const topController = + navigationController?.topViewController ?? + arrayItem(viewControllers, count - 1); + const screenId = controllerScreenId(topController); + const controllerView = screenControllerView(topController); + if ( + !screenId || + !controllerView || + controllerView.hidden === true || + (controllerView.alpha ?? 1) <= 0.01 || + !screenContentIsReady(screenId, registry) + ) { + return false; + } + return screenHasStableReadyMountedContent(screenId, registry, controllerView); +} +export function currentNavigationStackStableLayoutKey( + navigationController: any, +) { + 'worklet'; + + return ( + navigationController?.__nativeScriptStableStackLayoutKey ?? + associatedObjectForKey( + navigationController, + STACK_STABLE_LAYOUT_KEY_ASSOCIATION_KEY, + ) ?? + '' + ); +} +export function navigationStackCanSkipStableLayout( + navigationController: any, + registry: NativeScriptStackRegistry, + layoutKey: string, +) { + 'worklet'; + + return ( + navigationStackHasStableReadyContent(navigationController, registry) && + currentNavigationStackStableLayoutKey(navigationController) === layoutKey diff --git a/.oracle-context/ns-rns-first-paint/metro.config.js b/.oracle-context/ns-rns-first-paint/metro.config.js new file mode 100644 index 000000000..b75923c12 --- /dev/null +++ b/.oracle-context/ns-rns-first-paint/metro.config.js @@ -0,0 +1,312 @@ +const path = require('path'); +const fs = require('fs'); +const { getDefaultConfig } = require('expo/metro-config'); +const exclusionList = require(path.join( + path.dirname(require.resolve('metro-config/package.json')), + 'src/defaults/exclusionList' +)).default; + +const projectRoot = __dirname; +const forksRoot = path.resolve(projectRoot, '..'); +const nativeScriptRoot = path.resolve(forksRoot, '../NativeScriptRuntime'); +const appNodeModules = path.join(projectRoot, 'node_modules'); +const reactNativeScreensRoot = path.join(forksRoot, 'react-native-screens'); +const reactNativeScreensSourceRoot = path.join(reactNativeScreensRoot, 'src'); +const useReactNativeScreensLib = + process.env.RNS_DEMO_USE_SCREENS_SRC !== '1' && + process.env.EXPO_PUBLIC_RNS_DEMO_USE_SCREENS_SRC !== '1'; +const reactNativeScreensPackageRoot = + useReactNativeScreensLib + ? path.join(reactNativeScreensRoot, 'lib/module') + : path.join(reactNativeScreensRoot, 'src'); +const reactNavigationRoot = path.join(forksRoot, 'react-navigation'); +const reactNavigationNodeModules = path.join(reactNavigationRoot, 'node_modules'); + +const localPackages = { + '@nativescript/react-native': path.join(nativeScriptRoot, 'packages/react-native'), + '@react-navigation/core': path.join(reactNavigationRoot, 'packages/core/src'), + '@react-navigation/elements': path.join(reactNavigationRoot, 'packages/elements/src'), + '@react-navigation/native': path.join(reactNavigationRoot, 'packages/native/src'), + '@react-navigation/native-stack': path.join(reactNavigationRoot, 'packages/native-stack/src'), + '@react-navigation/routers': path.join(reactNavigationRoot, 'packages/routers/src'), + 'expo-router': path.join(forksRoot, 'expo/packages/expo-router'), + immer: path.join(reactNavigationNodeModules, 'immer'), + 'react-native-screens': reactNativeScreensPackageRoot, + 'use-sync-external-store': path.join( + reactNavigationNodeModules, + 'use-sync-external-store' + ), + valibot: path.join(reactNavigationNodeModules, 'valibot'), +}; + +const reactNavigationSourceRoots = [ + localPackages['@react-navigation/core'], + localPackages['@react-navigation/elements'], + localPackages['@react-navigation/native'], + localPackages['@react-navigation/native-stack'], + localPackages['@react-navigation/routers'], +]; + +const localPackageRoots = [ + localPackages['@nativescript/react-native'], + ...reactNavigationSourceRoots, + path.join(forksRoot, 'expo/packages/expo-router'), + localPackages.immer, + reactNativeScreensRoot, + localPackages['react-native-screens'], + reactNativeScreensSourceRoot, + localPackages['use-sync-external-store'], + localPackages.valibot, +]; + +const singletonModules = [ + 'react', + 'react-native', + 'react-native-safe-area-context', + 'react-native-screens', + 'react-native-worklets', + 'react-native-reanimated', +]; +const singletonForkRoots = [ + nativeScriptRoot, + reactNavigationRoot, + reactNativeScreensRoot, +]; + +function escapePathForRegex(filePath) { + return filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +const config = getDefaultConfig(projectRoot); + +process.env.METRO_TRANSFORM_TRACE_UPSTREAM = + config.transformer.babelTransformerPath; +config.transformer.babelTransformerPath = path.join( + projectRoot, + 'scripts/metro-transform-logger.js' +); + +function resolveSourcePackage(moduleName, packageName, sourceRoot) { + if (moduleName !== packageName && !moduleName.startsWith(`${packageName}/`)) { + return null; + } + const subpath = moduleName === packageName ? 'index' : moduleName.slice(packageName.length + 1); + const basePath = path.join(sourceRoot, subpath); + const candidates = [ + basePath, + `${basePath}.ios.js`, + `${basePath}.ios.tsx`, + `${basePath}.ios.ts`, + `${basePath}.native.js`, + `${basePath}.native.tsx`, + `${basePath}.native.ts`, + `${basePath}.tsx`, + `${basePath}.ts`, + `${basePath}.js`, + path.join(basePath, 'index.ios.js'), + path.join(basePath, 'index.ios.tsx'), + path.join(basePath, 'index.ios.ts'), + path.join(basePath, 'index.native.js'), + path.join(basePath, 'index.native.tsx'), + path.join(basePath, 'index.native.ts'), + path.join(basePath, 'index.tsx'), + path.join(basePath, 'index.ts'), + path.join(basePath, 'index.js'), + ]; + return candidates.find((candidate) => fs.existsSync(candidate)) ?? null; +} + +function resolveReactNativeScreensFabricSpec(moduleName, originModulePath) { + const libModuleRoot = path.join(reactNativeScreensRoot, 'lib/module'); + const packagePrefix = 'react-native-screens/'; + let candidatePath = null; + + if (moduleName.startsWith('.')) { + candidatePath = path.resolve(path.dirname(originModulePath), moduleName); + } else if (moduleName.startsWith(packagePrefix)) { + candidatePath = path.join( + reactNativeScreensPackageRoot, + moduleName.slice(packagePrefix.length) + ); + } + + if ( + !candidatePath || + !candidatePath.startsWith(libModuleRoot) || + !candidatePath.includes(`${path.sep}fabric${path.sep}`) || + !path.basename(candidatePath).includes('NativeComponent') + ) { + return null; + } + + const sourceSubpath = path.relative(libModuleRoot, candidatePath); + const basePath = path.join(reactNativeScreensSourceRoot, sourceSubpath); + const candidates = [ + `${basePath}.ios.tsx`, + `${basePath}.ios.ts`, + `${basePath}.native.tsx`, + `${basePath}.native.ts`, + `${basePath}.tsx`, + `${basePath}.ts`, + `${basePath}.js`, + ]; + return candidates.find((candidate) => fs.existsSync(candidate)) ?? null; +} + +function resolveReactNativeScreensRelativePlatformFile(moduleName, originModulePath) { + if (!moduleName.startsWith('.')) { + return null; + } + + const resolvedOriginModulePath = path.isAbsolute(originModulePath) + ? originModulePath + : path.resolve(projectRoot, originModulePath); + if ( + /(?:^|\/)NativeScriptScreenStack\.ios(?:\.chunk[1-5])?\.[jt]sx?$/.test( + resolvedOriginModulePath + ) && + /^\.\/NativeScriptScreenStack\.ios\.chunk[1-5]$/.test(moduleName) + ) { + const chunkPath = path.resolve( + path.dirname(resolvedOriginModulePath), + `${moduleName}.js` + ); + if (fs.existsSync(chunkPath)) { + return chunkPath; + } + } + const packageRoots = [ + path.join(reactNativeScreensRoot, 'lib/module'), + reactNativeScreensSourceRoot, + ]; + + if (!packageRoots.some((root) => resolvedOriginModulePath.startsWith(root))) { + return null; + } + + const basePath = path.resolve( + path.dirname(resolvedOriginModulePath), + moduleName + ); + const exactJavaScriptFile = `${basePath}.js`; + if (path.basename(basePath).includes('.ios.chunk') && fs.existsSync(exactJavaScriptFile)) { + return exactJavaScriptFile; + } + const candidates = [ + `${basePath}.ios.js`, + `${basePath}.ios.tsx`, + `${basePath}.ios.ts`, + `${basePath}.native.js`, + `${basePath}.native.tsx`, + `${basePath}.native.ts`, + `${basePath}.tsx`, + `${basePath}.ts`, + exactJavaScriptFile, + path.join(basePath, 'index.ios.js'), + path.join(basePath, 'index.ios.tsx'), + path.join(basePath, 'index.ios.ts'), + path.join(basePath, 'index.native.js'), + path.join(basePath, 'index.native.tsx'), + path.join(basePath, 'index.native.ts'), + path.join(basePath, 'index.tsx'), + path.join(basePath, 'index.ts'), + path.join(basePath, 'index.js'), + ]; + + return candidates.find((candidate) => fs.existsSync(candidate)) ?? null; +} + +config.watchFolders = localPackageRoots; +config.resolver.blockList = exclusionList( + [ + ...singletonModules.map( + (moduleName) => + new RegExp( + `^${escapePathForRegex(appNodeModules)}\\/.*\\/node_modules\\/${escapePathForRegex(moduleName)}(\\/.*)?$` + ) + ), + ...singletonModules.flatMap((moduleName) => + singletonForkRoots.map( + (root) => + new RegExp( + `^${escapePathForRegex(path.join(root, 'node_modules', moduleName))}(\\/.*)?$` + ) + ) + ), + ...[ + path.join(projectRoot, 'ios'), + path.join(projectRoot, 'ios/Pods'), + path.join(projectRoot, 'ios/build'), + path.join(projectRoot, '.expo/dev/logs'), + path.join(projectRoot, '.expo/xcodebuild.log'), + path.join(reactNativeScreensRoot, 'ios'), + path.join(reactNativeScreensRoot, 'android'), + path.join(reactNativeScreensRoot, 'FabricExample'), + path.join(reactNativeScreensRoot, 'TVOSExample'), + path.join(reactNativeScreensRoot, 'docs'), + path.join(reactNativeScreensRoot, 'apps'), + ].map( + (blockedPath) => + new RegExp(`^${escapePathForRegex(blockedPath)}(\\/.*)?$`) + ), + new RegExp( + `^${escapePathForRegex(reactNativeScreensRoot)}\\/lib\\/.*\\.map$` + ), + new RegExp( + `^${escapePathForRegex(reactNativeScreensRoot)}\\/lib\\/.*\\.test\\.[cm]?js$` + ), + new RegExp( + `^${escapePathForRegex(reactNativeScreensRoot)}\\/src\\/.*\\.test\\.[jt]sx?$` + ), + ] +); +const extraNodeModules = { + ...config.resolver.extraNodeModules, + ...localPackages, +}; +config.resolver.extraNodeModules = new Proxy(extraNodeModules, { + get(target, name) { + if (typeof name !== 'string') { + return target[name]; + } + return target[name] ?? path.join(appNodeModules, name); + }, +}); +const upstreamResolveRequest = config.resolver.resolveRequest; +config.resolver.resolveRequest = (context, moduleName, platform) => { + const reactNativeScreensRelativePlatformFile = + resolveReactNativeScreensRelativePlatformFile( + moduleName, + context.originModulePath + ); + if (reactNativeScreensRelativePlatformFile) { + return { + type: 'sourceFile', + filePath: reactNativeScreensRelativePlatformFile, + }; + } + + const fabricSpecSourceFile = resolveReactNativeScreensFabricSpec( + moduleName, + context.originModulePath + ); + if (fabricSpecSourceFile) { + return { type: 'sourceFile', filePath: fabricSpecSourceFile }; + } + + const sourceFile = + resolveSourcePackage(moduleName, '@react-navigation/core', localPackages['@react-navigation/core']) ?? + resolveSourcePackage(moduleName, '@react-navigation/elements', localPackages['@react-navigation/elements']) ?? + resolveSourcePackage(moduleName, '@react-navigation/native', localPackages['@react-navigation/native']) ?? + resolveSourcePackage(moduleName, '@react-navigation/native-stack', localPackages['@react-navigation/native-stack']) ?? + resolveSourcePackage(moduleName, '@react-navigation/routers', localPackages['@react-navigation/routers']) ?? + resolveSourcePackage(moduleName, 'react-native-screens', localPackages['react-native-screens']); + if (sourceFile) { + return { type: 'sourceFile', filePath: sourceFile }; + } + return upstreamResolveRequest + ? upstreamResolveRequest(context, moduleName, platform) + : context.resolveRequest(context, moduleName, platform); +}; + +module.exports = config; diff --git a/.oracle-context/ns-rns-first-paint/push-header-delegate.ts b/.oracle-context/ns-rns-first-paint/push-header-delegate.ts new file mode 100644 index 000000000..dcd9a0365 --- /dev/null +++ b/.oracle-context/ns-rns-first-paint/push-header-delegate.ts @@ -0,0 +1,420 @@ + ctx?.retain?.(animationBlock); + ctx?.retain?.(completionBlock); + const didSchedule = animateAlongsideTransitionCompletion( + coordinator, + animationBlock, + completionBlock, + ); + if ( + didSchedule !== true && + navigationController.__nativeScriptHeaderAppearanceToken === token + ) { + releaseCoordinatorBlocks(); + navigationController.__nativeScriptHeaderAppearanceToken = undefined; + } +} +export const __nativeScriptConfigureNavigationAppearanceForTests = + configureNavigationAppearance; +export function configureModalScreenController( + controller: any, + props: Readonly, + registry: NativeScriptStackRegistry, + ctx?: any, +) { + 'worklet'; + + if (!isModalPresentation(props.stackPresentation)) { + return; + } + setControllerPresentedModalScreenId(controller, props.screenId); + + // Direct port of RNSScreenView's stackPresentation/stackAnimation/ + // gestureEnabled setters: the presented RNSScreen controller owns UIKit + // presentation state, not the surrounding stack. + controller.modalPresentationStyle = modalPresentationStyle( + props.stackPresentation, + ); + controller.modalTransitionStyle = modalTransitionStyleForProps(props); + controller.modalInPresentation = props.gestureEnabled === false; + setupAdaptivePresentationControllerDelegate(controller, props, ctx); + const sheetPresentationController = controller.sheetPresentationController; + if ( + props.stackPresentation === 'modal' && + sheetPresentationController && + 'prefersPageSizing' in sheetPresentationController + ) { + sheetPresentationController.prefersPageSizing = true; + } + updateFormSheetPresentationStyle(controller, props, registry, true); + setupBackdropTapGestureRecognizer(controller, props, ctx); +} +export function configureScreenControllerHeader( + controller: any, + props: Readonly, + ctx?: any, + isTopScreen = false, + canGoBack = false, + registry?: NativeScriptStackRegistry, + animated = false, + updateNavigationAppearance = true, +) { + 'worklet'; + + const resolvedRegistry = + registry ?? getRegistry(globalThis as Record); + const headerConfig = props.headerConfig; + configureExtendedLayout(controller, headerConfig); + const navigationItem = controller?.navigationItem; + setNativeValueForKey(controller, 'title', headerConfig?.title ?? ''); + if (navigationItem) { + if (headerConfig?.hidden === true) { + setNativeValueForKey(navigationItem, 'title', headerConfig?.title ?? ''); + controller.__nativeScriptHeaderSubviewKey = headerSubviewRecordsKey( + props.screenId, + ); + const navigationController = controller.navigationController; + if (navigationController && updateNavigationAppearance) { + configureNavigationAppearance( + navigationController, + headerConfig, + ctx, + resolvedRegistry, + animated, + ); + } + emitHeaderHeightChange(controller, props, ctx); + return; +export function prepareControllerForStackExposure( + navigationController: any, + screenId: string | undefined, + controller: any, + registry: NativeScriptStackRegistry, + ctx: any, + isTopScreen: boolean, + canGoBack: boolean, + refreshHostedContent = true, + configureHeader = true, + restoreHostView = true, +) { + 'worklet'; + + if (!screenId || !controller) { + return false; + } + const props = registry.screenProps[screenId]; + if (!props) { + return false; + } + if (restoreHostView) { + restoreScreenControllerViewFromHostHandle(controller, screenId, registry); + } + traceWorkletEvent( + 'prepareStackExposure-after-host-restore', + `screen=${screenId} restore=${restoreHostView ? 1 : 0}`, + ); + const controllerView = ensureScreenControllerView(controller); + traceWorkletEvent( + 'prepareStackExposure-after-ensure-view', + `screen=${screenId} hasView=${controllerView ? 1 : 0}`, + ); + const navigationView = navigationController?.view; + const targetBounds = navigationView?.bounds ?? navigationView?.frame; + traceWorkletEvent( + 'prepareStackExposure-after-target-bounds', + `screen=${screenId} hasBounds=${targetBounds ? 1 : 0}`, + ); + if (controllerView) { + if ( + controller.__nativeScriptViewIsSnapshot === true || + controllerView.__nativeScriptOwningViewController !== controller + ) { + restoreScreenControllerView(controller, controllerView); + } + traceWorkletEvent( + 'prepareStackExposure-after-view-restore', + `screen=${screenId}`, + ); + controllerView.hidden = false; + controllerView.alpha = 1; + controllerView.userInteractionEnabled = true; + controllerView.autoresizingMask = flexibleSizeMask(); + traceWorkletEvent( + 'prepareStackExposure-after-view-props', + `screen=${screenId}`, + ); + if (targetBounds && rectHasPositiveSize(targetBounds)) { + const targetFrame = rectWithZeroOrigin(targetBounds); + setViewFrameIfNeeded(controllerView, targetFrame); + setViewBoundsIfNeeded( + controllerView, + boundsRectForFilledHostedSubview(controllerView, targetFrame), + ); + } + traceWorkletEvent('prepareStackExposure-after-frame', `screen=${screenId}`); + } + if (configureHeader) { + configureScreenControllerHeader( + controller, + props, + registry.screenContexts[screenId] ?? ctx, + isTopScreen, + canGoBack, + registry, + ); + } + traceWorkletEvent( + 'prepareStackExposure-after-header', + `screen=${screenId} header=${configureHeader ? 1 : 0}`, + ); + if (refreshHostedContent) { + certifyScreenContentReadyFromHostDescendants(screenId, registry); + } + const needsContentRefresh = + refreshHostedContent && !screenContentIsReady(screenId, registry); + traceWorkletEvent( + 'prepareStackExposure-before-content-ready', + `screen=${screenId} needsRefresh=${needsContentRefresh ? 1 : 0}`, + ); + const didRefreshContent = refreshHostedContent + ? refreshScreenContentReady( + screenId, + controller, + registry, + needsContentRefresh, + ) + : false; + traceWorkletEvent( + 'prepareStackExposure-after-content-ready', + `screen=${screenId} did=${didRefreshContent ? 1 : 0}`, + ); + const needsContentRefreshAfterRefresh = + refreshHostedContent && !screenContentIsReady(screenId, registry); + if (controllerView && needsContentRefreshAfterRefresh) { + layoutScreenHostedReactSubviews( + controller, + controllerView, + registry, + screenId, + true, + true, + ); + rememberScreenHostedViewRefreshKey( + controller, + screenId, + registry, + controllerView, + ); + screenControllerLayoutDidChange(controller); + } + if (needsContentRefreshAfterRefresh) { + refreshScreenContentWrapperHost( + screenId, + registry, + true, + true, + true, + false, + 'prepare-stack-exposure', + ); + } + prepareVisibleScreenForImmediateInteraction( + screenId, + registry, + controllerView, + false, + ); + return didRefreshContent; +} +export function reparentNavigationControllerForModalContent( + ); + } + return; + } + if (canAnimateNativeChange && previousKey != null && previousCount > 0) { + const isPush = + nextCount === previousCount + 1 && + idsEqual(previousIds, availableIds.slice(0, previousCount)); + const isPushFromNativeStack = + nativeIds.length > 0 && + nextCount === nativeIds.length + 1 && + idsEqual(nativeIds, availableIds.slice(0, nativeIds.length)); + const isPop = + nextCount < previousCount && + idsEqual(availableIds, previousIds.slice(0, nextCount)); + traceWorkletEvent( + 'reconcileStack-diff', + `stack=${stackId} canAnimate=${canAnimateNativeChange ? 1 : 0} isPush=${ + isPush ? 1 : 0 + } isPushNative=${isPushFromNativeStack ? 1 : 0} isPop=${ + isPop ? 1 : 0 + } nativeLen=${nativeIds.length} prev=${previousCount} next=${nextCount}`, + ); + if ( + (isPush || isPushFromNativeStack) && + nativeIds.length <= previousCount + ) { + const pushedScreenId = availableIds[nextCount - 1]; + const pushedController = controllers[nextCount - 1]; + prepareControllerForStackExposure( + navigationController, + pushedScreenId, + pushedController, + registry, + ctx, + true, + nextCount > 1, + true, + true, + true, + ); + const didPreparePushedContent = + prepareScreenHostedContentForNativeTransition( + pushedController, + pushedScreenId, + registry, + ); + const pushedContentIsReady = screenContentIsReady( + pushedScreenId, + registry, + ); + traceWorkletEvent( + 'reconcileStack-push-after-prepare', + `stack=${stackId} screen=${pushedScreenId} content=${ + didPreparePushedContent ? 1 : 0 + } ready=${pushedContentIsReady ? 1 : 0}`, + ); + if (!didPreparePushedContent || !pushedContentIsReady) { + registry.stackPendingReconcileKeys[stackId] = nextKey; + schedulePostLayoutStackReconcile( + stackId, + registry, + ctx, + true, + nextKey, + registry.stackTransactionRevisions[stackId], + ); + traceWorkletEvent( + 'reconcileStack-push-deferred-content', + `stack=${stackId} screen=${pushedScreenId}`, + ); + updateStackBackGestureState(navigationController); + return; + } + const token = markTransition( + stackId, + registry, + ctx, + false, + pushedScreenId, + false, + true, + false, + ); + traceWorkletEvent( + 'reconcileStack-push-after-mark', + `stack=${stackId} screen=${pushedScreenId}`, + ); + traceWorkletEvent( + 'reconcileStack-push-before-animate', + `stack=${stackId} fn=${typeof animateStackPush} parent=${ + nativeObjectStableHandle(navigationController) ?? '' + } screen=${pushedScreenId}`, + ); + traceWorkletEvent( + 'reconcileStack-push-before-native', + `stack=${stackId} screen=${pushedScreenId}`, + ); + cancelActiveTouchesForStackContainer(stackId, registry); + let didAnimatePush = false; + try { + didAnimatePush = animateStackPush( + navigationController, + controllers, + pushedController, + pushedScreenId, + ); + } catch (error) { + traceWorkletEvent( + 'reconcileStack-push-animate-error', + `stack=${stackId} error=${String( + (error as Error)?.message ?? error, + )}`, + ); + throw error; + } + traceWorkletEvent( + 'reconcileStack-push-after-animate', + `stack=${stackId} did=${didAnimatePush ? 1 : 0}`, + ); + if (didAnimatePush) { + applyStackNativeState(stackId, registry, { + count: nextCount, + key: nextKey, + }); + const pushedControllerView = screenControllerView(pushedController); + if (pushedControllerView) { + prepareVisibleScreenForImmediateInteraction( + pushedScreenId, + registry, + pushedControllerView, + false, + ); + } + cancelActiveTouchesForStackScreen( + registry, + registry.stackNativeKeys[ctx.props.stackId], + ); + if ( + shouldIgnoreNoOpNativeWillShow( + currentIds, + activeIds, + nextScreenId, + previousNativeIds, + ) + ) { + traceWorkletEvent( + 'stack-willShow-noop-current', + `stack=${ + ctx.props.stackId + } screen=${nextScreenId} current=${currentIds.join( + ',', + )} native=${previousNativeIds.join(',')}`, + ); + return; + } + configureHeaderForWillShowViewController( + registry, + ctx, + viewController, + activeIds, + currentIds, + animated === true, + ); + const nativeStackIsAlreadyShorter = + shouldEmitNativeStackChange(currentIds, activeIds) && + previousNativeIds.length > currentIds.length; + const closing = + nativeStackIsAlreadyShorter || + (nextIndex >= 0 && + nextIndex < Math.max(0, currentIds.length - 1)); + const affectedScreenId = closing + ? activeIds[activeIds.length - 1] ?? + currentIds[currentIds.length - 1] + : nextScreenId ?? activeIds[activeIds.length - 1]; + if (!nextScreenId) { + return; + } + if (registry.stackTransitioning[ctx.props.stackId]) { + return; + } + markTransition( + ctx.props.stackId, + registry, + ctx, + closing, + affectedScreenId, + true, + false, + ); + }, + navigationControllerDidShowViewControllerAnimated( diff --git a/.oracle-context/ns-rns-first-paint/readiness-and-wrapper.ts b/.oracle-context/ns-rns-first-paint/readiness-and-wrapper.ts new file mode 100644 index 000000000..438feb5d3 --- /dev/null +++ b/.oracle-context/ns-rns-first-paint/readiness-and-wrapper.ts @@ -0,0 +1,870 @@ + if ( + isModalPresentation(registry.screenProps[ids[index]]?.stackPresentation) + ) { + return index; + } + } + return -1; +} +export function configureExtendedLayout( + controller: any, + headerConfig?: ScreenStackHeaderConfigProps, +) { + 'worklet'; + + if (!controller) { + return; + } + const headerIsVisible = headerConfig != null && headerConfig.hidden !== true; + controller.edgesForExtendedLayout = + headerIsVisible && headerConfig?.translucent !== true + ? rectEdgeAllExceptTop() + : rectEdgeAll(); +} +export function createPlaceholderViewController() { + 'worklet'; + + const UIViewController = nativeClassValue('UIViewController'); + if (!UIViewController || typeof UIViewController.alloc !== 'function') { + return null; + } + const placeholder = createNativeClassInstance(UIViewController); + if (placeholder?.view) { + placeholder.view.backgroundColor = nativeColor( + undefined, + 'systemBackgroundColor', + ); + } + configureExtendedLayout(placeholder); + return placeholder; +} +export function createArray(values: any[]) { + 'worklet'; + + const NSArray = nativeValue('NSArray'); + if (NSArray && typeof NSArray.arrayWithArray === 'function') { + try { + return NSArray.arrayWithArray(values); + } catch (_error) { + return values; + } + } + contentWrapperView: any, + childrenView?: any, +) { + 'worklet'; + + const wrapper = rememberScreenContentWrapperView( + screenId, + registry, + contentWrapperView, + ); + if (!wrapper) { + return null; + } + markNativeScriptScreenContentWrapperIdentity(wrapper, childrenView); + return wrapper; +} +export function screenContentWrapperVisibleDescendantCount( + view: any, + depth = 0, +): number { + 'worklet'; + + if ( + !view || + depth > 32 || + view.hidden === true || + (typeof view.alpha === 'number' && view.alpha <= 0.01) + ) { + return 0; + } + const subviews = view.subviews; + const count = arrayCount(subviews); + let visibleCount = 0; + for (let index = 0; index < count; index += 1) { + const subview = arrayItem(subviews, index); + if ( + !subview || + subview.hidden === true || + (typeof subview.alpha === 'number' && subview.alpha <= 0.01) || + nativeObjectDescription(subview).includes( + 'NativeScriptDetachedChildrenTouchSentinel', + ) + ) { + continue; + } + visibleCount += 1; + visibleCount += screenContentWrapperVisibleDescendantCount( + subview, + depth + 1, + ); + } + return visibleCount; +} +export function discoverScreenContentWrapperView( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + screenView?: any, +) { + 'worklet'; + + if (!screenId) { + return null; + } + const resolvedScreenView = + screenView ?? screenControllerView(registry.screens[screenId]); + const subviews = resolvedScreenView?.subviews; + const count = arrayCount(subviews); + for (let index = 0; index < count; index += 1) { + const subview = arrayItem(subviews, index); + const rootView = screenContentWrapperRootViewInTree(subview); + if (rootView) { + return rememberScreenContentWrapperView(screenId, registry, rootView); + } + if (screenContentWrapperComponentDescriptionMatches(subview)) { + return rememberScreenContentWrapperView( + screenId, + registry, + screenContentWrapperFabricViewForMountedView( + subview, + resolvedScreenView, + screenId, + ), + ); + } + } + return null; +} +export function viewHasVisibleHostedReactContent( + view: any, + depth = 0, +): boolean { + 'worklet'; + + if ( + !view || + depth > 24 || + view.hidden === true || + (typeof view.alpha === 'number' && view.alpha <= 0.01) || + view.accessibilityElementsHidden === true + ) { + return false; + } + const description = nativeObjectDescription(view); + if ( + description.includes('RCTParagraphComponentView') || + description.includes('RCTScrollViewComponentView') || + description.includes('RCTEnhancedScrollView') || + description.includes('RCTText') || + (description.includes('RCTViewComponentView') && depth > 0) + ) { + return true; + } + const subviews = view.subviews; + const count = arrayCount(subviews); + for (let index = 0; index < count; index += 1) { + if ( + viewHasVisibleHostedReactContent(arrayItem(subviews, index), depth + 1) + ) { + return true; + } + } + return false; +} +export function screenViewHasVisibleHostedReactContent( + screenView: any, + excludedView?: any, +) { + 'worklet'; + + const subviews = screenView?.subviews; + const count = arrayCount(subviews); + for (let index = 0; index < count; index += 1) { + const subview = arrayItem(subviews, index); + if ( + !subview || + (excludedView && + (nativeObjectsEqual(subview, excludedView) || + viewIsDescendantOfView(subview, excludedView))) || + contentWrapperShellDescriptionMatches(subview) + ) { + continue; + } + if (viewHasVisibleHostedReactContent(subview)) { + return true; + } + } + return false; +} +export function screenHasSeparateVisibleHostedContent( + screenView: any, + contentWrapperView: any, +) { + 'worklet'; + + return screenViewHasVisibleHostedReactContent(screenView, contentWrapperView); +} +export function screenContentWrapperChildHostView(contentWrapperView: any) { + 'worklet'; + + const childHost = + nativeScriptScreenContentWrapperChildrenViewForView(contentWrapperView); + if ( + !childHost || + childHost === contentWrapperView || + nativeObjectsEqual(childHost, contentWrapperView) + ) { + return null; + } + return childHost; +} +export function rememberCommittedScreenContentWrapper( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + contentWrapperView: any, +) { + 'worklet'; + + if (!screenId || !contentWrapperView) { + return; + } + const committedHandle = + nativeObjectStableHandle(contentWrapperView) ?? + registry.screenContentWrapperHostHandles?.[screenId]; + if (committedHandle) { + const committedHandles = + registry.screenContentWrapperCommittedHandles ?? + (registry.screenContentWrapperCommittedHandles = {}); + committedHandles[screenId] = committedHandle; + } +} +export function rememberMountedScreenContentWrapperFabricChild( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + contentWrapperView: any, +) { + 'worklet'; + + if (!screenId || !contentWrapperView) { + return false; + } + const mountedHandle = + nativeObjectStableHandle(contentWrapperView) ?? + registry.screenContentWrapperHostHandles?.[screenId]; + if (!mountedHandle) { + return false; + } + const mountedHandles = + registry.screenContentWrapperMountedFabricChildHandles ?? + (registry.screenContentWrapperMountedFabricChildHandles = {}); + mountedHandles[screenId] = mountedHandle; + return true; +} +export function clearMountedScreenContentWrapperFabricChild( + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + if (!screenId) { + return; + } + const mountedHandles = registry.screenContentWrapperMountedFabricChildHandles; + if (mountedHandles) { + mountedHandles[screenId] = undefined; + } +} +export function screenContentWrapperMatchesCommittedHandle( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + contentWrapperView: any, +) { + 'worklet'; + + if (!screenId || !contentWrapperView) { + return false; + } + const committedHandle = + registry.screenContentWrapperCommittedHandles?.[screenId]; + if (!committedHandle) { + return false; + } + const currentHandle = + nativeObjectStableHandle(contentWrapperView) ?? + registry.screenContentWrapperHostHandles?.[screenId]; + if (currentHandle && currentHandle === committedHandle) { + return true; + } + const committedWrapperView = nativeObjectFromHandle(committedHandle); + return !!( + committedWrapperView && + (nativeObjectsEqual(committedWrapperView, contentWrapperView) || + viewIsDescendantOfView(committedWrapperView, contentWrapperView) || + viewIsDescendantOfView(contentWrapperView, committedWrapperView)) + ); +} +export function screenContentWrapperMatchesMountedFabricChild( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + contentWrapperView: any, +) { + 'worklet'; + + if (!screenId || !contentWrapperView) { + return false; + } + const mountedHandle = + registry.screenContentWrapperMountedFabricChildHandles?.[screenId]; + if (!mountedHandle) { + return false; + } + const currentHandle = + nativeObjectStableHandle(contentWrapperView) ?? + registry.screenContentWrapperHostHandles?.[screenId]; + if (currentHandle && currentHandle === mountedHandle) { + return true; + } + const mountedWrapperView = nativeObjectFromHandle(mountedHandle); + return !!( + mountedWrapperView && + (nativeObjectsEqual(mountedWrapperView, contentWrapperView) || + viewIsDescendantOfView(mountedWrapperView, contentWrapperView) || + viewIsDescendantOfView(contentWrapperView, mountedWrapperView)) + ); +} +export function screenContentWrapperHasCommittedChildHostContent( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + contentWrapperView: any, +) { + 'worklet'; + + if (!screenId || !contentWrapperView) { + return false; + } + const childHost = screenContentWrapperChildHostView(contentWrapperView); + const isNativeScriptWrapperRoot = + nativeScriptScreenContentWrapperRootViewForView(contentWrapperView) != null; + if (!isNativeScriptWrapperRoot && !childHost) { + return false; + } + const visibleDescendantCount = + registry.screenContentWrapperVisibleDescendantCounts?.[screenId] ?? + screenContentWrapperVisibleDescendantCount(contentWrapperView); + if ( + visibleDescendantCount > 0 || + viewHasVisibleHostedReactContent(contentWrapperView) + ) { + rememberCommittedScreenContentWrapper( + screenId, + registry, + contentWrapperView, + ); + return true; + } + if ( + screenContentWrapperMatchesCommittedHandle( + screenId, + registry, + contentWrapperView, + ) + ) { + return true; + } + return false; +} +export function screenContentWrapperHasLiveMountedHostContent( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + contentWrapperView: any, + screenView?: any, +) { + 'worklet'; + + if (!screenId || !contentWrapperView) { + return false; + } + const resolvedScreenView = + screenView ?? screenControllerView(registry.screens[screenId]); + if ( + resolvedScreenView && + !contentWrapperIsMountedInScreenView(contentWrapperView, resolvedScreenView) + ) { + return false; + } + if (resolvedScreenView?.window != null && contentWrapperView.window == null) { + return false; + } + if ( + screenContentWrapperVisibleDescendantCount(contentWrapperView) > 0 || + viewHasVisibleHostedReactContent(contentWrapperView) + ) { + rememberCommittedScreenContentWrapper( + screenId, + registry, + contentWrapperView, + ); + return true; + } + return false; +} +export function screenContentWrapperHasVisibleMountedFabricChildContent( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + contentWrapperView: any, + screenView?: any, +) { + 'worklet'; + + if ( + !screenId || + !contentWrapperView || + !screenContentWrapperMatchesMountedFabricChild( + screenId, + registry, + contentWrapperView, + ) + ) { + return false; + } + const resolvedScreenView = + screenView ?? screenControllerView(registry.screens?.[screenId]); + if ( + resolvedScreenView && + !contentWrapperIsMountedInScreenView(contentWrapperView, resolvedScreenView) + ) { + return false; + } + const childHost = screenContentWrapperChildHostView(contentWrapperView); + const isNativeScriptWrapperRoot = + nativeScriptScreenContentWrapperRootViewForView(contentWrapperView) != null; + if (!isNativeScriptWrapperRoot && !childHost) { + return false; + } + const visibleDescendantCount = + registry.screenContentWrapperVisibleDescendantCounts?.[screenId] ?? + screenContentWrapperVisibleDescendantCount(contentWrapperView); + if ( + visibleDescendantCount > 0 || + viewHasVisibleHostedReactContent(contentWrapperView) + ) { + rememberCommittedScreenContentWrapper( + screenId, + registry, + contentWrapperView, + ); + return true; + } + return false; +} +export function normalizeScreenContentWrapperChildHost( + contentWrapperView: any, +) { + 'worklet'; + + const childHost = screenContentWrapperChildHostView(contentWrapperView); + if (!contentWrapperView || !childHost) { + return false; + } + let didMutate = false; + if (!nativeObjectsEqual(childHost.superview, contentWrapperView)) { + childHost.removeFromSuperview?.(); + contentWrapperView.addSubview?.(childHost); + didMutate = true; + } + const contentWrapperFrame = contentWrapperView.frame; + const contentWrapperBounds = contentWrapperView.bounds; + contentWrapperView, + screenView, + screenController, + screenOwner, + ) + ) { + traceWorkletEvent( + 'content-wrapper-mount-defer-chain', + `screen=${screenId} wrapper=${ + nativeObjectStableHandle(contentWrapperView) ?? '' + } screen=${nativeObjectStableHandle(screenView) ?? ''}`, + ); + return false; + } + return true; +} +export const __nativeScriptScreenContentWrapperCanMountInScreenViewForTests = + screenContentWrapperCanMountInScreenView; +export function screenHasVisibleHostedContent( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + screenView?: any, +) { + 'worklet'; + + if (!screenId) { + return false; + } + const contentWrapperView = liveScreenContentWrapperViewFromRegistry( + screenId, + registry, + ); + const resolvedScreenView = + screenView ?? screenControllerView(registry.screens?.[screenId]); + if (!contentWrapperView) { + return screenViewHasVisibleHostedReactContent(resolvedScreenView); + } + const wrapperMountedInScreenView = + !!resolvedScreenView && + contentWrapperIsMountedInScreenView(contentWrapperView, resolvedScreenView); + if ( + wrapperMountedInScreenView && + viewHasVisibleHostedReactContent(contentWrapperView) + ) { + return true; + } + return screenHasSeparateVisibleHostedContent( + resolvedScreenView, + contentWrapperView, + ); +} +export function screenHasHostReadyVisibleDescendants( + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + if ( + !screenId || + !screenContentWrapperHasHostReadySignal(screenId, registry) + ) { + return false; + } + if ( + (registry.screenContentWrapperVisibleDescendantCounts?.[screenId] ?? 0) > 0 + ) { + return true; + } + return screenHasVisibleHostedContent( + screenId, + registry, + screenControllerView(registry.screens?.[screenId]), + ); +} +export function screenHeaderSubviewsAreReady( + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + if (!screenId) { + return false; + } + const expectedHeaderSubviewCount = + registry.screenHeaderSubviewExpectedCounts[screenId] ?? 0; + const headerSubviewRecords = headerSubviewRecordsForScreen(screenId); + const headerSubviewRecordCount = headerSubviewRecords.length; + if ( + registry.screenHeaderConfigs[screenId] && + registry.screenHeaderSubviewExpectedCountResolved[screenId] !== true && + (expectedHeaderSubviewCount > 0 || headerSubviewRecordCount > 0) + ) { + traceWorkletEvent( + 'header-subview-not-ready', + `screen=${screenId} reason=expected-unresolved hasConfig=1 records=${headerSubviewRecordCount} expected=${expectedHeaderSubviewCount}`, + ); + return false; + } + if (headerSubviewRecordCount < expectedHeaderSubviewCount) { + traceWorkletEvent( + 'header-subview-not-ready', + `screen=${screenId} reason=records records=${headerSubviewRecordCount} expected=${expectedHeaderSubviewCount}`, + ); + return false; + } + if (expectedHeaderSubviewCount <= 0) { + return true; + } + for (const record of headerSubviewRecords) { + if (record.type === 'searchBar' || record.type === 'back') { + continue; + } + const sizeKey = headerSubviewPublishedSizeKey(record.nativeView); + if (!headerSubviewPublishedSizeIsReady(record)) { + traceWorkletEvent( + 'header-subview-not-ready', + `screen=${screenId} reason=size type=${record.type} size=${sizeKey}`, + ); + return false; + } + } + return true; +} +export const __nativeScriptScreenHeaderSubviewsAreReadyForTests = + screenHeaderSubviewsAreReady; +export function screenHasCommittedContentForNativeStack( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + screenView?: any, +) { + 'worklet'; + + if (!screenId) { + return false; + } + const contentWrapperView = liveScreenContentWrapperViewFromRegistry( + screenId, + registry, + ); + if (contentWrapperView) { + return ( + screenContentWrapperHasVisibleMountedFabricChildContent( + screenId, + registry, + contentWrapperView, + screenView, + ) || + screenContentWrapperHasCommittedChildHostContent( + screenId, + registry, + contentWrapperView, + ) + ); + } + return screenHasVisibleHostedContent(screenId, registry, screenView); +} +export function modalPresentationAncestorIdForContentScreen( + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + if (!screenId) { + return undefined; + } + const separatorIndex = screenId.indexOf(':'); + if (separatorIndex <= 0) { + return undefined; + } + const modalId = screenId.slice(0, separatorIndex); + const modalProps = registry.screenProps?.[modalId]; + if (!modalProps || !isModalPresentation(modalProps.stackPresentation)) { + return undefined; + } + return modalId; +} + } + if (screenRequiresContentWrapperForReady(screenId, registry)) { + return screenContentWrapperHasCommittedHostReadyContent( + screenId, + registry, + screenView, + ); + } + return screenHasCommittedContentForNativeStack( + screenId, + registry, + screenView, + ); +} +export function screenCanCertifyContentReady( + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + if (!screenId) { + return false; + } + const requiresContentWrapper = screenRequiresContentWrapperForReady( + screenId, + registry, + ); + const hasCommittedContent = screenHasCommittedContentForReady( + screenId, + registry, + ); + if (screenCanUseNativeStackControllerViewForReady(screenId, registry)) { + return !!( + screenControllerView(registry.screens?.[screenId]) && hasCommittedContent + ); + } + return ( + screenHostIsReady(screenId, registry) && + (!requiresContentWrapper || hasCommittedContent) + ); +} +export function certifyScreenContentReadyFromHostDescendants( + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + 'worklet'; + + if ( + !screenId || + screenContentIsReady(screenId, registry) || + !screenHostIsReady(screenId, registry) || + !screenHasHostReadyVisibleDescendants(screenId, registry) + ) { + return false; + } + ensureScreenContentWrapperMounted(screenId, registry); + if (!screenCanCertifyContentReady(screenId, registry)) { + return false; + } + registry.screenContentReady[screenId] = true; + return true; +} +export function stackHasActiveUIKitTransitionCoordinator( + stackId: string | undefined, + registry: NativeScriptStackRegistry, + screenId?: string, +) { + 'worklet'; + + const navigationController = stackId ? registry.stacks?.[stackId] : undefined; + const screenController = screenId ? registry.screens?.[screenId] : undefined; + return !!( + transitionCoordinatorForController(navigationController) || + transitionCoordinatorForController(screenController) || + transitionCoordinatorForController(navigationController?.topViewController) + ); +} +export function shouldDeferReadyContentWrapperRefreshDuringTransition( + screenId: string | undefined, + registry: NativeScriptStackRegistry, +) { + '', + nativeObjectStableHandle(contentWrapperView) ?? '', + registry.screenNativePropsKeys?.[screenId] ?? '', + registry.screenContentReady?.[screenId] ? 'ready' : 'pending', + viewLayoutKey(screenView), + viewWindowOriginKey(screenView), + viewLayoutKey(contentWrapperView), + viewWindowOriginKey(contentWrapperView), + directFabricContentWrapperHasCoherentHostedViewport(contentWrapperView) + ? 'coherent' + : 'stale', + nativeObjectStableHandle(screenView?.superview) ?? '', + nativeObjectStableHandle(contentWrapperView?.superview) ?? '', + ].join('|'); +} +export function screenHasCurrentStableReadyExposure( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + screenView: any, +) { + 'worklet'; + + if (!screenId || !screenView) { + return false; + } + const expectedKey = screenStableReadyExposureKey( + screenId, + registry, + screenView, + ); + return !!( + expectedKey && + registry.screenStableReadyExposureKeys?.[screenId] === expectedKey + ); +} +export function screenHasStableReadyMountedContentBase( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + screenView: any, +) { + 'worklet'; + + if (!screenId || !screenView || !screenContentIsReady(screenId, registry)) { + return false; + } + const contentWrapperView = liveScreenContentWrapperViewFromRegistry( + screenId, + registry, + ); + const hasCommittedWrapperContent = + contentWrapperView && + screenContentWrapperHasLiveMountedHostContent( + screenId, + registry, + contentWrapperView, + screenView, + ); + const isModalContainerWithNestedContent = + isModalPresentation(registry.screenProps?.[screenId]?.stackPresentation) && + presentedModalHasNestedContentScreen(screenId, registry); + if (isModalContainerWithNestedContent && !hasCommittedWrapperContent) { + traceWorkletEvent( + 'screen-stable-content-miss', + `screen=${screenId} reason=modal-nested-container wrapper=${ + contentWrapperView ? 1 : 0 + }`, + ); + return false; + } + if ( + contentWrapperView && + !directFabricContentWrapperHasCoherentHostedViewport(contentWrapperView) + ) { + traceWorkletEvent( + 'screen-stable-content-miss', + `screen=${screenId} reason=direct-fabric-child-viewport`, + ); + return false; + } + return !!( + (!contentWrapperView || + (contentWrapperIsMountedInScreenView(contentWrapperView, screenView) && + hiddenAncestorBetweenViewAndAncestor(contentWrapperView, screenView) == + null)) && + (hasCommittedWrapperContent || + screenHasVisibleHostedContent(screenId, registry, screenView)) + ); +} +export function rememberScreenStableReadyExposure( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + screenView: any, +) { + 'worklet'; + + if ( + !screenId || + !screenView || + screenView.userInteractionEnabled === false || + !screenHasStableReadyMountedContentBase(screenId, registry, screenView) + ) { + return false; + } + const contentWrapperView = liveScreenContentWrapperViewFromRegistry( + screenId, + registry, + ); + if ( + contentWrapperView && + contentWrapperView.__nativeScriptEmptyContentWrapperShell !== true && + contentWrapperView.userInteractionEnabled === false + ) { + return false; + } + const exposureKey = screenStableReadyExposureKey( + screenId, + registry, + screenView, + ); + if (!exposureKey) { + return false; + } + const exposureKeys = + registry.screenStableReadyExposureKeys ?? + (registry.screenStableReadyExposureKeys = {}); + exposureKeys[screenId] = exposureKey; + return true; +} +export function screenHasStableReadyMountedContent( + screenId: string | undefined, + registry: NativeScriptStackRegistry, + screenView: any, +) { + 'worklet'; + + if (!screenHasStableReadyMountedContentBase(screenId, registry, screenView)) { diff --git a/.oracle-context/ns-rns-first-paint/relevant-tests.ts b/.oracle-context/ns-rns-first-paint/relevant-tests.ts new file mode 100644 index 000000000..4ca7a4bd2 --- /dev/null +++ b/.oracle-context/ns-rns-first-paint/relevant-tests.ts @@ -0,0 +1,597 @@ + expect(refreshUIKitHostView).toHaveBeenCalled(); + expect( + globalObject.__rnsNativeScriptLayoutHostedSubviewChain, + ).toHaveBeenCalled(); + expect(registry.screenContentWrapperHostReadyKeys.screen).toBe( + 'wrapper-handle|77|window', + ); + expect(registry.screenContentWrapperVisibleDescendantCounts.screen).toBe( + 77, + ); + expect(registry.screenContentReady.screen).toBe(true); + } finally { + NativeScriptRuntime.refreshUIKitHostViewDirectOwner = + previousRefreshUIKitHostView; + + if (previousNativeObjectFromHandle === undefined) { + delete (NativeScriptRuntime as any).nativeObjectFromHandle; + } else { + (NativeScriptRuntime as any).nativeObjectFromHandle = + previousNativeObjectFromHandle; + } + + if (previousLayoutHostedSubviewChain === undefined) { + delete globalObject.__rnsNativeScriptLayoutHostedSubviewChain; + } else { + globalObject.__rnsNativeScriptLayoutHostedSubviewChain = + previousLayoutHostedSubviewChain; + } + + if (previousRegistry === undefined) { + delete globalObject.__rnsNativeScriptStackRegistry; + } else { + globalObject.__rnsNativeScriptStackRegistry = previousRegistry; + } + } + }); + + it('does not certify a displayable empty wrapper as committed stack content', () => { + const NativeScriptRuntime = jest.requireActual( + '@nativescript/react-native', + ); + const globalObject = global as Record; + const previousRegistry = globalObject.__rnsNativeScriptStackRegistry; + const previousReconcileStack = globalObject.__rnsNativeScriptReconcileStack; + const previousNativeObjectFromHandle = (NativeScriptRuntime as any) + .nativeObjectFromHandle; + const previousRefreshUIKitHostView = + NativeScriptRuntime.refreshUIKitHostViewDirectOwner; + const frame = { + origin: { x: 0, y: 0 }, + size: { height: 874, width: 402 }, + }; + const window = {}; + const screenView: any = { + __rnsNativeScriptScreenView: true, + alpha: 1, + bounds: frame, + frame, + hash: 'detail-screen-host', + hidden: false, + refreshSurfaceTouchHandler: jest.fn(), + subviews: [], + userInteractionEnabled: true, + window, + }; + const emptyContentWrapperView: any = { + __rnsNativeScriptScreenContentWrapperChildrenView: null, + __rnsNativeScriptScreenContentWrapperRootView: true, + alpha: 1, + bounds: frame, + description: 'RNSScreenContentWrapper.NativeScript', + frame, + hash: 'detail-wrapper-host', + hidden: false, + subviews: [], + superview: screenView, + userInteractionEnabled: true, + window, + }; + const detailController: any = { + __nativeScriptScreenId: 'detail', + __nativeScriptScreenView: screenView, + view: screenView, + }; + const navigationController: any = { + topViewController: detailController, + view: { bounds: frame, frame, window }, + viewControllers: [detailController], + }; + const reconcileStack = jest.fn(); + const registry: any = { + screenContentReady: {}, + screenContentWrapperCommittedHandles: {}, + screenContentWrapperHostHandles: {}, + screenContentWrapperHostReadyKeys: {}, + screenContentWrapperRefreshKeys: {}, + screenContentWrapperViews: {}, + screenContentWrapperVisibleDescendantCounts: {}, + screenHostHandles: { detail: 'detail-screen-host' }, + screenHostRefreshKeys: {}, + screenParents: { detail: 'stack' }, + screenProps: { detail: { screenId: 'detail' } }, + screenSurfaceTouchRefreshKeys: {}, + screens: { detail: detailController }, + stackActiveScreenIds: { stack: ['root', 'detail'] }, + stackContexts: { stack: {} }, + stackNativeCounts: { stack: 1 }, + stackNativeKeys: { stack: 'root' }, + stackPendingReconcileKeys: {}, + stackTransitioning: {}, + stacks: { stack: navigationController }, + stackUpdatingModals: {}, + }; + + screenView.subviews = [emptyContentWrapperView]; + globalObject.__rnsNativeScriptReconcileStack = reconcileStack; + globalObject.__rnsNativeScriptStackRegistry = registry; + NativeScriptRuntime.refreshUIKitHostViewDirectOwner = jest.fn(() => false); + (NativeScriptRuntime as any).nativeObjectFromHandle = jest.fn( + (handle: string) => { + if (handle === 'detail-wrapper-host') { + return emptyContentWrapperView; + } + + if (handle === 'detail-screen-host') { + return screenView; + } + + return null; + }, + ); + + try { + nativeScriptScreenContentWrapperHostReadyOnUI( + 'detail', + 'detail-wrapper-host', + 0, + true, + ); + + expect(registry.screenContentReady.detail).toBeUndefined(); + expect( + registry.screenContentWrapperCommittedHandles.detail, + ).toBeUndefined(); + expect( + __nativeScriptScreenHasStableReadyMountedContentForTests( + 'detail', + registry, + screenView, + ), + ).toBe(false); + } finally { + NativeScriptRuntime.refreshUIKitHostViewDirectOwner = + previousRefreshUIKitHostView; + + if (previousNativeObjectFromHandle === undefined) { + delete (NativeScriptRuntime as any).nativeObjectFromHandle; + } else { + (NativeScriptRuntime as any).nativeObjectFromHandle = + previousNativeObjectFromHandle; + } + + if (previousReconcileStack === undefined) { + delete globalObject.__rnsNativeScriptReconcileStack; + } else { + globalObject.__rnsNativeScriptReconcileStack = previousReconcileStack; + } + + if (previousRegistry === undefined) { + delete globalObject.__rnsNativeScriptStackRegistry; + } else { + globalObject.__rnsNativeScriptStackRegistry = previousRegistry; + } + } + }); + + it('waits for Fabric content certification before native-stack UIKit work', () => { + const NativeScriptRuntime = jest.requireActual( + '@nativescript/react-native', + ); + const previousNativeObjectFromHandle = (NativeScriptRuntime as any) + .nativeObjectFromHandle; + const frame = { + origin: { x: 0, y: 0 }, + size: { height: 874, width: 402 }, + }; + const window = {}; + const rootScreenView: any = { + __rnsNativeScriptScreenView: true, + alpha: 1, + bounds: frame, + frame, + hash: 'root-screen-host', + hidden: false, + subviews: [], + userInteractionEnabled: true, + window, + }; + const detailScreenView: any = { + __rnsNativeScriptScreenView: true, + alpha: 1, + bounds: frame, + frame, + hash: 'detail-screen-host', + hidden: false, + subviews: [], + userInteractionEnabled: true, + window, + }; + const rootHostedLeaf: any = { + alpha: 1, + description: 'RCTViewComponentView', + hidden: false, + subviews: [], + userInteractionEnabled: true, + window, + }; + const rootWrapperView: any = { + __rnsNativeScriptScreenContentWrapperRootView: true, + alpha: 1, + bounds: frame, + description: 'RNSScreenContentWrapper.NativeScript', + frame, + hash: 'root-wrapper-host', + hidden: false, + subviews: [rootHostedLeaf], + superview: rootScreenView, + userInteractionEnabled: true, + window, + }; + const detailWrapperView: any = { + __rnsNativeScriptScreenContentWrapperRootView: true, + alpha: 1, + bounds: frame, + description: 'RNSScreenContentWrapper.NativeScript', + frame, + hash: 'detail-wrapper-host', + hidden: false, + subviews: [], + superview: detailScreenView, + userInteractionEnabled: true, + window, + }; + const detailHostedLeaf: any = { + alpha: 1, + description: 'RCTViewComponentView', + hidden: false, + subviews: [], + userInteractionEnabled: true, + window, + }; + rootHostedLeaf.superview = rootWrapperView; + rootScreenView.subviews = [rootWrapperView]; + detailScreenView.subviews = [detailWrapperView]; + const rootController: any = { + __nativeScriptScreenId: 'root', + __nativeScriptScreenView: rootScreenView, + view: rootScreenView, + }; + const detailController: any = { + __nativeScriptScreenId: 'detail', + __nativeScriptScreenView: detailScreenView, + view: detailScreenView, + }; + const navigationController: any = { + topViewController: rootController, + view: { bounds: frame, frame, window }, + viewControllers: [rootController], + }; + const registry: any = { + screenContentReady: { root: true }, + screenContentWrapperCommittedHandles: { root: 'root-wrapper-host' }, + screenContentWrapperHostHandles: { + root: 'root-wrapper-host', + }, + screenContentWrapperHostReadyKeys: {}, + screenContentWrapperMountedFabricChildHandles: { + root: 'root-wrapper-host', + }, + screenContentWrapperRefreshKeys: {}, + screenContentWrapperViews: { + root: rootWrapperView, + }, + screenContentWrapperVisibleDescendantCounts: { + root: 1, + }, + screenHostHandles: { + detail: 'detail-screen-host', + root: 'root-screen-host', + }, + screenParents: { detail: 'stack', root: 'stack' }, + screenProps: { + detail: { activityState: 2, screenId: 'detail' }, + root: { activityState: 2, screenId: 'root' }, + }, + screenSurfaceTouchRefreshKeys: {}, + screens: { detail: detailController, root: rootController }, + stackActiveScreenIds: { stack: ['root', 'detail'] }, + stackContexts: { stack: {} }, + stackMountedScreenIds: { stack: ['root', 'detail'] }, + stackNativeCounts: { stack: 1 }, + stackNativeKeys: { stack: 'root' }, + stackPendingReconcileKeys: {}, + stackTransitioning: {}, + stackUsesFabricMountedChildren: { stack: true }, + stackUpdatingModals: {}, + stacks: { stack: navigationController }, + }; + + (NativeScriptRuntime as any).nativeObjectFromHandle = jest.fn( + (handle: string) => { + if (handle === 'root-wrapper-host') { + return rootWrapperView; + } + + if (handle === 'detail-wrapper-host') { + return detailWrapperView; + } + + if (handle === 'root-screen-host') { + return rootScreenView; + } + + if (handle === 'detail-screen-host') { + return detailScreenView; + } + + return null; + }, + ); + + try { + expect( + __nativeScriptStackTopScreenCanStartNativeUpdateForTests( + ['root', 'detail'], + registry, + 'stack', + ), + ).toBe(false); + expect( + __nativeScriptStackCanReconcileFromPropsUpdateForTests( + 'stack', + registry, + ['root', 'detail'], + ), + ).toBe(false); + expect(registry.screenContentReady.detail).toBeUndefined(); + + registry.screenProps.detail.stackPresentation = 'modal'; + registry.screenContentReady.detail = undefined; + + expect( + __nativeScriptStackTopScreenCanStartNativeUpdateForTests( + ['root', 'detail'], + registry, + 'stack', + ), + ).toBe(false); + expect( + __nativeScriptStackCanReconcileFromPropsUpdateForTests( + 'stack', + registry, + ['root', 'detail'], + ), + ).toBe(false); + + registry.screenContentWrapperHostHandles.detail = 'detail-wrapper-host'; + registry.screenContentWrapperViews.detail = detailWrapperView; + registry.screenContentWrapperVisibleDescendantCounts.detail = 0; + registry.screenProps.detail.stackPresentation = undefined; + + expect( + __nativeScriptStackTopScreenCanStartNativeUpdateForTests( + ['root', 'detail'], + registry, + 'stack', + ), + ).toBe(false); + expect(registry.screenContentReady.detail).toBeUndefined(); + + registry.screenProps.detail.stackPresentation = 'modal'; + registry.screenContentReady.detail = undefined; + + expect( + __nativeScriptStackTopScreenCanStartNativeUpdateForTests( + ['root', 'detail'], + registry, + 'stack', + ), + ).toBe(false); + expect(registry.screenContentReady.detail).toBeUndefined(); + registry.screenProps.detail.stackPresentation = undefined; + + registry.screenContentWrapperMountedFabricChildHandles.detail = + 'detail-wrapper-host'; + + expect( + __nativeScriptStackTopScreenCanStartNativeUpdateForTests( + ['root', 'detail'], + registry, + 'stack', + ), + ).toBe(false); + expect(registry.screenContentReady.detail).toBeUndefined(); + expect( + registry.screenContentWrapperCommittedHandles.detail, + ).toBeUndefined(); + expect( + __nativeScriptStackCanReconcileFromPropsUpdateForTests( + 'stack', + registry, + ['root', 'detail'], + ), + ).toBe(false); + + detailHostedLeaf.superview = detailWrapperView; + detailWrapperView.subviews = [detailHostedLeaf]; + registry.screenContentWrapperVisibleDescendantCounts.detail = 1; + registry.screenProps.detail.stackPresentation = undefined; + registry.screenContentReady.detail = undefined; + + expect( + __nativeScriptStackTopScreenCanStartNativeUpdateForTests( + ['root', 'detail'], + registry, + 'stack', + ), + ).toBe(true); + expect( + __nativeScriptStackCanReconcileFromPropsUpdateForTests( + 'stack', + registry, + ['root', 'detail'], + ), + ).toBe(true); + expect(registry.screenContentReady.detail).toBe(true); + + registry.screenContentReady.detail = undefined; + registry.stackNativeKeys.stack = undefined; + registry.stackNativeCounts.stack = 0; + + expect( + __nativeScriptStackTopScreenCanStartNativeUpdateForTests( + ['root', 'detail'], + registry, + 'stack', + ), + ).toBe(true); + expect( + __nativeScriptStackCanReconcileFromPropsUpdateForTests( + 'stack', + const pushSource = stackSource.slice( + stackSource.indexOf('const isPush ='), + stackSource.indexOf('if (isPop && nativeIds.length > nextCount)'), + ); + const popSource = stackSource.slice( + stackSource.indexOf('if (isPop && nativeIds.length > nextCount)'), + stackSource.indexOf( + 'const previousTopScreenId = previousIds[previousCount - 1]', + ), + ); + const beforeNativeIndex = pushSource.indexOf( + "'reconcileStack-push-before-native'", + ); + const animateCallIndex = pushSource.indexOf( + 'didAnimatePush = animateStackPush(', + ); + const markTransitionIndex = pushSource.indexOf( + 'const token = markTransition', + ); + const prepareExposureIndex = pushSource.indexOf( + 'prepareControllerForStackExposure(', + ); + const prepareVisibleIndex = pushSource.indexOf( + 'prepareVisibleScreenForImmediateInteraction(', + pushSource.indexOf('if (didAnimatePush) {'), + ); + const emitTransitionIndex = pushSource.indexOf( + "emitTransition(ctx, 'start', false, pushedScreenId)", + ); + + expect(stackSource).not.toContain('stackContentReadyRetryTokens'); + expect(stackSource).not.toContain('function scheduleContentReadyReconcile'); + expect(stackSource).not.toContain( + 'function pushedScreenHostedContentIsReady', + ); + expect(pushSource).not.toContain('configureScreenControllerHeader('); + expect(pushSource).not.toContain('configureSourceBackButton('); + expect(stackSource).toContain('configureHeaderForWillShowViewController('); + expect(pushSource).toContain('prepareControllerForStackExposure('); + expectSourceToContain( + pushSource, + 'prepareControllerForStackExposure(\n' + + ' navigationController,\n' + + ' pushedScreenId,\n' + + ' pushedController,\n' + + ' registry,\n' + + ' ctx,\n' + + ' true,\n' + + ' nextCount > 1,\n' + + ' true,\n' + + ' true,\n' + + ' true,\n' + + ' );', + ); + expect(pushSource).not.toContain('pushedScreenHasReadyHostDescendants'); + expect(pushSource).not.toContain('pushedScreenHasVisibleHostedContent'); + expect(pushSource).not.toContain('shouldForcePushedContentRefresh'); + expectSourceToContain( + stackSource, + 'const contentIsReady = screenContentIsReady(screenId, registry);\n' + + ' const hasCommittedNativeStackContent = screenHasCommittedContentForNativeStack(\n' + + ' screenId,\n' + + ' registry,\n' + + ' controllerView,\n' + + ' );\n' + + 'const alreadyPreparedCommittedContent =\n' + + ' contentIsReady &&\n' + + ' (!contentWrapperView || wrapperAlreadyMounted) &&\n' + + ' hasCommittedNativeStackContent;', + ); + const nativeTransitionPrepareSource = stackSource.slice( + stackSource.indexOf( + 'function prepareScreenHostedContentForNativeTransition', + ), + stackSource.indexOf( + 'export const __nativeScriptPrepareScreenHostedContentForNativeTransitionForTests', + ), + ); + expect(nativeTransitionPrepareSource).not.toContain( + 'screenHasCurrentStableReadyExposure', + ); + expectSourceToContain( + stackSource, + 'refreshScreenContentReady(screenId, controller, registry, true);\n\n' + + ' const didNormalizeWrapper = ensureScreenContentWrapperMounted(', + ); + expectSourceToContain( + stackSource, + 'const needsContentRefreshAfterRefresh =\n' + + ' refreshHostedContent && !screenContentIsReady(screenId, registry);', + ); + expectSourceToContain( + stackSource, + 'if (controllerView && needsContentRefreshAfterRefresh) {\n' + + ' layoutScreenHostedReactSubviews(', + ); + expectSourceToContain( + stackSource, + 'if (needsContentRefreshAfterRefresh) {\n' + + ' refreshScreenContentWrapperHost(', + ); + expectSourceToContain( + pushSource, + 'const didPreparePushedContent =\n' + + ' prepareScreenHostedContentForNativeTransition(\n' + + ' pushedController,\n' + + ' pushedScreenId,\n' + + ' registry,\n' + + ' );', + ); + expectSourceToContain( + pushSource, + 'const pushedContentIsReady = screenContentIsReady(\n' + + ' pushedScreenId,\n' + + ' registry,\n' + + ' );', + ); + const postAnimateSource = pushSource.slice( + pushSource.indexOf('if (didAnimatePush) {'), + pushSource.indexOf( + 'scheduleTransitionFallback(', + pushSource.indexOf('if (didAnimatePush) {'), + ), + ); + expect(postAnimateSource).not.toContain( + 'layoutNavigationStackViews(navigationController);', + ); + expect(postAnimateSource).not.toContain('flushKnownUIKitHostView('); + expect(postAnimateSource).toContain( + 'prepareVisibleScreenForImmediateInteraction(', + ); + expect(popSource).not.toContain( + 'prepareVisibleScreenForImmediateInteraction(', + ); + expect(popSource).toContain('prepareRevealedScreenForNativePop('); + expect( + popSource.indexOf('prepareRevealedScreenForNativePop('), + ).toBeLessThan(popSource.indexOf('const token = markTransition')); + expect(popSource).toContain('if (!didPrepareRevealedScreen) {'); + expect(popSource).not.toContain('prepareControllerForStackExposure('); + expect(pushSource).not.toMatch( + /if\s*\(\s*!didPreparePushedContent\s*&&\s*!screenContentIsReady\(\s*pushedScreenId,\s*registry,?\s*\)\s*\)/, + ); + expect(pushSource).not.toContain( + '!screenHeaderSubviewsAreReady(pushedScreenId, registry)', + ); diff --git a/.oracle-context/ns-rns-first-paint/rns-port-trace-export-src.json b/.oracle-context/ns-rns-first-paint/rns-port-trace-export-src.json new file mode 100644 index 000000000..7aa07d37e --- /dev/null +++ b/.oracle-context/ns-rns-first-paint/rns-port-trace-export-src.json @@ -0,0 +1,127 @@ +{ + "demo": [], + "exportedAt": 43694914, + "nsrns": [ + "[NSRNS_TRACE 43694435] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694437] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694437] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0", + "[NSRNS_TRACE 43694446] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694447] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694450] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694450] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694454] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694455] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694456] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694456] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176200e00 container=0x16ee1de00 native=0x16f9af100 children=0x16f9af100 resolved=0x16f9af100 screenView=0x16f9cca80", + "[NSRNS_TRACE 43694456] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1", + "[NSRNS_TRACE 43694456] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694456] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694461] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694461] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694461] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694468] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80", + "[NSRNS_TRACE 43694468] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function", + "[NSRNS_TRACE 43694469] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694469] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694470] content-wrapper-resolve-nearest-screen-child screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa08700 screenView=0x16f9cca80 resolved=0x176201500 super=0x16f9cca80", + "[NSRNS_TRACE 43694470] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176201500 container=0x16ee1e100 native=0x16fa08700 children=0x16fa08700 resolved= screenView=0x16f9cca80", + "[NSRNS_TRACE 43694471] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694471] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16f9af100 container=0x16f9af100 native= children= resolved=0x16f9af100 screenView=0x16f9cca80", + "[NSRNS_TRACE 43694471] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1", + "[NSRNS_TRACE 43694471] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694471] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694475] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694475] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694475] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694482] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80", + "[NSRNS_TRACE 43694483] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function", + "[NSRNS_TRACE 43694483] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694483] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694484] content-wrapper-resolve-fallback screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa75900 screenView=0x16f9cca80 resolved= super=", + "[NSRNS_TRACE 43694484] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16fa75900 container=0x16fa75900 native= children= resolved= screenView=0x16f9cca80", + "[NSRNS_TRACE 43694487] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694487] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694497] stack-host-transaction-enter stack=rn-ns-stack-1 active=Home-FXqHwBeMBsCdyK5TIjTRS,Detail--si7XIE3EagJ78yAR7spP revision=2 fabricChildren=3 props=1", + "[NSRNS_TRACE 43694500] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694500] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0", + "[NSRNS_TRACE 43694509] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694509] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694514] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694514] content-wrapper-host-ready-resolved screen=Home-FXqHwBeMBsCdyK5TIjTRS handle=0x15d960700 resolved=0x15d960700 registered=0x15d960700 window=0 descendants=77", + "[NSRNS_TRACE 43694515] content-wrapper-host-ready screen=Home-FXqHwBeMBsCdyK5TIjTRS stack=rn-ns-stack-1 modal=0 ready=1 previousReady=1 descendants=77 window=0", + "[NSRNS_TRACE 43694515] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694515] content-wrapper-host-ready-noop-reject screen=Home-FXqHwBeMBsCdyK5TIjTRS stack=rn-ns-stack-1 reason=offwindow previous=0x15d960700 handle=0x15d960700 previousReady=0x15d960700|77|window ready=0x15d960700|77|offwindow window=0 pending= native=Home-FXqHwBeMBsCdyK5TIjTRS\u001fDetail--si7XIE3EagJ78yAR7spP didAppear=1", + "[NSRNS_TRACE 43694515] content-wrapper-host-ready-state screen=Home-FXqHwBeMBsCdyK5TIjTRS stack=rn-ns-stack-1 contentReady=1 wrapperCanStart=0 canUse=1 ctx=1 defer=0 skipHosted=0 prepareTab=0 transitioning=1", + "[NSRNS_TRACE 43694515] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694515] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694515] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0", + "[NSRNS_TRACE 43694521] screen-touch-refresh-decision screen=Home-FXqHwBeMBsCdyK5TIjTRS reason=content-wrapper-host-ready screenView=0x15d960e00 controller=4372564992 wrapper=0x15d960700 current=0 skip=0 skipHosted=0 transitioning=1 defer=0 nonTopDefer=0 offwindow=0 forceLayout=0 forceTouch=0 requestedTouch=0 reattach=0 alreadyLayout=0", + "[NSRNS_TRACE 43694522] screen-touch-refresh screen=Home-FXqHwBeMBsCdyK5TIjTRS did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function", + "[NSRNS_TRACE 43694522] content-wrapper-resolve-root screen=Home-FXqHwBeMBsCdyK5TIjTRS mounted=0x15d960700 screenView=0x15d960e00 resolved=0x15d960700 super=0x15d960e00", + "[NSRNS_TRACE 43694522] content-wrapper-ensure screen=Home-FXqHwBeMBsCdyK5TIjTRS screenView=0x15d960e00 wrapper=0x15d960700 super=0x15d960e00 mounted=1 wrapperWindow=0 screenWindow=0", + "[NSRNS_TRACE 43694531] content-wrapper-host-ready-total 16ms screen=Home-FXqHwBeMBsCdyK5TIjTRS", + "[NSRNS_TRACE 43694535] stack-host-transaction-enter stack=rn-ns-stack-1 active=Home-FXqHwBeMBsCdyK5TIjTRS,Detail--si7XIE3EagJ78yAR7spP revision=2 fabricChildren=3 props=1", + "[NSRNS_TRACE 43694537] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694537] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694540] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694540] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694544] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694545] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694572] transition-fallback-deferred-coordinator-miss stack=rn-ns-stack-1 screen=Detail--si7XIE3EagJ78yAR7spP closing=0", + "[NSRNS_TRACE 43694575] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1", + "[NSRNS_TRACE 43694586] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9afd40 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694586] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176201880 container=0x16ee1e400 native=0x16f9afd40 children=0x16f9ad180 resolved=0x16f9af100 screenView=0x16f9cca80", + "[NSRNS_TRACE 43694586] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1", + "[NSRNS_TRACE 43694586] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694587] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694591] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694591] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694592] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694604] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80", + "[NSRNS_TRACE 43694605] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function", + "[NSRNS_TRACE 43694605] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694605] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694606] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694606] content-wrapper-transaction-sync screen=Detail--si7XIE3EagJ78yAR7spP children=1 mounted=1 ready=1", + "[NSRNS_TRACE 43694606] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694606] content-wrapper-host-ready-resolved screen=Detail--si7XIE3EagJ78yAR7spP handle=0x16f9af100 resolved=0x16f9af100 registered=0x16f9af100 window=0 descendants=1", + "[NSRNS_TRACE 43694606] content-wrapper-host-ready-ignore-offwindow-stale screen=Detail--si7XIE3EagJ78yAR7spP stack=rn-ns-stack-1 descendants=1", + "[NSRNS_TRACE 43694608] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694608] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176200e00 container=0x16ee1de00 native=0x16f9af100 children=0x16f9af100 resolved=0x16f9af100 screenView=0x16f9cca80", + "[NSRNS_TRACE 43694608] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1", + "[NSRNS_TRACE 43694608] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694608] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694613] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694613] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694614] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694621] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80", + "[NSRNS_TRACE 43694622] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function", + "[NSRNS_TRACE 43694622] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694622] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694624] content-wrapper-resolve-nearest-screen-child screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa08700 screenView=0x16f9cca80 resolved=0x176201500 super=0x16f9cca80", + "[NSRNS_TRACE 43694624] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x176201500 container=0x16ee1e100 native=0x16fa08700 children=0x16fa08700 resolved= screenView=0x16f9cca80", + "[NSRNS_TRACE 43694624] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694624] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16f9af100 container=0x16f9af100 native= children= resolved=0x16f9af100 screenView=0x16f9cca80", + "[NSRNS_TRACE 43694624] content-wrapper-remember screen=Detail--si7XIE3EagJ78yAR7spP wrapper=0x16f9af100 super=0x16f9cca80 window=1", + "[NSRNS_TRACE 43694624] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694625] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694630] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694630] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694630] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694638] touch-refresh view=0x16f9cca80 screen=1 should=1 own=1 attached=1 window=0x104a00000 super=0x16f9eed80", + "[NSRNS_TRACE 43694639] screen-touch-refresh screen=Detail--si7XIE3EagJ78yAR7spP did=1 detachedWrapper=0 custom=1 own=1 reattach=0 has=function", + "[NSRNS_TRACE 43694639] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694639] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694640] content-wrapper-resolve-fallback screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16fa75900 screenView=0x16f9cca80 resolved= super=", + "[NSRNS_TRACE 43694640] content-wrapper-mount-child screen=Detail--si7XIE3EagJ78yAR7spP component=0x16fa75900 container=0x16fa75900 native= children= resolved= screenView=0x16f9cca80", + "[NSRNS_TRACE 43694643] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694643] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694649] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694649] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694652] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694652] content-wrapper-ensure screen=Detail--si7XIE3EagJ78yAR7spP screenView=0x16f9cca80 wrapper=0x16f9af100 super=0x16f9cca80 mounted=1 wrapperWindow=1 screenWindow=1", + "[NSRNS_TRACE 43694657] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80", + "[NSRNS_TRACE 43694657] content-wrapper-resolve-root screen=Detail--si7XIE3EagJ78yAR7spP mounted=0x16f9af100 screenView=0x16f9cca80 resolved=0x16f9af100 super=0x16f9cca80" + ], + "reason": "push" +} \ No newline at end of file diff --git a/HANDOFF-FABLE5.md b/HANDOFF-FABLE5.md new file mode 100644 index 000000000..26d216c6f --- /dev/null +++ b/HANDOFF-FABLE5.md @@ -0,0 +1,1412 @@ +# Handoff for Claude (Fable 5) — NativeScript react-native-screens Parity + +> Written 2026-07-05 by a fresh assistant, distilled from the Codex session +> `019f0fa7` (thread "NativeScriptRuntime", cwd `/Users/dj/Developer/NativeScriptRuntime`) +> plus the on-disk `HANDOFF.md` / `PROGRESS.md` / `RN_API.md`. +> +> This is a concise entry point. The authoritative running log is the existing +> `HANDOFF.md` (176 KB, dated updates through 2026-07-02) and `PROGRESS.md` +> (803 KB). Read those for full detail. This file captures the goal, the current +> state as of the last session turn (2026-07-05 20:52 EDT), and how to continue. + +## Goal + +Reach practical **parity with upstream `react-native-screens` (RNS)** for the +NativeScript TypeScript / UIKit port. The port must follow upstream RNS +mechanics as closely as possible. Where NativeScript lacks a primitive that +upstream RNS/native/Fabric relies on, add the **generic** primitive to the +NativeScript React Native runtime / Fabric / UI-worklet surface — do not add +port-specific hacks. + +The goal is **not complete**. Do not mark it complete until the port matches +original RNS behavior on the dedicated simulators in pixels, latency, touch +reliability, and modal / stack / tab behavior. + +## Hard rules (from the user — keep following these) + +- No subagents. +- No hacks that hide races: no fixed delays, timers, or retry loops. +- Do **not** add native ObjC/Swift module code for the RNS port. UIKit + views/controllers are implemented in TS through NativeScript. If a primitive + is missing, expose it generically from the runtime, then use it from TS. +- Pixel-visible / SimDeck behavior is the source of truth. Passing Jest + AX / + layout state is **not** proof — tests have gone green while pixels were wrong. +- Work one parity bug at a time, always comparing against the original RNS app. +- Test on the dedicated **simulators only** — no physical iPhone/iPad for this PR. +- Keep RN-module work isolated from the `refactor` (Node-API) branch. + +## Repos & branch + +- **Runtime (this repo):** `/Users/dj/Developer/NativeScriptRuntime` + - Branch: **`codex/rn-module-fabric-turbomodule-worklets`** (draft PR #46), + created from `refactor` at `f3d0b3f4ac6f6ff5753d321e7bb7ecc7e78f3443`. + - `refactor` branch is reserved for the separate Node-API / direct-engine + backend PR — do not mix RN-module work into it. + - HEAD: `2730d717 docs: update RNS parity progress`. Working tree is **dirty** + (~38 changed files); this is expected. Do not revert prior/user changes. +- **Screens fork (most active code):** `/Users/dj/Developer/RNModuleForks/react-native-screens` +- **Port demo app (simulator testbed):** `/Users/dj/Developer/RNModuleForks/nativescript-uikit-demo` +- **Original comparison app:** `/Users/dj/Developer/RNModuleForks/nativescript-uikit-demo-original` + +Always check status before editing (both repos are dirty): + +```sh +git -C /Users/dj/Developer/NativeScriptRuntime status --short +git -C /Users/dj/Developer/RNModuleForks/react-native-screens status --short +``` + +## Simulators & Metro + +- **Port sim:** `NS Screens Only iPhone 17 178137` + - UDID `BF759806-2EBB-49ED-AD8E-413A7790ADE0`, bundle `org.nativescript.uikit.demo` + - Port Metro on `8082`, dev-client URL: + `nativescriptuikitdemo://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8082` +- **Original sim:** `NS Screens Original iPhone 17 178137` + - UDID `3931FD88-6C29-44AA-BD73-4A40C4334B5B`, bundle `org.nativescript.uikit.demo.original` + - Original Metro on `8083`, dev-client URL: + `originalnativetabsdemo://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8083` + +Drive/inspect the sim with **SimDeck** (`simdeck use `, browser at +`http://127.0.0.1:4310`). If SimDeck returns malformed HTTP / connection +refusals, fall back to Callstack's **`agent-device`** CLI (npm package +`agent-device`, not `@callstack/…`): + +```sh +npx --yes agent-device open org.nativescript.uikit.demo --platform ios --udid BF759806-2EBB-49ED-AD8E-413A7790ADE0 +npx --yes agent-device snapshot -i --platform ios --udid BF759806-2EBB-49ED-AD8E-413A7790ADE0 +xcrun simctl io BF759806-2EBB-49ED-AD8E-413A7790ADE0 screenshot /tmp/port.png +``` + +> Note: earlier in the week `simctl`/CoreSimulatorService was intermittently +> returning `Connection refused` / `DEVICE_NOT_FOUND`, which blocked pixel +> verification for several turns. As of the 2026-07-05 session the simulator was +> working again and pixel probes were running. If it breaks again, that harness +> state — not the port code — is usually the blocker. + +## Update 2026-07-05 ~23:30 EDT (Fable session): modal blank body FIXED, latency parity still open + +Root causes found (simulator + `NS_NS_FABRIC_DEBUG=1` native mutation log + lldb +view-hierarchy dump — all three were needed): + +1. **Present-before-children (ordering).** Fabric applies `Update` mutations + before `Insert` mutations within one transaction. The immediate reconcile + added on 2026-07-05 fired from the root stack's `updateProps` — + mid-transaction — so `presentViewController` ran before ANY of the modal + subtree inserts, and present's ~150 ms of synchronous UIKit work then + blocked the rest of the mutation loop. Upstream `RNSScreenStackView` only + presents from `mountingTransactionDidMount` → `dispatch_async(main)` → + `maybeAddToParentAndUpdateContainer` (see ios/RNSScreenStack.mm:1283-1370) + — i.e. after all inserts *and* layout. + **Fix:** `stackCanApplyPendingPushBeforeMountedChildren` now refuses when + the pending (not-yet-Fabric-mounted) screens include a modal presentation + (`pendingPushScreenIdsIncludeModalPresentation`, chunk2). The modal present + defers to the existing transaction path → `schedulePostLayoutStackReconcile` + (the port's equivalent of upstream's post-layout dispatch_async). + +2. **Wrong view remembered as content wrapper (paint).** In dev, RNS's + `DebugContainer` wraps modal content in RN's `AppContainer` (for LogBox), + which renders a sibling `DebuggingOverlay` view AFTER the content wrapper. + The port's heuristic wrapper resolution (nearest full-size RCTView child) + flip-flopped between the real wrapper and the overlay wrapper, remembered + the overlay, and `applyScreenControllerBackgroundColor` painted it with the + screen background — an opaque full-size cover ABOVE the real content + (hit-testing passed through, so it was "interactive but invisible", and on + cold presents it never repaired). Upstream identifies the wrapper by native + class (`RNSScreenContentWrapper`). + **Fix:** `ScreenContentWrapper` (iOS) now renders with + `nativeID="RNSScreenContentWrapper"`, and `screenContentWrapperForFabricChild` + rejects any resolved view that is not the explicit wrapper when one exists + (`viewIsExplicitNativeScriptScreenContentWrapper` / + `screenViewExplicitNativeScriptScreenContentWrapperChild`, chunk2 + chunk5). + +3. **Wasteful mid-update work (latency, partially fixed).** + - `ensureSelectedTabStackContainerHosted` reparented the LIVE root stack + container on every stack update (any intermediate UIKit wrapper view made + its superview check fail) — ~60 ms per present. Now it leaves + window-attached, correctly-contained stack views alone. + - Duplicate `transactionCommitted` deliveries and duplicate + `schedulePostLayoutStackReconcile` dispatches piled dead blocks on the + main queue ahead of the presentation-critical reconcile. Both now dedupe + via `stackPostLayoutReconcilePending`. + +**Verified with pixels (port sim):** modal body now paints INSIDE the sheet +from the first visible frame (350 ms capture shows the sheet mid-slide with +full body content — upstream-matching behavior; previously the body was blank +at 250/450 ms and permanently blank on cold presents). Push/pop lanes still +pass. All suites green: screens Jest (367), tsc, bob build, runtime JS tests +(after fixing two stale source-shape assertions the previous session left +failing: runtime-callback-policy + uikit-host-detached-wrapper-api), +check:ffi-boundaries, demo typecheck. + +**Still open — present-start latency.** The original app shows full +`modal-content` at 250 ms (all 7 verifier gates pass on the original sim; the +present must start ≈ +100 ms there). The port's present now starts ≈ ++250-300 ms, so `present-250ms` still fails (the 250 ms frame is still root). +Measured no-trace breakdown after this session's fixes: press→commit (JS) +~50-90 ms, Fabric mutation loop ~90 ms (per-child worklet chains: +content-wrapper resolve/layout/certify + host creation, 15-30 ms per screen +child — upstream's mountChildComponentView is bookkeeping-only), post-loop +reconcileModal ~50 ms (incl. two ~12 ms layoutHostedReactSubviews passes). +Next surgical direction: make the mid-transaction worklets bookkeeping-only +(defer wrapper layout/certify/touch-refresh work to the single +transactionCommitted pass), dedupe the double prepare-layout in +reconcileModal, and re-measure with `NS_NS_FABRIC_DEBUG=1` +(SIMCTL_CHILD_NS_NS_FABRIC_DEBUG=1 + `log stream` — see below). + +### Latency pass (same session, later): present-start moved from ~+250-300 ms to ~+140-170 ms + +Three more upstream-shaped cuts landed after the correctness fixes (all fork, +all suites green): + +- **Bookkeeping-only mid-transaction mounts.** `mountFabricScreenContentChild` + now returns after registry bookkeeping while a screen is inside a Fabric + mounting transaction (`beginScreenFabricMountBatch` set from the screen's + `mounted`/`update`/`mountingTransactionWillMount` hooks, cleared in + `transactionCommitted`/`hostReady`). The full layout/readiness/touch tail + runs once per screen from the `transactionCommitted` sync — mirroring + upstream `RNSScreenView.mountChildComponentView` (insert-only) + + one container update per transaction. +- **Snapshot ≠ delta.** The screen `transactionCommitted` treated a non-empty + `transaction.children` *snapshot* as "children changed", so every screen + (including the unchanged Home base screen, 78 descendants) re-ran the full + wrapper layout chain on every transaction (~120 ms trace during a modal + present). Now requires `hasModifiedChildren === true`. +- (Earlier in the session: live root-stack container rehost removed; + duplicate transactionCommitted / post-layout reconcile dispatches deduped.) + +Measured effect (trace-mode, same push/pop→present flow): loop-end→reconcile +gap 264 ms → 116 ms; `modal-present-call` at +427 vs +575. **Pixels at 250 ms: +the sheet now covers the root ~80 % risen with the full body content painted +inside it** (previously the 250 ms frame was still root). The original at +250 ms shows the settled sheet — the port is ~60-90 ms behind on present +start, purely latency. + +`present-250ms` still fails, partly for a verifier-classification reason worth +knowing: the port's 250 ms frame (sheet mostly risen, content aboard, root +fully covered) matches NEITHER verifier state — `modal-sheet-started` requires +the root's Present button still visible (`presentBlue`), `modal-content` +requires the Dismiss button at its settled position. Do not weaken the gate; +close the latency gap instead. + +Remaining measured hotspots (next targets, real ms): +1. `reconcileModal` ≈ 104 ms (slow-worklet label) — controller/header + configuration (`configureScreenControllerHeader`, + `configureNavigationAppearance` UINavigationBarAppearance work) plus two + mandated ~13 ms layout passes (`layoutNavigationStackViews + 'modal-base-refresh'` + `layoutPresentedModalNestedContentScreen`; tests + 'repairs stale hosted RN wrapper frames after UIKit modal presentation' and + 'starts modal presentation from the upstream modal controller list…' pin + the first pass — removing it needs a fresh stale-frame repro first). +2. Mutation-loop residuals ~60-70 ms: lazy UIKit host creation for the new + nested-stack / modal-screen hosts (~27-32 ms each, `createController` + + configure through the bridge) and the stack/screen `update()` worklets + (register/publish/containment). Upstream's per-view cost is µs ObjC. + Candidate: make `createController` alloc-only and defer configure to the + post-transaction pass. +3. Repeated `content-wrapper-resolve-root` / `stack-host-record-published` + chains (~8-15 ms each) still fire several times per transaction. + +### Second latency pass (2026-07-06): FULL verifier pass at 350 ms; 250 ms still open + +- **Recycled-wrapper registry bug fixed (the big one).** After push/pop, RN + recycles the popped screen's wrapper component view into the next screen + (the modal-header), but the port's cross-screen wrapper registry still + mapped the view to the dead screen, so wrapper resolution returned null on + every mount (`skip-registered-screen`), the prepared fast-path never + engaged, and reconcileModal ran a forced double layout on every + present-after-pop. Registrations are now validated (registered screen must + still host the view) and cleared when stale. `modal-present-call` worklet: + 104 ms → 19 ms. This likely also removes forced refreshes in other + push-after-pop lanes that reuse recycled wrappers. +- **Runtime transaction-delivery dedupe** (see RN_API.md 2026-07-06 note): + the host-creation replay no longer delivers a partial transactionCommitted + mid-transaction (`isApplyingMountingTransaction` guard); didMount delivers + the complete one exactly once. TS handlers got idempotence early-exits so + repeat deliveries are cheap. NOTE: runtime .mm changed — the demo app binary + must be rebuilt (`xcodebuild -workspace NativeScriptUIKitDemo.xcworkspace + -scheme NativeScriptUIKitDemo -configuration Debug -sdk iphonesimulator + -derivedDataPath build/DerivedData build`, then `simctl install`). +- **Verifier state: full pass at `FIRST_PAINT_DELAY_MS=350`** (all 7 gates, + `present-350ms=modal-content`). At 250 ms the port's sheet is ~92-97 % + risen with the full body aboard; the original is settled. Video-frame + comparison (simctl recordVideo on both sims, identical tap protocol) puts + the port's present-start roughly 150-350 ms behind the original — note the + verifier's delays measure from the simdeck tap *command return*, not touch + delivery, so treat its absolute numbers as relative only. +- Remaining measured latency work: mutation-loop host creation for fresh + hosts (~20-30 ms each — candidate: alloc-only `createController`, configure + deferred post-transaction) and stack/screen `update()` publish/containment + chains. press→commit is shared JS with the original and likely equal. + +### Fundamental audit + touch overhaul (2026-07-06, "we are doing something fundamentally wrong") + +The user reported taps (e.g. "Push Detail") not reacting instantly and general +performance far below the original. A 4-agent architecture audit (full JSON +reports preserved in `docs/audits/2026-07-06-fundamental-audit-*.json`) plus +video/burst measurements identified and fixed three port-specific mechanisms +sitting on EVERY interaction: + +1. **Per-tap worklet hit-testing (removed).** `RNSScreenNativeScriptView` + overrode `hitTest:withEvent:` as a worklet doing registry scans and + recursive bridged subtree walks; UIKit hit-tests several times per touch, + so every tap paid multiple ObjC→JS crossings before delivery — and the + override returned null on wrapper-resolution failure, making taps DEAD + instead of falling through (the historical "unreliable touches"). + Upstream RNSScreenView has no hitTest override. Removed (chunk4); the + stack-level hitTest (header routing + edge-pop, upstream has an ObjC + equivalent) and the header-subview wrapper hitTest were kept. +2. **Duplicate RCTSurfaceTouchHandler per screen (removed).** + `screenRequiresOwnSurfaceTouchHandler` returned true for EVERY screen in a + registered stack, attaching a second surface handler whose gesture + negotiation coupled with the root handler on every touch. Upstream + attaches one extra handler only for window-detached modals. Now only + modal presentations (and their nested content) qualify (chunk2). +3. **Worklet fan-out on every Fabric commit (gated, runtime).** Fabric + delivers mounting-transaction callbacks to every registered observer view + for every transaction in the app; the runtime crossed into the worklet + (`mountingTransactionWillMount`) for every live host on every commit — + a one-line text update paid ~10 crossings with tree walks + (`ensureScreenContentWrapperMounted` etc.). Now gated natively on whether + the transaction actually touches the host's tag + (NativeScriptUIViewComponentView.mm, upstream-mirror of the native + mutation scan). REQUIRES APP BINARY REBUILD. + +Measured effect (burst-screenshot protocol, both sims, identical taps): +before — port's non-nav header tap ~+900 ms and push-start ~+650 ms behind +the original; after — both land in the same first ~460 ms sample bucket as +the original (indistinguishable at this resolution). One header-counter +commit's main-thread span dropped from ~258 ms (21 lifecycle crossings, 6 +transaction deliveries) before the gating. All lanes re-verified: push, pop, +header custom-view tap (counter increments), present, dismiss; screens Jest +367 green after updating the touch-policy/hitTest source-shape tests. + +**Remaining fundamentals (the roadmap, from the audits — see +docs/audits/*.json for full evidence):** +- Ownership inversion: upstream makes the Fabric component view the + controller's view (`RNSScreen.mm initWithView:self`); the port evicts + children into TS-owned UIKit trees, forcing an 84-map registry and + ensure/certify/refresh repair machinery. Adopting the upstream invariant + would let most of chunk2/chunk3's compensation code be deleted. +- Per-lifecycle JSON: every host lifecycle call re-serializes and re-parses + props + nativeMountInfo under a recursive mutex even when unused + (runUIKitHostLifecycle / NativeApiJsi). Make payloads lazy/revision-gated. +- hostReady churn: notifyHostReadyIfNeeded funnels from layoutSubviews, + didMoveToWindow and ~23 prop setters, each rebuilding a subview-topology + key; upstream has no readiness protocol at all. +- `registerNativeScriptStackController` re-runs full host-record publishing + + tab containment on 5 hooks; `uikitHostPropsJson` re-serializes on every + React render of every host, and inline-callback identity churn re-applies + props natively on unrelated commits. + +Useful new tooling from this session: +- `SIMCTL_CHILD_NS_NS_FABRIC_DEBUG=1 xcrun simctl launch …` + + `xcrun simctl spawn log stream --predicate 'eventMessage CONTAINS "NS_NS_FABRIC_DEBUG"'` + gives the native mutation/mount timeline without JS-trace overhead. +- `xcrun lldb -p ` + `expression -l objc -O -- [view recursiveDescription]` + settles "mounted but invisible" questions definitively. + +## Current state (as of prior session turn, 2026-07-05 20:52 EDT — superseded by the update above) + +The active sub-problem is **modal first-paint parity**: tapping to present a +modal should show the modal body content by ~250 ms, matching upstream RNS. + +**Fixed this session (verified with tests + trace):** + +- Root/parent stack now reconciles from the **stack-host update path** as soon + as `stackCanReconcileFromPropsUpdate` passes, instead of waiting for the next + Fabric transaction. This moved the `presentViewController` call from ~+229 ms + to **~+131 ms** after the press — the root-stack timing bug is resolved. +- Nested modal-content stacks are **guarded** so they do not reconcile + immediately before their content wrapper exists (that guard was needed because + the broadened immediate-reconcile initially fired too early for + `Modal:modal-header` before modal props were registered). +- Added pre-presentation runtime flush attempts via the existing generic + `flushUIKitHostView` / owner-stack flush APIs (`flushKnownUIKitHostView`). + +**Still broken (the open bug to continue):** + +- At the 250 ms capture the modal **shell is visible but the body is blank**. + The nested modal content **wrapper mounts too late (~+230–250 ms)**, so the + Fabric child carrying the modal body is not present when the frame is taken. + The pixel verifier reports `unknown` instead of `modal-content`. +- The plain host flush and the owner/stack flush did **not** pull the wrapper + mount earlier in the no-trace run. + +**Diagnosis / next direction (Codex's own conclusion):** this is no longer a +tap-handling problem. It is that **Fabric child / content-wrapper +materialization for the nested modal screen is delayed vs upstream RNS**. The +correct next fix is likely in the **NativeScript RN-module / Fabric host API +ordering** (making the modal content wrapper materialize synchronously *before* +`presentViewController`), not another modal-specific workaround. Follow the +mental model: (1) what does upstream RNS do, (2) what exact primitive does it +rely on, (3) does NativeScript expose that to TS/worklets, (4) if not add the +generic primitive, (5) only then patch the port. + +## Files touched + +### Screens fork — `/Users/dj/Developer/RNModuleForks/react-native-screens` (primary work) + +- `src/components/native-stack/native-script/NativeScriptScreenStack.ios.tsx` + and its build chunks (`…ios.chunk2.tsx`, `…ios.chunk5.tsx`) — stack host + update reconcile path, modal-content guard, pre-presentation flush hooks, + selected-tab accessibility publishing. +- `src/components/tabs/native-script/NativeScriptTabs.ios.tsx` — selected + embedded-stack ownership stamping and readiness checks. +- `src/components/native-stack/native-script/NativeScriptScreenStack.test.ts` + and `…/tabs/native-script/NativeScriptTabs.test.ts` — assertions updated to + require immediate reconcile when the ready predicate passes (transaction path + kept as fallback). +- Built output under `lib/module/…` / `lib/commonjs/…` via `npx bob build`. + +### Runtime (this repo) — changed vs HEAD (`git status`, ~38 files) + +- `packages/react-native/ios/` — `NativeScriptUIView.mm/.h`, + `NativeScriptUIKitHost.h`, `NativeScriptUIViewManager.mm`, + `NativeScriptNativeApiModule.mm`, and `Fabric/NativeScriptUIViewComponentView.mm/.h` + (host-refresh / Fabric host primitives — e.g. the scroll-view zero-origin fill + frame fix and `flushUIKitHostView` surface). +- `packages/react-native/src/` — `index.ts`, `index.d.ts`, + `NativeScriptUIViewNativeComponent.ts` (renamed the RNS-named trace hook to the + generic `__nativeScriptUIKitHostTraceEvents` / `[NativeScript UIKitHost]`). +- `packages/react-native/test/*.test.js` (16 files) — runtime JS guard tests. +- `NativeScript/ffi/shared/bridge/*.mm` and `NativeScript/ffi/hermes/NativeApiJsi.mm` + — JSI/FFI bridge fixes (JS subclass prototype/accessor/init identity — see the + recent `fix:` commits in `git log`). +- Tracking docs: `HANDOFF.md`, `PROGRESS.md`, `RN_API.md`. + +## How to run / test + +Screens (fast inner loop — run after every change): + +```sh +cd /Users/dj/Developer/RNModuleForks/react-native-screens +./node_modules/.bin/jest src/components/native-stack/native-script/NativeScriptScreenStack.test.ts --runInBand +./node_modules/.bin/jest src/components/tabs/native-script/NativeScriptTabs.test.ts --runInBand +./node_modules/.bin/tsc --noEmit --pretty false --skipLibCheck +npx bob build # rebuild lib/ so the demo/simulator picks up changes +``` + +Runtime (when the RN-module / Fabric API surface changes): + +```sh +cd /Users/dj/Developer/NativeScriptRuntime +for f in packages/react-native/test/*.test.js; do node "$f"; done +npm run check:ffi-boundaries +``` + +Demo typecheck: + +```sh +cd /Users/dj/Developer/RNModuleForks/nativescript-uikit-demo +npm run typecheck +``` + +**Simulator pixel gate (the real pass/fail).** Rebuild the fork, restart the +no-trace Metro on 8082, relaunch the app, then run the first-paint pixel probe. +The verifier and its env profile (from the last session): + +```sh +cd /Users/dj/Developer/RNModuleForks/nativescript-uikit-demo +npm run start -- --clear --port 8082 # no-trace server (clean signal) +xcrun simctl terminate BF759806-2EBB-49ED-AD8E-413A7790ADE0 org.nativescript.uikit.demo || true +xcrun simctl openurl BF759806-2EBB-49ED-AD8E-413A7790ADE0 'nativescriptuikitdemo://expo-development-client/?url=http%3A%2F%2Flocalhost%3A8082' +FIRST_PAINT_OUT_DIR=/tmp/rns-first-paint FIRST_PAINT_PROFILE=port \ +FIRST_PAINT_DELAY_MS=250 SECOND_PAINT_DELAY_MS=450 \ +node scripts/verify-react-nav-first-paint-pixels.js +``` + +For trace-based timing (to see wrapper-mount / present-call timestamps) set +`EXPO_PUBLIC_NS_RNS_TRACE=1 EXPO_PUBLIC_RNS_DEMO_TRACE=1 +EXPO_PUBLIC_RNS_DEMO_TRACE_EXPORT=1` on the Metro start, then read the exported +trace from the app container: + +```sh +DATA_DIR=$(xcrun simctl get_app_container BF759806-2EBB-49ED-AD8E-413A7790ADE0 org.nativescript.uikit.demo data) +cat "$DATA_DIR/Documents/rns-port-trace-export.json" # shape varies; parse defensively +``` + +Always run the **no-trace** pixel gate for the actual verdict (trace overhead +skews the ~250 ms edge). Stop Metro (`Ctrl-C` / clear ports 8082 & 8083) when +done so no stray servers are left. + +## Verification discipline + +- SimDeck screenshot / pixel comparison against the original app is the final + gate — Jest green + AX descendants is not enough. +- Compare port vs original on both UDIDs for: repeated tab switches into React + Nav, first push after tab switch, repeated push/pop, modal present/dismiss + (incl. interactive dismiss), toolbar / menu / custom-header actions. +- Do not claim the branch is PR-ready. Known-passing lanes exist, but the modal + first-paint blank-body bug is open and PR hygiene (removing diagnostic + leftovers) is pending. + +## Open questions + +- Which generic NativeScript Fabric/host primitive can force the nested modal + content wrapper to mount **synchronously before** `presentViewController`? + That is the crux of the remaining bug. +- Is there residual trace/diagnostic scaffolding in the fork that should be + stripped before the PR? +- Confirm the modal/native title & toolbar-padding items (currently "watch + only") don't regress once first-paint is fixed — get a fresh pixel repro + before touching that code. + +## Where to read more + +- `HANDOFF.md` — full running log, dated updates, "Known Broken Behavior", + "Suspect Code Path", "Next Surgical Direction", "Commands Likely Needed". +- `PROGRESS.md` — turn-by-turn progress detail. +- `RN_API.md` — the runtime / Fabric / worklet API surface added for the port. + +## 2026-07-06 (late session): interactive dismissal + back swipe fixed end to end + +Both user-reported gesture bugs are fixed and verified by deterministic +simulator lanes on the port build: + +- `scripts/verify-modal-interactive-dismiss.js` (7 steps): **PASS** at + `SETTLE_MS=9000` (at 4000 only step 5 — push-after-modal-cycle — can lag + past the assert; it completes and step 6 passes). +- `scripts/verify-back-swipe-pop.js` (5 steps, NEW): **PASS** at 4000 and + 9000. Push → edge-swipe pop → push → pop, two full cycles. +- Fork Jest 311/311, `tsc` clean, `bob build` clean. + +### Root causes found and fixed (in dependency order) + +1. **Modal swipe-dismiss corrupted state (user bug 6)** — the screen host's + `mountChild` hook re-registered the screen with + `controller.__nativeScriptScreenContext` when that expando was absent on + the wrapper it received, clobbering `registry.screenContexts[id]` with + `undefined`. Native-initiated dismissals then had no emit channel, so + `onDismissed` never reached react-navigation and its route state kept the + dead Modal (later pushes rendered as modals per native-stack semantics). + Fix: `liveScreenContextForRecord` (chunk2) — a live emit channel is never + replaced by a dead one; remember/retain paths route through it. +2. **Back swipe did nothing (user bug 4), part 1** — the port never installed + a delegate on `interactivePopGestureRecognizer` (upstream sets the stack + view as delegate, RNSScreenStack.mm:321); UIKit's default delegate refuses + when custom bar buttons are installed. On iOS 26 the (undelegated) + `interactiveContentPopGestureRecognizer` also suppressed the classic edge + pop. +3. **Worklet dead-closure crash genre** — delegate objects built via + `ctx.delegate` inside *some* rehydrated worklet contexts throw + `TypeError: undefined is not a function` from the FIRST captured call: + every captured module function in that specific closure instance is + `undefined`. Confirmed empirically (probe traces + `NS_NS_CB_DEBUG=1` + source dumps in `Callbacks.mm`); healthy at build-time in the same code + path, dead at ObjC dispatch for particular instances. The same genre + causes the boot-time `NativeScript failed to run UIKit host + RNSTabsScreenIOS` logs (still open, see below). + **Working pattern** (now used for the gesture delegate, same as the modal + completion-handler fix): NativeClass **class methods with capture-free + bodies** that resolve handlers through `globalThis` keys installed by + `installRefreshVisibleStackContentHandler()` (worklet-module entry-point + pass, provably healthy). +4. **Back swipe, part 2 (the fix shape)** — `RNSScreenStackNativeScriptView` + (the stack container view, like upstream's RNSScreenStackView) now + conforms to `UIGestureRecognizerDelegate`; `installNativeBackGestureDelegate` + assigns it to both UIKit pop recognizers (`native-pop`, + `native-content-pop`). Decision logic lives in module worklets + (`stackContainerBackGesture*`, chunk4, placed AFTER their captured deps — + the `declares worklet helpers before worklets that capture them` test + guards this ordering; violating it recreates the dead-closure genre at + module scope). Content-pop interop now defaults ON + (`__NSRNS_ENABLE_NATIVE_CONTENT_POP_GESTURE !== false`), matching upstream + iOS 26. +5. **Interactive pop left UIKit wedged** — two port-only behaviors broke the + percent-driven transition: (a) `updateNativeBackGesture` toggled + `popGesture.enabled` by `count>1`, disabling the recognizer mid-gesture + (coordinator leak, `nav.transitionCoordinator` stuck non-nil, all later + pushes silently swallowed; verified via lldb). Upstream never toggles + `enabled` — the delegate gates. (b) `cancelTouchesInParent` called + `UIGestureRecognizer.reset()` directly (UIKit-internal API). Both removed. + +### Runtime (NativeScriptRuntime repo) changes + +- `NativeScript/ffi/shared/bridge/Callbacks.mm`: `NS_NS_CB_DEBUG=1` (launch + with `SIMCTL_CHILD_NS_NS_CB_DEBUG=1`) logs callback exceptions with the + stored function's `toString()` — keep; it is the tool for the dead-closure + hunt. +- `packages/react-native/native-api/**` is a **vendored copy**; syncing is + manual `cp` (see `scripts/build_react_native_turbomodule.sh:73-85` — the + full script fails at metagen). Runtime .mm edits do nothing until copied + + `xcodebuild` + `simctl install`. +- `wrapDelegateMethods` bind experiment was reverted (worklets hydration + already binds `this` via `valueUnpacker`; not the failure mode). + +### Open items (next session) + +1. **Dead-closure root cause** — the one systemic bug left. Deterministic + repro at every boot: `log show … 'NativeScript failed to run UIKit host'` + (tabs host, `reactNativeFabricViewLayoutTraits` capture dead inside + `fabricLayoutRectForMountedTabsChild`). Facts: bundle-mode OFF, classic + `valueUnpacker` (eval + bind objectToUnpack), unpacked closure record has + entries but the VALUES are undefined for specific instances; chunk import + graph is acyclic. Suspect the serialize side captured a module-scope + `__closure` map evaluated before the factory consts it references (Babel + plugin emits `const X = factory({dep: dep, …})` — order matters), or the + C++ Serializable cycle handling. The order-guard Jest test enumerates + violations — currently zero; extend it to index.ts/tabs if the boot error + persists. +2. **Latency tail**: push-after-modal-cycle can exceed 4 s (lane step 5) and + the first present after tab mount got slower with the delegate work — + re-profile (`EXPO_PUBLIC_NS_RNS_TRACE=1`, `reconcileStack-sync`, + `modal-present-call`), then resume the perf roadmap (host-creation diet, + update() chains, present-250ms gate vs original). +3. PR hygiene: strip diagnostic traces added this session + (`native-dismiss-branch`, `screen-host-dispose`, `clear-screen-record`, + `screen-controller-retain`, didShow flag dump — all traceEventsEnabled + gated, but review), drop `HANDOFF-FABLE5.md` scratch sections, re-run the + full battery (fork Jest + tsc + runtime Jest + ffi + both lanes + pixel + first-paint gate + present-latency vs original). +4. `emitScreenDismissedForController` in `viewDidDisappear` + the didShow + native-dismiss path both emit now; double-emission is guarded by + `__nativeScriptDismissEventEmitted`, but audit for pops-with-preventNativeDismiss. + +## 2026-07-07: worklet capture-order root cause FIXED; latency lever identified + +### Dead-closure genre: root cause found and fixed +The worklets Babel plugin rewrites every module-level worklet into +`var X = Factory({dep1, dep2, ...})` evaluated in source order. Any captured +module function declared LATER in the file is `undefined` at factory time and +is baked dead into `X.__closure` — the map the serializer ships. Live JS-thread +calls still work (lexical var assigned by call time), so the failure only +appears in rehydrated copies. Verified end-to-end on +`reactNativeFabricViewLayoutTraits -> reactNativeFabricViewLayoutTraitsForHandle` +(bundle inspection: factory arg evaluates one line before the dep's factory). + +Fixes (all committed): +- `scratchpad worklet-order-audit/fix.js` reordered 14 declarations in + packages/react-native/src/index.ts (+ hoisted the `*GlobalName` const block + above first worklet use — const-after-use makes the captured KEY undefined, + which produced the `defineProperty` "property is not configurable" boot + crash when two definers collided on the literal "undefined" key). +- Fork: 18 moves in tabs, 5 split, 8 chunk4, 1 chunk3. The fork's + `declares worklet helpers before worklets that capture them` Jest test only + covers stack chunks + headerconfig + tabs and skips NESTED function + references and method-name false-positives; the session auditor (kept at + the scratchpad, reproduce from this doc if lost: TS AST, top-level function + decls, worklet directive, identifier refs excluding property/method names, + flag refs with higher declaration index; plus a variant flagging top-level + const/let referenced by earlier worklets) found real hazards the test + missed. PORT THE IMPROVED CHECKER INTO THE JEST TEST + add an equivalent + test for packages/react-native/src/index.ts. +- Boot-time `NativeScript failed to run UIKit host RNSTabsScreenIOS` errors: + GONE after the reorders (was every boot). +- `updateSurfaceTouchHandlerOriginsForView` (chunk2): try/catch guard — dead + native wrapper during dismissal teardown threw "requires a native receiver" + and aborted the app (reproduced under trace Metro). + +### Verified state +- Fork Jest 367/367 (stack + tabs), runtime guard tests 37/37, tsc + bob + clean, boot logs clean. +- Both gesture lanes pass functionally; back swipe and interactive modal + dismissal remain correct. + +### THE remaining defect: press -> transition-start latency (user bug 3) +Measured with demo traces (`[RNS_DEMO_TRACE]` press vs transition-start): +- original: present 60-88 ms, push 40-66 ms, pop 35-55 ms. +- port: present 737-1053 ms, push 1007-1073 ms, pop ~444 ms — AND unstable: + on cold tab mount or without a follow-up React commit, presents/pushes can + exceed 4 s (lane step-1/3/5 'root' failures at SETTLE_MS=4000; pass at 9000). + +Push timeline breakdown (port, trace run, press = +0): +- +186 Fabric commit reaches stack host props update (`stack-host-update`); + Detail screen host created ~+194; screen content children mount +303. +- +306..+588 DEAD GAP waiting for `content-wrapper-direct-fabric-layout` + positive=1 (wrapper layout needs the screen to be sized — which upstream + gets by PUSHING FIRST; chicken-and-egg). +- +660 screen-host-ready -> +676 reconcile isPush -> +693 pushViewController. +- The stack host's own `transactionCommitted` for the INSERT commit arrived + at **+941**, i.e. with the app's SECOND commit — the first commit's didMount + notification for the stack host appears to be suppressed/deferred + (check `NativeScriptUIViewComponentView.mm` willMount gating + `transactionTouchesThisHost` + `isApplyingMountingTransaction` replay skip + + TS `stack-host-update-defer-transaction` interplay). When that notification + is late/absent, the push rides the screen-host-ready fallback, whose timing + depends on wrapper layout => the 1-4 s variance. + +Next surgical steps (in order): +1. Runtime: make the stack host's transactionCommitted fire for the commit + that INSERTS a child (didMount path) — verify with NS_NS_FABRIC_DEBUG=1 + that the first commit delivers; the +941 second-commit delivery is the bug. +2. Fork: drop the content-ready gate on the PUSH path (upstream + RNSScreenStack pushes at updateContainer time with no content gating; RN + lays content into the already-pushed screen during the animation). The + blank-first-paint causes this gate papered over were fixed at the runtime + level (willMount gating, replay dedupe, mount batching) — re-verify with + the first-paint pixel gate afterwards (it currently fails `present-350ms` + because nothing is on screen at 350 ms; original shows the sheet by then). +3. Re-run: both lanes at SETTLE_MS=4000 (no-trace Metro), first-paint gate at + 350 ms, and the press->transition-start measurement (target: within ~2x of + original). `scripts/measure-present-latency.js` exists in the demo repo. +4. Tab first-mount hang (user bug 1): re-measure after (1)+(2) — the same + commit-delivery + gating machinery dominates the tab's first stack mount. + +### PR hygiene still pending +- Strip/gate diagnostic traces added during debugging (native-dismiss-branch, + screen-host-dispose, clear-screen-record, screen-controller-retain, + didShow flag dump, adaptive-delegate-*, presentation-did-dismiss). +- Demo repo is NOT a git repo; lane scripts live on disk only + (verify-modal-interactive-dismiss.js, verify-back-swipe-pop.js, + measure-present-latency.js). + +## 2026-07-07 (later): latency gap — four blockers found and fixed + +Profiling tool added: `NS_NS_HOST_PROFILE=1` (launch via SIMCTL_CHILD_) logs +every host lifecycle crossing >=2 ms (`NS_NS_HOST_PROFILE phase=

+ms=`) plus `txn-will/txn-did` brackets per transaction — this is what +found everything below. Section timers (`traceSlowWorklet`) were added inside +the screen transactionCommitted hook and tabs mountChild. + +### Fixed +1. **Permanent main-thread layout loop (the big one).** + `refreshContainerViewFrameAndHost` ran from `layoutSubviews` and called + `[_containerView setNeedsLayout]` unconditionally — a self-invalidating + 60/120 Hz loop per mounted host, saturating the main thread forever. + This starved Fabric mount transactions: on no-trace builds presses looked + completely dead (trace builds only worked because tracing throttled the + loop). Removed the unconditional invalidation + (`NativeScriptUIViewComponentView.mm`); frame changes still invalidate via + refreshContainerViewFrameIfNeeded. +2. **490 ms in ONE worklet call**: the new screen's `transactionCommitted` + replayed every Fabric child through `mountFabricScreenContentChild` with + the batch flag already cleared, so each child paid the full + layout/readiness tail; `layoutHostedSubviewChain` + `layoutIfNeeded` also + ran even when Fabric had already laid the wrapper out. Now: batch stays + active during the replay (children = bookkeeping, upstream + mountChildComponentView), ONE readiness/touch/host-ready pass afterwards, + and the manual chain layout runs only for the zero-size (modal cold + materialization) seed path. +3. **~40 ms × N repeated hostReady events**: the runtime re-emits hostReady on + every host frame refresh; the screen hostReady hook now early-exits when + the content is certified for the same host view (dead-wrapper/new-host + cases still take the full pass). +4. **~900 ms tab-switch hang (user bug 1)**: + `embeddedNavigationControllerRecordForMountedTabsChild` probed the + cross-chunk host-record resolver per BFS-visited view (64 views), and that + resolver scans EVERY registered stack with superview-chain walks per call. + Dropped the per-descendant probe (the descent reaches the stack container + view, whose association-based direct record answers) and added a + stacks-generation-keyed negative memo in + `stackNavigationControllerRecordForHostViewFromExternalHost`. + Result: 907 ms -> 41-75 ms. + +Also: `syncUIKitHostPropsFromNative` now skips the per-crossing JSON.parse +when the payload string is unchanged per registration (kept although the +measured win was small — payload strings usually differ by revision). + +### Measured state (dev Metro, demo-trace-only, press -> transition-start) +- original: present 60-88 ms, push 40-66 ms, pop 35-55 ms +- port BEFORE this session: push ~1050 ms (and unbounded/no-op on no-trace + builds due to the layout loop) +- port AFTER: push 480-660 ms, present 615-690 ms, pop ~465 ms; UIKit + pushViewController fires at ~330 ms; worklet->JS emit lag is ~5 ms. +- Milestones for a push (+0 press): +153 stack-host-update (React render + + commit + host-update crossings), +312 reconcile isPush, +330 + pushViewController, +492 willShow (main-thread contention with the + transactionCommitted crossing), +497 JS transition-start. + +### Remaining gap (~280 ms vs original) — next levers, in order +1. +0->+153 commit window: the port's screen/stack host updateProps + + update() crossings run INSIDE the Fabric commit; original commits in + ~40 ms. Candidate: defer update() work out of the commit (upstream + updateProps is bookkeeping-only). +2. +153->+312: child-insert -> reconcile notification path (the insert + mutation reaches the stack host's mountChild, but the reconcile rides a + scheduled hop). Candidate: reconcile directly from the stack's + transactionCommitted (now cheap and synchronous with didMount). +3. +330->+492: crossings contending with the transition on main + (mountingTransactionWillMount 33 ms x2 per commit is pure crossing + overhead for a trivial hook — candidate: skip the willMount crossing for + hosts whose hook is a no-op, or batch it into didMount). + +### Verified after all changes +- Both lanes PASS at SETTLE_MS=4000 on NO-TRACE Metro (the strict verdict). +- Fork Jest 367/367 (two stale source-shape assertions updated: + descendant host-record probe removal), runtime guard tests 37/37, tsc + clean, boot logs clean. +- KNOWN INSTRUMENT FLAW: `verify-react-nav-first-paint-pixels.js` at 350 ms + fails with byte-identical screenshots across runs — simdeck screenshot + returns cached/stale frames at sub-second cadence; the gate measures the + tool, not the app. Lane classifiers with 4 s settles are unaffected. Use + the willShow trace milestone (~330 ms) for present-start until screenshots + are trustworthy (consider `xcrun simctl io screenshot` comparison). +- Demo change: EXPO_PUBLIC_RNS_DEMO_TRACE no longer force-enables fork + worklet tracing (only EXPO_PUBLIC_NS_RNS_TRACE does) so press->transition + latency can be measured without the tracing handicap. + +## 2026-07-07 (final): remaining-floor analysis + what was kept/reverted + +Additional profiling infra (all committed): +- `NS_NS_HOST_PROFILE_INNER` (Callbacks/module): runSync-lambda-interior ms — + proved crossings have ZERO mutex wait; all cost is real JS in the hooks. +- `__NS_NS_HOST_PROFILE` worklet global (set at install when env present) + + `NS_NS_HOST_PROFILE_SECTIONS` in runUIKitHostLifecycleFromNative — proved + the shared prologue (mount-info/props JSON) is now ~0 ms; the cost is hook + bodies. +- Section timers: screen mounted() (`screen-mounted-*`), + configureScreenController (`configure-layout-hosted`, `configure-header`, + `configure-layout-nav`). + +Landed this pass: +- Stack `refresh` hook skips while the stack is transitioning (UIKit lays the + moving views out repeatedly mid-animation; each refresh re-ran controller/ + container registration at 10-50 ms on main). Post-transition repair still + runs from didShow/finishTransition. Both lanes green. +- Stack transactionCommitted reconciles push/pop-shaped targets DIRECTLY + (upstream updateContainer is synchronous at didMount); only targets + involving modal presentation keep the post-layout dispatch hop (upstream + dispatch_async before presentViewController). + +Attempted and REVERTED (do not blindly retry — tests encode why): +- Trimming the screen willMount hook to bookkeeping-only broke presents and + pushes outright: the pre-mutation `ensureScreenControllerView` + + `normalizeScreenControllerViewFrame` + `ensureScreenContentWrapperMounted` + are load-bearing (mutations need the materialized view chain). +- Gating `layoutHostedSubviewChain` by level-0 geometry (positive size / + size-matches-root) broke: (a) modal presentation (dismiss lane steps 1/3/5 + no-op), (b) the stale half-height-viewport repair (`does not stable-skip + direct Fabric content whose hosted viewport is still half-height`) because + stale frames live DEEPER than level 0. The correct future shape is an + explicit `contentIsFreshFromFabric` flag threaded from the mounted() path + only (fresh Fabric mounts cannot have stale nested frames; repairs keep the + walk). + +Current measured floor (dev Metro, demo-trace only): +- push 470-490 ms, present 590-720 ms, pop ~470 ms press->transition-start + (JS emit lag ~5 ms; UIKit push begins ~330 ms after press). +- Structure: ~150 ms React/commit+host-update crossings inside the mount, + ~90 ms hook bodies (willMount ensure ~34, committed ~35, stack update ~10, + header subviews ~16), remainder reconcile/prepare + UIKit. +- Tab-switch mounted(): configure ~440 ms (layout-hosted walk + header + config); tabs child-mount search fixed earlier (907 -> ~70 ms). + +End-game moves for true parity (in order of expected value): +1. **Batched native ensure/normalize helper** (generic runtime API): the hook + bodies spend their time in dozens of tiny interop calls + (frame reads/writes, superview checks). One ObjC helper per pattern + (materialize controller view + normalize frame + wrapper-mounted check) + turns ~30 calls into 1 (~34 ms -> ~2 ms per touched screen). Same for + layoutHostedSubviewChain (there is already a native + NativeScriptLayoutHostedSubviewChain used by NativeScriptUIView — expose + it to the worklet surface and have chunk3 call it instead of the JS walk). +2. **Release-build parity measurement**: both apps currently run dev Metro; + the port executes its port logic in unoptimized dev JS while upstream's + equivalent is compiled ObjC — the honest "bridge cost" comparison needs a + release bundle at least once. +3. `contentIsFreshFromFabric` flag (above) to skip repair walks on fresh + mounts safely. + +All gates green at this commit: both lanes at SETTLE_MS=4000 (no-trace), +fork Jest 367/367, tsc clean, runtime guard tests 37/37, boot logs clean. + +## 2026-07-07 (physics): measured interop cost + fan-out verdict + +On-device micro-benchmark (worklet, dev Hermes): plain JS 100k ops = 4.9 ms; +1k string builds = 0.2 ms; **1k bridged property reads (view.bounds) = +8.8 ms → ~9 µs per interop call** (JSI host object -> metadata -> ffi +marshal; not raw objc_msgSend). Therefore the port's latency is COUNT-driven: +a 34 ms hook = ~4k native touches; a push at ~500 ms = ~50k, where upstream +does ~100. The architectural divergence is the wrapper/registry/repair state +machine fanning out per lifecycle event — not per-call overhead and not the +crossing mechanism (runSync interior == total; no mutex wait). + +Landed: all 390 statement-form traceWorkletEvent call sites gated (their +template args did handle conversions/string builds even with tracing off). +Numbers hold at push ~500 ms / present ~650 ms / pop ~480 ms on dev Metro. + +The honest path to 50 ms-class parity, in order: +1. Count-reduction by architecture: adopt upstream's hosting shape for the + screen (component view IS the controller view; children stay Fabric-owned; + no wrapper reparenting -> the readiness/repair/touch machinery and its + tens of thousands of reads become structurally unnecessary). The props + plumbing (attachController/mountChildrenDirectlyToChildrenView etc.) + already half-exists; this is the real port-parity refactor. +2. Native batch helpers for whatever walks remain (~9 µs -> ~50 ns per op). +3. Release-bundle measurement for the residual JS share. +Beware: bob rebuilds mid-lane cause bundle-retransform races — always +relaunch and let Metro finish transforming before running lanes (first lane +after a rebuild can report false first-action no-ops). + +## 2026-07-07 (attribution): per-section call counters + the stable-skip wins + +Tooling that made this possible (all committed): +- Bridge counters: `NativeScript/ffi/shared/bridge/InteropProfiler.h` holds + C++17 inline atomics (`nsInteropProfiler::gCalls/gNs`) ticked by every + `ffi_call` AND both GSD fast-path `invoker(ctx)` sites. The engine TUs + include bridge sources inside `namespace nativescript { namespace {` — the + counters must live in a header included at FILE scope by each engine .mm, + or you get anon-namespace duplicates/link errors. +- `NS_NS_HOST_PROFILE_INNER ... interopCalls=N interopMs=X` per phase from + the module; `__nsInteropCalls()` on the worklet global; fork-side + `traceSlowWorklet`/`traceTimestamp` fire under `__NS_NS_HOST_PROFILE` + (bypassing trace gates) and print `NSRNS_PROF