Skip to content

fix(core): keep detached TextNode renders from being silently dropped - #1173

Closed
JSap0914 wants to merge 1 commit into
anomalyco:mainfrom
JSap0914:fix/textnode-detached-render
Closed

JSap0914 wants to merge 1 commit into
anomalyco:mainfrom
JSap0914:fix/textnode-detached-render

Conversation

@JSap0914

Copy link
Copy Markdown
Contributor

Fixes #1147

TextNodeRenderable.requestRender() did this.parent?.requestRender(), which becomes a silent no-op exactly when a node is transiently detached (parent === null) while still being mutated — the normal state during a SolidJS <For>/<Index> re-key. The render request is lost, so streaming text can freeze mid-update until the tree rebuilds.

Each node now retains the active RenderContext (threaded down on every attach — add, replace, insertBefore, including the StyledText fan-out, and seeded in RootTextNodeRenderable). When parent is null, requestRender() falls back to that retained context, so a detached-but-still-updating node self-heals. A node that was never attached still reaches no context, and the attached happy path is unchanged.

How I verified

  • Added a render request propagation block to TextNode.test.ts (3 cases): a detached node forwards to its retained context; a nested descendant self-heals when the whole subtree is detached; a never-attached orphan never reaches a context.
  • bun test src/renderables/TextNode.test.ts → 55 pass; broader TextNode + Markdown suites → 210 pass / 0 fail.
  • Negative control: reverting only TextNode.ts makes the two behavioral tests fail (render request count plateaus at 2), confirming they guard the fix.
  • oxlint --deny-warnings and oxfmt --check clean on the changed files.

TextNodeRenderable.requestRender() did `this.parent?.requestRender()`,
which becomes a no-op when a node is transiently detached (parent ===
null) while still being mutated — e.g. during a SolidJS <For>/<Index>
re-key. The render request is lost and streaming text freezes mid-update.

Retain a RenderContext reference threaded down on every attach (add,
replace, insertBefore, including StyledText fan-out), seeded in
RootTextNodeRenderable, and fall back to it in requestRender() when
parent is null. A node that was never attached still reaches no context.
Copilot AI review requested due to automatic review settings June 14, 2026 04:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR ensures render requests aren’t lost when a TextNodeRenderable becomes temporarily detached by retaining and propagating a RenderContext reference throughout the text-node subtree.

Changes:

  • Retain a RenderContext on TextNodeRenderable, propagate it to descendants, and fall back to it when parent === null.
  • Update node insertion paths (add, insertAt, set) to propagate the current context to newly attached children.
  • Add tests validating render request propagation for detached nodes and nested descendants.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/core/src/renderables/TextNode.ts Retains/propagates RenderContext and uses it as a fallback for detached nodes’ requestRender() calls.
packages/core/src/renderables/TextNode.test.ts Adds coverage for render request propagation behavior on detach, including nested descendants and orphans.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

) {
super(options)
this.textParent = textParent
this._ctx = ctx
Comment on lines 63 to 87
public requestRender(): void {
this.markDirty()
this.parent?.requestRender()
if (this.parent) {
this.parent.requestRender()
} else {
// Detached (parent === null) but possibly still being mutated: fall back to
// the retained RenderContext so the render request is not silently dropped.
this._ctx?.requestRender()
}
}

/**
* Propagate the active RenderContext down this text-node subtree. The reference is
* retained even after the node is later detached (parent === null) so a still-updating
* detached node can keep reaching the renderer via requestRender().
*/
protected setRenderContext(ctx: RenderContext | undefined): void {
if (this._ctx === ctx) return
this._ctx = ctx
for (const child of this._children) {
if (typeof child !== "string") {
child.setRenderContext(ctx)
}
}
}
Comment on lines +1098 to +1100
function makeRoot(ctx: RenderContext) {
return new RootTextNodeRenderable(ctx, { id: "root" }, undefined as unknown as TextRenderable)
}
@viralcodex

Copy link
Copy Markdown
Contributor

@JSap0914 please address the review commets

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming text freeze: TextNodeRenderable.requestRender() silently drops renders when the node is transiently detached (parent === null)

4 participants