Skip to content

feat(react): clean element template registry on remove#2569

Merged
Yradex merged 1 commit into
lynx-family:mainfrom
Yradex:slice/element-template/04
May 8, 2026
Merged

feat(react): clean element template registry on remove#2569
Yradex merged 1 commit into
lynx-family:mainfrom
Yradex:slice/element-template/04

Conversation

@Yradex
Copy link
Copy Markdown
Collaborator

@Yradex Yradex commented May 7, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Node removal now includes and cleans up removed subtree handle IDs to ensure detached subtrees are released properly.
  • Tests

    • Updated fixtures and tests to assert the new remove-node payload structure (including subtree handle ID lists).
  • Chores

    • Strengthened type definitions for element-template update command streams with explicit tuple-shaped command types.

Overview

  • Element Template removal used to detach the child from native, but the runtime registry still held strong references for the removed subtree handles.
  • This PR makes removeNode carry the exact subtree handle ids that should be released, so the main-thread patch executor can clean the registry after native detach succeeds.
  • The native detach contract stays unchanged: __RemoveNodeFromElementTemplate still receives only the resolved target slot and child ref; registry cleanup remains an internal runtime follow-up.

Runtime Contract

The removeNode command now has an explicit tuple shape:

[
  ElementTemplateUpdateOps.removeNode,
  targetHandleId,
  elementSlotIndex,
  childHandleId,
  removedSubtreeHandleIds,
]

The important ordering is:

// Background producer
const removedSubtreeHandleIds = collectElementTemplateSubtreeHandleIds(child);
emitRemoveNode(parent.instanceId, slotId, child.instanceId, removedSubtreeHandleIds);

// Main-thread consumer
const targetRef = ElementTemplateRegistry.get(targetHandleId);
const childRef = ElementTemplateRegistry.get(childHandleId);

if (!targetRef || !childRef) {
  // Nothing has been detached, so registry entries must remain intact.
  return;
}

__RemoveNodeFromElementTemplate(targetRef, elementSlotIndex, childRef);

// Native remove only detaches from the slot. Release ET runtime refs after
// that succeeds so the removed JS subtree can be collected.
for (const handleId of removedSubtreeHandleIds) {
  ElementTemplateRegistry.delete(handleId);
}

Key Points

  • Registry cleanup is tied to successful native removal, so missing target or child handles keep the subtree entries intact for error reporting and consistency.
  • The payload is still an internal runtime protocol. The tuple aliases make producer/consumer expectations explicit without adding hot-path shape validation.
  • Debug formatting and fixture snapshots were updated so the command stream contract stays visible in tests.

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).
  • Changeset added, and when a BREAKING CHANGE occurs, it needs to be clearly marked (or not required).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2cc0a1f0-2754-4218-b4ff-98c633d3b40d

📥 Commits

Reviewing files that changed from the base of the PR and between cd6685e and edcd696.

📒 Files selected for processing (16)
  • packages/react/runtime/__test__/element-template/debug/alog.test.ts
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate-compiled/children.mixed-operations/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate-compiled/children.removes-missing/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate/children.creates-missing-nodes-recursively/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate/children.missing-slot-record-on-background/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate/coverage.raw-text-key-branches/output.txt
  • packages/react/runtime/__test__/element-template/runtime/background/hydrate.test.ts
  • packages/react/runtime/__test__/element-template/runtime/background/instance.test.ts
  • packages/react/runtime/__test__/element-template/runtime/background/update/compiled-fixtures.test.tsx
  • packages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsx
  • packages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.test.tsx
  • packages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.ts
  • packages/react/runtime/src/element-template/background/instance.ts
  • packages/react/runtime/src/element-template/debug/alog.ts
  • packages/react/runtime/src/element-template/protocol/types.ts
  • packages/react/runtime/src/element-template/runtime/patch.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/react/runtime/test/element-template/fixtures/hydrate/background-hydrate/children.creates-missing-nodes-recursively/output.txt
  • packages/react/runtime/test/element-template/fixtures/hydrate/background-hydrate/children.missing-slot-record-on-background/output.txt
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/react/runtime/test/element-template/fixtures/hydrate/background-hydrate/coverage.raw-text-key-branches/output.txt
  • packages/react/runtime/src/element-template/runtime/patch.ts
  • packages/react/runtime/src/element-template/debug/alog.ts
  • packages/react/runtime/test/element-template/runtime/background/update/compiled-fixtures.test.tsx
  • packages/react/runtime/test/element-template/runtime/patch/element-template-patch.test.tsx
  • packages/react/runtime/test/element-template/fixtures/hydrate/background-hydrate-compiled/children.removes-missing/output.txt
  • packages/react/runtime/test/element-template/fixtures/hydrate/background-hydrate-compiled/children.mixed-operations/output.txt
  • packages/react/runtime/src/element-template/protocol/types.ts
  • packages/react/runtime/src/element-template/background/instance.ts

📝 Walkthrough

Walkthrough

Adds explicit removedSubtreeHandleIds to the element-template removeNode opcode: typed protocol tuples, background emission of subtree handle lists, formatter/debug updates, patch handler registry cleanup, and comprehensive test/fixture updates to reflect the new payload shape.

Changes

Element Template removeNode Subtree Handle Tracking

Layer / File(s) Summary
Protocol Command Types
packages/react/runtime/src/element-template/protocol/types.ts
Introduces opcode-aligned tuple command types (CreateTemplateCommand, SetAttributeCommand, InsertNodeCommand, RemoveNodeCommand) and updates ElementTemplateUpdateCommandStream to use the typed union; RemoveNodeCommand includes removedSubtreeHandleIds: number[].
Command Generation
packages/react/runtime/src/element-template/background/instance.ts
removeChild now calls collectElementTemplateSubtreeHandleIds(child) and includes the result as the final argument in the removeNode opcode payload.
Debug Formatting
packages/react/runtime/src/element-template/debug/alog.ts
FormattedElementTemplateUpdateCommand adds removedSubtreeHandleIds: number[] to the removeNode variant; formatter reads and attaches this field from the command stream.
Command Application & Cleanup
packages/react/runtime/src/element-template/runtime/patch.ts
removeNode handler reads removedSubtreeHandleIds array and deletes each handle from ElementTemplateRegistry after successful native element detachment.
Debug Utilities
packages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.ts, packages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.test.tsx
FormattedUpdateEntry type and formatUpdateStream function support the new removedSubtreeHandleIds field on formatted removeNode entries; tests updated to assert formatting includes this field.
Runtime Behavior Tests
packages/react/runtime/__test__/element-template/runtime/background/instance.test.ts, packages/react/runtime/__test__/element-template/runtime/background/hydrate.test.ts, packages/react/runtime/__test__/element-template/runtime/background/update/compiled-fixtures.test.tsx
Tests verify that removeNode opcode payloads include removedSubtreeHandleIds for child removals, including nested subtrees and adjusted placeholder argument wrapping in hydrate streams.
Patch Application Tests
packages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsx
Tests verify registry cleanup for removed subtrees on success, registry preservation on errors, and correct handling of multi-level subtrees with descendant handles.
Formatter Tests
packages/react/runtime/__test__/element-template/debug/alog.test.ts, packages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.test.tsx
Tests confirm that formatted removeNode output includes the removedSubtreeHandleIds field with expected values.
Fixture Snapshots
packages/react/runtime/__test__/element-template/fixtures/hydrate/.../output.txt
Fixture outputs updated to reflect the new removeNode opcode structure with removedSubtreeHandleIds arrays (serialized as single-element arrays where applicable).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • lynx-family/lynx-stack#2550: Extends ALog/formatting and command-stream handling related to element-template update op encoding that this PR modifies.
  • lynx-family/lynx-stack#2568: Implements subtree-handle collection and commit-context teardown behavior related to removed-subtree handling.

Suggested reviewers

  • HuJean
  • hzy
  • colinaaa

Poem

🐰 I hopped through the template, counted each twig,
Collected the handles — both small and big.
I bundled them tidy, then set them adrift,
Cleared from the registry with a gentle swift.
A clean little hop — no stray handles to rig.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(react): clean element template registry on remove' accurately and directly describes the main purpose of the PR—extending the removeNode command to clean registry entries for removed subtrees.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

⚠️ No Changeset found

Latest commit: edcd696

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Yradex Yradex force-pushed the slice/element-template/04 branch from 1e1a54a to 1897322 Compare May 7, 2026 07:34
@codecov
Copy link
Copy Markdown

codecov Bot commented May 7, 2026

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
.../element-template/test-utils/debug/updateRunner.ts 0.00% 1 Missing ⚠️
...act/runtime/src/element-template/protocol/types.ts 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 7, 2026

Merging this PR will not alter performance

✅ 81 untouched benchmarks
⏩ 26 skipped benchmarks1


Comparing Yradex:slice/element-template/04 (edcd696) with main (0d51ee8)

Open in CodSpeed

Footnotes

  1. 26 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 7, 2026

Web Explorer

#9452 Bundle Size — 900.02KiB (0%).

edcd696(current) vs 0d51ee8 main#9446(baseline)

Bundle metrics  Change 1 change
                 Current
#9452
     Baseline
#9446
No change  Initial JS 44.46KiB 44.46KiB
No change  Initial CSS 2.22KiB 2.22KiB
No change  Cache Invalidation 0% 0%
No change  Chunks 9 9
No change  Assets 11 11
Change  Modules 229(-0.43%) 230
No change  Duplicate Modules 11 11
No change  Duplicate Code 27.28% 27.28%
No change  Packages 10 10
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#9452
     Baseline
#9446
No change  JS 495.88KiB 495.88KiB
No change  Other 401.92KiB 401.92KiB
No change  CSS 2.22KiB 2.22KiB

Bundle analysis reportBranch Yradex:slice/element-template/04Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 7, 2026

React Example with Element Template

#145 Bundle Size — 198.84KiB (+0.13%).

edcd696(current) vs 0d51ee8 main#139(baseline)

Bundle metrics  Change 3 changes
                 Current
#145
     Baseline
#139
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
Change  Cache Invalidation 26.6% 0%
No change  Chunks 0 0
No change  Assets 4 4
Change  Modules 79(+1.28%) 78
No change  Duplicate Modules 23 23
Change  Duplicate Code 40.31%(-0.17%) 40.38%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  Change 1 change Regression 1 regression
                 Current
#145
     Baseline
#139
No change  IMG 145.76KiB 145.76KiB
Regression  Other 53.08KiB (+0.5%) 52.81KiB

Bundle analysis reportBranch Yradex:slice/element-template/04Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 7, 2026

React Example

#7880 Bundle Size — 234.22KiB (0%).

edcd696(current) vs 0d51ee8 main#7874(baseline)

Bundle metrics  no changes
                 Current
#7880
     Baseline
#7874
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 4 4
No change  Modules 188 188
No change  Duplicate Modules 76 76
No change  Duplicate Code 45.08% 45.08%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#7880
     Baseline
#7874
No change  IMG 145.76KiB 145.76KiB
No change  Other 88.46KiB 88.46KiB

Bundle analysis reportBranch Yradex:slice/element-template/04Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 7, 2026

React MTF Example

#1010 Bundle Size — 205.14KiB (0%).

edcd696(current) vs 0d51ee8 main#1004(baseline)

Bundle metrics  no changes
                 Current
#1010
     Baseline
#1004
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 183 183
No change  Duplicate Modules 73 73
No change  Duplicate Code 44.6% 44.6%
No change  Packages 2 2
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#1010
     Baseline
#1004
No change  IMG 111.23KiB 111.23KiB
No change  Other 93.91KiB 93.91KiB

Bundle analysis reportBranch Yradex:slice/element-template/04Project dashboard


Generated by RelativeCIDocumentationReport issue

@relativeci
Copy link
Copy Markdown

relativeci Bot commented May 7, 2026

React External

#995 Bundle Size — 685.69KiB (0%).

edcd696(current) vs 0d51ee8 main#989(baseline)

Bundle metrics  no changes
                 Current
#995
     Baseline
#989
No change  Initial JS 0B 0B
No change  Initial CSS 0B 0B
No change  Cache Invalidation 0% 0%
No change  Chunks 0 0
No change  Assets 3 3
No change  Modules 17 17
No change  Duplicate Modules 5 5
No change  Duplicate Code 8.59% 8.59%
No change  Packages 0 0
No change  Duplicate Packages 0 0
Bundle size by type  no changes
                 Current
#995
     Baseline
#989
No change  Other 685.69KiB 685.69KiB

Bundle analysis reportBranch Yradex:slice/element-template/04Project dashboard


Generated by RelativeCIDocumentationReport issue

@Yradex Yradex force-pushed the slice/element-template/04 branch from 1897322 to cd6685e Compare May 7, 2026 10:12
@Yradex Yradex changed the title ci: dry-run slice/element-template/04 feat(react): clean element template registry on remove May 7, 2026
@Yradex Yradex marked this pull request as ready for review May 7, 2026 10:12
@Yradex Yradex requested review from HuJean and hzy as code owners May 7, 2026 10:12
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

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)

88-105: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Registry cleanup is skipped on error paths, leaking handles permanently

When resolveHandle fails for targetId or childId (lines 93–96), the continue jumps back to the top of the while loop and the removedSubtreeHandleIds cleanup (lines 101–103) is never executed. Since the background side has already committed the removal before dispatching this command, those handle IDs will never be reused — but their ElementTemplateRegistry entries remain, preventing GC of the native subtree forever.

The registry cleanup is independent of the native API call: even if __RemoveNodeFromElementTemplate cannot be invoked, the JS-side strong refs should still be released.

🐛 Proposed fix — move cleanup before the native guard
      case ElementTemplateUpdateOps.removeNode: {
        const targetId = stream[i++] as number;
        const elementSlotIndex = stream[i++] as number;
        const childId = stream[i++] as number;
        const removedSubtreeHandleIds = stream[i++] as number[];
        const nativeRef = resolveHandle(targetId, 'target');
        const childRef = resolveHandle(childId, 'child');
+       // Always release strong refs — background has already committed the
+       // removal and will never reuse these handle IDs, regardless of whether
+       // the native detach succeeds.
+       for (const handleId of removedSubtreeHandleIds) {
+         ElementTemplateRegistry.delete(handleId);
+       }
        if (!nativeRef || !childRef) {
          continue;
        }
        __RemoveNodeFromElementTemplate(nativeRef, elementSlotIndex, childRef);
-       // The native API only detaches from the slot. Releasing ET runtime's
-       // strong refs after a successful detach lets JS GC reclaim the subtree.
-       for (const handleId of removedSubtreeHandleIds) {
-         ElementTemplateRegistry.delete(handleId);
-       }
        break;
      }
🤖 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 88
- 105, The removeNode branch (ElementTemplateUpdateOps.removeNode) currently
skips clearing removedSubtreeHandleIds from ElementTemplateRegistry when
resolveHandle(targetId or childId) fails; move the cleanup so that the loop over
removedSubtreeHandleIds and ElementTemplateRegistry.delete(handleId) executes
regardless of resolveHandle success, i.e., perform the registry cleanup
immediately after reading removedSubtreeHandleIds and before the native
guard/continue, and then only call __RemoveNodeFromElementTemplate(nativeRef,
elementSlotIndex, childRef) if both resolveHandle(targetId, 'target') and
resolveHandle(childId, 'child') returned refs.
🤖 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/runtime/patch.ts`:
- Line 92: The code casts stream[i++] to removedSubtreeHandleIds without
checking it, which can throw if it's not iterable; in
applyElementTemplateUpdateCommands (in patch.ts) add a defensive guard right
after assigning removedSubtreeHandleIds: verify
Array.isArray(removedSubtreeHandleIds) (or at minimum typeof !== 'undefined' and
Symbol.iterator in object) and if not, replace it with an empty array (and
optionally log/debug) before the for (const handleId of removedSubtreeHandleIds)
loop so a malformed stream entry doesn't abort the entire patch loop.

---

Outside diff comments:
In `@packages/react/runtime/src/element-template/runtime/patch.ts`:
- Around line 88-105: The removeNode branch
(ElementTemplateUpdateOps.removeNode) currently skips clearing
removedSubtreeHandleIds from ElementTemplateRegistry when resolveHandle(targetId
or childId) fails; move the cleanup so that the loop over
removedSubtreeHandleIds and ElementTemplateRegistry.delete(handleId) executes
regardless of resolveHandle success, i.e., perform the registry cleanup
immediately after reading removedSubtreeHandleIds and before the native
guard/continue, and then only call __RemoveNodeFromElementTemplate(nativeRef,
elementSlotIndex, childRef) if both resolveHandle(targetId, 'target') and
resolveHandle(childId, 'child') returned refs.
🪄 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: eb19b5c9-8e46-4ead-82f1-b40aa157a619

📥 Commits

Reviewing files that changed from the base of the PR and between f2ad368 and cd6685e.

📒 Files selected for processing (16)
  • packages/react/runtime/__test__/element-template/debug/alog.test.ts
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate-compiled/children.mixed-operations/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate-compiled/children.removes-missing/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate/children.creates-missing-nodes-recursively/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate/children.missing-slot-record-on-background/output.txt
  • packages/react/runtime/__test__/element-template/fixtures/hydrate/background-hydrate/coverage.raw-text-key-branches/output.txt
  • packages/react/runtime/__test__/element-template/runtime/background/hydrate.test.ts
  • packages/react/runtime/__test__/element-template/runtime/background/instance.test.ts
  • packages/react/runtime/__test__/element-template/runtime/background/update/compiled-fixtures.test.tsx
  • packages/react/runtime/__test__/element-template/runtime/patch/element-template-patch.test.tsx
  • packages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.test.tsx
  • packages/react/runtime/__test__/element-template/test-utils/debug/updateRunner.ts
  • packages/react/runtime/src/element-template/background/instance.ts
  • packages/react/runtime/src/element-template/debug/alog.ts
  • packages/react/runtime/src/element-template/protocol/types.ts
  • packages/react/runtime/src/element-template/runtime/patch.ts

Comment thread packages/react/runtime/src/element-template/runtime/patch.ts
@Yradex Yradex force-pushed the slice/element-template/04 branch from cd6685e to edcd696 Compare May 7, 2026 12:10
@Yradex Yradex merged commit 3ce744a into lynx-family:main May 8, 2026
132 of 140 checks passed
@Yradex Yradex deleted the slice/element-template/04 branch May 8, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants