feat(react): support ET typed page update flow#2694
Conversation
|
📝 WalkthroughWalkthroughThis PR implements typed element template support with reload versioning and hydration lifecycle management. It introduces core helpers ( ChangesTyped Element Template Reload & Hydration Lifecycle
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/react/runtime/src/element-template/background/hydration-listener.ts`:
- Around line 40-49: Ensure hydrate payloads and instance items are validated
before property access: when treating data as
ElementTemplateHydrateCommitContext in the block around getReloadVersion(),
first check that payload is non-null and typeof payload === 'object' (and that
'reloadVersion' in payload) before reading payload.reloadVersion and assigning
payload.instances to instances; likewise, when iterating over instances (the
code paths around checks like 'templateKey' in before), guard each item to be
non-null and an object and confirm the expected keys exist before using 'in' or
accessing properties. Update the checks around variable names data, payload,
instances, and any loop variable (e.g., before) to bail out or skip malformed
entries to avoid runtime throws.
In `@packages/react/runtime/src/element-template/native/reload.ts`:
- Line 60: The assignment unconditionally merges an unknown updateData into
lynx.__initData which can introduce non-object keys; before calling
Object.assign in reload.ts (the line setting lynx.__initData = Object.assign({},
lynx.__initData, updateData)), validate that updateData is a plain object (e.g.,
typeof updateData === 'object' && updateData !== null &&
!Array.isArray(updateData') or use an isPlainObject helper) and only merge when
that check passes; if updateData fails the guard, skip the merge (or log/ignore
the payload) so lynx.__initData cannot be polluted with non-object values.
- Around line 29-31: reloadMainThread currently ignores reset/merge semantics
and always mutates lynx.__initData via Object.assign; update the block that
checks typeof data to honor options.resetPageData: if options.resetPageData is
true (and data is object/non-null), replace lynx.__initData with a shallow clone
of data (e.g., lynx.__initData = { ...data }) to wipe stale keys, otherwise
preserve current behavior and merge with Object.assign(lynx.__initData, data);
keep the existing isEmptyObject check and ensure behavior aligns with
reloadTemplate's expectations.
In `@packages/react/runtime/src/element-template/runtime/patch.ts`:
- Around line 193-199: The loop building resolvedListChildren reads
(listChildren[index]!).__etHandleRef directly and can throw for
null/undefined/malformed entries; before calling resolveHandle, validate the
entry from listChildren (e.g., ensure it is non-null/undefined and has a
__etHandleRef property), and if it fails validation, skip it (or report/log) and
continue so patch processing doesn’t abort; update the loop around
resolveHandle/`options.listChildren[${index}]` to perform this guard and only
call resolveHandle when the entry shape is valid.
- Around line 76-99: The createTypedElement case
(ElementTemplateUpdateOps.createTypedElement) is missing the DEV-mode validation
for handleId that createTemplate includes; add the same DEV-only checks for
invalid or duplicate handle IDs (using the handleId and elementTemplateRegistry
symbols) before calling __CreateTypedElementTemplate and skip/continue if
validation fails so we don't overwrite existing registry entries during
debugging.
In
`@packages/react/runtime/src/element-template/runtime/render/render-main-thread.ts`:
- Around line 48-53: The loop that calls insertRootIntoPage can throw before
mainThreadRootRefs is set, causing already-inserted roots to be untracked;
initialize mainThreadRootRefs = [] before iterating over rootRefs, and as you
successfully call insertRootIntoPage for each rootRef push that rootRef into
mainThreadRootRefs (or call a helper like trackMainThreadRoot) so the inserted
roots are recorded incrementally; ensure any thrown error is rethrown after
updating mainThreadRootRefs so cleanup routines that rely on mainThreadRootRefs
can remove the partially-inserted roots.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1f2bee66-ef1c-4f0f-80b9-94a57fd5463d
📒 Files selected for processing (72)
packages/react/runtime/__test__/core/lynx-page-data.test.tspackages/react/runtime/__test__/core/reload-version.test.tspackages/react/runtime/__test__/element-template/debug/alog.test.tspackages/react/runtime/__test__/element-template/debug/elementPAPICall.test.tspackages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate/_shared.tsxpackages/react/runtime/__test__/element-template/fixtures/hydrate/hydration-data/_shared.tsxpackages/react/runtime/__test__/element-template/fixtures/page/render-page/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/patch/_shared.tsxpackages/react/runtime/__test__/element-template/fixtures/render/child-siblings/papi.txtpackages/react/runtime/__test__/element-template/fixtures/render/component-slot-content/papi.txtpackages/react/runtime/__test__/element-template/fixtures/render/component/papi.txtpackages/react/runtime/__test__/element-template/fixtures/render/mapped-view-children/papi.txtpackages/react/runtime/__test__/element-template/fixtures/render/mixed-children/papi.txtpackages/react/runtime/__test__/element-template/fixtures/render/multiple-text/papi.txtpackages/react/runtime/__test__/element-template/fixtures/render/nested-templates/papi.txtpackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/_shared.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/appends-root-text-via-append-element/case.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/appends-root-text-via-append-element/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/creates-template-from-attrs-and-slot-text/case.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/creates-template-from-attrs-and-slot-text/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/handles-multiple-template-children-in-the-same-slot/case.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/handles-multiple-template-children-in-the-same-slot/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/handles-multiple-text-nodes-in-the-same-slot/case.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/handles-multiple-text-nodes-in-the-same-slot/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/ignores-non-attrs-opcode-payloads/case.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/ignores-non-attrs-opcode-payloads/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/inserts-nested-templates-into-parent-slots/case.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/inserts-nested-templates-into-parent-slots/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/keeps-slot-children-separated-and-ordered/case.tspackages/react/runtime/__test__/element-template/fixtures/render/opcodes-into-element-template/keeps-slot-children-separated-and-ordered/native-log.txtpackages/react/runtime/__test__/element-template/fixtures/render/react-example/papi.txtpackages/react/runtime/__test__/element-template/native/index.test.tspackages/react/runtime/__test__/element-template/native/main-thread-api.test.tspackages/react/runtime/__test__/element-template/native/reload.test.tspackages/react/runtime/__test__/element-template/runtime/background/commit-hook.test.tspackages/react/runtime/__test__/element-template/runtime/background/reload.test.tspackages/react/runtime/__test__/element-template/runtime/hydration/hydration-listener.test.tspackages/react/runtime/__test__/element-template/runtime/page/page.test.tspackages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsxpackages/react/runtime/__test__/element-template/runtime/patch/update-timing.test.tspackages/react/runtime/__test__/element-template/runtime/render/render-main-thread.contract.test.tspackages/react/runtime/__test__/element-template/runtime/render/render-main-thread.test.tspackages/react/runtime/__test__/element-template/test-utils/debug/compiledHydrationScenario.tspackages/react/runtime/__test__/element-template/test-utils/debug/hydratePayload.tspackages/react/runtime/__test__/element-template/test-utils/debug/renderFixtureRunner.tspackages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.test.tsxpackages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.tspackages/react/runtime/__test__/element-template/test-utils/mock/mockNativePapi.tspackages/react/runtime/__test__/element-template/test-utils/mock/mockNativePapi/templateTree.test.tspackages/react/runtime/__test__/element-template/test-utils/mock/mockNativePapi/templateTree.tspackages/react/runtime/__test__/snapshot/lifecycle/updateData.test.jsxpackages/react/runtime/src/core/lynx-page-data.tspackages/react/runtime/src/core/reload-version.tspackages/react/runtime/src/element-template/background/commit-hook.tspackages/react/runtime/src/element-template/background/hydration-listener.tspackages/react/runtime/src/element-template/debug/alog.tspackages/react/runtime/src/element-template/debug/elementPAPICall.tspackages/react/runtime/src/element-template/native/index.tspackages/react/runtime/src/element-template/native/main-thread-api.tspackages/react/runtime/src/element-template/native/patch-listener.tspackages/react/runtime/src/element-template/native/reload.tspackages/react/runtime/src/element-template/protocol/opcodes.tspackages/react/runtime/src/element-template/protocol/types.tspackages/react/runtime/src/element-template/runtime/page/page.tspackages/react/runtime/src/element-template/runtime/patch.tspackages/react/runtime/src/element-template/runtime/render/render-main-thread.tspackages/react/runtime/src/element-template/runtime/template/handle.tspackages/react/runtime/src/element-template/types.d.tspackages/react/runtime/src/snapshot/lifecycle/patch/commit.tspackages/react/runtime/src/snapshot/lifecycle/patch/updateMainThread.tspackages/react/runtime/src/snapshot/lifecycle/reload.tspackages/react/runtime/src/snapshot/lynx/calledByNative.ts
💤 Files with no reviewable changes (1)
- packages/react/runtime/src/element-template/runtime/template/handle.ts
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Merging this PR will not alter performance
Comparing Footnotes
|
React Example with Element Template#867 Bundle Size — 203.8KiB (+0.81%).2f20f3c(current) vs 60bdcd4 main#858(baseline) Bundle metrics
Bundle size by type
Bundle analysis report Branch Yradex:wt/pick-5645-75fc-2026052... Project dashboard Generated by RelativeCI Documentation Report issue |
React External#1715 Bundle Size — 698.11KiB (+0.01%).2f20f3c(current) vs 60bdcd4 main#1707(baseline) Bundle metrics
Bundle size by type
Bundle analysis report Branch Yradex:wt/pick-5645-75fc-2026052... Project dashboard Generated by RelativeCI Documentation Report issue |
Web Explorer#10174 Bundle Size — 903.53KiB (0%).2f20f3c(current) vs 60bdcd4 main#10165(baseline) Bundle metrics
Bundle size by type
|
| Current #10174 |
Baseline #10165 |
|
|---|---|---|
499.15KiB |
499.15KiB |
|
402.16KiB |
402.16KiB |
|
2.22KiB |
2.22KiB |
Bundle analysis report Branch Yradex:wt/pick-5645-75fc-2026052... Project dashboard
Generated by RelativeCI Documentation Report issue
React MTF Example#1732 Bundle Size — 208.77KiB (~+0.01%).2f20f3c(current) vs 60bdcd4 main#1723(baseline) Bundle metrics
Bundle size by type
Bundle analysis report Branch Yradex:wt/pick-5645-75fc-2026052... Project dashboard Generated by RelativeCI Documentation Report issue |
React Example#8598 Bundle Size — 237.82KiB (~+0.01%).2f20f3c(current) vs 60bdcd4 main#8589(baseline) Bundle metrics
Bundle size by type
Bundle analysis report Branch Yradex:wt/pick-5645-75fc-2026052... Project dashboard Generated by RelativeCI Documentation Report issue |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/react/runtime/src/element-template/runtime/patch.ts (1)
76-93:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject malformed typed-create payload shapes before calling native.
createTemplaterejects bad top-level payload shapes in DEV, butcreateTypedElementstill accepts them. For example, a non-arrayelementSlotsbecomesnullinresolveElementSlots(), and a primitive/non-objectoptionsis passed through unchanged byresolveRuntimeOptions(). That hides protocol bugs and can hand invalid data to__CreateTypedElementTemplateacross the native boundary.Suggested fix
case ElementTemplateUpdateOps.createTypedElement: { const handleId = stream[i++] as number; const type = stream[i++] as string; const attributes = stream[i++] as TypedElementAttributesCommand | null | undefined; const elementSlots = stream[i++] as number[][] | null | undefined; const options = stream[i++] as RuntimeOptionsCommand | null | undefined; if (__DEV__) { - const createError = validateCreateHandleId(handleId); + const createError = validateCreateTypedElementPayload(handleId, elementSlots, options); if (createError) { lynx.reportError(createError); continue; } } @@ function validateCreateHandleId(handleId: number): Error | null { if (!isValidHandleId(handleId)) { return new Error(`ElementTemplate update has invalid handleId ${String(handleId)}.`); } if (elementTemplateRegistry.get(handleId)) { return new Error(`ElementTemplate update received duplicate handleId ${handleId}.`); } return null; } +function validateCreateTypedElementPayload( + handleId: number, + elementSlots: number[][] | null | undefined, + options: RuntimeOptionsCommand | null | undefined, +): Error | null { + const handleError = validateCreateHandleId(handleId); + if (handleError) { + return handleError; + } + if (elementSlots != null && !Array.isArray(elementSlots)) { + return new Error( + 'ElementTemplate update typed create elementSlots must be an array, null, or undefined.', + ); + } + if (options != null && (typeof options !== 'object' || Array.isArray(options))) { + return new Error( + 'ElementTemplate update typed create options must be an object, null, or undefined.', + ); + } + if ( + options != null + && 'listChildren' in options + && options.listChildren != null + && !Array.isArray(options.listChildren) + ) { + return new Error( + 'ElementTemplate update options.listChildren must be an array, null, or undefined.', + ); + } + return null; +} + function validateCreateTemplatePayload( handleId: number, attributeSlots: SerializableValue[] | null | undefined, elementSlots: number[][] | null | undefined, ): Error | null {Also applies to: 189-199
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/react/runtime/src/element-template/runtime/patch.ts` around lines 76 - 93, The createTypedElement handler (case ElementTemplateUpdateOps.createTypedElement) currently passes malformed elementSlots and options to native; add explicit runtime-shape checks before calling native: after validateCreateHandleId and before resolveElementSlots/resolveRuntimeOptions, verify elementSlots is either null/undefined or an array and options is either null/undefined or an object, and if not call lynx.reportError with a descriptive message and continue to skip the native call; apply the same guards to the other createTypedElement-like branch around lines 189-199 so __CreateTypedElementTemplate never receives primitive/non-array payloads.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/react/runtime/src/element-template/runtime/patch.ts`:
- Around line 76-93: The createTypedElement handler (case
ElementTemplateUpdateOps.createTypedElement) currently passes malformed
elementSlots and options to native; add explicit runtime-shape checks before
calling native: after validateCreateHandleId and before
resolveElementSlots/resolveRuntimeOptions, verify elementSlots is either
null/undefined or an array and options is either null/undefined or an object,
and if not call lynx.reportError with a descriptive message and continue to skip
the native call; apply the same guards to the other createTypedElement-like
branch around lines 189-199 so __CreateTypedElementTemplate never receives
primitive/non-array payloads.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 67b95089-63b5-4d04-a0fb-54a8c07dd1fe
📒 Files selected for processing (6)
packages/react/runtime/__test__/element-template/fixtures/render/sparse-element-slot/papi.txtpackages/react/runtime/__test__/element-template/native/reload.test.tspackages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsxpackages/react/runtime/__test__/element-template/test-utils/debug/renderFixtureRunner.tspackages/react/runtime/src/element-template/native/reload.tspackages/react/runtime/src/element-template/runtime/patch.ts
Summary by CodeRabbit
New Features
updatePageoperations.Tests
Overview
Element Template now owns the page/update lifecycle through typed ET primitives instead of relying on legacy page append semantics. This change aligns the typed element protocol, page root handling, immediate
updatePage, andreloadTemplateflow so main-thread create, background hydrate, and later update events all use the same ET handle and reload-version boundaries.Key Points
createTypedElementupdate command so the main thread can create typed native nodes through__CreateTypedElementTemplate(type, attributes, slots, uid, options).__CreateTypedElementTemplate("page", null, null, "0", null), then inserts or removes React roots through page slot0.updatePageandreloadTemplatesupport by sharing initData merge/reset logic, rebuilding main/background ET state on reload, and resetting template/event/runtime state at the reload boundary.reloadVersionthrough hydrate and update commit payloads so stale events from before a reload are ignored while the previous array-shaped hydrate payload remains accepted.Runtime Contract
The update stream can now carry typed creates:
elementSlotsare resolved from handle ids toElementRef[][]before native create. For options, this phase only resolves list-specificoptions.listChildrenhandle references; generic nested option refs remain deferred.Hydrate/update payloads can now be wrapped as:
The background listener still accepts the old array payload for compatibility. Serialized typed nodes are modeled in the protocol, but hydrate matching remains compiled-node only for now; typed roots are reported and dropped until typed hydrate support lands.
Checklist